Recurring & Unscheduled MIT
A saved card can be charged with no customer present (an MIT — merchant-initiated transaction), but the networks distinguish what the cardholder agreed to when the card was stored. That agreement is a mandate, and Bahn records it on the transaction that established it.
There are two shapes:
| Agreement | What the cardholder consented to | Typical use | How to establish it |
|---|---|---|---|
| Unscheduled (default) | The merchant may keep the card on file and charge it again, on no fixed schedule. | Top-ups, pay-as-you-go, re-billing a failed order, one-click repeat purchase. | A SetupIntent, or a PaymentIntent with setup_future_usage=off_session — optionally with setup_future_usage_cadence=unscheduled, which is also the default |
| Recurring | The merchant may charge a fixed schedule of payments. | Subscriptions, instalment plans, memberships. | A PaymentIntent with setup_future_usage=off_session and payment_method_options.card.setup_future_usage_cadence=recurring |
Stating the right one matters for disputes: the markers Bahn sends to Visa and Mastercard on the storing authorization are replayed on every later MIT against that mandate, and they are what evidences the cardholder's consent if the charge is disputed. A recurring instalment billed under an unscheduled marker is a weaker position than one billed under the agreement the cardholder actually accepted.
WARNING
The agreement is fixed by the transaction that stores the credential — it cannot be changed afterwards. If a customer starts a subscription, establish a recurring mandate at that moment; you cannot upgrade an unscheduled mandate to a recurring one later. Establish a new one instead.
Preliminary
- Webhook
- API Integration
- Save Card & Recurring Payments — the SetupIntent /
payment_method_idmechanics this guide builds on
High-Level Flow
sequenceDiagram
autonumber
participant User as User
participant SDK as SDK<br/>(Embedded)
participant MerchantAPI as Merchant API<br/>(Backend)
participant API as Bahn API
Note over User, API: Step 1 — a CIT establishes the agreement
User ->> MerchantAPI: Subscribe / accept terms
MerchantAPI ->> API: CreatePaymentIntent(customer_id,<br/>setup_future_usage=off_session,<br/>payment_method_options.card.setup_future_usage_cadence=recurring)
Note over API: both fields are creation-only
alt Path A — SDK confirms with the client secret (recommended)
API -->> MerchantAPI: {payment_intent, client_secret}
MerchantAPI -->> SDK: {client_secret}
SDK ->> API: ConfirmPaymentIntentWithClientSecret(card)
Note over API: mandate_data captured from the<br/>customer's own request
else Path B — server-side confirm at creation
Note over MerchantAPI, API: same call carries confirm=true,<br/>off_session=false and mandate_data
API -->> MerchantAPI: {payment_intent}
end
Note over API: storing authorization states the<br/>recurring agreement to the network
API -->> SDK: 3DS challenge (customer present)
User ->> SDK: Complete 3DS
API ->> MerchantAPI: Webhook: payment_intent.status.succeeded
Note over API: mandate created, recorded on the charge
Note over MerchantAPI, API: Step 2 — read back the mandate_id
MerchantAPI ->> API: GetPaymentIntent(id,<br/>expand=["payment_intent.latest_charge"])
API -->> MerchantAPI: {latest_charge.payment_method_details.card.mandate_id}
Note over MerchantAPI: store mandate_id with the subscription
Note over MerchantAPI, API: Step 3 — each renewal is an MIT
MerchantAPI ->> API: CreatePaymentIntent(confirm=true, off_session=true,<br/>payment_method_id, mandate_id)
Note over API: replays the recurring agreement<br/>recorded on that mandate
API ->> MerchantAPI: Webhook: payment_intent.status.succeeded
Step 1 — Establish the Agreement with a CIT
The mandate is created by a customer-initiated transaction where the customer is present and authenticates. Both fields that describe the agreement go on the create call:
POST /api/payment_intents| Field | Required | Description |
|---|---|---|
setup_future_usage | Yes | Must be off_session — this is what stores a credential you can later charge without the customer. |
payment_method_options.card.setup_future_usage_cadence | No | recurring makes this payment the initial transaction of a recurring agreement; unscheduled keeps the credential on file under no schedule. Omitting the field means unscheduled. |
customer_id | Yes | The customer the credential is stored against. A credential cannot be stored without one. |
Both fields are creation-only
ConfirmPaymentIntent ignores payment_method_options, and ConfirmPaymentIntentWithClientSecret does not accept it at all. setup_future_usage_cadence set at confirm time has no effect — the agreement would silently fall back to unscheduled. Set it when you create the intent.
From there, confirm the intent one of two ways.
Path A — SDK confirms with the client secret
Create the intent without confirm, hand the client_secret to your frontend, and let the SDK confirm it. This is the normal checkout flow (One Time Payment) and the recommended path.
Request
{
"amount": "10000",
"currency": "usd",
"customer_id": "cus_xxxxxxxx",
"setup_future_usage": "off_session",
"payment_method_options": {
"card": {
"setup_future_usage_cadence": "recurring"
}
},
"description": "Pro plan — first month"
}Response
{
"payment_intent": {
"id": "pi_xxxxxxxx",
"client_secret": "pi_xxxxxxxx_secret_xxxxxxxx",
"status": "requires_payment_method",
"setup_future_usage": "off_session",
"customer_id": "cus_xxxxxxxx"
}
}You do not send mandate_data on this path. When the SDK confirms with the client secret, Bahn records the customer's acceptance from that request itself — the real client IP, the User-Agent, and the acceptance timestamp — which the browser SDK cannot report reliably on its own and your backend cannot observe at all. This is the better provenance for a consent record, which is why this path is preferred.
Path B — Server-side confirm at creation
Create and confirm in one call with confirm=true. Because no customer-facing request is involved, you must supply mandate_data.customer_acceptance yourself, in the same create call.
Request
{
"amount": "10000",
"currency": "usd",
"customer_id": "cus_xxxxxxxx",
"payment_method_id": "pm_xxxxxxxx",
"confirm": true,
"off_session": false,
"setup_future_usage": "off_session",
"payment_method_options": {
"card": {
"setup_future_usage_cadence": "recurring"
}
},
"mandate_data": {
"customer_acceptance": {
"type": "online",
"accepted_at": "2026-09-07T10:15:30Z",
"online": {
"ip_address": "203.0.113.42",
"user_agent": "Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/537.36"
}
}
}
}| Field | Required | Description |
|---|---|---|
confirm | Yes | true to create and confirm in one call. |
off_session | Yes | Must be false. The establishing transaction is customer-initiated; off_session=true combined with setup_future_usage is rejected. |
mandate_data.customer_acceptance.type | Yes | online or offline. |
mandate_data.customer_acceptance.accepted_at | Yes | When the customer accepted the agreement. |
mandate_data.customer_acceptance.online.ip_address | Yes for online | The customer's IP address at acceptance. |
mandate_data.customer_acceptance.online.user_agent | Yes for online | The customer's User-Agent at acceptance. |
TIP
mandate_data supplied at creation is stored on the intent, so it also satisfies a later confirm that carries none. Supplying it is harmless on Path A — but the acceptance Bahn captures from the SDK's own request is more defensible than one your backend reconstructs, so prefer Path A's automatic capture where you can.
3DS on the establishing transaction
3DS on this payment is what carries authentication forward to every later MIT. Requesting that it be skipped is refused outright rather than ignored: payment_method_options.card.request_three_d_secure=never combined with setup_future_usage is rejected, so you are never left believing a stored credential was authenticated when it wasn't. See Controlling 3DS.
Establishing via a SetupIntent
A SetupIntent also stores an off-session credential, and is the right choice when you are saving a card without charging it. It has no payment_method_options, so it can only create an unscheduled mandate.
To establish a recurring mandate, use a PaymentIntent that actually charges the first instalment — which is also what the networks expect the initial transaction of a recurring agreement to be.
INFO
setup_future_usage_cadence is either recurring or unscheduled, and omitting it means unscheduled. A value this API version doesn't recognise degrades to unscheduled rather than being rejected — a recurring agreement is never stated to the network by accident.
Step 2 — Retrieve the mandate_id
The mandate is created asynchronously, when the storing authorization succeeds. Its id is recorded on the charge that created it, at payment_method_details.card.mandate_id.
The charge is not included in a PaymentIntent by default. Ask for it with expand:
GET /api/payment_intents/{id}?expand=payment_intent.latest_chargeexpand is a list of strings, not a single value. Each element is one dotted path, and you pass a list by repeating the parameter once per element:
GET /api/payment_intents/{id}?expand=payment_intent.latest_charge&expand=some.other.path| Parameter | Type | Description |
|---|---|---|
expand | string[] | Sub-resource paths to include in the response. Repeat the parameter once per path. Only payment_intent.latest_charge is supported today. |
Each path is dotted from the resource it hangs off — payment_intent.latest_charge, not latest_charge — so paths stay unambiguous as sub-resources of sub-resources are added.
A comma-separated value is not a list
?expand=a,b is read as one path literally named a,b, not as two paths. Since unrecognised paths are ignored rather than rejected (see below), that request succeeds and quietly expands nothing. Repeat the parameter instead.
Response
{
"payment_intent": {
"id": "pi_xxxxxxxx",
"status": "succeeded",
"customer_id": "cus_xxxxxxxx",
"payment_method_id": "pm_xxxxxxxx",
"latest_charge_id": "ch_xxxxxxxx",
"latest_charge": {
"id": "ch_xxxxxxxx",
"status": "succeeded",
"payment_method_details": {
"type": "card",
"card": {
"brand": "visa",
"last4": "4242",
"network_transaction_id": "123456789012345",
"mandate_id": "mand_xxxxxxxx"
}
}
}
}
}Store mandate_id alongside payment_method_id on your subscription record. Both are needed to bill it.
Wait for success before reading
mandate_id is populated once the payment reaches succeeded. Read the PaymentIntent after the payment_intent.status.succeeded webhook rather than immediately after confirming — a processing intent may not carry it yet.
Notes on expand:
- An
expandpath this API version doesn't know is ignored, not rejected — a client written against a newer surface keeps working. latest_chargeis omitted if the intent has no charge yet.- The same
card.mandate_idappears on later MIT charges, where it reports the mandate that charge exercised rather than one it created — useful for reconciling a renewal back to its agreement.
Step 3 — Charge the Mandate (MIT)
Each renewal or unscheduled re-bill is a PaymentIntent confirmed with off_session=true, naming the mandate it exercises.
POST /api/payment_intentsRequest
{
"amount": "10000",
"currency": "usd",
"customer_id": "cus_xxxxxxxx",
"payment_method_id": "pm_xxxxxxxx",
"mandate_id": "mand_xxxxxxxx",
"confirm": true,
"off_session": true,
"description": "Pro plan — renewal"
}| Field | Required | Description |
|---|---|---|
payment_method_id | Yes | The stored credential. Inline card data and confirmation tokens are rejected for off-session payments. |
off_session | Yes | true — no customer is present. |
mandate_id | No | The mandate this payment exercises. Omitted, the server treats the charge as unscheduled and resolves a mandate itself — see below. |
confirm | Yes | true. mandate_id is read at confirmation only. |
An MIT carries no setup_future_usage — it exercises an existing agreement rather than creating one, and the two combined are rejected. It needs no mandate_data either: the consent was captured once, by the CIT in Step 1.
Bahn reads the mandate, replays the agreement recorded on it to the network, and the payment proceeds like any other — see One Time Payment for authorization, capture, and webhooks.
When you omit mandate_id
The server treats the charge as unscheduled and resolves a mandate itself, using its own strategy over the payment method's active unscheduled agreements. Recurring agreements are deliberately excluded from that search.
Which mandate it picks is not part of the contract. Don't depend on the selection: if more than one unscheduled agreement can exist against a card, the one you get may not be the one you meant.
| Situation | Pass mandate_id? |
|---|---|
| Recurring / subscription / instalment charge | Yes — required in practice. The fallback never selects a recurring agreement, so a subscription left to the default is billed as unscheduled, under the wrong agreement. |
| Several agreements against the same card (e.g. a subscription plus ad-hoc top-ups) | Yes. Name the one you are billing — the fallback cannot tell them apart. |
| Unscheduled card-on-file charge, exactly one agreement per card | Optional. Only one candidate exists, so the fallback resolves to it. |
Naming a mandate explicitly is always safe, and is the recommended default.
Mandates created before this feature
A mandate that recorded no network marker — every mandate created before recurring agreements were supported — is continued as unscheduled. That is the safe direction (an unscheduled marker on a recurring instalment is a downgrade the networks accept; the reverse is a dispute you lose), but it means existing subscriptions keep billing as unscheduled until you establish a new recurring mandate.
Rules & Limits
Establishing the agreement (Step 1):
setup_future_usageandpayment_method_options.card.setup_future_usage_cadenceare creation-only.ConfirmPaymentIntentignorespayment_method_options;ConfirmPaymentIntentWithClientSecretdoes not accept it.setup_future_usage=off_sessionrequiresmandate_data.customer_acceptance— supplied by you at creation, or captured for you when the SDK confirms with the client secret.setup_future_usagerequires acustomer_id.setup_future_usagecannot be combined withoff_session=true, nor withrequest_three_d_secure=never.- A SetupIntent can only create an unscheduled mandate.
setup_future_usage=on_sessionwithsetup_future_usage_cadence=recurringstates the agreement on the authorization but creates no mandate — mandate creation is off-session only. Useoff_sessionwhen you intend to bill without the customer.
Charging the mandate (Step 3):
mandate_idmay only be supplied on create-with-confirm (POST /api/payment_intentswithconfirm=true).ConfirmPaymentIntentdoes not accept it.mandate_idsent withconfirm=falseis ignored.mandate_idon an on-session payment (off_session=false) is rejected — a mandate is only exercised when the customer is absent.- Omitting
mandate_idon an off-session payment does not fail: the server resolves an unscheduled mandate by its own strategy, and which one is unspecified. - A named mandate must belong to the
payment_method_idbeing charged and beactive, otherwise the request is rejected. - There is no public endpoint for listing or reading mandates.
mandate_idreaches you on the charge, as in Step 2 — persist it.
Errors
| Condition | Response |
|---|---|
setup_future_usage=off_session and no mandate_data from either the create or the confirm call | mandate_data is required for setup_future_usage=off-session |
setup_future_usage with off_session=true | setup_future_usage is not allowed for off-session payment |
setup_future_usage with request_three_d_secure=never | request_three_d_secure=never is not allowed with setup_future_usage |
setup_future_usage without a customer_id | Customer is required to save a payment method |
Named mandate does not exist, belongs to another payment method, or is not active | no active mandate found for this payment method |
No mandate_id given and the payment method has no active unscheduled mandate | no active mandate found for this payment method |
mandate_id given with off_session=false | cannot provide mandate in on-session payment |
off_session=true without payment_method_id | payment_method_id is required for off-session |
See Error Handling for the error envelope and retry guidance.
Webhooks
MIT charges emit the same events as any payment — treat the webhook as the authoritative outcome.
| Event | Description |
|---|---|
payment_intent.status.succeeded | Charge captured. Mark the renewal as paid. |
payment_intent.payment_failed | Declined. Retry per your dunning policy, or ask the customer to update their card — which means establishing a new mandate. |
charge.captured | Charge was captured. |