Add the payment method
The checkout.hosted.attach_payment_method operation: POST /api/v1/public/checkout_sessions/{sessionId}/payment_method in the REST API and the checkout_hosted_attach_payment_method tool in the MCP server.
/api/v1/public/checkout_sessions/{sessionId}/payment_methodRecords the payment method the customer entered (the Stripe Payment Element's pm_…, or a fake test card) and
reads its card's country from the processor. The card country joins the location evidence in a new evidence
set and the sale is quoted again (spec §13.3). If the total changes, the page shows the new total, the
customer confirms it, and the payment intent is updated before the charge (checkout.hosted.confirm refuses
an outdated total with checkout_total_changed).
The brand's risk rules run again with the card's fingerprint and country (spec §20): a refused card is
risk_payment_declined, and the customer can try another.
Public: the hosted page calls it with the session key.
At a glance
- REST:
POST /api/v1/public/checkout_sessions/{sessionId}/payment_method - MCP tool:
checkout_hosted_attach_payment_method - Operation ID:
checkout.hosted.attach_payment_method - 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. |
paymentMethod | string | Yes | The payment method the customer entered (Stripe pm_…; the fake processor's pm_fake_…). |
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
A card from Germany
POST /api/v1/public/checkout_sessions/cs_01m4tx5nj8emwbyf7y9vcf48aa/payment_method
Idempotency-Key: 3f1c9a52-7d0e-4b8f-9a61-0c2d5e8b7f14
Content-Type: application/json
{
"key": "Zk3tq8Vb2xQ9mN4rP7sL1yH6cJ0wE5aD-uT_fG8hK2o",
"paymentMethod": "pm_fake_card_de"
}
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": {
"ref": "pm_fake_card_de",
"type": "card",
"cardCountry": "DE",
"brand": "visa",
"last4": "4242"
},
"upToDate": true
},
"legal": {
"termsAccepted": true,
"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_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. |
checkout_payment_not_started | 409 | The payment step hasn't started (no payment intent yet). | Start the payment step first, so the payment can be prepared for the quoted total. |
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.