- Checkout
- API reference
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. Errors returned by capture, refund,
cancel and the other post-purchase operations are Order Management’s, not Checkout’s. See
Order Management.
Rules that hold for every Checkout API error
Section titled “Rules that hold for every Checkout API error”2xxis success.4xxis a problem in the request.5xxis 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_idfrom the error object.
The codes Checkout returns
Section titled “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, 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
Section titled “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.
{ "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 |
order_lines[X].total_tax_amount | Line X’s tax amount doesn’t reconcile across the order | See Order lines and tax |
order_tax_amount | The order total tax doesn’t equal the sum of the lines | See Order lines and tax |
order_lines | Order lines are malformed, violate a field restriction, or are missing | Check the shapes in the Checkout API reference |
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 |
billing_address.{field}, shipping_address.{field} | An address field isn’t formatted as expected | Check the customer data requirements in the Checkout API reference |
locale | The locale isn’t well-formed — two letters, or two letters and a region | Use a locale from the supported list |
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
Section titled “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
Section titled “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.
This one answers in an older error shape and is the exception to everything on
Error handling conventions — http_status_code, http_status_message and
internal_message, with no error_code and no correlation_id:
{ "http_status_code": 401, "http_status_message": "Unauthorized", "internal_message": "authorization_error"}403 — the operation is not permitted
Section titled “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
Section titled “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
Section titled “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
Section titled “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
Section titled “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.
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
Section titled “Related”- Error handling conventions — the error object shape shared by every Kustom API.
- Order lines and tax — fix the
order line and tax fields most
BAD_VALUEerrors name. - Server-side callbacks — diagnose a purchase that stops at a callback rather than at an API call.
- Render Checkout snippet — the container rules these failures usually trace back to.
- Authentication and environments — resolve a
401.
This page is also available as Markdown — append .md to its URL — and is indexed in llms.txt.