# Troubleshooting Checkout

The error codes the Checkout API returns during a session, what each one means for the customer in front of the screen, and the browser-side mistakes that break the checkout iframe — re-initialization, detached iframes, global styles and popup-blocking headers.

Checkout failures come from two directions. The API rejects a request, and you get a status
code and — usually — an error object naming the field at fault. Or nothing is
rejected at all and the iframe simply misbehaves, because of something the surrounding page
is doing to it. This page covers both — the Checkout API error codes and what each one
costs the customer, then the handful of front-end patterns that reliably break the embed.

For the shape of the error object itself — `error_code`, `error_messages`,
`correlation_id` and how to use them — see
[Error handling conventions](/api/errors/). Errors returned by capture, refund,
cancel and the other post-purchase operations are Order Management's, not Checkout's. See
[Order Management](/order-management/).

## Rules that hold for every Checkout API error

- `2xx` is success. `4xx` is a problem in the request. `5xx` is a problem at Kustom.
- Requests to the Checkout API time out after 30 seconds and return `504`.
- Don't retry a rejected request against the same order. The one exception is `409`, which
  is a concurrency signal and asks you to retry — see below.
- Checkout orders are valid for 48 hours after the last merchant update or customer
  interaction, then expire. Expiry is normal and isn't an error to escalate.
- To see the underlying failure, open the **PSP logs** dashboard or **Developer Logs** in
  the Kustom Portal and search the `correlation_id` from the error object.

## The codes Checkout returns

Checkout has a small, closed set of error codes. Anything else you see under a Kustom
hostname belongs to another API — Order Management, customer tokens, the Hosted Payment
Page — each of which has its own vocabulary.

| Status | `error_code` | What it means |
| --- | --- | --- |
| 400 | `BAD_VALUE` | A field value Kustom will not accept |
| 400 | `BAD_REQUEST` | The request itself is malformed, or the operation does not apply to this order |
| 401 | — | The credentials are not accepted |
| 403 | `FORBIDDEN` | You tried to set a field only Kustom may write |
| 403 | `READ_ONLY_ORDER` | The order is completed or aborted. Nothing more can be changed |
| 403 | — | The merchant id is not active |
| 404 | — | No such order for these credentials |
| 405, 406, 415 | `HTTP_ERROR` | The request never reached an order — wrong method, `Accept` or `Content-Type` |
| 409 | — | Too many requests in flight against this one order |
| 500 | `INTERNAL_SERVER_ERROR` | Something failed inside Kustom |

Three of those rows have no code because they have no body at all: an inactive merchant, a
missing order and a concurrency conflict are answered with the status alone. Branch on the
status for those, and on `error_code` everywhere else.

Every code here is in the [error catalogue](/api/errors/), with its retry policy and — where
the code describes the order's state rather than your request — the status the call actually
needs.

## 400 BAD_VALUE — a field Kustom will not accept

The most common Checkout error by a wide margin. The message names the property to fix and
reads `Bad value: <field>`, often with the validator's own complaint in parentheses, so read
it before anything else.

```json
{
  "error_code": "BAD_VALUE",
  "error_messages": ["Bad value: order_lines[0].tax_rate (must be less than or equal to 10000)"],
  "correlation_id": "f4a1c0e2-3b7d-4e91-8c55-0b2a6d7e9f10"
}
```

| Field named | What is wrong | What to do |
| --- | --- | --- |
| `order_lines[X].tax_rate` | The rate on line X doesn't agree with the tax values elsewhere on the order | See [Order lines and tax](/checkout/tax-and-order-lines/) |
| `order_lines[X].total_tax_amount` | Line X's tax amount doesn't reconcile across the order | See [Order lines and tax](/checkout/tax-and-order-lines/) |
| `order_tax_amount` | The order total tax doesn't equal the sum of the lines | See [Order lines and tax](/checkout/tax-and-order-lines/) |
| `order_lines` | Order lines are malformed, violate a field restriction, or are missing | Check the shapes in the [Checkout API reference](/api/checkout/) |
| `purchase_currency` | The currency is malformed, or isn't configured for your store in the order's country | The message lists the currencies configured for that country. See [Selling into several markets](/checkout/use-cases/multi-market/) |
| `billing_address.{field}`, `shipping_address.{field}` | An address field isn't formatted as expected | Check the customer data requirements in the [Checkout API reference](/api/checkout/) |
| `locale` | The locale isn't well-formed — two letters, or two letters and a region | Use a locale from the [supported list](/checkout/use-cases/multi-market/#supported-languages-and-locales) |

Two fields can also be individually valid and rejected together. That message reads
`Invalid combination of fields:` and ends with the names of the fields at odds — sending
`customer.type` as `organization` while `options.allowed_customer_types` lists only
`person`, for example.

What it costs the customer depends on which call failed. On create, the checkout never
renders and the customer can't use Kustom at all. On update, they are stuck looking at a
cart that won't move until you correct the request.

When amounts or order lines are named and the order was fine a moment ago, diff the last
successful request against the failing one. Discounts, coupons, shipping fees and taxes
applied inside the checkout are the usual cause.

## 400 BAD_REQUEST — the request, not a field

`BAD_VALUE` names a field. `BAD_REQUEST` is what you get when there's no single field to
name: a body Kustom can't parse as an order, or an update that doesn't apply to the order
you sent it to — most often a post-purchase update on an order that isn't in a
post-purchase state.

## 401 — the credentials are not accepted

The credentials used to authenticate are invalid. If several credentials have been issued
for the store, confirm you're using the current pair, and that Playground keys aren't
being sent to production or the reverse. See
[Authentication and environments](/api/authentication/).

This one answers in an older error shape and is the exception to everything on
[Error handling conventions](/api/errors/) — `http_status_code`, `http_status_message` and
`internal_message`, with no `error_code` and no `correlation_id`:

```json
{
  "http_status_code": 401,
  "http_status_message": "Unauthorized",
  "internal_message": "authorization_error"
}
```

## 403 — the operation is not permitted

Two codes and one empty body share this status.

| `error_code` | Meaning and what to do |
| --- | --- |
| `FORBIDDEN` | You sent a field Kustom owns. The message names it. Read the order back and send only what you set |
| `READ_ONLY_ORDER` | The order is completed or aborted, so no update of any kind is accepted. `Resource is read-only` is the whole message. Read the order and handle it as finished |
| *(no body)* | The merchant id hasn't been activated. Contact your delivery manager or merchant support — this isn't a code fix |

`READ_ONLY_ORDER` is normally invisible to the customer. It shows up most often when your
update races the purchase: the customer completed the order between your read and your write.

## 404 — no such order

An empty 404 with no body. The order id doesn't exist for these credentials, it has
expired, or it has been blocked. Checkout orders live for 48 hours after the last update or
customer interaction, so an order left open overnight is gone by morning — and this is the
status that says so. Create a new order. There's nothing to recover.

## 409 — too many requests against one order

An empty 409 carrying a `Retry-After` header, in seconds. Kustom serializes merchant
operations per order and allows two in flight at a time. A third arrives to a locked order
and is refused rather than queued. This isn't account-level rate limiting — it's one order
being written from several places at once.

Wait out `Retry-After` and retry the same request. If you see it steadily, the cause is
usually a webhook handler and a cart update writing to the same order concurrently, or a
retry loop that fires before its predecessor has returned.

## 500 INTERNAL_SERVER_ERROR

Something failed inside Kustom. Create a new order, and if it repeats, send the
`correlation_id` to merchant support.

## Browser-side problems that break the iframe

None of these produce an API error. The request succeeds and the checkout misbehaves
anyway.

**Re-initializing the checkout without reloading the page.** Re-initialization doesn't
clean up the previous listeners and iframes, so you end up with two of everything. To
reflect an order update, suspend and resume instead. See
[Checkout JavaScript events](/checkout/client-side-events/).

**Injecting the snippet somewhere other than the main page.** The snippet must go into the
main document, not into an iframe of your own. Anywhere else and both the UI and the
functionality degrade.

**Detaching or re-creating iframes.** Important state travels in the iframe URLs, and
message passing between frames depends on them staying put. Removing an iframe from the
DOM and re-inserting it breaks both.

**Modifying the iframe URLs.** Same reason. The parameters in those URLs are parsed by the
checkout. Rewriting them breaks the parsing.

**Global iframe styles.** The checkout's iframes carry inline styles, which already have
high specificity. A blanket `iframe { ... }` rule — especially with `!important` — fights
them and can disable functionality outright. Style a wrapper element instead.

**`transform` on the body element.** It re-parents fixed positioning, so the fullscreen
iframe no longer covers the viewport and content gets clipped.

**Old libraries that patch built-in browser functions.** Prototype.js and its contemporaries
modify native prototypes, and the checkout depends on the originals. If you need the
behavior, implement it as a local function rather than by overwriting a built-in.

**A payment method popup that opens twice.** If a popup-based payment method opens both a
popup window and an in-page view at once, your page is sending
`Cross-Origin-Opener-Policy: same-origin`. Because the checkout runs on a different domain,
that header severs the popup from the page. On every page that renders the checkout
snippet, send `same-origin-allow-popups` instead.

## Related

- [Error handling conventions](/api/errors/) — the error object shape shared by
  every Kustom API.
- [Order lines and tax](/checkout/tax-and-order-lines/) — fix the
  order line and tax fields most `BAD_VALUE` errors name.
- [Server-side callbacks](/checkout/callbacks/) — diagnose a purchase that stops at
  a callback rather than at an API call.
- [Render Checkout snippet](/checkout/integrate/render-checkout-snippet/#when-it-breaks) — the
  container rules these failures usually trace back to.
- [Authentication and environments](/api/authentication/) — resolve a `401`.
