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: 1. Create a Payment object in Composable Commerce that represents the planned payment. 2. Integrate with a PSP to authorize and capture funds. 3. Update the Payment object in Composable Commerce with transaction statuses from the PSP. 4. 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 The [Payment](https://docs.commercetools.com/urn?urn=ctp%3Aapi%3Atype%3APayment) object stores the planned amount, PSP and method metadata, and transaction history in the following fields: `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. It contains an optional `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](https://docs.commercetools.com/learning-model-your-business-structure/state-machines/state-machines-page.md) for your payments. An Order or Cart can reference a set of Payments using the `PaymentInfo` [object](https://docs.commercetools.com/api/projects/orders.md#paymentinfo). A Payment can also reference a customer or an anonymous session. ## Handle multiple payment attempts During checkout, customers may try different payment methods, especially if a payment fails. To maintain a clear audit trail, create a new, dedicated `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 Compare the Cart `version` before and after payment authorization to detect changes. First, retrieve the Cart `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. Certain factors—such as price changes, discount changes (including invalidations and expirations), and changes in applicable tax rates—cause the Order creation to fail. These factors do not increment the Cart `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. An alternative strategy is to [freeze the Cart](https://docs.commercetools.com/api/projects/carts.md#freeze-cart) before payment, which locks its contents and price. However, this can add complexity, as you would need to implement logic to unfreeze the Cart if the customer decides to cancel the payment and modify their selections. Before adopting this approach, understand how [Cart freeze works](https://docs.commercetools.com/api/carts-orders-overview.md#freeze-a-cart) and the [impact of changes on frozen Carts](https://docs.commercetools.com/api/carts-orders-overview.md#impact-of-changes-on-frozen-carts). Depending on your business requirements, you might want to: - Proceed to create the Order if the `totalPrice` of 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. When creating a Payment in Composable Commerce, always include the PSP interface, method, and (if available) the PSP's payment reference (`interfaceId`). ```ts 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(); } ``` If the Cart total changes due to changes in quantity, Prices, Discounts, or Shipping, update the Payment with `changeAmountPlanned` before authorizing or capturing Payment. ## Step 2: Link the Payment to the Cart Before payment process initiation, link the Payment to the Cart. This ensures the Order's `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. ```ts 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 vs. Capture Authorization: Hold funds but do not take them. In Composable Commerce, this is represented by [TransactionType](https://docs.commercetools.com/urn?urn=ctp%3Aapi%3Atype%3ATransactionType) = `Authorization`. Capture: Actually charge the customer. In Composable Commerce, this is represented by [TransactionType](https://docs.commercetools.com/urn?urn=ctp%3Aapi%3Atype%3ATransactionType) = `Charge`. Some PSPs allow both in a single step (`sale`). Others require explicit capture. Architect's choice Choose between immediate capture and authorization with delayed 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): 1. Your frontend calls your BFF or backend to initiate the payment session and receives a session identifier. 2. The session identifier is passed on to the PSP's SDK to render the Payment UI. 3. The PSP SDK in the frontend collects payment data through the Payment UI and returns a token. 4. Your frontend sends the token to your backend. 5. Create a Payment (planned amount = Cart total). Include PSP interface and optional `interfaceId`. 6. Link the Payment to the Cart. 7. Authorize with the PSP using the client token/intentId. 8. Add an Authorization Transaction (state `Pending`, then `Success` or `Failure`). Optionally, record the PSP payload via `addInterfaceInteraction`. 9. Create the Order from the Cart. Capture payment later, typically on fulfillment. Payment capturing might happen from external systems, like an order management system (OMS). Listen to capture events from the PSP (for example, via Webhook) and add a `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. ```ts // 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. ```ts // 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). ```mermaid sequenceDiagram participant Client participant BFF participant CC as Composable Commerce participant PSP %% 1–2: Frontend collects & sends token/intent note over Client: PSP SDK collects payment data
→ returns token/intentId Client->>BFF: Send token/intentId %% 3: Create Payment (planned amount = Cart total, PSP interface, optional interfaceId) BFF->>CC: Create Payment (amountPlanned=Cart total,
paymentInterface=PSP_NAME, method, [interfaceId=pspIntentId]) CC-->>BFF: paymentId, version %% 4: Link Payment to Cart BFF->>CC: addPayment(paymentId) %% 5: Server-side authorization with PSP BFF->>PSP: Authorize(ct_paymentKey, token/intentId, amount) PSP-->>BFF: auth result (pspAuthId, status, payload) %% 6: Mirror Authorization to Payment (Pending → Success/Failure) BFF->>CC: addTransaction(type=Authorization, state=Pending,
interactionId=pspAuthId, amount=Cart total) opt Store raw PSP payload for audit BFF->>CC: addInterfaceInteraction(payload) end alt Authorization approved BFF->>CC: transition Authorization state → Success %% 7: Create Order from Cart BFF->>CC: Create Order from Cart alt Order created successfully CC-->>BFF: Order BFF-->>Client: Show payment success and order confirmation else Order creation failed (for example, out of stock) BFF->>PSP: Cancel Authorization PSP-->>BFF: cancel result BFF->>CC: addTransaction(type=CancelAuthorization,
state=Success, interactionId=pspCancelId) opt Store raw PSP payload for audit BFF->>CC: addInterfaceInteraction(payload) end BFF-->>Client: Show error (allow change & retry) end else Authorization failed or canceled BFF->>CC: transition Authorization state → Failure BFF-->>Client: Show error (allow change & retry) end ``` Although storing the raw PSP response payloads is optional, we recommended it since it helps with troubleshooting, reconciliation, and compliance. You can store the raw PSP response payload using the `addInterfaceInteraction` action. To optimize performance, include the `addInterfaceInteraction` action in the same request as the `addTransaction` action in the `actions` array for the request. Later, transition the Transaction to `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. 1. Create Payment and link the Payment to Cart. 2. Initiate the Payment with PSP and receive a `pspSessionId` or redirect URL. 3. Add a Transaction (type = `Authorization` or `Charge`, depending on PSP), `state = Pending`, set `interactionId = pspSessionId`. 4. Redirect shopper to PSP. 5. Handle webhook/return: set Transaction state to `Success` or `Failure`. 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. ```ts // 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: ```mermaid sequenceDiagram participant Client participant BFF participant Composable Commerce participant PSP Client->>BFF: Choose APM BFF->>Composable Commerce: Create Payment (+ addPayment) BFF->>PSP: Start session (amount, return URLs) PSP-->>BFF: sessionId, redirectUrl BFF->>Composable Commerce: addTransaction (Authorization or Charge, state=Pending, interactionId=sessionId) BFF-->>Client: redirectUrl Client->>PSP: Redirect & complete PSP-->>BFF: Webhook (Success/Failure) BFF->>Composable Commerce: changeTransactionState alt Success BFF->>Composable Commerce: Create Order from Cart else Failure/cancel BFF->>Client: communicate failure, allow retry (or update and retry) end ``` ## Typical unified PSP flow Most businesses integrate with a single payment service provider (PSP), like Adyen or Stripe, to support a broad range of payment methods - including credit cards, PayPal, and other Alternative Payment Methods (APMs) - through one unified API and session. This means the both direct and redirect payment flows are handled together, and your integration manages their results in a consistent way. 1. 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. 2. Present payment methods to the shopper: The frontend displays the available payment options (like credit card, PayPal, Klarna) from the PSP, often using its embedded components to ensure PCI compliance and handle 3D Secure authentication. 3. 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. 4. 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 `Transaction` of type `Authorization` with state `Pending` to record the authorization attempt. 5. 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 `interfaceId` or confirmation. Immediately update the Payment object and add a `Transaction` of type `Authorization` with state `Success`. - If further challenge/redirect is needed (for example, 3D Secure, PayPal), redirect the customer and wait for completion. The Payment object remains `Pending` until you receive a response. 6. 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. 7. 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: ```mermaid sequenceDiagram actor Customer participant Frontend participant BFF as BFF/Backend participant CT as commercetools participant PSP Customer->>Frontend: Checkout Frontend->>BFF: GET /carts/payments/methods BFF->>CT: Recalculate Cart CT-->>BFF: Cart (latest totals/version) BFF->>PSP: POST /paymentMethods (amount, currency) PSP-->>BFF: Payment methods BFF-->>Frontend: Payment methods Customer->>Frontend: Selects payment method %% Card data handled by PSP components (no PAN to BFF) note over Frontend,PSP: PSP Elements / Web Components collect card data
and tokenize it (merchant not PCI DSS compliant) Customer->>Frontend: Enters card details in PSP iframe Frontend->>PSP: Card data (inside PSP-hosted fields) PSP-->>Frontend: tokenOrIntentId Customer->>Frontend: Clicks pay button Frontend->>BFF: POST /carts/payments {tokenOrIntentId} %% Ensure Cart is up to date BFF->>CT: Recalculate Cart CT-->>BFF: Cart (latest totals/version) %% Create Payment + link to Cart + Authorization(Pending) BFF->>CT: Create Payment (amountPlanned=Cart total,
paymentInterface, method, interfaceId=session/intent) CT-->>BFF: Payment BFF->>CT: Assign Payment to Cart CT-->>BFF: BFF->>CT: addTransaction(type=Authorization,
state=Pending, amount=Cart total, interactionId=session/intent) %% Call PSP to authorize / start payment using the token BFF->>PSP: POST /payments {tokenOrIntentId, session info} PSP-->>BFF: Result (authorized / refused / redirectRequired + redirectUrl) alt Immediate authorization/decline (no extra redirect) BFF->>CT: Update Payment (Authorization state=Success/Failure,
store PSP response & interactions) alt Authorization approved BFF->>BFF: Validate payment amount vs Cart amount alt Amount mismatch BFF->>PSP: Cancel Authorization PSP-->>BFF: BFF->>CT: addTransaction(type=CancelAuthorization,
state=Success) BFF-->>Frontend: Show error (cart changed, retry) else Amounts OK BFF->>BFF: Generate WebOrderNumber BFF->>CT: Create Order from Cart CT-->>BFF: Order or error alt Out of stock / order creation failed BFF->>PSP: Cancel Authorization PSP-->>BFF: BFF->>CT: addTransaction(type=CancelAuthorization,
state=Success) BFF-->>Frontend: Show error (allow change & retry) else Order created BFF-->>Frontend: Payment success (include order number) end end else Authorization failed BFF-->>Frontend: Show payment failure (allow retry) end else Redirect / 3DS challenge required BFF-->>Frontend: redirectUrl / redirect instructions Frontend->>PSP: Redirect / complete challenge PSP-->>BFF: Webhook or POST /payments/details result %% Update Payment with final auth result BFF->>CT: Update Payment (Authorization state=Success/Failure,
store PSP response & interactions) alt Payment authorized BFF->>BFF: Validate payment amount vs Cart amount alt Amount mismatch BFF->>PSP: Cancel Authorization PSP-->>BFF: BFF->>CT: addTransaction(type=CancelAuthorization,
state=Success) BFF-->>Frontend: Show error (cart changed, retry) else Amounts OK BFF->>BFF: Generate WebOrderNumber BFF->>CT: Create Order from Cart CT-->>BFF: Order or error alt Out of stock / order creation failed BFF->>PSP: Cancel Authorization PSP-->>BFF: BFF->>CT: addTransaction(type=CancelAuthorization,
state=Success) BFF-->>Frontend: Show error (allow change & retry) else Order created BFF-->>Frontend: Payment success (include order number) end end else Payment failed or canceled BFF-->>Frontend: Show payment failure (allow retry) end end ``` 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.