Start the payment step
The checkout.hosted.start_payment operation: POST /api/v1/public/checkout_sessions/{sessionId}/payment in the REST API and the checkout_hosted_start_payment tool in the MCP server.
/api/v1/public/checkout_sessions/{sessionId}/paymentPrepares the payment for the current quote's total at the brand's processor: a payment intent with the
session and the quote in its metadata, created once per quote version ({session}:intent:{quoteVersion}), or
the session's intent updated when the total changed. The session keeps one active intent. The answer says
which payment component to show (fake in development, or the Stripe Payment Element with the connection's
publishable key); the browser gets Stripe's client secret from checkout.hosted.payment_secret.
Call it again after a re-quote: it brings the intent up to date. Public: the hosted page calls it with the session key.
The brand's risk rules run first (spec §20): a refused payment is risk_payment_declined and no intent is
made; a payment that needs 3-D Secure gets an intent that asks the bank for it.
At a glance
- REST:
POST /api/v1/public/checkout_sessions/{sessionId}/payment - MCP tool:
checkout_hosted_start_payment - Operation ID:
checkout.hosted.start_payment - Scope: Brand
- Permission:
checkout:write - Risk tier:
write - Dry run: Not supported
- Idempotency:
Idempotency-Keyrequired - Modes: Test and live
- Authentication: None (a public endpoint, rate-limited per IP address)
- Since: M4
Request
Path parameters
| Field | Type | Description |
|---|---|---|
sessionId | cs_… ID | The checkout session's ID (cs_…). |
Body
A JSON object (Content-Type: application/json):
| Field | Type | Required | Description |
|---|---|---|---|
key | string | No | The session key from the hosted URL (?key=…). Required without an API key; signed-in callers with checkout:write don't need it. |
Response
200 OK with a JSON object:
| Field | Type | Description |
|---|---|---|
id | cs_… ID | |
object | "checkout.hosted_session" | |
livemode | boolean | |
status | one of open, processing, complete, expired, blocked | |
mode | one of payment, subscription, topup, setup | |
currency | string | |
brand | object | |
brand.slug | string | |
brand.name | string | |
brand.primaryColor | string or null | |
brand.termsUrl | string or null | |
brand.privacyUrl | string or null | |
brand.supportEmail | string or null | |
seller | object | The merchant disclosure: “Sold by {legal name}, {address}, {VAT ID}” (spec §29.1). |
seller.legalName | string | |
seller.address | string | |
seller.taxId | string | |
lineItems | array of object | |
lineItems[].id | string | |
lineItems[].description | string | |
lineItems[].quantity | integer | |
lineItems[].amount | money | |
lineItems[].recurring | object or null | |
lineItems[].adjustableQuantity | object or null | |
lineItems[].discount | money or null | |
subscription | object or null | Subscription sessions: the trial, what is due today and the promotion code. |
topup | object or null | Top-up sessions (spec §14.3): what the customer's wallet receives. |
setup | object or null | Setup sessions: whether saving the card also pays the subscription's first invoice. |
collectBusinessDetails | one of auto, required, never | |
prefill | object | |
prefill.email | string or null | |
prefill.name | string or null | |
prefill.country | string or null | |
prefill.subdivision | string or null | |
prefill.business | object or null | |
quote | object or null | |
blocked | object or null | |
waitlist | object or null | |
payment | object | |
payment.started | boolean | |
payment.intent | one of payment, setup or null | payment: a charge now; setup: the card is saved without a charge. |
payment.component | one of fake, stripe_payment_element, redirect or null | How the payment details are collected: the fake processor's test cards, the Stripe Payment Element, or redirect (the buyer pays on the processor's page, Mollie; confirm answers its address). |
payment.publishableKey | string or null | |
payment.paymentMethod | object or null | |
payment.upToDate | boolean | The prepared payment is for the current quote's total. |
legal | object | |
legal.termsAccepted | boolean | |
legal.immediatePerformanceRequired | boolean | |
legal.immediatePerformanceText | string | |
successUrl | string or null | Set once the session is complete: successUrl?cs_id=…. |
cancelUrl | string or null | |
expiresAt | date-time | |
locale | object | What decides the page's language (spec §14.2): an explicit choice on the page, then preferred, then the browser's languages, then brand, then English. |
locale.preferred | array of string | The session's language, then its customer's, where set (BCP 47): the page's language before the visitor's. |
locale.brand | array of string | The brand's checkout languages; the first is its default. |
Examples
Start the payment
POST /api/v1/public/checkout_sessions/cs_01m4tx5nj8emwbyf7y9vcf48aa/payment
Idempotency-Key: 3f1c9a52-7d0e-4b8f-9a61-0c2d5e8b7f14
Content-Type: application/json
{
"key": "Zk3tq8Vb2xQ9mN4rP7sL1yH6cJ0wE5aD-uT_fG8hK2o"
}
Response (200):
{
"id": "cs_01m4tx5nj8emwbyf7y9vcf48aa",
"object": "checkout.hosted_session",
"livemode": false,
"status": "open",
"mode": "payment",
"currency": "EUR",
"brand": {
"slug": "genno",
"name": "Genno",
"primaryColor": null,
"termsUrl": "https://genno.example/terms",
"privacyUrl": "https://genno.example/privacy",
"supportEmail": null
},
"seller": {
"legalName": "Etna Labs AG",
"address": "Dammstrasse 16, 6300 Zug, Switzerland",
"taxId": "CHE-181.927.786 MWST"
},
"lineItems": [
{
"id": "1",
"description": "Genno credits",
"quantity": 1,
"amount": {
"amount": "100000000",
"currency": "EUR"
},
"recurring": null,
"adjustableQuantity": null,
"discount": null
}
],
"subscription": null,
"topup": null,
"setup": null,
"collectBusinessDetails": "auto",
"prefill": {
"email": "anna@example.com",
"name": null,
"country": "DE",
"subdivision": null,
"business": null
},
"quote": {
"id": "tq_01m4tx5nj8emwbyf7y9vcf48ab",
"decision": "charge",
"customerType": "b2c",
"country": "DE",
"lines": [
{
"id": "1",
"description": "Genno credits",
"quantity": 1,
"treatment": "charge",
"net": {
"amount": "84033613",
"currency": "EUR"
},
"tax": {
"amount": "15966387",
"currency": "EUR"
},
"gross": {
"amount": "100000000",
"currency": "EUR"
},
"ratePpm": "190000",
"jurisdictionId": "DE",
"jurisdictionName": "Germany",
"taxName": "VAT",
"regimeId": "EU_OSS_NON_UNION",
"pendingRegistration": false,
"taxBehavior": "inclusive"
}
],
"totals": {
"net": {
"amount": "84033613",
"currency": "EUR"
},
"tax": {
"amount": "15966387",
"currency": "EUR"
},
"gross": {
"amount": "100000000",
"currency": "EUR"
}
},
"inclusive": true,
"reverseCharge": false,
"reverseChargeWording": null,
"validUntil": "2026-10-14T10:30:00.000Z",
"unverifiedRules": false
},
"blocked": null,
"waitlist": null,
"payment": {
"started": true,
"intent": "payment",
"component": "fake",
"publishableKey": null,
"paymentMethod": null,
"upToDate": true
},
"legal": {
"termsAccepted": false,
"immediatePerformanceRequired": true,
"immediatePerformanceText": "I want the service to start now, and I understand that I lose my right to withdraw once it has started."
},
"successUrl": null,
"cancelUrl": "https://shop.example/cart",
"expiresAt": "2026-10-15T10:00:00.000Z",
"locale": {
"preferred": [],
"brand": ["en"]
}
}
Errors
| Code | HTTP status | When | What to do |
|---|---|---|---|
resource_not_found | 404 | No such checkout session for this brand and mode, or (on the hosted page) the session key is missing or wrong. | Check the ID and that your credentials are scoped to the right organization, entity, brand and mode. |
checkout_session_not_open | 409 | The session is complete, expired, or a payment for it is being processed (details.status). | The session is complete, expired or waiting for a payment's outcome (details.status). Create a new session for a new purchase. |
checkout_details_required | 409 | The customer's details aren't in yet. | Submit the customer's email and country first, so the tax can be quoted. |
checkout_session_blocked | 409 | The sale is paused for the customer's location. | Offer the waitlist, or let a business customer enter a valid tax ID (reverse charge). details.blocked_reason says why the sale is paused. |
checkout_quote_expired | 409 | The quote is older than 30 minutes or from another day: submit the details again. | Submit the customer's details again to get a fresh quote (quotes are valid for 30 minutes). |
processor_not_configured | 400 | The brand has no usable processor connection in this mode. | Connect a processor to the brand's legal entity in this mode (console → entity → Processor connections), or reactivate a disconnected one. |
processor_request_failed | 502 | The processor refused or failed the request (retryable says whether to retry). | If retryable is true, retry with the same Idempotency-Key after a short wait; otherwise check details.code. |
checkout_session_changed | 409 | The session changed while the request ran; send it again. | Send the request again. |
risk_payment_declined | 400 | The brand's risk rules refused the payment (§20; WP-M9-risk). The answer doesn't say which rule. | Use another payment method, or contact the seller. The seller's team sees why in the console (Brand → Risk); the answer never says. |
It can also return these common errors: validation_failed, api_version_unsupported, invalid_json, unsupported_media_type, scope_required, idempotency_key_required, idempotency_key_reused, idempotency_in_progress, dry_run_not_supported, rate_limited, internal_error.