Skip to main content
POST
Create Checkout Session
Base URL: https://api.nozle.app/engine/api/v1. Create a payment-gated plan checkout from a trusted merchant backend. Auth: secret key only. Publishable keys are always rejected.
The merchant backend derives customer_id from its authenticated user or team. Do not trust a browser-provided customer ID. The response can contain a Stripe or Razorpay checkout, a processing state, or a completed or scheduled transition.

Modes

Plan checkout

Use this when a customer is selecting a plan.
string
required
External customer ID. The customer must already exist.
string
required
The plan code to subscribe the customer to (e.g. pro).
Plan checkout is payment-gated. Free-to-paid checkout uses an incomplete subscription. Paid-to-paid upgrades keep the old subscription active in a durable pending intent and apply the billing change only after the payment provider confirms the required payment and Nozle fulfills the change. If prepaid credits fully cover a paid plan change, Nozle applies it without an external payment and returns type: "completed". A downgrade or other cycle-end transition may return type: "scheduled". Checkout is only for plans that need an up-front payment. For a customer with no subscription, a free plan, a plan with a trial, or a plan billed in arrears returns 422 checkout_not_required: start it with subscribe instead. A plan with a pay-in-advance fixed charge is the exception, even with a trial or billed in arrears: checkout invoices the fixed charge in the request and returns a payable session for it, like any paid plan. The customer’s saved card is never charged off-session for that invoice. Entity checkout is the exception and completes these plans with payment_source: "none". Changing the plan of an existing subscription needs a plan transition policy for that pair of plans, except from a free plan to a paid plan. The selected self-service downgrade flow uses a signed quote with the end-of-period transition endpoint described below.

Invoice checkout

string
Nozle invoice to collect payment for. With invoice_id, plan_code is not required. It cannot be combined with subscription_id.

Selected subscription and signed quote

string
External subscription ID. Select it explicitly for self-service. An unselected checkout for a customer with multiple active subscriptions is rejected as ambiguous.
string
Opaque signed quote from POST /subscriptions/preview. Requires subscription_id; forward it unchanged after the customer confirms the amounts and dates.
Use an Idempotency-Key header for the confirmed action and retain it across uncertain retries. A new-subscription checkout retried while its first attempt is unpaid reopens that attempt; see Retry a new-subscription checkout. Invalidated or expired new quotes require a fresh preview/confirmation; replay of an already accepted action reuses its recorded checkout. See Subscription self-service for quote, downgrade and recovery contracts.

Return URL

string
URL Stripe should return to after embedded checkout completes. It must use an exactly allowlisted HTTPS origin. The Engine checks it against the origins configured for the Nozle deployment; the origins you register for your organization (checkout_return_origins) are accepted by Core’s checkout routes, but this Engine check does not read them yet. If omitted, Nozle uses the configured dashboard return URL. See return URLs and your organization’s origins.
string
Backward-compatible alias for return_url.

Request examples

Plan checkout:

Response

string
required
stripe, razorpay, processing, completed, or scheduled. This endpoint never returns hosted. The SDK types still declare it, and only the deprecated POST /credits/purchase of the Engine API returns it. That endpoint requires an Idempotency-Key header and a currency field (the ISO 4217 code of the top-up package, for example USD), and answers 400 with the code idempotency_key_required or currency_required without them; a retry with the same key returns the first purchase. Use Create Credit Top-up Purchase (POST /credit-top-up-purchases) instead.
string
Stripe results only: the cs_ secret of an embedded Stripe Checkout Session. Mount it with Stripe Embedded Checkout (the React Checkout component does this). Plan and invoice checkout never return a Stripe-hosted URL. Credit top-ups do: see Create Credit Top-up Purchase.
string
Camel-case alias for client_secret.
string
Nozle invoice ID being paid. A paid upgrade returns plan_upgrade_intent_id instead.
string
Paid upgrades only: the pending upgrade. Use it with the subscription scope to poll status; the old plan stays active until payment succeeds.
integer
Amount due for the checkout session, in the invoice currency’s smallest unit.
string
Invoice currency.

Razorpay and recovery fields

A razorpay result supplies checkout_id, order_id, public key_id, amount_cents, currency, expires_at, and invoice_id or plan_upgrade_intent_id. A mandate checkout (register_mandate: true) adds customer_id, recurring: true, and mandate_max_amount_cents. Open the Razorpay modal, then verify the handler’s signature on your backend. Activation requires captured payment and completed fulfillment. See the Razorpay flow. A processing result supplies checkout_id and status. Razorpay returns it while the order is not ready yet or after it expired. Stripe returns it for a replayed quoted upgrade while the payment is complete and its webhook is pending. Recover it rather than starting another checkout. A bulk Entity checkout first answers {"entity_subscription_checkout": {"type": "processing", "status": "open", "checkout_id": "...", ...}}. The status of a processing result is an internal operation status. The nested checkout of an expired unpaid Razorpay order in checkout status is {"type": "processing", "status": "ready"} while the top-level status is expired. Both scoped verification and status recovery are described in Subscription self-service. A Razorpay payment link is never a result of this endpoint. It comes from an invoice payment URL (POST /invoices/{id}/payment_url, which returns {"invoice_payment_details": {"customer_id", "external_customer_id", "payment_provider", "invoice_id", "payment_url"}}) or from a credit top-up purchase, and has no checkout ID to poll.

Retry a new-subscription checkout

A new-subscription checkout leaves an incomplete subscription until its first payment succeeds. Calling checkout again for the same customer and plan while that subscription is incomplete reopens the first checkout for the same invoice instead of failing, with or without the same Idempotency-Key. For Razorpay the reopened checkout is the same order. An invoice checkout (invoice_id in the request) for the invoice_id of the first response works too. Asking for a different plan replaces the abandoned checkout: the old checkout expires as described below and the new checkout is returned. 409 subscription_checkout_in_progress (entity_subscription_checkout_in_progress for a single Entity checkout) is returned only while the provider is still processing a payment for the old checkout, or when that checkout turns out to be paid. The same applies to single Entity checkouts.

Unpaid checkout expiry

An unpaid plan checkout or single Entity checkout expires after 23 hours, the same lifetime as an Entity checkout batch. The Stripe Checkout Session and the Razorpay order (expires_at) expire at the same time. When it expires, Nozle:
  • cancels the incomplete subscription with cancelation_reason: timeout and sends subscription.canceled;
  • closes the payment-gated invoice;
  • returns the coupons, credit-note credits and wallet credits that invoice used;
  • expires the Stripe Checkout Session or Razorpay order that is still open.
A checkout that was paid in time is activated instead. While the provider is still processing a payment, the checkout is kept and checked again an hour later. A checkout whose payment fails also closes its invoice and returns the same credits.

Upgrade checkout that ran out of time

Before a paid upgrade checkout whose time ran out is reported expired or replaced, Nozle asks the payment provider what became of it:
  • Closed without a payment (a Stripe Checkout Session that is still open is expired at Stripe first, or a Razorpay order without a payment): the upgrade expires, and a new checkout opens a new session or order.
  • Paid in time, webhook late: Nozle settles the payment as the webhook would and applies the upgrade. No second checkout is offered, so the customer is not charged twice.
  • Payment still processing, or the provider cannot be reached: nothing changes and a new checkout answers 409 plan_upgrade_checkout_in_progress. Poll the checkout status and try again later.
Once a late payment has been applied, a new checkout of the same plan answers 422 plan_already_active, with Stripe and Razorpay alike. If the checkout request that found the late payment asked for another plan, it answers 409 plan_upgrade_checkout_in_progress with the checkout_id of the paid checkout; reload the subscription before you start the next change.

Credit-funded response

When no external payment remains due:
A completed result has payment_source credits when the change was funded without an external payment (including a free-to-free change, which still creates a zero-amount invoice) and none when nothing was invoiced. Entity checkouts add external_entity_id and external_subscription_id to every result.

Scheduled response

subscription_id is the pending subscription; there is no separate pending_subscription_id field. A new scheduled change while a change is pending replaces that change and returns a new pending ID. A paid upgrade checkout is refused with 409 subscription_change_pending while a change is pending: withdraw it first. See Paid upgrades: current limits. effective_at is an RFC 3339 timestamp rendered in the customer’s timezone. For a customer in Asia/Kolkata it reads 2026-11-01T00:00:00.000000+05:30, which is the same instant as 2026-10-31T18:30:00Z. Parse it as a timestamp and do not assume a trailing Z. A scheduled change takes effect at that instant: the active subscription is terminated and the pending subscription becomes active, keeping its billing_time. These limits were observed on a paid upgrade checkout that returns a stripe result with plan_upgrade_intent_id. Configure around them until Core is fixed.
  • Invoice grace period. If the billing entity has an invoice grace period above 0 days, the upgrade invoice stays a draft. The customer’s payment is still collected, but Nozle cannot apply the upgrade. With credit_action: "credit" the upgrade ends in needs_review with plan_upgrade_amount_changed, because the quoted amount credited the unused time and the draft invoice did not. Without a credit, the payment handler fails with payment_status_update_on_draft_invoice and the upgrade stays pending. Set the grace period to 0 for billing entities that sell paid upgrades, or refund and reconcile an affected payment by hand.
  • Pending change. A paid upgrade checkout is refused with 409 subscription_change_pending while the subscription has a scheduled cancellation or a scheduled plan change. Withdraw the pending downgrade with POST /subscriptions/transitions/withdraw, or keep the subscription, then start the upgrade. Cancelling at period end, scheduling a plan change or applying an immediate downgrade cancels an open upgrade checkout: its Stripe Checkout Session or Razorpay order is expired. A payment the provider is already processing is left to settle.
  • Status. Poll the upgrade with GET /checkout/{plan_upgrade_intent_id}?customer_id=…&subscription_id=… (Engine names). status is awaiting_payment until payment, then succeeded with fulfillment_status succeeded and an invoice_id. needs_review means the payment was taken but the upgrade was not applied. See Subscription self-service.
  • Failed upgrades leave nothing behind. A quoted upgrade with nothing to pay that fails after the change was prepared (for example because the grace period keeps its invoice a draft) is rolled back: the subscription stays on its plan, no credit note is created, and the same request can be retried once the cause is fixed. An upgrade whose credit for the unused time of the old plan cannot be computed (the credit usage of a plan family interval switch is unavailable) fails with 503 credit_usage_unavailable and applies nothing, instead of applying without the credit. Retry later.
With a grace period of 0 and no pending change, a credited upgrade applies as quoted: the old subscription is terminated, the new one is active, and billing_time is unchanged.

Errors

The Engine checks the request in this order: valid JSON body, customer scope, the subscription_id combinations, the required fields, then return_url, then the Idempotency-Key length. Only then is the request sent to Core, which returns the other errors below.

Webhook requirement

Stripe must be able to reach your Nozle public API URL. When a Stripe provider is connected, Nozle registers the Stripe webhook endpoint for that tenant’s Stripe account:
Checkout relies on these Stripe events:
  • payment_intent.succeeded
  • payment_intent.payment_failed
  • payment_intent.canceled
The same endpoint also receives setup_intent.succeeded, payment_method.detached, customer.updated, charge.refund.updated and charge.dispute.closed. See the Stripe integration. Razorpay webhooks are configured by you in the Razorpay dashboard. See the Razorpay integration for the URL, headers, and events.