- Merchant creates a checkout session (server-side)
- Returns
checkoutUrlto the frontend - Customer clicks the link and lands on the hosted checkout page
- Customer fills in billing details, previews tax, and completes the order
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
PassingoriginOrderId 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.
Issuing the link with an SDK
Every SDK gives plan change its own method withoriginOrderId required, so the “only valid together with originOrderId” rejections above cannot be expressed:
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.
Building the link the customer opens
The returnedcheckoutUrl 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:
?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.
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 mode —sessionId, 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
changeTimingyou 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_periodchange confirms the card with a$0payment 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.
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.