Skip to main content
Create a checkout session that locks product version, pricing, and currency. This is the first step in the checkout flow for both one-time and subscription products. The typical flow is:
  1. Merchant creates a checkout session (server-side)
  2. Returns checkoutUrl to the frontend
  3. Customer clicks the link and lands on the hosted checkout page
  4. Customer fills in billing details, previews tax, and completes the order
Authentication: API Key or Store Slug

Request Body (API Key)

Request Body (Store Slug)

Store Slug authentication does not support priceSnapshot, expiresInSeconds, metadata, orderMerchantExternalId, withTrial, changeAmount, changeCreditAmount, includePaymentMethods, or excludePaymentMethods. These fields are silently ignored to prevent price tampering and session manipulation from the client side. Payment method selection is a merchant-side commercial decision (channel fees, settlement terms), so a browser cannot narrow or widen it — Store Slug sessions always offer everything the currency supports.
ID-naming convention: the same flat dual-key names (orderMerchantExternalId on checkout-side, refundTicketMerchantExternalId on refund-ticket-side) are used across request bodies, webhook payloads, and every GraphQL type that carries the value — so a value written at checkout can be read back from Order, Payment, Refund, or a webhook payload under the same field name.
There is no cancelUrl. If the buyer closes the cashier or abandons payment, nothing redirects them back to your site. The session simply expires (45 minutes by default), the order ends canceled without a charge, and no webhook fires — there is nothing for you to handle. cancelUrl, returnUrl and similar fields are silently ignored if you send them. Keep your own “back to store” link on the page that opened the cashier.

Price Snapshot Object

For one-time products, amount replaces the product’s price outright. For subscription products it replaces the regular period price only — the trial period price comes from the product version locked into the session and cannot be overridden here.

Billing Detail Object

billingDetail only pre-fills the cashier. The country you send becomes the initial value of the editable country selector (a merchant value takes priority over IP-based detection), the buyer can change it, and the buyer’s final choice is what the order records. No session or URL parameter locks or hides the billing fields. Payment methods are not driven by the country: they are resolved from product type × currency plus includePaymentMethods / excludePaymentMethods; the country is used only for the tax preview and for which address fields are shown.

Plan Change Mode

Passing originOrderId switches this endpoint into plan change mode: instead of a new-purchase link, the returned checkoutUrl points at a change confirmation page for an existing subscription — the path segment after the store slug is change instead of checkout (…/store/{storeSlug}/change/{sessionId}). A plan change is two steps — this endpoint issues the link, and the customer confirms on that page. A plan change is modelled as one cancellation plus one new subscription: the old order reaches its cancelled end state and a new order runs the normal creation lifecycle. It is not an in-place edit of the existing subscription. Who can issue the link: API Key (yours, server-side) or a customer session token. Store Slug is anonymous, so it has no subscription to attribute the change to — passing originOrderId with Store Slug auth always returns 403. Every SDK gives plan change its own method with originOrderId required, so the “only valid together with originOrderId” rejections above cannot be expressed:
These three sign with your API Key, so the pricing fields and withTrial are honored. Use checkout.authenticated.createPlanChange() (TypeScript / Next.js authenticatedPlanChange) or Checkout.Authenticated.CreatePlanChange (Go) to have the customer session token appended for you — that is the token the confirmation page needs, described next. For the customer-issued half, call customer.createPlanChangeSession() on a customer session (client.customer(token) in TypeScript, client.Customer(token) in Go, customerAction(token, "createPlanChangeSession", …) in Next.js). It runs under the customer’s own credential, so the three 403 checks above apply and the API-Key-only fields are absent from its params — the platform would drop them unannounced. The returned checkoutUrl carries neither a buyer credential nor an environment flag — the session was created with your server-side credential, so the platform cannot vouch for the buyer at that point. Opening the bare URL renders the comparison page, but the confirm step fails with a missing-authorisation message. You append both parts yourself:
Query parameter first, fragment second. Drop ?test=true in production. Without a token the page falls back to asking the buyer to verify their email — a working but slower path. A wrong-environment link used to fail with Environment mismatch between request and session at the confirm step.
changeAmount, changeCreditAmount and withTrial are API Key only. With any other credential they are silently ignored, the same way priceSnapshot is on a new purchase — you will not get an error telling you they were dropped.

What the origin subscription supplies

In plan change mode these values come from the subscription you are changing, not from your request body: The currency must also match the origin subscription — cross-currency plan changes are rejected.

Pricing the change

Two fields, two ways to price the same change. They are mutually exclusive — sending both returns 400. Both are display format strings on the same unit and tax basis (tax inclusive), and both must leave a non-negative result: an amount above the target plan’s charge for this period is rejected, and so is a credit above it. The two rejections are separate messages so you can tell which field to lower. The same number means the opposite thing under each field, which is why the platform never picks one for you. Pick whichever matches how you priced the change: changeAmount when you know the final figure, changeCreditAmount when you know the discount. Either one counts as “the merchant priced this change” for the timing rules below. Internally the session stores one figure — the charge for this period — so a credit you specify is converted at issue time using the target plan’s charge at that moment.

When the new plan takes effect

changeTiming accepts immediate or next_period. Omit it and the platform derives it from the change direction, which it works out by comparing pre-tax prices: an upgrade defaults to immediate, a downgrade or same-price switch defaults to next_period.
An upgrade inside the last 24 hours does not silently fall back to next period. The upgrade default is immediate, so within 24 hours of the next charge a request that omits changeTiming gets a 400 — send changeTiming: "next_period" explicitly. A downgrade already defaults to next_period, so the same window does not affect it.

One confirmed change at a time

You can issue and submit several candidate changes for the same subscription while the customer makes up their mind. The subscription is locked to one change only once a change has been confirmed by the payment channel; after that, further attempts return 409.

Response

The response body is identical to new-purchase modesessionId, checkoutUrl, expiresAt, and no extra fields. The change direction, the resolved timing, and the prorated credit preview are written into the session for the confirmation page to read; they are not echoed back. Two consequences:
  • If you omit changeTiming you will not learn which tier the platform derived. Send it explicitly when you need certainty.
  • The prorated credit is an estimate taken at issue time and is recalculated at submission against the days actually remaining. Do not present it to the customer as a final figure.
  • A next_period change confirms the card with a $0 payment record. In the Dashboard transaction list that row shows the plan price with Charged $0.00 underneath, and it is excluded from the subscription’s charge count.
For what arrives after the change lands, see subscription.plan_changed and the two plan-change events. For the customer-facing behavior of upgrades and downgrades, see Subscriptions.

Session Locks

When a checkout session is created, the following values are locked and cannot change during the session lifetime: productVersionId, productName, priceInfo, storeName, billingPeriod, withTrial, theme, buyerEmail, billingDetail In plan change mode the session additionally locks the change context: the origin subscription’s snapshot, the change direction, the resolved timing and effective moment, the prorated credit preview, and the amount you specified (a changeCreditAmount is stored as the charge it converts to). The confirmation page reads all of it from the session, so none of it can be overridden at submission — the target plan, timing, and amount are settled the moment the link is issued. Email normalization: All email fields (email, buyerEmail, contactEmail) are normalized server-side via trim().toLowerCase() before storage, cache writes, and downstream calls. Foo@Bar.COM and foo@bar.com are treated as the same account.

Example Request

Success Response (200)

Response Fields

Errors

Retry policy: Never retry 4xx — fix the request and resubmit. The 409s below are exceptions in kind, not in policy: they report the subscription’s state, not a malformed body, so resubmitting the same request keeps failing until that state changes. Retry 5xx with exponential backoff (start 5s, max 3 attempts).
Payment acceptance and production approval are two independent conditions: the former is a platform-controlled switch (which can suspend the whole store or individual payment methods), the latter is the production-approval (KYB) gate. Either one alone yields a 403 in the prod environment. Neither applies in the test environment.
The default session TTL is 45 minutes (2700 seconds). When using API Key auth, you can customize this with expiresInSeconds; no upper bound is enforced. Sessions lock the product version and pricing at creation time, so price changes will not affect existing sessions.