> For clean Markdown of any page, append .md to the page URL.
> For a complete documentation index, see https://docs.todaytixgroup.com/llms.txt.
> For AI client integration (Claude Code, Cursor, etc.), connect to the MCP server at https://docs.todaytixgroup.com/_mcp/server.

# Purchase Flow

Six steps (one optional) to browse, reserve, and purchase tickets via the TTG API, then what to do after checkout.

```
List Events → Browse Occurrences → Browse Inventory → Create Cart → Add Extras (optional) → Checkout → After Checkout
```

> This page documents direct v3 API integration. For TTG-hosted checkout, see [Whitelabel Checkout](https://developers.todaytixgroup.com/docs/checkout). For agent/reseller flows on the v1/v4 stack, see [Powered by TodayTix Agent APIs](https://developers.todaytixgroup.com/docs/pxt-agent-apis).

## 1. List Events

```
GET /api/v3/events?locationId={locationId}
```

Returns a paginated list of events the authenticated retailer has access to. The `locationId` query parameter is required — it filters events by market (e.g., New York, London). Add `query` to narrow the list to events matching a search term.

Use `limit` and `offset` for pagination. Each event includes `id`, `name`, `startDate`, `endDate`, and `location`.

## 2. Browse Occurrences

```
GET /api/v3/events/{eventId}/occurrences
```

Returns the event's occurrences — its dates and times — with pricing summaries, sold-out dates included, 50 to a page unless you set `limit` (up to 1000): page with `offset` until you have `pagination.total` (see [Pagination](/guides/api-conventions#pagination)). The exception: when the event or your retailer account is set to list only dates with something on sale (for example, to hide dates that are not announced yet), every date with nothing to buy is left out. Use `availabilityLevel` to drive the calendar UI: `SOLD_OUT` (nothing to buy on this date — `fromPrice` is also null), `LOW` (tickets are running out), or `null` (no signal — display normally). Treat any other value as no signal. A null `fromPrice` means nothing is on sale for this occurrence (sold out, off sale, or not yet on sale); it can't be bought.

The discount fields represent the best available discount across all price options: `maxDiscountAmount` whenever there is a discount, and `maxDiscountPercentage` too on events that show savings as a percentage. They may come from a different price option than `fromPrice`.

## 3. Browse Inventory

```
GET /api/v3/events/{eventId}/occurrences/{occurrenceId}/inventory-items
```

Returns purchasable items, 50 to a page unless you set `limit` (up to 1000). A large venue has more seats than that, so page with `offset` until you have `pagination.total` before you draw a seat map. Use `inventoryVariant._type` to determine the type:

| Type | Description | Quantity Rule |
|---|---|---|
| `AssignedSeat` | Specific seat (section, row, seat) | Must be `1` |
| `GeneralAdmission` | GA area | `1` to `quantityAvailable` |

Keep the number of tickets in one order within the event's `orderLimit` (`min` to `max`), which the occurrence also carries. A GA item with `quantityAvailable: 0` is a sold-out area kept on display: it has no `priceOptions`, so show it as unavailable and don't add it to a cart.

For full details on each type, attributes, and pricing, see [Inventory Types](/guides/inventory-types).

**Party size:** send `?quantity=` with the number of tickets the customer wants. An assigned seat with `selectionGroup` can be picked: add every seat in the group to the cart together. Any other seat is unavailable at that party size. See [Seats for a party](/guides/inventory-types#seats-for-a-party).

**Pricing:** `sellPrice` is the all-in price customers pay (fees and tax included). `listPrice` is the price before a discount — display it as strikethrough. [Price Options](/guides/inventory-types#price-options) says when each is sent.

Example response (trimmed):

```json
{
  "data": [
    {
      "id": "v1_eyJwcm92aWRlcklkIjoxMDAwLCJzZWF0SWQiOiJPUkNIRVNUUkEtQS0xMDEifQ",
      "inventoryVariant": {
        "_type": "AssignedSeat",
        "id": "ORCHESTRA-A-101",
        "name": "Orchestra A 101",
        "section": "Orchestra",
        "row": "A",
        "seat": "101"
      },
      "quantityAvailable": 1,
      "priceOptions": [
        {
          "id": "eyJwcm92aWRlckNvZGUiOiJGVUxMUFJJQ0UiLCJob2xkQ29kZSI6IjE1NzM5In0",
          "category": { "name": "Adult" },
          "listPrice": { "amount": 15000, "currency": "USD" },
          "sellPrice": { "amount": 12500, "currency": "USD" }
        }
      ]
    }
  ]
}
```

## 4. Create Cart

```
POST /api/v3/carts
```

Reserves inventory and starts a hold timer. All items must be from the same occurrence. A selection that would leave a single seat on its own returns `409 ORPHANED_SEATS`; ask the customer to choose different seats. The cart is created in a currency — the event's base currency unless the body names a `currency` — and carries it on the response. To pay in another currency, `PATCH` the cart before checkout (see [Currency](/guides/api-conventions#currency)).

```json
{
  "items": [
    {
      "occurrenceId": "456",
      "inventoryItemId": "v1_eyJwcm92aWRlcklkIjoxMDAwLCJzZWF0SWQiOiJPUkNIRVNUUkEtQS0xMDEifQ",
      "priceOptionId": "eyJwcm92aWRlckNvZGUiOiJGVUxMUFJJQ0UiLCJob2xkQ29kZSI6IjE1NzM5In0",
      "quantity": 1
    }
  ]
}
```

A cart is accepted with fewer tickets than the event's `orderLimit.min`, but placing the order for it returns `400 EVENT_MINIMUM_NOT_MET`, so keep the selector within `orderLimit` (see [Browse Inventory](#3-browse-inventory)) rather than letting the customer reach checkout with too few.

When the customer picks seats again, create a new cart and send the old cart's ID as `previousCartId`. Once the new cart is created, the old hold is released, so the customer doesn't hold two sets of seats. The release never fails the create: an old cart that is gone or already an order is left alone. When the request carries a customer access token, the other carts created with that same token are also released before the new one is held, whether or not you send `previousCartId`. This release is best-effort on the same terms: it never fails the create, a cart whose order is being placed is left alone, and carts the customer made with another token are not touched.

The response includes `expiresAt` (UTC) — display a countdown. When the cart expires, inventory is released and `GET /api/v3/carts/{cartId}` returns `410 CART_EXPIRED` (see [Cart state](/guides/error-handling#cart-state)). Create a new cart to continue.

To release held inventory early: `DELETE /api/v3/carts/{cartId}`. It returns `204`, also for a cart already deleted or expired. A cart ID that doesn't exist returns `404`, and a cart that became an order returns `409 CART_ALREADY_ORDERED`.

### Vouchers

A voucher is a promo or access code the customer types in. Apply it in one of two ways:

- **When you create the cart**, send it as `voucherCode` on `POST /api/v3/carts`. An access code that unlocks hidden seats must go here: applying it to a cart that already exists is too late to unlock them. Send the same `voucherCode` on the occurrences and `inventory-items` requests, so the customer sees the dates and seats it unlocks. A code that can't be used is ignored on `POST /carts`, with no error: the cart is created without it.
- **On a cart that already exists**, send it to `POST /api/v3/carts/{cartId}/vouchers` as `{ "code": "SUMMER20" }`. It returns `201` with the cart repriced, or `400 INVALID_VOUCHER` when the code can't be used. Some codes are limited to one use per customer: for a guest, add the customer's `email` so the code can be checked. `DELETE /api/v3/carts/{cartId}/vouchers/{code}` removes it.

The cart's `voucher` shows the code applied and its `discount`.

## 5. Add Extras (optional)

Some events offer extras — add-ons such as merchandise, drinks, or parking. The cart response lists them under `options.extras`, each in the same inventory-item shape as seats and GA (see [Inventory Types](/guides/inventory-types#extra)). The list is empty when the event offers none.

```json
{
  "data": {
    "options": {
      "extras": [
        {
          "id": "v1_eyJwcm92aWRlcklkIjoxLCJzZWF0SWQiOiIxOCJ9",
          "inventoryVariant": { "_type": "Extra", "id": "ICE CREAM", "name": "Ice Cream", "attributes": [] },
          "priceOptions": [
            { "id": "eyJpc0FkZE9uIjp0cnVlLCJwcmljZVR5cGVJZCI6IjExIn0", "category": { "name": "Adult" }, "sellPrice": { "amount": 4500, "currency": "USD" }, "maxQuantityPerOrder": 4 },
            { "id": "eyJpc0FkZE9uIjp0cnVlLCJwcmljZVR5cGVJZCI6IjEyIn0", "category": { "name": "Child" }, "sellPrice": { "amount": 1500, "currency": "USD" }, "maxQuantityPerOrder": 2 }
          ],
          "quantityAvailable": 20,
          "maxQuantityPerOrder": 6
        }
      ]
    }
  }
}
```

Add them to the cart in one request, each with its inventory item ID, one of its price option IDs and a quantity. Here the customer adds one Adult and one Child Ice Cream:

```
POST /api/v3/carts/{cartId}/items
```

```json
{
  "items": [
    {
      "occurrenceId": "456",
      "inventoryItemId": "v1_eyJwcm92aWRlcklkIjoxLCJzZWF0SWQiOiIxOCJ9",
      "priceOptionId": "eyJpc0FkZE9uIjp0cnVlLCJwcmljZVR5cGVJZCI6IjExIn0",
      "quantity": 1
    },
    {
      "occurrenceId": "456",
      "inventoryItemId": "v1_eyJwcm92aWRlcklkIjoxLCJzZWF0SWQiOiIxOCJ9",
      "priceOptionId": "eyJpc0FkZE9uIjp0cnVlLCJwcmljZVR5cGVJZCI6IjEyIn0",
      "quantity": 1
    }
  ]
}
```

Returns `201` with the updated cart: the seat (12500) and the two Ice Creams (4500 and 1500) make a `total` of 18500. Each extra appears in `items[]` with an `Extra` inventory variant, and in the cart total and `receiptLines[]`. The request is held as a unit: either every extra is added, or nothing changes and one error comes back. When one item is at fault (a malformed field, a price option the item's occurrence cannot take, an occurrence the cart cannot take), the error's `param` names it, for example `items[1].priceOptionId`. A refusal of the summed quantity — over a cap or the remaining availability — or by the venue's system names no item. Two items naming the same extra and price option, or an item naming an extra already on the cart, merge into one line with the quantities summed.

### Changing your mind

An extra already on the cart is addressed by the `id` of the line it came back as:

```
PATCH  /api/v3/carts/{cartId}/items/{itemId}   { "quantity": 2 }
DELETE /api/v3/carts/{cartId}/items/{itemId}
```

Both return `200` with the cart repriced. `items[].id` names the line, not the inventory — the same extra at two price options is two lines, each addressable on its own, so changing one leaves the other alone.

- `quantity` is the number to end up holding, up or down from the number held now. It is at least `1`: to drop the item, send `DELETE` rather than `PATCH` with `0`.
- Raising a quantity is subject to the same limits as adding: above `maxQuantityPerOrder` returns `400`, and a quantity that can no longer be held returns `409 INVENTORY_EXHAUSTED`.
- `DELETE` is idempotent: an `itemId` the cart does not hold returns the current cart. `PATCH` on one returns `404` with `details.resourceType` of `cartItem`.
- Dropping the last extra leaves the customer's tickets and hold expiry untouched.

Rules:

- Only extras can be added to, changed on, or removed from an existing cart. An `itemId` for seats or GA returns `400`. To change seats, create a new cart with `previousCartId` (see [Create Cart](#4-create-cart)); to hold nothing at all, `DELETE /api/v3/carts/{cartId}`.
- `occurrenceId` is the occurrence the item is for. Most extras run at the cart's occurrence — send that. An extra with its own schedule (a guided tour, parking) carries `requirements.occurrence` and runs at its own times — send the occurrence the customer picked for it, with the `priceOptionId` of the category they chose from that occurrence's `inventory-items`. That occurrence must start at or after `requirements.occurrence.earliestStart` and before `latestStart`; one outside those bounds returns `400 OCCURRENCE_NOT_COMPATIBLE`, and one no longer on sale returns `409 OCCURRENCE_NOT_AVAILABLE`. Any other occurrence returns `400 MIXED_OCCURRENCES_NOT_SUPPORTED`.
- Quantity is capped by the lowest of `quantityAvailable`, the item's `maxQuantityPerOrder`, and the price option's `maxQuantityPerOrder`. Asking for more returns `400`. When several items in one request name the same extra, the cap applies to their summed quantity.
- An extra with `minQuantityPerOrder` needs at least that many units, counted across its price options, by the time the order is placed. The cart accepts fewer while the customer edits it, so lines can be added, changed and removed in any order; placing an order that still holds fewer returns `400 MINIMUM_QUANTITY_NOT_MET`.
- If the extras can no longer be held by the time the request reaches the venue's system, the request returns `409 INVENTORY_EXHAUSTED`. If the cart has expired, it returns `410 CART_EXPIRED` — create a new cart.

## 6. Checkout

```
POST /api/v3/orders
Idempotency-Key: 3f8c1e2a-9d4b-4f6a-8b21-7c5e0a1d2e34
```

```json
{
  "cartId": "6aac14d3-5826-4da8-98b6-9e68c28629d6",
  "expectedTotal": { "amount": 18500, "currency": "USD" },
  "contact": {
    "name": "John Smith",
    "email": "john.smith@example.com",
    "phone": "+12125550123"
  },
  "payment": {
    "method": "INVOICE"
  }
}
```

Send an `Idempotency-Key` header: a UUID v4 you make for each attempt to buy. It is optional and strongly recommended, so a retry after a timeout never buys twice (see [Idempotency Keys](/guides/error-handling#idempotency-keys)). `contact.phone` is optional, in E.164 format, unless your retailer needs one on every order. To choose whether TTG emails the customer a confirmation, send `sendConfirmationEmail`: `false` to stop it, `true` to send it. Leave it out to use your retailer's default.

`expectedTotal` is the cart's `total`, echoed back exactly — same `amount`, same `currency`. `GET /api/v3/carts/{cartId}` returns:

```json
{ "data": { "total": { "amount": 18500, "currency": "USD" } } }
```

Send that object straight through as `expectedTotal` (as above) — don't recompute it. A value that doesn't match returns `400` with code `EXPECTED_TOTAL_MISMATCH`.

On success, the response is the order. Its tickets and barcodes are covered in [After Checkout](#7-after-checkout).

### Paying

Pick `payment.method` from the cart, not from your own code. `GET /api/v3/carts/{cartId}` lists the methods that cart can be paid with in `options.paymentMethods`. The one exception is a cart with nothing to pay: send `NONE`, which the cart never lists.

```json
{ "data": { "options": { "paymentMethods": [ { "method": "INVOICE" } ] } } }
```

| `method` | What it is | What the order sends with it |
|---|---|---|
| `INVOICE` | Billed to your account and settled later | Nothing else |
| `CARD` | A card, freshly entered or saved | `gateway` and `card` |
| `APPLE_PAY` | Apple Pay wallet token | `gateway` and `applePay` |
| `GOOGLE_PAY` | Google Pay wallet token | `gateway` and `googlePay` |
| `KLARNA` | Pay later; the shopper approves in Klarna's widget or on Klarna's own page | `gateway`, `klarna` and a return URL, over two calls |
| `AFTERPAY` | Pay later; the shopper approves on Afterpay's page | `gateway`, `afterpay` and a return URL, over two calls |
| `NONE` | Nothing to pay — the cart's `total.amount` is `0` | Nothing else |

The fields each method's sub-object takes are on the Place an order endpoint in the API Reference. `KLARNA` and `AFTERPAY` are listed only on a cart read with `country`, and take two calls: see [Klarna and Afterpay](#klarna-and-afterpay).

**`INVOICE`** is listed only when your retailer is set up to pay on account and the caller may pay on account: your secret key on its own, or your secret key together with an on-account access token. A publishable key is never offered `INVOICE`, and neither is a request that carries a customer's token, because the permissions then come from that customer. An order that sends `INVOICE` anyway is refused with `400 PAYMENT_METHOD_NOT_AVAILABLE` and nothing is charged (on a cart with nothing to pay, the refusal is `403 INSUFFICIENT_PERMISSIONS`). Send only the method. An `INVOICE` payment takes no other fields:

```json
"payment": { "method": "INVOICE" }
```

**`NONE`** is for a cart that costs nothing, for example when a voucher covers the whole order. Check `total.amount` and send `NONE` only when it is `0`. A voucher doesn't always bring the total to zero — on a cart priced in another currency it can still leave a balance — so don't guess from the voucher. `NONE` on a cart that still costs something is refused: with a non-zero `expectedTotal`, as `400 INVALID_FIELD_VALUE` on `payment.method`; with `0`, as `400 EXPECTED_TOTAL_MISMATCH`.

```json
{
  "cartId": "6aac14d3-5826-4da8-98b6-9e68c28629d6",
  "expectedTotal": { "amount": 0, "currency": "USD" },
  "contact": { "name": "John Smith", "email": "john.smith@example.com" },
  "payment": { "method": "NONE" }
}
```

An order that owes money and names a method the cart doesn't list is refused with `400 PAYMENT_METHOD_NOT_AVAILABLE`. The methods the cart takes are in `details.availablePaymentMethods`, and nothing is charged.

If the customer is checking out on a device you can see — a browser or a mobile app — describe it with the optional `X-TT-Device-Data` header, as JSON:

```
X-TT-Device-Data: {"device_id":"<your id for the browser or device>","device_type":"web","device_os":"ios"}
```

| Field | Value |
|---|---|
| `device_type` | `web` or `native` |
| `device_os` | operating system name, lowercase |
| `device_id` | your own identifier for the browser or device |

Sending it scores card payments for fraud on more signal, so fewer genuine customers are declined. A server-to-server integration with no customer device in the request path leaves it off; the order places normally without it.

#### Klarna and Afterpay

`KLARNA` and `AFTERPAY` are listed only when you read the cart with the `country` query parameter. Whether either can be offered depends on the shopper's country and the amount to pay. Once you know where the shopper is, read the cart again with `country` (an ISO 3166-1 alpha-2 code, e.g. `country=GB`). The parameter doesn't change the cart.

`KLARNA` takes two calls, because the shopper approves the purchase in Klarna's own widget:

1. Place the order with `method: KLARNA`, `gateway: ADYEN`, `klarna: { "country": "GB" }` — the same country you sent on the cart request — and `payment.options.threeDSecure.returnUrl`. Klarna is paid in that country, whatever the contact phone number is. It answers `402` with code `PAYMENT_AUTHENTICATION_REQUIRED` and `details.action` — a JSON string. Parse it and hand it to the Klarna SDK to show the widget.
2. When the shopper approves, place the order again, under a new `Idempotency-Key`, with the same fields plus the two values the SDK returns, in `payment.options.threeDSecure.details`: `authorization_token` and `paymentData`. Send `details` only on this second call — omit it entirely on the first.

To send the shopper to Klarna's own page instead of showing the widget — for example inside a mobile app, where Klarna does not allow its purchase flow in an embedded web view — add `"flow": "REDIRECT"` to `klarna` on both calls. The first call then answers with a `details.action` whose `type` is `redirect`, and the two calls follow the `AFTERPAY` steps below: send the shopper to the action's `url`, and on the second call send the `redirectResult` they come back with in `payment.options.threeDSecure.details`. Leaving `flow` out, or sending `WIDGET`, keeps the widget.

If the second call never arrives, the authorization Klarna is holding is released, so an abandoned checkout doesn't leave the shopper on the hook.

`AFTERPAY` also takes two calls, because the shopper approves the purchase on Afterpay's own page — Clearpay in the UK, Cash App Afterpay in the US:

1. Place the order with `method: AFTERPAY`, `gateway: ADYEN`, `afterpay: { "country": "GB" }` — the same country you sent on the cart request — and `payment.options.threeDSecure.returnUrl`. It answers `402` with code `PAYMENT_AUTHENTICATION_REQUIRED` and `details.action` — a JSON string whose `type` is `redirect`. Parse it and send the shopper to its `url`.
2. Afterpay sends the shopper back to your `returnUrl` with a `redirectResult` query parameter. Place the order again, under a new `Idempotency-Key`, with the same fields plus that value in `payment.options.threeDSecure.details`: `{ "redirectResult": "…" }`. Send `details` only on this second call — omit it entirely on the first.

The shopper pays the first installment when they approve, so the second call matters: if it is refused for good, or never arrives, the authorization Afterpay is holding is released and the installment is returned. A refusal you can fix leaves it open (see [Checkout errors](/guides/error-handling#checkout-post-orders)).

## 7. After Checkout

### Keep the cart ID

The `cartId` is the credential for the order it becomes: any integration with a TTG API key that holds it can read that order, including the customer's name, email address and tickets. Store it on your server with your own record of the order, and treat it like a password — don't put it in links or logs.

Your API key alone can't read an order: an integration reads one with the cart ID it was placed with. That includes orders a customer placed through TTG's own checkout, which you can read through the API only if you have their cart ID.

### Find out whether a checkout went through

After a timeout, a crash, or a `409 CART_ALREADY_ORDERED`, look the order up by its cart:

```
GET /api/v3/orders?cartId={cartId}
```

`data` holds the order the cart produced, or is empty if the cart never became an order. The order's `status` says what to do next:

| `status` | Meaning | What to do |
|---|---|---|
| `PROCESSING` | The payment is still settling | Look it up again shortly |
| `FAILED` | The payment didn't complete. This is final. | Let the customer pay again: place the order again with `POST /api/v3/orders` on the same cart, under a new `Idempotency-Key` (a key that failed answers `409`) |
| `COMPLETED` | The order is placed | Fetch the full order (below) |
| `CANCELLED` | The order was placed, then canceled. No credit voucher was issued | Fetch the full order to show the customer |
| `VOUCHER_ISSUED` | The order was placed, then canceled, and the customer was given a credit voucher | Fetch the full order: `cancellationVoucher` holds the voucher |
| `REFUNDED` | The order was placed, then refunded | Fetch the full order to show the customer |
| `null` | No payment status recorded yet | Treat it like `PROCESSING` |

### Fetch the full order

```
GET /api/v3/orders/{orderId}?cartId={cartId}
```

This returns an order once it is placed, canceled or refunded. While it is still settling, or after its payment failed, it answers `404` — use the lookup above instead.

### Cancel or refund an order

The API has no endpoint to cancel or refund an order. To cancel or refund one, contact TTG support. Fetch the order again afterward to see its new `status`.

`status` describes the whole order. When a cancellation gave the customer a credit voucher, the order's `cancellationVoucher` holds it.

### Tickets and barcodes

Each item's tickets are in `items[].tickets[]`. A ticket's `barcode` (`value`, `format`, optional `imageUrl`) is `null` until the order is paid and the venue releases its tickets, which some venues do only closer to the show. Fetch the order again later to pick barcodes up, and handle a `null` barcode in your UI.

Each item's `delivery` says how the customer gets their tickets: `delivery.method.name` names the delivery method, and `delivery.instructions` (when present) tells the customer what to do. Show both to the customer.

## Receipt Rendering

Both cart and order responses include `receiptLines[]`. Render them in order — do not compute totals client-side.

Each line has `sign`: `positive` = charge, `negative` = credit/discount. Each `total.amount` is a whole number, `0` or more.

## Common Errors

| Step | HTTP | Error Code | Cause | Recovery |
|---|---|---|---|---|
| Browse Inventory, Create Cart, Add Extras | `409` | `OCCURRENCE_NOT_AVAILABLE` | Sales have closed for the occurrence — on Add Extras, the one picked for an extra with its own schedule | Pick another occurrence |
| Create Cart, Add Extras | `400` | `MIXED_OCCURRENCES_NOT_SUPPORTED` | The items are for more than one occurrence | Use one occurrence per cart |
| Create Cart | `400` | `MIXED_ITEM_TYPES_NOT_SUPPORTED` | The items mix general admission and assigned seats | Use a separate cart for each |
| Create Cart | `400` | `EVENT_NOT_SELLABLE` | The event is closed, or is sold only as an extra with another event | Pick another event |
| Create Cart | `409` | `ORPHANED_SEATS` | Holding these seats would leave a single seat on its own | Choose different seats |
| Create Cart, Add Extras | `409` | `INVENTORY_EXHAUSTED` | Items no longer available | Browse inventory again |
| Vouchers | `400` | `INVALID_VOUCHER` | The code is unknown, expired, or not valid for these items | Show `message`. For a guest, send `email` with a code limited to one use per customer |
| Add Extras, Checkout | `410` | `CART_EXPIRED` | Cart timed out, was deleted, or no longer holds inventory | Create a new cart |
| Checkout | `400` | `EXPECTED_TOTAL_MISMATCH` | `expectedTotal` doesn't match the cart `total` | Read the cart again and send its `total` exactly as returned, under a new `Idempotency-Key` |
| Checkout | `400` | `PAYMENT_METHOD_NOT_AVAILABLE` | `payment.method` or `payment.gateway` isn't one the cart can be paid with | Pay with a method from `details.availablePaymentMethods`, under a new `Idempotency-Key` |
| Checkout | `400` | `MISSING_REQUIRED_FIELD` | This retailer needs a phone number, and `contact.phone` wasn't sent | Collect it and place the order again under a new `Idempotency-Key` |
| Checkout | `400` | `INVALID_FIELD_VALUE` | A field can't be used; `param` names it. With `details.reason` `ORDER_PHONE_NUMBER_INVALID`, the retailer can't use the phone number; with `ORDER_EMAIL_INVALID`, the venue won't accept the email address | Fix the field (ask the customer for another number or a corrected email) and place the order again under a new `Idempotency-Key` |
| Checkout | `400` | `INVALID_VOUCHER` | The voucher on the cart failed its last check, after the payment was authorized | Remove the voucher, read the cart again, and place the order under a new `Idempotency-Key` |
| Checkout | `400` | `MINIMUM_QUANTITY_NOT_MET` | The cart holds fewer of an extra than its `minQuantityPerOrder` | Add more of the extra, or remove it, then place the order under a new `Idempotency-Key` |
| Checkout | `400` | `EVENT_MINIMUM_NOT_MET` | The cart holds fewer tickets than the event's `orderLimit.min` | Create a new cart with at least the minimum, then place the order under a new `Idempotency-Key` |
| Add Extras, Checkout | `409` | `CART_ALREADY_ORDERED` | Cart already became an order (possibly since canceled) | Look it up with `GET /orders?cartId={cartId}` — do not buy again. See [After Checkout](#7-after-checkout). |
| Checkout | `402` | `PAYMENT_AUTHENTICATION_REQUIRED` | The shopper has to pass a card check, or approve a `KLARNA` or `AFTERPAY` payment | Run `details.action`, then place the order again with the result under a new `Idempotency-Key`. See [Klarna and Afterpay](#klarna-and-afterpay) |
| Checkout | `402` | `PAYMENT_DECLINED` | The issuer refused the payment | Show `message` and let the customer pay another way, under a new `Idempotency-Key` |
| Checkout | `403` | `RISK_CHECK_FAILED` | Risk checks declined the order | Don't retry, with any payment method. Show `message` and send the customer to customer support |
| Checkout | `403` | `INSUFFICIENT_PERMISSIONS` | The customer isn't allowed to buy, or the key can't use this payment method (such as `INVOICE` without your secret key) | Customer: show `message`, don't retry. Key: use the right credential ([Error Handling](/guides/error-handling#authentication--authorization)) |
| Checkout | `409` | `TICKET_LIMIT_EXCEEDED` | The venue limits how many tickets one customer can buy, counting earlier orders | Show `message`, which is the venue's own wording and says how many tickets the customer has left, if any. Retrying the same order cannot succeed; a smaller order might |

For the full error format and retry strategy, see [Error Handling](/guides/error-handling).