The payment stage converts a customer's purchase intent into a financial transaction. In Composable Commerce, the Payment object stores information about the PSP, the payment method, related transactions, and the current state of the Payment.
Your backend for frontend (BFF) is responsible for the following:
- Create a Payment object in Composable Commerce that represents the planned payment.
- Integrate with a PSP to authorize and capture funds.
- Update the Payment object in Composable Commerce with transaction statuses from the PSP.
- Handle payment retries and failures gracefully.
Before integrating a payment flow, you must understand the Payment object, which tracks payment information in Composable Commerce.
The Payment object
amountPlanned, paymentMethodInfo, interfaceId, paymentStatus, and transactions.It does not process payments itself; a PSP handles the actual authorization, capture, and settlement of funds.
paymentStatus reflects the overall status of a payment by summarizing its financial transactions. It is distinct from the state of individual transactions within the payment. This allows you to understand the payment's progress at a glance without examining each transaction.interfaceCode and interfaceText that you define. These fields are flexible and allow you to represent your specific payment statuses, such as "Paid", "Pending", or "Refunded". You can also link a state to the paymentStatus using a StateReference. This allows you to model custom workflows and statuses for your payments.PaymentInfo object. A Payment can also reference a customer or an anonymous session.Handle multiple payment attempts
Payment object for each attempt. Add each new Payment to the Cart. Avoid updating or removing older Payment objects. They contain valuable information about the transaction history.This approach ensures that regardless of which payment method eventually succeeds, all associated data remains intact and available for troubleshooting or reconciliation.
Use middleware for PSP integrations
The recommended solution to implement a payment integration is using middleware (your BFF) to limit complexity and third-party constraints. A standard PSP integration comprises two components: synchronous and asynchronous. A synchronous component starts the payment process by exchanging checkout data between the store and payment provider, and an asynchronous component receives payment-related notifications from the PSP.
Maintain Cart integrity during payment
Gracefully handle cases where the cart changes between payment initiation and Order creation to increase successful order rates. The Cart can change when:
- A customer has multiple browser tabs open. They modify the cart in one tab while the payment process is ongoing in another tab.
- A price changes or a discount expires during the time the customer is completing the payment process.
Detect cart changes
version before and after payment authorization to detect changes.version before calling your PSP to authorize (or authorize and capture) the payment. Then, recalculate the Cart and retrieve its latest version just before you create the Order. If the versions differ, the Cart has changed.version directly since the changes happen on the respective entities (Product, DiscountCode, TaxRate). The changes are only reflected in the Cart when the Cart is recalculated, and the Cart version is incremented at that time.Depending on your business requirements, you might want to:
- Proceed to create the Order if the
totalPriceof the new Cart is equal to, or less than, the authorized amount. - Cancel authorization (or refund, in case of capture), notify the customer that the Cart has changed, and ask them to review and confirm before proceeding (re-initiate payment).
When using Cart freeze during checkout, consider your unfreezing strategy with care to avoid negative impacts on the customer experience.
Unfreeze the Cart when:
- The shopper cancels on your "cancel/return" route.
- The PSP notifies of a Failure or Canceled event via webhook.
- A server-side timer expires—for example, after 3-10 minutes with no PSP completion.
Consider how your business wants to handle these situations:
-
When a shopper wants to make Cart changes after payment redirection but before completing it:
- Allow Cart changes (unfreeze Cart) and restart or revalidate payment, or
- Prevent Cart changes and instruct customers to complete the Order first.
-
When a shopper closes the tab where they started their payment before completing it:
- Return to the payment step when they resume checkout.
Implementation steps
A typical payment flow involves creating a Payment object, linking it to the Cart, and then using a PSP to handle the financial transaction.
Step 1: Create a Payment object
During checkout, a customer may try different payment methods or start multiple payments in separate tabs. Create a dedicated Payment resource to track each initiated payment process, even if aborted and restarted.
interfaceId).async function createPayment(
amount: number, // Amount in cents, for example, 1000 ($10)
currency: string, // for example, USD
paymentInterface: string, // for example, 'STRIPE', 'ADYEN'
method: string, // for example, CREDIT_CARD
pspPaymentRef?: string, // PSP charge/intent ID, if known now
key?: string // your idempotency key for Payment
) {
const paymentDraft: PaymentDraft = {
key, // optional but recommended
interfaceId: pspPaymentRef, // can also be set later via update
amountPlanned: { centAmount: amount, currencyCode: currency },
paymentMethodInfo: { method, paymentInterface }, // Optionally add a name, for example, name: { en: "Credit Card" }
};
return apiRoot
.payments()
.post({
body: paymentDraft,
})
.execute();
}
changeAmountPlanned before authorizing or capturing Payment.Step 2: Link the Payment to the Cart
paymentInfo.payments references are populated when the Order is created. It also helps avoid "dangling authorization" throughout the payment process.Dangling authorization happens when a capture is delayed, abandoned, or has failed to occur after authorization. In this stage, the funds are held but not yet settled, causing several issues:
- Funds are locked on the customer's card unnecessarily.
- Payment status becomes confusing.
- Discrepancies between authorized amounts and captured amounts create risk.
If a payment is not captured, cancel or void authorizations to release the hold on funds.
For state management, use Composable Commerce. Handle webhook events from the PSP to track the full payment flow asynchronously.
Avoid multiple "leftover" authorizations that do not correspond to a finalized payment.
Link each Payment to the Cart without updating or removing previous Payments; each may represent a distinct transaction. This approach ensures you can track all payment attempts, maintain idempotency for retries, and reliably determine which Payment succeeded when creating the Order.
async function linkPaymentToCart(
cartId: string,
version: number,
paymentId: string
) {
return apiRoot
.carts()
.withId({ ID: cartId })
.post({
body: {
version,
actions: [
{
action: "addPayment",
payment: { id: paymentId, typeId: "payment" },
},
],
},
})
.execute();
}
Always link the Payment to the Cart before Order creation. Composable Commerce snapshots Payment details into the Order.
PSP integration models
Conceptually, there are two primary models for integrating with a Payment Service Provider (PSP). Both methods start with a call to your BFF or backend to create a payment session by securely calling the PSP's API.
- Client-side tokenization: This model is typically used for card payments. It uses the PSP's UI components on the frontend to collect and tokenize the customer's payment information before sending them to your BFF or backend to authorize or capture payments. Only the tokens are stored; raw card details from your backend are never stored or transmitted.
- Server-side redirect: This model is typically used for alternative payment methods (APMs), such as bank transfers or regional wallets. It uses a redirect URL provided by the PSP to present the payment flow to the customer.
While these patterns are still useful for understanding the core flows, modern payment platforms like Stripe often merge both models into a unified integration. With Stripe, for example, a single session (either via Checkout or PaymentIntent) can support multiple payment methods, dynamically presenting the right ones to the customer based on their location, currency, and device. This means you don't need separate flows for each payment type; Stripe's API and UI handle most of the complexity for you.
Conceptual payment integration models
Client-side tokenization
This model is used for card payments (for example, Visa, Mastercard). In this model, you collect payment details using the PSP's frontend SDK. Your backend should never receive raw card details, like the Primary Account Number (PAN) data (the digits on the front of the credit card). This makes compliance with the Payment Card Industry Data Security Standard (PCI DSS) easier.
When implementing client-side tokenization, decide how to handle the payment authorization and capture steps. Understanding the difference between these transaction types is crucial for designing your payment flow.
Authorization.Charge.sale). Others require explicit capture.- Immediate capture: For digital goods.
- Authorization + delayed capture: Authorization first, capture later. For example, for physical goods until shipment.
The following sequence follows the order after authorization pattern (authorization first, capture later):
- Your frontend calls your BFF or backend to initiate the payment session and receives a session identifier.
- The session identifier is passed on to the PSP's SDK to render the Payment UI.
- The PSP SDK in the frontend collects payment data through the Payment UI and returns a token.
- Your frontend sends the token to your backend.
- Create a Payment (planned amount = Cart total). Include PSP interface and optional
interfaceId. - Link the Payment to the Cart.
- Authorize with the PSP using the client token/intentId.
- Add an Authorization Transaction (state
Pending, thenSuccessorFailure). Optionally, record the PSP payload viaaddInterfaceInteraction. - Create the Order from the Cart.
Charge Transaction and State (for example, Success or Failure, depending your PSP response) to the Payment object to keep Composable Commerce as a source of truth.// 1) Create Payment
const payment = await apiRoot
.payments()
.post({
body: {
key: orderAttemptKey, // idempotency in your BFF
amountPlanned: {
currencyCode: cart.totalPrice.currencyCode,
centAmount: cart.totalPrice.centAmount,
},
paymentMethodInfo: {
paymentInterface: "PSP_NAME",
method: "CREDIT_CARD",
},
interfaceId: pspIntentId, // if already known
},
})
.execute();
// 2) Link Payment to Cart
await apiRoot
.carts()
.withId({ ID: cart.id })
.post({
body: {
version: cart.version,
actions: [
{
action: "addPayment",
payment: { typeId: "payment", id: payment.body.id },
},
],
},
})
.execute();
// 3) Authorize with PSP (server-side; using token/intent from frontend)
// Note to learner: implement according to your PSP specifics
const { pspAuthId, status } = await psp.authorize(tokenOrIntent, amount);
// 4) Mirror PSP to Payment (Authorization)
await apiRoot
.payments()
.withId({ ID: payment.body.id })
.post({
body: {
version: payment.body.version,
actions: [
{
action: "addTransaction",
transaction: {
type: "Authorization",
amount: cart.totalPrice,
interactionId: pspAuthId,
state: "Pending", // or "Success"/"Failure" based on PSP response
timestamp: new Date().toISOString(),
},
},
],
},
})
.execute();
// Optionally store raw PSP payload for audit
await apiRoot
.payments()
.withId({ ID: payment.body.id })
.post({
body: {
version: payment.body.version + 1,
actions: [
{
action: "addInterfaceInteraction",
type: { typeId: "type", id: yourInterfaceInteractionTypeId }, // a Type for structured fields
fields: { payload: JSON.stringify(pspResponse) },
},
],
},
})
.execute(); // generates PaymentInteractionAdded Message
Alternatively, you can implement an immediate capture (sale), depending on your business requirements. In this flow, capture happens immediately after a successful authorization. Some PSPs do authorization and capture in one step. Mirror to Payment as a Charge Transaction.
// Add successful Charge transaction
await apiRoot
.payments()
.withId({ ID: paymentId })
.post({
body: {
version,
actions: [
{
action: "addTransaction",
transaction: {
type: "Charge",
amount: cart.totalPrice,
interactionId: pspChargeId,
state: "Success",
timestamp: new Date().toISOString(),
},
},
],
},
})
.execute();
Let's look at this model in a sequence diagram:
For simplicity, the following diagram omits PSP webhook handling. In production, handle asynchronous notifications from the PSP to update Payment transactions and transaction states accurately (for example, async cancelations/refund and capture events) and allow for async Order creation (if sync failed).
addInterfaceInteraction action.addInterfaceInteraction action in the same request as the addTransaction action in the actions array for the request.Success or Failure based on the PSP response (immediate capture) or webhook event (authorization first, capture later). Update the Payment using an addInterfaceInteraction action.Server-side API-only
In this model, the backend initiates the payment with the PSP and returns a redirect URL or client instructions. This model is typically used for Alternative Payment Methods (APMs) like bank transfers, wallets, and hosted pages. The frontend only redirects or confirms.
- Create Payment and link the Payment to Cart.
- Initiate the Payment with PSP and receive a
pspSessionIdor redirect URL. - Add a Transaction (type =
AuthorizationorCharge, depending on PSP),state = Pending, setinteractionId = pspSessionId. - Redirect shopper to PSP.
- Handle webhook/return: set Transaction state to
SuccessorFailure.
On success:
- If the PSP authorized only, add Charge later—for example, on fulfillment.
- If the PSP charged, proceed.
- Create Order from the cart.
On failure:
- Retry or communicate failure to user.
// 1) Payment + link (same as above)
// 2) Start Alternative Payment Methods (APM) session with PSP
// Note to learner: implement according to your PSP specifics
const { pspSessionId, redirectUrl } = await psp.startPayment(
cart.totalPrice,
returnUrls
);
// 3) Track Pending transaction
await apiRoot
.payments()
.withId({ ID: paymentId })
.post({
body: {
version,
actions: [
{
action: "addTransaction",
transaction: {
type: "Authorization", // or 'Charge' if PSP immediately debits on success
amount: cart.totalPrice,
interactionId: pspSessionId,
state: "Pending",
timestamp: new Date().toISOString(),
},
},
],
},
})
.execute();
// 5) On PSP webhook/return: transition state
await apiRoot
.payments()
.withId({ ID: paymentId })
.post({
body: {
version,
actions: [
{ action: "changeTransactionState", transactionId, state: "Success" },
], // or 'Failure'
},
})
.execute();
// 6) On success: create order
await apiRoot
.orders()
.post({ body: { cart: { id: cart.id }, version: cart.version, orderNumber } })
.execute();
Let's look at this model in a sequence diagram:
Typical unified PSP flow
- Initiate payment session with PSP: At checkout, the backend for frontend (BFF) initiates a payment session or intent with the PSP, often passing the Cart total, customer details, and redirect URLs (success and cancel redirects). The PSP responds with an available list of supported payment methods.
- Present payment methods to the shopper: The frontend displays the available payment options (like credit card, PayPal, Klarna) from the PSP, often usng its embedded components to ensure PCI compliance and handle 3D Secure authentication.
- Customer selects payment method:
- For direct methods (like credit cards), the customer enters card details through the PSP's UI component, which handles tokenization and security checks.
- For redirect methods (like PayPal), the backend receives a redirect link from the PSP to send the customer to the provider's authentication/approval page.
- Initiate payment attempt: The BFF creates a Payment object in Composable Commerce and links it to the Cart immediately before sending any authorization request to the PSP. The Payment Object includes the planned amount, method details, and PSP metadata (for example, session or intent ID) and a
Transactionof typeAuthorizationwith statePendingto record the authorization attempt. - Authorize with the PSP:
The BFF requests authorization from the PSP (either with a card token or by initiating a redirect flow for an alternative method):
- If "OK" (approved), the PSP returns the
interfaceIdor confirmation. Immediatly update the Payment object and add aTransactionof typeAuthorizationwith stateSuccess. - If futher challenge/redirect is needed (for example, 3D Secure, PayPal), redirect the customer and wait for completion. The Payment object remains
Pendinguntil you receive a response.
- If "OK" (approved), the PSP returns the
- Update Payment Transaction status: The backend uses data from the PSP (direct response or webhook) to update the Payment object in Composable Commerce with the actual transaction result - ensuring payments are only marked successful once verified.
- Create the Order: Once payment is confirmed by the PSP (ideally via webhook or final redirect), the backend creates the Order from the Cart with its linked Payments.
Let's look at a production-like unified PSP flow in a sequence diagram:
Unified PSP integrations are preferred because:
- They cover all major payment types in one integration.
- They delegate security and PCI compliance to the PSP.
- They enable payment orchestration: handling retries, status updates, and reconciliation in one consistent way.
- They ensure Order creation is only triggered once confirmed payment is received, protecting against fraudulent manipulation or redirect/success URLs.
Handle payment failures and retries
Payment failures can occur for various reasons, such as insufficient funds, network issues, or user cancellations. To handle these scenarios effectively:
- Frontend: Show clear error and offer alternative methods.
- Backend:
- Create a new Payment on every payment attempt.
- Update Payment object with failure transaction.
- Maintain idempotency: if a retry occurs, ensure you do not double-authorize.
- Avoid dangling authorizations by canceling or voiding if payment will not be captured.
Key takeaways
- Composable Commerce tracks payment data but does not process financial transactions.
- The PSP handles sensitive data. Your backend should only handle tokens or transaction IDs.
- Always link the Payment to the Cart before creating an Order.
- Use webhooks for asynchronous status updates from the PSP.
- Design payment flows for idempotency to prevent duplicate charges.
- Detect Cart changes by comparing versions before and after payment authorization.
- Create a new Payment object for each payment attempt to maintain a clear audit trail.
The next step in the checkout sequence is Order placement and confirmation. You will convert the Cart and the linked Payment into an Order, handle edge cases, and trigger post-purchase workflows.