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

# Create Checkout Session

> Lock product version, pricing, and currency for checkout

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

```
POST /v1/actions/checkout/create-session
```

**Authentication:** API Key or Store Slug

## Request Body (API Key)

| Field                                             | Type      | Required | Description                                                                                                                                                                                                                                                                                                                                                                                                                                                 |
| ------------------------------------------------- | --------- | -------- | ----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| `productId`                                       | string    | Yes      | Product ID in Short ID format (`PROD_xxx`)                                                                                                                                                                                                                                                                                                                                                                                                                  |
| `currency`                                        | string    | Yes      | ISO 4217 currency code (e.g. `USD`, `EUR`, `JPY`)                                                                                                                                                                                                                                                                                                                                                                                                           |
| `priceSnapshot`                                   | object    | No       | Override pricing at session creation (API Key only, see below)                                                                                                                                                                                                                                                                                                                                                                                              |
| `withTrial`                                       | boolean   | No       | Enable trial period (subscription products only)                                                                                                                                                                                                                                                                                                                                                                                                            |
| `buyerEmail`                                      | string    | No       | Pre-fill customer's email on the checkout page                                                                                                                                                                                                                                                                                                                                                                                                              |
| `billingDetail`                                   | object    | No       | Pre-fill billing details (see below)                                                                                                                                                                                                                                                                                                                                                                                                                        |
| `successUrl`                                      | string    | No       | Override redirect URL after successful payment                                                                                                                                                                                                                                                                                                                                                                                                              |
| `expiresInSeconds`                                | number    | No       | Session TTL in seconds (default: `2700` = 45 minutes, API Key only)                                                                                                                                                                                                                                                                                                                                                                                         |
| `darkMode`                                        | boolean   | No       | Enable dark mode for the checkout page                                                                                                                                                                                                                                                                                                                                                                                                                      |
| `metadata`                                        | object    | No       | Custom key-value pairs attached to the session                                                                                                                                                                                                                                                                                                                                                                                                              |
| `orderMerchantExternalId`                         | string    | No       | Your business-side order reference (max 128 chars).                                                                                                                                                                                                                                                                                                                                                                                                         |
| `language`                                        | string    | No       | Cashier UI language (IETF BCP 47). Must be one of the supported enum values: en, pt-BR, es-MX, id-ID, vi-VN, ru-RU, en-KE, es-PE, es-CO, es-CL, zh-Hant-TW, zh-Hant-HK, th-TH, ja-JP, en-NG, ko-KR, en-HK, zh-Hans-HK, pl-PL, tr-TR, zh-Hans, ms-MY. Sets the checkout page's default language; the customer can switch it on the page.                                                                                                                     |
| `includePaymentMethods` / `excludePaymentMethods` | string\[] | No       | Mutually exclusive — pass at most one. **include** is a whitelist: offer only these (`card`, `applepay`, `googlepay`, `wechat`), and every value must be supported by the product type × currency pair. **exclude** is a blacklist: offer everything the currency supports except these, with values the currency does not offer silently ignored, so one blacklist can be reused across currencies. Omit both to offer every method the currency supports. |

## Request Body (Store Slug)

| Field           | Type    | Required | Description                                                                                                                                                                                                                                                                                                                             |
| --------------- | ------- | -------- | --------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| `productId`     | string  | Yes      | Product ID in Short ID format (`PROD_xxx`)                                                                                                                                                                                                                                                                                              |
| `currency`      | string  | Yes      | ISO 4217 currency code                                                                                                                                                                                                                                                                                                                  |
| `buyerEmail`    | string  | No       | Pre-fill customer's email                                                                                                                                                                                                                                                                                                               |
| `billingDetail` | object  | No       | Pre-fill billing details (see below)                                                                                                                                                                                                                                                                                                    |
| `successUrl`    | string  | No       | Override redirect URL after successful payment                                                                                                                                                                                                                                                                                          |
| `darkMode`      | boolean | No       | Enable dark mode for the checkout page                                                                                                                                                                                                                                                                                                  |
| `language`      | string  | No       | Cashier UI language (IETF BCP 47). Must be one of the supported enum values: en, pt-BR, es-MX, id-ID, vi-VN, ru-RU, en-KE, es-PE, es-CO, es-CL, zh-Hant-TW, zh-Hant-HK, th-TH, ja-JP, en-NG, ko-KR, en-HK, zh-Hans-HK, pl-PL, tr-TR, zh-Hans, ms-MY. Sets the checkout page's default language; the customer can switch it on the page. |

<Note>
  Store Slug authentication does not support `priceSnapshot`, `expiresInSeconds`, `metadata`, `orderMerchantExternalId`, `withTrial`, `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.
</Note>

<Note>
  **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.
</Note>

## Price Snapshot Object

| Field         | Type    | Required | Description                                                      |
| ------------- | ------- | -------- | ---------------------------------------------------------------- |
| `amount`      | string  | Yes      | Price as a display format string (e.g., "29.00" = \$29.00)       |
| `taxIncluded` | boolean | No       | Whether the amount already includes tax (default: `false`)       |
| `taxCategory` | string  | Yes      | Tax category for tax calculation (e.g., `saas`, `digital_goods`) |

## Billing Detail Object

| Field          | Type    | Required | Description                                                                                                                                                  |
| -------------- | ------- | -------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------ |
| `country`      | string  | Yes      | ISO 3166-1 alpha-2 country code (e.g. `US`, `JP`, `DE`)                                                                                                      |
| `isBusiness`   | boolean | Yes      | Whether this is a business purchase                                                                                                                          |
| `postcode`     | string  | No       | Postal or ZIP code                                                                                                                                           |
| `state`        | string  | No       | State or province code (e.g. `CA`, `NY`, `ON`). Whether a given billing country needs it is reported by the `preview-tax` endpoint in `rules.requiredFields` |
| `businessName` | string  | No       | Legal business name. Supply it alongside `isBusiness: true` so it appears on the invoice                                                                     |
| `taxId`        | string  | No       | Tax registration number, e.g. an EU VAT number. Whether a given billing country needs it is reported by the `preview-tax` endpoint in `rules.requiredFields` |

<Warning>
  Passing `billingDetail` couples the cashier to the order's billing country: it then offers only that country's payment market, and the buyer cannot switch markets. The country that applies is the one on the finished order, not the one you sent. A billing country outside the payment markets we cover applies no restriction.

  Check this against the payment methods your store allows before you enable it — if the coupled market offers none of them, the order cannot be paid. Omit `billingDetail` to leave the cashier unrestricted; that is the default.
</Warning>

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

**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

<CodeGroup>
  ```typescript SDK theme={"system"}
  import { WaffoPancake } from "@waffo/pancake-ts";

  const client = new WaffoPancake({
    merchantId: process.env.WAFFO_MERCHANT_ID!,
    privateKey: process.env.WAFFO_PRIVATE_KEY!,
  });

  const session = await client.checkout.createSession({
    productId: "PROD_7J3K5L8M2N4P6Q9R",
    currency: "USD",
    buyerEmail: "customer@example.com",
    language: "en",
    successUrl: "https://example.com/thank-you",
  });

  console.log(session.sessionId);   // "cs_550e8400-e29b-41d4-a716-446655440000"
  console.log(session.checkoutUrl); // "https://checkout.waffo.ai/..."
  console.log(session.expiresAt);   // "2026-01-22T10:30:00.000Z"
  ```

  ```bash cURL (API Key) theme={"system"}
  curl -X POST https://api.waffo.ai/v1/actions/checkout/create-session \
    -H "Content-Type: application/json" \
    -H "X-Merchant-Id: MER_2D5F8G3H1K4M6N9P" \
    -H "X-Timestamp: 1711800000" \
    -H "X-Signature: BASE64_RSA_SIGNATURE" \
    -d '{
      "productId": "PROD_7J3K5L8M2N4P6Q9R",
      "currency": "USD",
      "buyerEmail": "customer@example.com",
      "language": "en",
      "successUrl": "https://example.com/thank-you"
    }'
  ```

  ```bash cURL (Store Slug) theme={"system"}
  curl -X POST https://api.waffo.ai/v1/actions/checkout/create-session \
    -H "Content-Type: application/json" \
    -H "X-Store-Slug: my-store-abc123" \
    -H "X-Environment: test" \
    -d '{
      "productId": "PROD_7J3K5L8M2N4P6Q9R",
      "currency": "USD"
    }'
  ```

  ```python Python theme={"system"}
  import requests

  response = requests.post(
      "https://api.waffo.ai/v1/actions/checkout/create-session",
      headers={
          "Content-Type": "application/json",
          "X-Merchant-Id": "MER_2D5F8G3H1K4M6N9P",
          "X-Timestamp": "1711800000",
          "X-Signature": "BASE64_RSA_SIGNATURE",
      },
      json={
          "productId": "PROD_7J3K5L8M2N4P6Q9R",
          "currency": "USD",
          "buyerEmail": "customer@example.com",
          "language": "en",
      },
  )

  data = response.json()["data"]
  print(data["checkoutUrl"])
  ```
</CodeGroup>

## Success Response (200)

```json theme={"system"}
{
  "data": {
    "sessionId": "cs_550e8400-e29b-41d4-a716-446655440000",
    "checkoutUrl": "https://checkout.waffo.ai/my-store-abc123/checkout/cs_550e8400-e29b-41d4-a716-446655440000",
    "expiresAt": "2026-01-22T10:30:00.000Z"
  }
}
```

## Response Fields

| Field         | Type   | Description                                                   |
| ------------- | ------ | ------------------------------------------------------------- |
| `sessionId`   | string | Checkout session ID (`cs_` + UUID format, not a Short ID)     |
| `checkoutUrl` | string | Full URL to redirect the customer to the hosted checkout page |
| `expiresAt`   | string | ISO 8601 timestamp when the session expires                   |

## Errors

> **Retry policy:** Never retry 4xx — fix the request and resubmit. Retry 5xx with exponential backoff (start 5s, max 3 attempts).

| Status | `errors[0].message`                                                                     | What it means                                                                                                                     | Recommended handling                                      |
| ------ | --------------------------------------------------------------------------------------- | --------------------------------------------------------------------------------------------------------------------------------- | --------------------------------------------------------- |
| 400    | `Missing or invalid header: x-context-environment`                                      | Environment header missing or not `test` / `prod`                                                                                 | Fix the header, resubmit                                  |
| 400    | `Invalid JSON body`                                                                     | Request body is not valid JSON                                                                                                    | Fix the body, resubmit                                    |
| 400    | `Missing required fields: productId, currency`                                          | `productId` or `currency` is missing                                                                                              | Fix the body, resubmit                                    |
| 400    | `Invalid currency format: "X". Must be 3 uppercase letters (e.g., "USD", "EUR", "JPY")` | Currency is not 3 uppercase letters (ISO 4217)                                                                                    | Use a valid ISO 4217 code, resubmit                       |
| 400    | `Invalid language, must be one of: en, pt-BR, … ms-MY`                                  | `language` is not one of the supported cashier languages                                                                          | Use a supported `language`, or omit it                    |
| 400    | `includePaymentMethods and excludePaymentMethods are mutually exclusive`                | Both lists were provided                                                                                                          | Pass only one of them                                     |
| 400    | `Invalid includePaymentMethods, must be one of: card, applepay, googlepay, wechat`      | A value outside the enum                                                                                                          | Use supported values, or omit the field                   |
| 400    | `Currency X is not supported for onetime payments`                                      | The currency is outside the payment method matrix (one-time: USD, EUR, GBP, HKD, JPY, CNY; subscription: USD, EUR, GBP, HKD, JPY) | Use a supported currency                                  |
| 400    | `Payment methods not supported for X (onetime): wechat`                                 | `includePaymentMethods` asks for a method this product type × currency does not offer                                             | Drop the unsupported methods                              |
| 400    | `No payment method remains for X (onetime) after applying excludePaymentMethods`        | The blacklist removed everything                                                                                                  | Exclude fewer methods                                     |
| 400    | `Expected format: PROD_xxx, got "..."`                                                  | `productId` Short ID could not be decoded                                                                                         | Fix `productId` format, resubmit                          |
| 400    | `orderMerchantExternalId must not be empty`                                             | Field was provided but is empty after trim                                                                                        | Omit the field or pass a non-empty value                  |
| 400    | `orderMerchantExternalId must be at most 128 characters`                                | Value exceeds 128 chars                                                                                                           | Shorten to 128 chars or less                              |
| 400    | `Currency X is not supported for this product`                                          | Product has no price snapshot for the requested currency                                                                          | Use a currency supported by the product                   |
| 401    | `Unauthorized`                                                                          | Invalid API Key signature or Store Slug                                                                                           | Verify auth headers                                       |
| 403    | `Store is not active`                                                                   | Store status is not `active`                                                                                                      | Activate the store first                                  |
| 403    | `Store is not accepting payments`                                                       | In the `prod` environment, the platform has not enabled payment acceptance (`payin_enable`) for this store                        | Platform-side switch — contact Waffo support              |
| 403    | `Store is not approved for production payments`                                         | In the `prod` environment, the store has not passed production approval (`prod_enabled`, i.e. KYB review)                         | Use the `test` environment, or complete KYB review        |
| 403    | `Product does not belong to this store`                                                 | Product is not owned by the visiting store                                                                                        | Verify the product/store pair                             |
| 404    | `Store not found`                                                                       | No store matches the given slug                                                                                                   | Verify slug and environment                               |
| 404    | `Product not found`                                                                     | Product does not exist                                                                                                            | Verify the `productId`                                    |
| 404    | `Product not found or not active for this environment`                                  | Product has no active version for the given environment                                                                           | Activate a product version for this environment           |
| 500    | `Internal server error`                                                                 | Unexpected server-side failure                                                                                                    | Retry with exponential backoff (start 5s, max 3 attempts) |

<Note>
  `payin_enable` and `prod_enabled` are two independent conditions: `payin_enable` is a platform-controlled switch on payment acceptance, `prod_enabled` is the production-approval (KYB) gate. Either one alone yields a 403 in the `prod` environment. Neither applies in the `test` environment.
</Note>

<Tip>
  The default session TTL is 45 minutes (2700 seconds). When using API Key auth, you can customize this with `expiresInSeconds`. Sessions lock the product version and pricing at creation time, so price changes will not affect existing sessions.
</Tip>
