Skip to content

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:

AgreementWhat the cardholder consented toTypical useHow 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
RecurringThe 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 ​

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
FieldRequiredDescription
setup_future_usageYesMust be off_session — this is what stores a credential you can later charge without the customer.
payment_method_options.card.setup_future_usage_cadenceNorecurring 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_idYesThe 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"
      }
    }
  }
}
FieldRequiredDescription
confirmYestrue to create and confirm in one call.
off_sessionYesMust be false. The establishing transaction is customer-initiated; off_session=true combined with setup_future_usage is rejected.
mandate_data.customer_acceptance.typeYesonline or offline.
mandate_data.customer_acceptance.accepted_atYesWhen the customer accepted the agreement.
mandate_data.customer_acceptance.online.ip_addressYes for onlineThe customer's IP address at acceptance.
mandate_data.customer_acceptance.online.user_agentYes for onlineThe 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_charge

expand 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
ParameterTypeDescription
expandstring[]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 expand path this API version doesn't know is ignored, not rejected — a client written against a newer surface keeps working.
  • latest_charge is omitted if the intent has no charge yet.
  • The same card.mandate_id appears 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_intents

Request

{
  "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"
}
FieldRequiredDescription
payment_method_idYesThe stored credential. Inline card data and confirmation tokens are rejected for off-session payments.
off_sessionYestrue — no customer is present.
mandate_idNoThe mandate this payment exercises. Omitted, the server treats the charge as unscheduled and resolves a mandate itself — see below.
confirmYestrue. 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.

SituationPass mandate_id?
Recurring / subscription / instalment chargeYes — 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 cardOptional. 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_usage and payment_method_options.card.setup_future_usage_cadence are creation-only. ConfirmPaymentIntent ignores payment_method_options; ConfirmPaymentIntentWithClientSecret does not accept it.
  • setup_future_usage=off_session requires mandate_data.customer_acceptance — supplied by you at creation, or captured for you when the SDK confirms with the client secret.
  • setup_future_usage requires a customer_id.
  • setup_future_usage cannot be combined with off_session=true, nor with request_three_d_secure=never.
  • A SetupIntent can only create an unscheduled mandate.
  • setup_future_usage=on_session with setup_future_usage_cadence=recurring states the agreement on the authorization but creates no mandate — mandate creation is off-session only. Use off_session when you intend to bill without the customer.

Charging the mandate (Step 3):

  • mandate_id may only be supplied on create-with-confirm (POST /api/payment_intents with confirm=true). ConfirmPaymentIntent does not accept it.
  • mandate_id sent with confirm=false is ignored.
  • mandate_id on an on-session payment (off_session=false) is rejected — a mandate is only exercised when the customer is absent.
  • Omitting mandate_id on 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_id being charged and be active, otherwise the request is rejected.
  • There is no public endpoint for listing or reading mandates. mandate_id reaches you on the charge, as in Step 2 — persist it.

Errors ​

ConditionResponse
setup_future_usage=off_session and no mandate_data from either the create or the confirm callmandate_data is required for setup_future_usage=off-session
setup_future_usage with off_session=truesetup_future_usage is not allowed for off-session payment
setup_future_usage with request_three_d_secure=neverrequest_three_d_secure=never is not allowed with setup_future_usage
setup_future_usage without a customer_idCustomer is required to save a payment method
Named mandate does not exist, belongs to another payment method, or is not activeno active mandate found for this payment method
No mandate_id given and the payment method has no active unscheduled mandateno active mandate found for this payment method
mandate_id given with off_session=falsecannot provide mandate in on-session payment
off_session=true without payment_method_idpayment_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.

EventDescription
payment_intent.status.succeededCharge captured. Mark the renewal as paid.
payment_intent.payment_failedDeclined. Retry per your dunning policy, or ask the customer to update their card — which means establishing a new mandate.
charge.capturedCharge was captured.

Developers