> ## Documentation Index
> Fetch the complete documentation index at: https://docs.nozle.dev/llms.txt
> Use this file to discover all available pages before exploring further.

# Create Checkout Session

Create a payment-gated plan checkout from a trusted merchant backend.

**Auth:** secret key only. Publishable keys are always rejected.

<Info>
  The merchant backend derives `customer_id` from its authenticated user or team. Do not trust a browser-provided customer ID. The response contains a hosted URL or Stripe Checkout `client_secret`, or reports an immediately completed or scheduled transition.
</Info>

## Modes

### Plan checkout

Use this when a customer is selecting a plan.

<ParamField body="customer_id" type="string" required>
  External customer ID. The customer must already exist.
</ParamField>

<ParamField body="plan_code" type="string" required>
  The plan code to subscribe the customer to (e.g. `pro`).
</ParamField>

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 Stripe confirms payment.

If prepaid credits fully cover a paid plan change, Nozle applies it without Stripe and returns `type: "completed"`. Free plans may also complete immediately. A downgrade or other cycle-end transition returns `type: "scheduled"`; browser callers still use this same endpoint.

## Return URL

<ParamField body="return_url" type="string">
  URL Stripe should return to after embedded checkout completes. It must use an exactly allowlisted HTTPS origin. If omitted, Nozle uses the configured dashboard return URL.
</ParamField>

<ParamField body="success_url" type="string">
  Backward-compatible alias for `return_url`.
</ParamField>

## Request examples

Plan checkout:

```json theme={null}
{
  "customer_id": "customer_123",
  "plan_code": "growth",
  "return_url": "https://app.example.com/settings/billing"
}
```

## Response

<ResponseField name="type" type="string" required>
  `stripe`, `completed`, or `scheduled`.
</ResponseField>

<ResponseField name="client_secret" type="string">
  Stripe Checkout Session client secret. Pass this to `stripe.initEmbeddedCheckout()`.
</ResponseField>

<ResponseField name="url" type="string">
  Hosted checkout URL. Navigate the current page to this URL.
</ResponseField>

<ResponseField name="clientSecret" type="string">
  Camel-case alias for `client_secret`.
</ResponseField>

<ResponseField name="invoice_id" type="string">
  Nozle invoice ID being paid.
</ResponseField>

<ResponseField name="amount_cents" type="integer">
  Amount due for the checkout session, in the invoice currency's smallest unit.
</ResponseField>

<ResponseField name="currency" type="string">
  Invoice currency.
</ResponseField>

### Credit-funded response

When no external payment remains due:

```json theme={null}
{
  "type": "completed",
  "status": "succeeded",
  "payment_source": "credits",
  "subscription_id": "subscription_uuid",
  "plan_code": "pro",
  "invoice_id": "invoice_uuid",
  "amount_cents": 0,
  "currency": "USD"
}
```

### Scheduled response

```json theme={null}
{
  "type": "scheduled",
  "status": "pending",
  "subscription_id": "subscription_uuid",
  "plan_code": "starter"
}
```

## 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:

```text theme={null}
{NOZLE_PUBLIC_API_URL}/webhooks/stripe/{organization_id}?code={stripe_provider_code}
```

Checkout relies on these Stripe events:

* `payment_intent.succeeded`
* `payment_intent.payment_failed`
* `payment_intent.canceled`
