Create a checkout session
The checkout.sessions.create operation: POST /api/v1/checkout/sessions in the REST API and the checkout_sessions_create tool in the MCP server.
/api/v1/checkout/sessionsCreates a hosted checkout and returns its url: send the customer there. The page collects the customer's
details, quotes the tax (spec §9), takes the payment and shows the receipt; the sale is recorded only when the
processor confirms it (webhook).
payment: one-time prices and credit packs.subscription: recurring prices (spec §16.2). The first invoice is paid in the checkout and the card is saved for renewals; with a trial (subscriptionData.trialDays, default the prices' own) the card is only saved, or not asked for at all withsubscriptionData.trialRequiresPaymentMethod: false. Lines can let the customer change the quantity (adjustableQuantity), andallowPromotionCodesadds a promotion code field.topup: credits for an existing customer's wallet (spec §14.3,customerrequired): one credit pack, a fixed pack or a custom amount within the pack's bounds. The buyer must use the customer's email; a customer who has paid before is taxed as an existing customer. The wallet gets the purchased credits (the amount before tax, or the amount paid under the brand'sgrosscredit basis) and the pack's bonus credits.setup: no line items: the customer saves a card for a customer (customerrequired).- Line items: 1 to 20 prices of one currency, by ID or lookup key, with a quantity (1 to 1,000) or, for a custom-amount price, the amount in integer micros. Prices become immutable once a session uses them.
- Customer: an existing customer, or an email to pre-fill. New buyers become customers when they're quoted.
- Expiry: 24 hours by default (30 minutes to 24 hours with
expiresAt). Test sessions show a test banner.
The URL carries a session key: treat it like the session itself and give it only to the customer.
At a glance
- REST:
POST /api/v1/checkout/sessions - MCP tool:
checkout_sessions_create - Operation ID:
checkout.sessions.create - Scope: Brand
- Permission:
checkout:write - Risk tier:
write - Dry run: Supported (send
Ownrecord-Dry-Run: true) - Idempotency:
Idempotency-Keyrequired - Modes: Test and live
- Authentication: API key with the
checkout:writescope - Since: M4
Request
Body
A JSON object (Content-Type: application/json):
| Field | Type | Required | Description |
|---|---|---|---|
mode | one of payment, subscription, topup, setup | No | payment: one-time prices and credit packs; subscription: recurring prices, starting a subscription; topup: one credit pack (a fixed pack or a custom amount) added to an existing customer's wallet; setup: save a payment method of an existing customer, nothing is charged. |
lineItems | array of object | No | What the customer buys: 1 to 20 prices of one currency (none in setup mode). |
lineItems[].price | price_… ID | No | The price's ID (price_…). Give price or lookupKey. |
lineItems[].lookupKey | string | No | The price's lookup key in this brand, e.g. credits_100. |
lineItems[].quantity | integer (1 to 1000) | No | How many (1 to 1000); custom-amount prices are sold one at a time. |
lineItems[].customAmount | object | No | For a custom-amount price: the amount the customer pays, within the price's bounds. |
lineItems[].customAmount.amount | string | Yes | Integer micros as a string (12500000 = 12.50), zero or more. |
lineItems[].customAmount.currency | string | Yes | ISO 4217 currency code; every line of a quote uses the same one. |
lineItems[].adjustableQuantity | object | No | Subscription sessions: let the customer change the quantity (seats) within these bounds (spec §16.6). |
lineItems[].adjustableQuantity.min | integer (1 to 1000) | Yes | |
lineItems[].adjustableQuantity.max | integer (1 to 1000) | Yes | |
customer | cus_… ID | No | An existing customer of the brand (cus_…); required in setup and topup mode. |
customerEmail | string | No | Pre-fills the email on the hosted page (ignored with customer). |
successUrl | string | No | Where the customer goes after paying; ?cs_id={session ID} is added. Required except in setup mode (#435, #474). |
cancelUrl | string | No | Where the hosted page's back link goes. |
locale | string | No | The customer's language (invoices use it when supported). |
collectBusinessDetails | one of auto, required, never | No | auto: offer “Buying for a business?”; required: business customers only; never: consumers only. |
expiresAt | date-time | No | When the session expires: 30 minutes to 24 hours from now (default 24 hours). |
allowPromotionCodes | boolean | No | Subscription sessions: offer a promotion code field on the hosted page. |
promotionCode | string | No | Subscription sessions: a promotion code applied for the customer. |
subscriptionData | object | No | Subscription sessions: the trial and the new subscription's metadata. |
subscriptionData.trialDays | integer (1 to 730) | No | Trial days (default: the shortest trial of the prices). |
subscriptionData.trialRequiresPaymentMethod | boolean | No | Collect a card for a trial (default true); without one, the trial starts without it. |
subscriptionData.metadata | object | No | Your own key-value pairs, returned with the session and in its events (e.g. your order ID). |
currency | string | No | Setup sessions: the currency the saved method will pay in (default: the entity's). |
metadata | object | No | Your own key-value pairs, returned with the session and in its events (e.g. your order ID). |
Response
201 Created with a JSON object:
| Field | Type | Description |
|---|---|---|
id | cs_… ID | |
object | "checkout.session" | |
livemode | boolean | |
entityId | ent_… ID | |
brandId | brd_… ID | |
mode | one of payment, subscription, topup, setup | |
status | one of open, processing, complete, expired, blocked | open, blocked (the sale is paused for the customer's location), processing, complete or expired. |
url | string or null | The hosted page (with its session key); null once complete or expired. |
lineItems | array of object | |
lineItems[].id | string | The line's ID in the session's quotes and its invoice. |
lineItems[].priceId | price_… ID | |
lineItems[].productId | prod_… ID | |
lineItems[].priceType | one of one_time, credit_pack, recurring | |
lineItems[].recurring | object or null | Recurring prices (subscription sessions): the billing interval and whether usage is metered. |
lineItems[].adjustableQuantity | object or null | The customer may change the quantity on the hosted page within these bounds. |
lineItems[].discount | money or null | Subscription sessions: what the promotion code takes off this line on the first invoice. |
lineItems[].description | string | The product's name when the session was created. |
lineItems[].quantity | integer | |
lineItems[].customAmount | money or null | |
lineItems[].amount | money | Before tax and discounts: gross for tax-inclusive prices, net for exclusive ones (a recurring price's first full period; zero for metered usage, billed later). |
lineItems[].taxBehavior | one of inclusive, exclusive | |
currency | string | |
customerId | cus_… ID or null | |
customerEmail | string or null | |
successUrl | string or null | |
cancelUrl | string or null | |
locale | string or null | |
collectBusinessDetails | one of auto, required, never | |
allowPromotionCodes | boolean | The hosted page offers a promotion code field. |
promotionCodeId | promo_… ID or null | The promotion code applied to the session. |
subscriptionData | object or null | Subscription sessions: the trial and the subscription's metadata. |
taxQuoteId | tq_… ID or null | The current quote (tax.quotes.get explains it). |
totals | object or null | The current quote's totals; null before the customer's details. |
blockedReason | one of evidence_conflict, unsupported_tax_category, registration_required, rate_unavailable, jurisdiction_blocked, tax_id_unavailable or null | |
waitlistEntryId | wl_… ID or null | |
paymentId | pay_… ID or null | |
invoiceId | inv_… ID or null | |
subscriptionId | sub_… ID or null | Subscription sessions: the subscription created; setup sessions: the one the method is for. |
paymentMethodId | pm_… ID or null | The payment method saved (subscription and setup sessions). |
expiresAt | date-time | |
completedAt | date-time or null | |
createdAt | date-time | |
metadata | object |
Examples
A credit pack by lookup key
The customer's email is pre-filled on the hosted page.
POST /api/v1/checkout/sessions
Authorization: Bearer or_test_…
Idempotency-Key: 3f1c9a52-7d0e-4b8f-9a61-0c2d5e8b7f14
Content-Type: application/json
{
"lineItems": [
{
"lookupKey": "credits_100",
"quantity": 1
}
],
"customerEmail": "anna@example.com",
"successUrl": "https://shop.example/thanks",
"cancelUrl": "https://shop.example/cart",
"metadata": {
"order": "ord_1042"
}
}
Response (201):
{
"id": "cs_01m4tx5nj8emwbyf7y9vcf48aa",
"object": "checkout.session",
"livemode": false,
"entityId": "ent_01m4tx5nj8emwbyf7y9vcf47x2",
"brandId": "brd_01m4tx5nj8emwbyf7y9vcf47x4",
"mode": "payment",
"status": "open",
"url": "https://ownrecord.io/c/genno/cs_01m4tx5nj8emwbyf7y9vcf48aa?key=Zk3tq8Vb2xQ9mN4rP7sL1yH6cJ0wE5aD-uT_fG8hK2o",
"lineItems": [
{
"id": "1",
"priceId": "price_01m4tx5nj8emwbyf7y9vcf48ad",
"productId": "prod_01m4tx5nj8emwbyf7y9vcf48ae",
"priceType": "credit_pack",
"recurring": null,
"adjustableQuantity": null,
"discount": null,
"description": "Genno credits",
"quantity": 1,
"customAmount": null,
"amount": {
"amount": "100000000",
"currency": "EUR"
},
"taxBehavior": "inclusive"
}
],
"currency": "EUR",
"customerId": null,
"customerEmail": "anna@example.com",
"successUrl": "https://shop.example/thanks",
"cancelUrl": "https://shop.example/cart",
"locale": null,
"collectBusinessDetails": "auto",
"allowPromotionCodes": false,
"promotionCodeId": null,
"subscriptionData": null,
"taxQuoteId": null,
"totals": null,
"blockedReason": null,
"waitlistEntryId": null,
"paymentId": null,
"invoiceId": null,
"subscriptionId": null,
"paymentMethodId": null,
"expiresAt": "2026-10-15T10:00:00.000Z",
"completedAt": null,
"createdAt": "2026-10-14T10:00:00.000Z",
"metadata": {
"order": "ord_1042"
}
}
A top-up of a customer's wallet
€40 of a custom-amount credit pack for an existing customer (spec §14.3).
POST /api/v1/checkout/sessions
Authorization: Bearer or_test_…
Idempotency-Key: 3f1c9a52-7d0e-4b8f-9a61-0c2d5e8b7f14
Content-Type: application/json
{
"mode": "topup",
"customer": "cus_01m4tx5nj8emwbyf7y9vcf48ac",
"lineItems": [
{
"lookupKey": "credits_custom",
"customAmount": {
"amount": "40000000",
"currency": "EUR"
}
}
],
"successUrl": "https://shop.example/wallet"
}
Response (201):
{
"id": "cs_01m4tx5nj8emwbyf7y9vcf48aa",
"object": "checkout.session",
"livemode": false,
"entityId": "ent_01m4tx5nj8emwbyf7y9vcf47x2",
"brandId": "brd_01m4tx5nj8emwbyf7y9vcf47x4",
"mode": "topup",
"status": "open",
"url": "https://ownrecord.io/c/genno/cs_01m4tx5nj8emwbyf7y9vcf48aa?key=Zk3tq8Vb2xQ9mN4rP7sL1yH6cJ0wE5aD-uT_fG8hK2o",
"lineItems": [
{
"id": "1",
"priceId": "price_01m4tx5nj8emwbyf7y9vcf48ad",
"productId": "prod_01m4tx5nj8emwbyf7y9vcf48ae",
"priceType": "credit_pack",
"recurring": null,
"adjustableQuantity": null,
"discount": null,
"description": "Genno credits",
"quantity": 1,
"customAmount": {
"amount": "40000000",
"currency": "EUR"
},
"amount": {
"amount": "40000000",
"currency": "EUR"
},
"taxBehavior": "inclusive"
}
],
"currency": "EUR",
"customerId": "cus_01m4tx5nj8emwbyf7y9vcf48ac",
"customerEmail": null,
"successUrl": "https://shop.example/wallet",
"cancelUrl": null,
"locale": null,
"collectBusinessDetails": "auto",
"allowPromotionCodes": false,
"promotionCodeId": null,
"subscriptionData": null,
"taxQuoteId": null,
"totals": null,
"blockedReason": null,
"waitlistEntryId": null,
"paymentId": null,
"invoiceId": null,
"subscriptionId": null,
"paymentMethodId": null,
"expiresAt": "2026-10-15T10:00:00.000Z",
"completedAt": null,
"createdAt": "2026-10-14T10:00:00.000Z",
"metadata": {}
}
Errors
| Code | HTTP status | When | What to do |
|---|---|---|---|
brand_not_live | 409 | A live-mode session for a brand that isn't live yet (its go-live checklist). | Use test mode, or complete the brand's go-live checklist first. |
organization_closing | 409 | A live-mode session while the organization is closing or closed. | Existing subscriptions, refunds and filings continue until the organization is closed. Contact the seller for anything else. |
checkout_mode_not_supported | 400 | The mode isn't sold yet (details.available_from names the milestone). Every mode is sold since M6. | Create the session with mode: payment (one-time prices and credit packs), subscription (recurring prices) or setup (save a payment method). Top-ups arrive with milestone M6 (details.available_from). |
resource_not_found | 404 | A line item's price or lookup key, or the customer, doesn't exist in this brand and mode. | Check the ID and that your credentials are scoped to the right organization, entity, brand and mode. |
price_archived | 409 | A line item's price or its product is archived. | Reactivate the price first (active: true), or use an active price. |
checkout_price_not_sellable | 400 | A line item's price doesn't fit the mode: recurring in a payment session, one-time in a subscription session, anything but a credit pack in a topup session. | Sell one-time prices and credit packs in payment mode and recurring prices in subscription mode. param names the line item. |
subscription_price_invalid | 400 | subscription: the prices mix currencies or intervals that aren't whole multiples of each other, or repeat a price. | Use active recurring prices of one currency whose intervals are whole multiples of each other (monthly with yearly, weekly with 2-weekly). details.reason says what's wrong and param names the item. |
checkout_promotion_code_not_allowed | 400 | allowPromotionCodes or promotionCode on a session that isn't a subscription session. | Create the session with allowPromotionCodes: true, or with promotionCode to apply one for the customer. Promotion codes apply to subscription sessions. |
promotion_code_not_redeemable | 409 | promotionCode is inactive, expired, used up or not valid for this customer (details.reason). | Check details.reason and tell the customer: the code may have expired, be used up, belong to another customer, need a larger purchase or be for first-time customers only. |
checkout_amount_invalid | 400 | A quantity is outside 1–1,000, or a custom amount is missing, outside the price's bounds or not whole minor units. | Give a quantity from 1 to 1,000 for fixed prices; for a custom-amount price give customAmount within the price's minimum and maximum, in whole minor units of its currency, with quantity 1. param names the line item. |
checkout_currency_mismatch | 400 | The line items' prices use more than one currency. | Put prices of one currency in a session; create one session per currency. |
checkout_session_expiry_invalid | 400 | expiresAt is less than 30 minutes or more than 24 hours away. | Set expiresAt between 30 minutes and 24 hours from now, or leave it out for 24 hours. |
customer_deleted | 400 | The given customer was deleted. | Create a new customer for this buyer. Deleted customers stay readable for the records they belong to, but can't be changed. |
validation_failed | 400 | A field is missing or malformed (param names it); a topup session without customer or with more than one line item. | Fix the fields listed in details.issues and send the request again. |
entitlement_required | 403 | The plan doesn't include live mode (live sessions), subscriptions (subscription) or usage billing (topup). | Upgrade the organization's plan, or remove something to get back under the limit. |
org_suspended | 403 | Live mode, while the organization is suspended (§37.5). | If this is your organization, pay the open Ownrecord invoice in Organization → Billing; it is active again as soon as it is paid. Renewals, refunds, the customer portal and filings keep working. |
It can also return these common errors: api_version_unsupported, invalid_json, unsupported_media_type, authentication_required, api_key_invalid, permission_denied, scope_required, idempotency_key_required, idempotency_key_reused, idempotency_in_progress, approval_required, approval_rejected, approval_expired, stale_approval, approval_policy_denied, rate_limited, internal_error.