Skip to main content

Payment Provider Plugins

Payment adapters handle payment processing for orders. Unchained supports INVOICE and GENERIC providers; card gateways such as Stripe use the generic provider type.

For an overview of how payment fits into the order lifecycle, see Order Lifecycle.

Payment Types​

TypeDescriptionUse Cases
INVOICEInvoice-based paymentsPre-paid or post-paid invoices
GENERICGateway-backed and other payment methodsStripe, Datatrans, crypto, bank transfer, cash

Creating a payment provider​

The recommended way is the registerPaymentProvider factory (or registerInvoicePayment for invoices). You supply only the behavior callbacks; the factory builds and registers the plugin.

Example: pre-paid invoice​

A pre-paid invoice blocks order confirmation until payment is received — charge: false keeps the order PENDING, and payLaterAllowed: false requires payment before confirmation.

import { registerInvoicePayment } from '@unchainedshop/core';

registerInvoicePayment({
adapterId: 'prepaid-invoice',
payLaterAllowed: false,
charge: false, // payment is collected out of band; order stays PENDING
});

Example: card payment with Stripe​

import Stripe from 'stripe';
import {
OrderPricingSheet,
PaymentError,
registerPaymentProvider,
} from '@unchainedshop/core';

const stripe = new Stripe(process.env.STRIPE_SECRET_KEY);

registerPaymentProvider({
adapterId: 'stripe-card',
type: 'GENERIC',
configurationError: process.env.STRIPE_SECRET_KEY
? null
: PaymentError.INCOMPLETE_CONFIGURATION,

// Create the intent and bind it to this order payment
sign: async (configuration, context) => {
if (!context.order) return null;
const pricing = OrderPricingSheet({
calculation: context.order.calculation,
currencyCode: context.order.currencyCode,
});
const intent = await stripe.paymentIntents.create({
amount: pricing.total().amount,
currency: context.order.currencyCode.toLowerCase(),
metadata: { orderPaymentId: context.orderPayment?._id ?? '' },
});
return intent.client_secret;
},

// Verify the completed intent against this order payment
charge: async (configuration, context) => {
const intentId = context.transactionContext?.paymentIntentId;
if (!intentId || !context.order || !context.orderPayment) return false;
const intent = await stripe.paymentIntents.retrieve(intentId);
const pricing = OrderPricingSheet({
calculation: context.order.calculation,
currencyCode: context.order.currencyCode,
});
const matchesOrder =
intent.metadata.orderPaymentId === context.orderPayment._id &&
intent.amount === pricing.total().amount &&
intent.currency === context.order.currencyCode.toLowerCase();
return intent.status === 'succeeded' && matchesOrder
? { transactionId: intent.id }
: false;
},

cancel: async (configuration, context) => {
if (context.orderPayment?.transactionId) {
await stripe.refunds.create({ payment_intent: context.orderPayment.transactionId });
}
return true;
},
});

Callback reference​

These are the callbacks you pass to registerPaymentProvider (each receives (configuration, context)).

charge​

Process the payment charge. Called during checkout.

Return ValueBehavior
{ transactionId }Payment successful, proceed with checkout
falsePayment not complete yet, order stays in PENDING
Throws errorAbort checkout, order stays in OPEN (cart)

Pass false (not a function) for providers where payment is collected out of band.

isActive / isPayLaterAllowed​

isActive (boolean, default true) toggles availability. isPayLaterAllowed (boolean, default false) controls whether order confirmation can proceed before payment completes — true = post-paid, false = pre-paid.

sign / validate​

sign(configuration, context) returns a client token (e.g. a Stripe client_secret) for the front-end SDK. validate(configuration, context) validates a stored credential.

cancel / confirm​

cancel refunds/voids a payment (called on order rejection); confirm captures a previously-authorized payment (called on CONFIRMED).

configurationError​

A PaymentError | null surfaced when the provider is misconfigured (for example PaymentError.INCOMPLETE_CONFIGURATION) so it is marked invalid instead of crashing checkout.

Webhooks & low-level adapters​

Most gateways confirm payments asynchronously via a webhook. To attach a webhook route (and for any behavior the factory doesn't expose), build a hand-written IPlugin with a routes entry and pluginRegistry.register() — see the shipped Stripe plugin and Plugin System.