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

# Prefilled checkout

> Create a cart, its lines, the buyer identity and a hosted checkout session in one Checkout SDK call with createPrefilledCheckout().

# Prefilled checkout

`createPrefilledCheckout()` does in one request what the [basic redirect flow](./overview#basic-redirect-flow) does in five: it creates a cart, adds its lines, sets the buyer identity, prices the cart with final tax, and opens a hosted checkout session. Use it when you already know who is buying what — a booking confirmed in your own system, an invoice, a seat reservation — and want to send the buyer straight to payment.

```ts theme={"theme":{"light":"github-light","dark":"github-dark"}}
import { CopeCart } from "cope-sdk"

const cope = new CopeCart({
  publishableKey: "cope_pk_live_...",
})

const handoff = await cope.createPrefilledCheckout({
  externalReference: "booking-4711",
  currency: "EUR",
  line: { productId: "prod_...", quantity: 2 },
  buyerIdentity: {
    email: "buyer@example.com",
    country: "DE",
    postal_code: "10115",
  },
  checkout: {
    success_url: "https://your-site.example/thank-you",
    cancel_url: "https://your-site.example/booking/4711",
    consents: [{ type: "buyer_tos" }],
  },
})

cope.redirectToCheckout(handoff)
```

To keep the buyer on your page, pass `embed_origin: window.location.origin` in `checkout` and mount the result with `mountCheckout()` exactly as in the [embedded checkout guide](./embedded-checkout).

## Input

| Field | Required | Notes |
| - | - | - |
| `externalReference` | Yes | Your identifier for this purchase. The SDK derives the request's `Idempotency-Key` from it (see [retries and idempotency](#retries-and-idempotency)), and COPE stores it on the cart as the `external_reference` metadata key, so it comes back on the order's webhooks. |
| `currency` | Yes | The cart's currency, for example `EUR`. |
| `line` or `lines` | Yes, exactly one | `line` takes one line; `lines` takes a non-empty array. Each line has `productId` (the product's `prod_` ID), an optional `planId` (a payment plan's integer `id` from `getProduct()`; omit it to use the product's first enabled plan) and an optional `quantity` (default `1`). All lines must belong to the same seller. |
| `buyerIdentity` | Yes | The same payload as `setBuyerIdentity()`. `email`, `country` and `postal_code` are required here. |
| `checkout` | Yes | The same payload as `checkout()`: `consents`, and optionally `success_url`, `cancel_url` and `embed_origin`. |
| `locale` | No | The checkout language. It must be one your business has enabled. |
| `metadata` | No | Your own reconciliation data, as described in [your own reference on an order](./overview#your-own-reference-on-an-order). `external_reference` and `intended_payment_method` are filled from the matching fields of this input. |
| `intendedPaymentMethod` | No | Stored as the `intended_payment_method` metadata key for your own reconciliation. It does not preselect a payment method. |
| `attribution` | No | Affiliate and UTM attribution, the same object `createCart()` takes. |

Prices always come from your catalogue: the request names products and plans, never amounts.

`success_url` and `cancel_url` are checked against the URLs you registered under the [redirect URL rules](./overview#redirect-urls), but not refused: where `checkout()` answers an unregistered URL with `422 invalid_redirect_url`, this call drops it and creates the checkout without it. Omit a field to use the first URL you registered for it. **Register at least one success URL and one cancel URL before you use this call:** while none is registered for a field, this call accepts whatever URL it is sent for that field, including one sent by anyone who has copied your publishable key.

## Result

| Field | Notes |
| - | - |
| `checkoutUrl`, `embedCheckoutUrl`, `embedOrigin` | Pass the result to `redirectToCheckout()` or `mountCheckout()`. |
| `checkoutId`, `checkoutToken` | The checkout session. The token opens it, so treat it like the checkout URL. |
| `status`, `expiresAt` | A new session is `open` and expires 30 minutes after it is created. |
| `totals` | The final totals, tax included, in integer minor units (`*_minor_units`). |
| `cartId`, `cartVersion` | For your logs. The response carries no cart secret, so this SDK instance cannot change the cart afterwards: create a new prefilled checkout instead. |

## Retries and idempotency

The SDK sends an `Idempotency-Key` derived from `externalReference`: `prefilled:<externalReference>:cart` when the reference is at most 120 characters of letters, digits, `.`, `_`, `:` and `-`, and otherwise `prefilled#<SHA-256 of the reference, in hex>:cart`. Two different references never share a key.

For 24 hours, repeating the call with the same reference and the same input returns the original checkout instead of creating a second one — including after the buyer has paid, so a repeated call cannot open a second payable checkout for the same reference. The SDK uses this to retry a request that timed out or failed with a `5xx` once by itself. While the first request is still being processed, a repeat is refused with `409 idempotency_conflict`; wait briefly and repeat it. Within those 24 hours, the same reference with **different** input is refused with `422 idempotency_body_mismatch`, so use a new reference for a new purchase. After 24 hours the same reference creates a new cart and checkout.

A refusal listed under [errors](#errors) is not stored against its key: correct the input or wait for the condition to clear, and call again with the same reference. An unexpected `500` is different: the request may have been interrupted part-way, and the same reference can then answer `409 idempotency_conflict` for up to 24 hours. Use a new reference if that happens.

## Who can call it

The call is authenticated by your publishable key alone, sent as the `X-Cope-Key` header. The key is public by design — it is in your page's source — and the endpoint answers browsers on any origin (`Access-Control-Allow-Origin: *`). So anyone who copies your key can stage a prefilled checkout for any of your products, with any buyer details, metadata and affiliate attribution they choose, at your catalogue prices. Nothing in this call can apply a discount or change a price: the buyer still pays your catalogue price before an order exists.

Treat what arrives on the order's webhooks accordingly. `metadata`, `external_reference`, `attribution` and the buyer identity were supplied by whoever made the call, so match the order against your own record — by `external_reference` or the order ID — before you fulfil anything, as described in [a link's value belongs to the buyer](./overview#a-links-value-belongs-to-the-buyer).

## HTTP request

The SDK sends `POST /api/cart/v1/prefilled-checkouts` with `X-Cope-Key` and `Idempotency-Key` headers and the input in snake\_case:

```json theme={"theme":{"light":"github-light","dark":"github-dark"}}
{
  "external_reference": "booking-4711",
  "currency": "EUR",
  "lines": [{ "product_id": "prod_...", "quantity": 2 }],
  "buyer_identity": { "email": "buyer@example.com", "country": "DE", "postal_code": "10115" },
  "checkout": {
    "success_url": "https://your-site.example/thank-you",
    "cancel_url": "https://your-site.example/booking/4711",
    "consents": [{ "type": "buyer_tos" }]
  }
}
```

A plan is named by `plan_id`, an integer. A successful call answers `201 Created`.

## Errors

Refusals use the cart API's `errors` envelope described in [errors](./overview#errors), and the SDK raises them as `CopeApiError`.

| Status | `code` | Meaning |
| - | - | - |
| `401` | `invalid_sdk_key` | The publishable key is missing, unknown or inactive. |
| `403` | `business_checkout_hold` | The business cannot accept checkout payments right now. |
| `422` | `validation_error` | A required field is missing or malformed; `field` names it (for example `buyer_identity.postal_code`). |
| `422` | `not_found` | A `product_id` or `plan_id` does not name one of your products or that product's plans. |
| `422` | `not_saleable` | The product or plan cannot be bought; `block_reason` says why when it is known. |
| `422` | `mixed_seller_cart` | The lines belong to more than one seller. |
| `422` | `product_unavailable`, `insufficient_stock` | A product is not available in the requested quantity. |
| `422` | `below_minimum_charge`, `above_maximum_charge` | The total is outside what the payment provider accepts. |
| `422` | `embed_origin_not_allowed` | `embed_origin` is not a registered embed origin. |
| `422` | `unsupported_checkout_locale`, `checkout_locale_not_allowed` | `locale` is unknown, or not enabled for your business. |
| `422` | `tax_address_invalid`, `tax_request_rejected` | Tax could not be calculated for the buyer's address. |
| `503` | `tax_service_unavailable` | Tax calculation is temporarily unavailable. Nothing was created; call again later with the same reference. |
| `409` | `idempotency_conflict` | A request with the same reference is still being processed. |
| `422` | `idempotency_body_mismatch` | The same reference was used with different input within 24 hours. |

An invalid phone number does not refuse the call: the checkout is created without it, and the raw response carries the problem in `field_errors`.


This documentation is built and hosted on [Mintlify](https://mintlify.com), a developer documentation platform.