Finalize payments on your server
In a standard one-time payment flow, your backend creates a PaymentIntent upfront and the SDK confirms it directly. But sometimes you need to inspect the card before committing to a payment — for example, to apply an issuer-based discount, adjust the order total, or enforce business rules based on card type or funding source.
This guide covers a deferred confirmation flow. The SDK collects the card and creates a ConfirmationToken instead of immediately confirming a payment. Your backend retrieves the token to read card metadata, applies any custom logic, and only then creates and confirms the PaymentIntent — entirely server-side.
High-Level Flow
sequenceDiagram
autonumber
participant User as User
participant MerchantWeb as Merchant Web<br/>(Frontend)
participant SDK as SDK<br/>(Embedded)
participant MerchantAPI as Merchant API<br/>(Backend)
participant API as Bahn API
Note over User, API: Phase 1: Create Order
User ->> MerchantWeb: Click "Pay Now" button
MerchantWeb ->> MerchantAPI: Create order
MerchantAPI -->> MerchantWeb: {order_id}
Note over User, API: Phase 2: Collect Card & Create ConfirmationToken
MerchantWeb ->> SDK: Initialize SDK
SDK -->> User: Display payment form (card input fields)
User ->> SDK: Enter card details (PAN, exp, CVV, billing)
SDK ->> API: CreateConfirmationToken
API -->> SDK: confirmation_token
SDK -->> MerchantWeb: confirmation_token
Note over User, API: Phase 3: Perform Custom Logic
MerchantWeb ->> MerchantAPI: FinalizeOrder(order_id, confirmation_token)
MerchantAPI ->> API: GetConfirmationToken
API -->> MerchantAPI: ConfirmationToken Object
MerchantAPI ->> MerchantAPI: Perform custom logic
MerchantAPI -->> MerchantWeb: Order detail
MerchantWeb -->> User: display order detail
Note over User, API: Phase 4: Confirm Payment
User ->> MerchantWeb: Confirm to pay
MerchantWeb ->> MerchantAPI: Confirm Order
MerchantAPI ->> API: CreatePaymentIntent(amount, currency)
API -->> MerchantAPI: PaymentIntent
MerchantAPI ->> API: ConfirmPaymentIntent(id, confirmation_token)
alt 3DS flow
API -->> MerchantAPI: Action required
MerchantAPI -->> MerchantWeb: Action required
MerchantWeb ->> SDK: HandleNextAction
SDK -->> User: Display 3DS challenge (OTP / biometric)
User ->> SDK: Complete authentication challenge
end
Note over MerchantAPI, API: Phase 5: Payment Result
par Notify
API ->> MerchantAPI: Webhook: charge.capture, payment_intent.status
MerchantAPI ->> MerchantAPI: Consumer webhook event
end
Note over User, API: Phase 6: Complete
API -->> MerchantAPI: Payment result
MerchantAPI -->> MerchantWeb: Order Status
MerchantWeb -->> User: Show order status
How It Works
Phase 1 — Create Order
- User clicks "Pay Now" — Merchant Web receives the action.
- Frontend creates an order — Merchant Web sends a request to your Merchant API to create an order.
- Backend returns the order ID — Merchant API responds with
{ order_id }. No PaymentIntent is created yet — the final payment amount may change once the card is known.
Phase 2 — Collect Card & Create ConfirmationToken
- Frontend initializes the SDK — Merchant Web initializes the embedded Bahn SDK. At this point the SDK is ready to collect card details.
- SDK renders the payment form — The SDK displays secure card input fields directly in the page. Your frontend never handles raw card data.
- User enters card details — The user fills in PAN, expiry, CVV, and billing details and submits.
- SDK creates a ConfirmationToken — The SDK calls
CreateConfirmationTokenwith the card data. Bahn tokenizes the card server-side and returns a short-livedconfirmation_token. - SDK surfaces the token — The SDK passes the
confirmation_tokento your Merchant Web frontend. The raw card data never leaves the SDK.
Phase 3 — Perform Custom Logic
- Frontend sends the token to your backend — Merchant Web calls your Merchant API with
FinalizeOrder(order_id, confirmation_token). - Backend retrieves token details — Your Merchant API calls
GetConfirmationTokento fetch the full token object, which includes card metadata such as issuer, BIN, and card type. - Backend performs custom logic — Your Merchant API executes any business rules based on the card details — for example, applying an issuer-based discount or adjusting the order total.
- Backend returns the updated order — Merchant API responds with the finalized order details.
- Frontend displays the order summary — Merchant Web renders the order details (including any applied discounts or adjustments) to the user.
Phase 4 — Confirm Payment
- User confirms payment — The user reviews the order summary and confirms to proceed.
- Frontend requests payment confirmation — Merchant Web calls your Merchant API to confirm the order.
- Backend creates a PaymentIntent — Your Merchant API calls
CreatePaymentIntentwith the finalamountandcurrency. - Backend confirms the PaymentIntent — Your Merchant API calls
ConfirmPaymentIntentwith theconfirmation_token. The entire payment confirmation happens server-side — the card data never re-enters your system.
If the card issuer requires additional authentication, the 3DS flow is triggered:
- Bahn signals action required — Bahn returns an action-required response to your Merchant API.
- Backend forwards the action — Merchant API passes the action-required signal to Merchant Web.
- Frontend delegates to the SDK — Merchant Web calls
HandleNextActionon the SDK. - SDK presents the 3DS challenge — The SDK renders the OTP or biometric challenge to the user.
- User completes the challenge — The user authenticates and the SDK proceeds.
If no 3DS is required, steps 18–22 are skipped.
Phase 5 — Payment Result
Once the payment is processed, Bahn notifies your backend:
- Webhook notification — Bahn delivers
charge.captureandpayment_intent.statusevents to your registered webhook endpoint. Your Merchant API consumes these events and updates its order state. The webhook is the authoritative source of truth for the final payment outcome.
Phase 6 — Complete
- Bahn sends the payment result — The Bahn API sends the final payment result to your Merchant API.
- Backend returns the order status — Merchant API responds to Merchant Web with the updated order status.
- User sees the outcome — Merchant Web renders the success or failure screen to the user.
Merchant API Responsibilities
In this flow, your backend drives the entire payment lifecycle: it creates the order, inspects the card token to apply business logic, and both creates and confirms the PaymentIntent server-side.
1. Create an Order
When the user initiates checkout, create an order in your system and return the order_id to the frontend. The SDK is initialized without a PaymentIntent at this point — the final amount may depend on the card used.
2. Finalize the Order
After the SDK collects card details, it returns a confirmation_token to your frontend, which forwards it to your backend.
Call GetConfirmationToken to retrieve the full token object and inspect card metadata:
POST /api/confirmation_token/get{
"confirmation_token": "ct_xxxxxxxx"
}The response includes card details such as:
| Field | Description |
|---|---|
card.brand | Card network (e.g. visa, mastercard). |
card.issuer | Name of the issuing bank. |
card.bin | First 6–8 digits of the card number. |
card.funding | Funding type: credit, debit, or prepaid. |
Use this information to apply any business rules — for example, applying a discount for cards from a specific issuer — then return the updated order detail to the frontend for the user to review.
3. Confirm the Payment
Once the user confirms the order, create and confirm the PaymentIntent in sequence. Both calls happen entirely on your backend.
Step 1 — Create a PaymentIntent
Call CreatePaymentIntent with the final order amount:
POST /api/payment_intents{
"amount": "10000",
"currency": "usd",
"description": "Order #1234",
"metadata": {
"order_id": "1234"
}
}| Field | Required | Description |
|---|---|---|
amount | Yes | Amount in the smallest currency unit (e.g. cents). |
currency | Yes | Three-letter ISO 4217 currency code. |
description | No | Human-readable description shown on the customer's statement. |
metadata | No | Arbitrary key-value pairs for storing additional information. |
Step 2 — Confirm the PaymentIntent
Call ConfirmPaymentIntent with the confirmation_token from the SDK:
POST /api/payment_intents/{id}/confirm{
"confirmation_token": "ct_xxxxxxxx"
}If the response indicates an action is required (3DS), forward the action payload to your frontend. Merchant Web will call HandleNextAction on the SDK, which will present the OTP or biometric challenge to the user. Once authentication completes, no further action is needed on your backend — Bahn will send the final result via webhook.
INFO
Your API key and payment_intent.id never leave your backend. The confirmation_token is a one-time-use token — it cannot be used to initiate a second payment.
4. Handle the Webhook
Once the payment reaches a terminal state, Bahn sends the result to your registered webhook endpoint. Your backend must treat the webhook as the authoritative source of truth when updating order state.
| Event | Description |
|---|---|
payment_intent.status.succeeded | Payment was captured successfully. |
payment_intent.status.canceled | Payment was canceled. |
charge.captured | Charge was captured. |