Skip to content

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”
  • 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.

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.

Statuserror_codeWhat it means
400BAD_VALUEA field value Kustom will not accept
400BAD_REQUESTThe request itself is malformed, or the operation does not apply to this order
401—The credentials are not accepted
403FORBIDDENYou tried to set a field only Kustom may write
403READ_ONLY_ORDERThe 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, 415HTTP_ERRORThe request never reached an order — wrong method, Accept or Content-Type
409—Too many requests in flight against this one order
500INTERNAL_SERVER_ERRORSomething 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 namedWhat is wrongWhat to do
order_lines[X].tax_rateThe rate on line X doesn’t agree with the tax values elsewhere on the orderSee Order lines and tax
order_lines[X].total_tax_amountLine X’s tax amount doesn’t reconcile across the orderSee Order lines and tax
order_tax_amountThe order total tax doesn’t equal the sum of the linesSee Order lines and tax
order_linesOrder lines are malformed, violate a field restriction, or are missingCheck the shapes in the Checkout API reference
purchase_currencyThe currency is malformed, or isn’t configured for your store in the order’s countryThe 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 expectedCheck the customer data requirements in the Checkout API reference
localeThe locale isn’t well-formed — two letters, or two letters and a regionUse 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.

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"
}

Two codes and one empty body share this status.

error_codeMeaning and what to do
FORBIDDENYou sent a field Kustom owns. The message names it. Read the order back and send only what you set
READ_ONLY_ORDERThe 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.

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.

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.

This page is also available as Markdown — append .md to its URL — and is indexed in llms.txt.