Skip to content

Errors and messages

The difference between a protocol error and a message in the answer, every code, and what a client should do about each.

On this page

A UCP call to a Monokaido shop can go wrong in two very different ways. Either the request could not be processed at all — a protocol error, with an HTTP error status — or it was processed and the shop has something to say about the outcome — a message, inside an ordinary 200 answer. This chapter shows how to tell the answers apart and lists every code the API returns, with what your client should do about each.

The examples use the shop https://shop.example.com and a platform whose profile is https://platform.example.com/.well-known/ucp. When to retry, and how, is covered in detail in Idempotency, concurrency and limits.

Three kinds of answer

KindHTTP statusBodyExample
Protocol error400–503{"code", "content", "continue_url"?} — no ucp objectAn unreadable UCP-Agent header, a reused Idempotency-Key, a rate limit
Business error200{"ucp": {…, "status": "error"}, "messages": […], "continue_url"} — no resourceAn unknown cart, a capability your profile does not declare, a cart in which nothing could be added
A resource with messages200, or 201 for a new cart or checkoutthe resource, with messages[]A checkout that still needs a shipping address, a quantity lowered to the stock

Two more kinds of answer are not UCP JSON at all:

  • The web server's own pages. The web server in front of the shop answers a few situations itself, before the API sees the request, with an HTML page: plain http://, a body over 1 MiB, too many requests per second, the API unreachable or too slow. See Answers that are not JSON.
  • The handoff page. continue_url is a page for the buyer's browser. The API serves it on the shop's domain, as HTML. Your client never needs to call it.

Telling them apart

Check an answer in this order:

#CheckThen it isRead
1Status 304not modified — only if you sent If-None-MatchNothing; your copy is current. A 304 has no body and no Content-Type.
2Content-Type is not application/jsona page of the web server (or the handoff page)The HTTP status only. There is no Retry-After.
3Status 400 or highera protocol errorcode, then Retry-After for 409, 429 and 503.
4Status 200 and ucp.status is "error"a business errormessages[0].code; there is no id and no resource.
5Status 200 or 201, no ucp.statusa resourceThe resource and its messages.

A successful answer never carries ucp.status: read its absence as success. No 2xx answer of the API other than 200 and 201 exists, and the API never redirects (the web server redirects plain http:// to https:// with a 301; call https:// directly).

// Sort one UCP answer into the kinds above (Node 20+ fetch Response).
async function readUcp(res) {
  const retryAfter = Number(res.headers.get('retry-after')) || undefined;
  if (res.status === 304) return { kind: 'not_modified' };
  if (!(res.headers.get('content-type') ?? '').startsWith('application/json')) {
    // nginx's own HTML page: 413, 429, 502 or 504. Decide by the status alone.
    return { kind: 'web_server', status: res.status };
  }
  const body = await res.json();
  if (res.status >= 400) {
    return { kind: 'protocol_error', status: res.status, code: body.code,
             content: body.content, continueUrl: body.continue_url, retryAfter };
  }
  if (body.ucp?.status === 'error') {
    return { kind: 'business_error', code: body.messages[0].code,
             messages: body.messages, continueUrl: body.continue_url };
  }
  return { kind: 'resource', status: res.status, body, messages: body.messages ?? [] };
}

Protocol errors

The body and headers

A protocol error has exactly two or three keys:

KeyTypeMeaning
codestringOne of the codes in the tables below. Branch on it, together with the status.
contentstringAn English sentence for your logs. A few name what was refused: the field sentences of invalid_request, and version_unsupported, which names your version. The others are fixed. Never parse content, and never show it to a buyer as it is.
continue_urlstringOnly on the platform profile errors and the profile discovery limits: the shop's home page, https://<primary domain>/.

Every protocol error is sent with Cache-Control: no-store. Retry-After, in whole seconds, is sent with these and no others:

AnswerRetry-After
409 idempotency_conflict, the same request still running2
429 rate_limited60
503 "Another request on this session is being processed — retry shortly"1
503 "Too many new platform profiles from this client, retry shortly"the seconds left in the current one-minute window, at least 1
503 "Platform profile discovery is busy, retry shortly"10
503 "This shop is temporarily unavailable"60
JSONA protocol error: the session is busy (503, Retry-After: 1)
{
  "code": "service_unavailable",
  "content": "Another request on this session is being processed — retry shortly"
}

Another request on the same cart or checkout is running and this one could not get its turn within 10 s, or 4 requests were already waiting (this request did not run) — or, rarely, another writer changed the session while this request ran (its result was not stored). The Idempotency-Key was released: retry the same request with the same key. Retry-After: 1. HTTP 503.

The protocol error codes are invalid_profile_url, profile_unreachable, profile_malformed, version_unsupported, invalid_request, request_too_large, not_found, idempotency_conflict, rate_limited, service_unavailable and internal_error. The negotiation code capabilities_incompatible is not among them: it arrives as a business error with HTTP 200.

The shop, the host and the route

HTTPcodecontentWhenRetry?What to do
404not_foundNo UCP business on this hostThe host serves no UCP: it is not a custom domain of a shop with verified DNS and an active certificate, UCP is switched off, the built-in storefront is off, the storefront key is inactive or expired, or the company's E-shop module is off. The answer never says which, on purpose. It is checked before UCP-Agent and before your body's fields, so it also answers a request without UCP-Agent; only the rate limit and an unreadable body are checked earlier.NoCheck the domain with the merchant. Call the host of the endpoint the shop's profile publishes.
404not_foundNot foundThe path or the method does not exist under /ucp/, for example POST /ucp/v1/nothing-here or DELETE /ucp/v1/catalog/search. An unknown method is 404, not 405.NoFix the path or the method.
503service_unavailableThis shop is temporarily unavailableThe shop is suspended on Monokaido's side (the company behind it is deactivated or locked), or the shop could not be looked up for a moment.Yes, after Retry-After (60 s)Retry a few times. If it persists, the shop is suspended: tell the buyer the shop is not available right now.

A missing cart, checkout or product is never a 404: it is a business error with HTTP 200.

Your platform profile

These come from the UCP-Agent header and the platform profile it names. All of them carry continue_url, the shop's home page. Each request runs the checks in this order: the header, the fetch, the version, then the capability negotiation.

HTTPcodecontentWhenRetry?What to do
400invalid_profile_urlThe UCP-Agent header must carry profile="https://…"The header is missing, empty, longer than 4096 characters or not parseable, the profile member is not a quoted string, or its URL breaks a rule: not https://, longer than 1000 characters, whitespace, a user name or password, no host.NoSend UCP-Agent: profile="https://platform.example.com/.well-known/ucp".
424profile_unreachableUnable to fetch the platform profileThe shop could not fetch your profile: DNS, connection, TLS, more than 3 s without data or 5 s in all, a redirect, a status other than 200 (304 included), more than 256 KiB, or an address that is not public. The failure is remembered for 60 s.Only after you fix the hosting, and not within 60 sServe the profile with 200, without redirects, from a public address.
422profile_malformedThe platform profile is not a valid UCP profileThe body is not JSON, is not an object, has no ucp object, or ucp.version is not a YYYY-MM-DD string. Remembered for 60 s.Only after you fix the documentServe a UCP profile; see Concepts and lifecycle.
422version_unsupportedProtocol version <version> is not supported. This business supports version 2026-04-08.A valid profile whose ucp.version is not exactly 2026-04-08, older or newer.Only after you change the profile. The shop fetches it again once its copy expires (your max-age, 60 seconds to 1 hour); the first request after that is still answered from the old copy while the new one is fetched.Declare "version": "2026-04-08".
503service_unavailableToo many new platform profiles from this client, retry shortlyYour client (one IPv4 address, or one IPv6 /64) already made the shop's server fetch 5 profile URLs it did not know yet within the current one-minute window.Yes, after Retry-AfterUse one stable profile URL. Every distinct URL string — another query string included — is a new profile.
503service_unavailablePlatform profile discovery is busy, retry shortlyThe shop's server has fetched 60 unknown profiles within the current one-minute window, for all clients together.Yes, after Retry-After (10 s)Retry. A profile the server already knows never waits for this limit.
JSONYour profile could not be fetched
{
  "code": "profile_unreachable",
  "content": "Unable to fetch the platform profile",
  "continue_url": "https://shop.example.com/"
}

A network error, a timeout, a redirect, any status other than 200, a body over 256 KiB, or a host resolving to a private address. Remembered for 60 s. HTTP 424.

The request

HTTPcodecontentWhenRetry?What to do
400invalid_requesta sentence that names the field (see Every field error)A field the API uses has the wrong JSON type or is out of its bounds. Unknown fields are ignored, never refused.NoFix the field. Nothing was executed.
400invalid_requestIdempotency-Key is not validThe key is longer than 255 characters or holds anything but printable ASCII without spaces — a header sent twice arrives joined with ", " and fails too.NoSend one key of 1–255 characters from ! to ~, for example a UUID.
400invalid_requestThe request is not validThe body is declared as JSON but is not valid JSON, is only white space, or is a bare JSON string, number, boolean or null.NoSend a JSON object.
415invalid_requestThe request is not validContent-Type names a charset that is not UTF (charset=iso-8859-2), or Content-Encoding is none of identity, gzip, deflate and br.NoSend Content-Type: application/json in UTF-8.
403invalid_requestThe request is not validA write that carries a Monokaido session cookie (accessToken, refreshToken, platformAccessToken, platformRefreshToken) from a foreign web origin. A server-side client never sends these.NoDo not send cookies.
400request_too_largeids holds at most 20 identifiersPOST /ucp/v1/catalog/lookup with more than 20 ids. Note the status: 400, not 413.NoSplit the lookup into batches of 20.
413request_too_largeRequest body too largeA body over the API's limits once decompressed: 2 MiB of JSON, or a form-encoded body (application/x-www-form-urlencoded) over 100 KiB or with more than 1 000 fields. The web server refuses a body over 1 MiB as sent before the API sees it (its own HTML 413), so a JSON body gets this JSON 413 only when it is sent compressed (Content-Encoding) and inflates past 2 MiB.NoSend JSON and keep bodies small: 100 lines take a few kilobytes.

The fields are checked only once the session is found and open. A PUT to an unknown, completed, canceled or handed-over session gets that session's answer — not_found, checkout_completed, checkout_canceled or handed_off — with HTTP 200, without its fields being read (only a body that is not a JSON object is refused first). Such an answer says nothing about whether your body was valid.

Send every body with Content-Type: application/json. A body sent as text/plain, as another +json type, or without a Content-Type, is not read: the catalog search then runs as an empty search, and lookup, product and every cart and checkout create or PUT answer 400 request must be an object. A form-encoded body (application/x-www-form-urlencoded) is parsed, but every value in it arrives as a string, so a numeric field such as quantity or pagination.limit is refused with 400 invalid_request. Do not use it.

Idempotency and concurrency

HTTPcodecontentWhenRetry?What to do
409idempotency_conflictThis Idempotency-Key was used for a different requestYou used the key before, on this shop with this profile URL, for another operation, another path id or another body.NoA bug in your client: use a new key for every logical write.
409idempotency_conflictA request with this Idempotency-Key is still being processedThe first request with this key and the same body has not finished yet.Yes, after Retry-After (2 s)Send the same request with the same key again. You then get the stored answer.
503service_unavailableAnother request on this session is being processed — retry shortlyAnother request on the same cart or checkout ran and this one did not get its turn within 10 s, or four requests were already waiting behind it. Nothing was done, and the key was released. Rarely, the same answer means that another writer changed the session while this request ran; the session was then not updated.Yes, after Retry-After (1 s)Retry the same request with the same key. Send one request at a time per session.

The first 409 has one rare second cause: two copies of the same request raced, and the first one failed and released the key while the second looked it up. If you are certain both copies were identical, one more retry with the same key runs the request.

Limits and failures

HTTPcodecontentWhenRetry?What to do
429rate_limitedToo many requestsYour IP exceeded the operation's limit: 600 a minute for reads, 300 for writes, 60 for the handoff routes, counted per operation across every Monokaido shop.Yes, after Retry-After (60 s)Slow down; see Rate limits.
500internal_errorUnexpected server errorAnything unexpected. The detail is logged on the server and never sent. A failed write released its Idempotency-Key.A few times, with backoffRetry reads as they are and writes with the same key. Report it if it repeats.
JSONRate limit of the operation
{
  "code": "rate_limited",
  "content": "Too many requests"
}

More than the operation’s per-minute limit from your IP (600 reads, 300 writes, 60 handoff). Retry-After: 60. HTTP 429.

Every field error

The content of a 400 invalid_request is one of these sentences. <i> is the index in the array you sent; <field> is the name of a text field.

contentRaised for
request must be an objectA body that is not a JSON object (an array, for example), or a body sent without Content-Type: application/json (another type, or none at all) — on lookup, product, and every cart and checkout create or PUT. An empty body sent as application/json reads as {} and gets the error of the first field it lacks instead.
<path> must be an objectline_items[<i>], line_items[<i>].item, buyer, context, fulfillment, fulfillment.methods[<i>], fulfillment.methods[].destinations[<i>], fulfillment.methods[].groups[0], discounts, filters, filters.price or pagination with another JSON type. fulfillment and discounts are read only when you negotiated that extension; otherwise they are ignored, whatever their shape.
<path>.<field> must be a stringA text field with another JSON type, for example buyer.email must be a string, context.language must be a string, request.query must be a string, fulfillment.methods[].destinations[0].postal_code must be a string.
line_items must be an arrayA cart create or PUT without line_items (a cart PUT always needs them), and a checkout create without a usable cart_id.
line_items holds at most 100 entriesMore than 100 lines.
line_items[<i>].item.id is requiredA line without item.id, with a blank one, or with one that is not a string.
line_items[<i>].quantity must be a positive integer0, a negative number, a fraction or a string such as "2". A missing quantity means 1.
fulfillment.methods must be an arrayfulfillment.methods that is not an array.
fulfillment.methods[].destinations must be an arraydestinations that is not an array.
fulfillment.methods[].destinations[<i>].address_country is not a recognised country; send an ISO 3166-1 alpha-2 code such as "SK"The chosen destination's country is neither an ISO alpha-2 or alpha-3 code nor a country name the shop recognises. Only the chosen destination is checked: the one selected_destination_id names, else the first.
discounts.codes must be an arraydiscounts.codes that is not an array.
discounts.codes holds at most 10 codesMore than 10 codes, counted as sent (before blanks and duplicates are removed).
pagination.cursor is not validA cursor this API did not issue, or one longer than 40 characters.
pagination.limit must be a positive integer0, a negative number, a fraction or a string.
filters.price.min must be a non-negative integer, filters.price.max must be a non-negative integerA price bound that is not a whole number of minor units, or below zero.
filters.categories must be an array of stringsfilters.categories that is not a list of strings.
filters.categories is not supported by this shop; search by query (and filters.price) insteadA category filter with any value, in search, lookup or product. An empty list is read as no filter.
ids must be a non-empty arrayA lookup without ids, or with an empty list.
id is requiredA product request without id, or with a blank one.
Idempotency-Key is not validSee Idempotency and concurrency.
The request is not validMalformed JSON, an unsupported charset or encoding (415), a refused cross-origin write (403).

What is adjusted instead of refused

Some values are corrected without an error. Compare what you sent with the answer:

You sendYou get
A quantity above 999 on one line999, without a message. Several lines of one variant are added up; a sum above 999 is lowered with the warning quantity_adjusted.
pagination.limit above 2020 results per page.
query longer than 200 charactersThe first 200 characters are searched.
A text field over its length (names 100, e-mail 254, phone 40, street 255, city 100, postal code 20 characters)The field cut to that length. Control characters become spaces; invisible formatting characters are removed.
context.language, context.currency or context.address_country the API cannot readThe value is dropped. For the language, your Accept-Language or else the shop's first language applies.
More than 10 entries in fulfillment.methods or in a method's destinationsOnly the first 10 are read.
A selected_option_id that is not a string, or an empty oneIgnored: the stored option stays selected. (null clears it.)
A selected_destination_id that is not a string, or that names no destinationIgnored: the first destination is used.
groups that is not an arrayIgnored, like a missing groups.
A line id, of any typeIgnored: lines are matched by item.id.
A lookup id that is not a string, or a duplicateLeft out.
A field the API does not knowIgnored.

Business errors

A business error is an HTTP 200 whose ucp.status is "error". It returns no resource — no id, no lines — only messages with one entry and a continue_url:

JSONUnknown, canceled or expired cart (HTTP 200)
{
  "ucp": {
    "version": "2026-04-08",
    "status": "error",
    "capabilities": {
      "dev.ucp.shopping.cart": [
        {
          "version": "2026-04-08"
        }
      ],
      "dev.ucp.shopping.discount": [
        {
          "version": "2026-04-08"
        }
      ]
    }
  },
  "messages": [
    {
      "type": "error",
      "code": "not_found",
      "content": "Cart not found or has expired",
      "severity": "unrecoverable"
    }
  ],
  "continue_url": "https://shop.example.com/"
}

Any cart operation on an id that is not a UUID, belongs to another shop, or names a canceled or expired cart. HTTP 200.

codecontentOperationscontinue_urlWhat to do
capabilities_incompatibleThe platform and this business share no version of the capability this operation needsAny /ucp/v1 operation whose root capability your profile does not declare at 2026-04-08. Every cart and checkout operation of a shop in catalog mode. Nothing runs, and no Idempotency-Key is used.The shop's home pageDeclare the capability in your profile. For a catalog-mode shop, stop after the catalog and give the buyer the product's url.
not_foundProduct not foundPOST /ucp/v1/catalog/product: an id the shop does not show.The shop's home pageSearch again.
not_foundCart not found or has expiredEvery cart operation: an id that is not a UUID, another shop's cart, a canceled or expired cart, a cart already deleted.The shop's home pageCreate a new cart.
not_foundCheckout session not foundEvery checkout operation: an id that is not a UUID, another shop's checkout, a checkout already deleted; also POST /ucp/v1/checkout-sessions with an unknown, canceled or expired cart_id.The shop's home pageCreate a new checkout. A canceled or expired checkout is not this: it is answered with status: "canceled".
out_of_stockAll requested items are currently out of stockCreating a cart or a checkout, or converting a cart, when lines were sent and every one of them is sold out. The session is discarded and has no id.The shop's home pageOffer other items, or read the catalog again later.
item_unavailableNone of the requested items can be bought in this shopThe same, when none of the lines could be added for mixed reasons: unknown ids, items that cannot be ordered, sold-out items — or only sold-out items when you sent discount codes too, whose refusals count as another reason.The shop's home pageCheck the variant ids with a catalog lookup.
handed_offThe buyer continued on the shop: the items are in the shop’s cart — finish the purchase therePOST /ucp/v1/checkout-sessions with the cart_id of a cart the buyer already took to the shop, or of a cart whose earlier checkout they took and which is no longer open. (While a checkout of the cart is still open, that checkout is answered instead, with 200.)The shop's cart page, https://<primary domain>/cartStop building on this cart. Give the buyer this continue_url if they need to find their cart again.

Every business error severity is unrecoverable: the same request will get the same answer. A business error on a write is a result like any other, so it is stored with the Idempotency-Key and a retry with that key gets it again — use a new key when you try again with a changed request.

Messages

The message object

KeyTypePresentMeaning
type"error", "warning", "info"alwayserror: something blocks or was refused. warning: something was changed or not applied, and the answer already shows the result. info: a remark.
codestringalwaysWhat happened. The codes are listed below; treat a code you do not know by its type and severity.
contentstringalwaysA fixed English sentence; on not_found in a lookup, the identifier that matched nothing.
pathstringon some codesA JSONPath into the answer it arrives in, for example $.line_items[1].quantity or $.buyer.email. The lines of an answer follow the shop's order, not your request's.
severity"recoverable", "requires_buyer_input", "unrecoverable"on errors onlyWho can resolve it. Warnings and info messages have none.

Where messages appears:

Answermessages
CheckoutAlways, possibly [].
CartOnly when there is at least one.
Catalog search and lookupOnly when there is at least one.
Product detailNever; a business error has its own.
Business errorAlways one.

Severity and what it asks of you

severityMeaningYour move
recoverableYour platform can resolve it with another request. Any such error keeps a checkout incomplete.Send what path names, in a PUT.
requires_buyer_inputOnly the buyer can resolve it, on the shop's website. It does not keep a checkout incomplete.Hand the buyer over with continue_url.
unrecoverableThis request, or this session, cannot go further.Stop; start a new session if the buyer still wants to buy.
(none — a warning)Something was adjusted or not applied; the answer shows the result.Show the buyer the new state, or ask whether it is still what they want.
(none — info)A remark that changes nothing.Nothing, or log it.

A checkout's status follows from its messages: any error with severity recoverable makes it incomplete; otherwise it is requires_escalation and carries payment_required. The messages of a checkout come in this order: line messages, discount messages, then what the checkout lacks, and payment_required last.

Line messages

Carts and checkouts, on create and on every write that sends line_items (also when a language change re-prices the lines). They appear only in the answer to the write that caused them: the line is already removed or lowered in that answer, so the next read of a checkout can return requires_escalation without them.

codetype / severitypathcontentWhenWhat to do / show the buyer
item_unavailableerror / recoverablenoneItem <id> is not available in this shopThe item.id is not a UUID — a SKU, for instance — or it is neither an active variant of a published product of the company nor such a product (a product id stands for its active variant created first). The storefront's exclusion filters are not applied here. The line is left out.Look the item up in the catalog and use the variant id. Tell the buyer the item is not sold here.
item_unavailableerror / recoverable$.line_items[<i>] while the line is still in the answer, else noneItem <id> cannot be bought right nowThe shop refused to add or change the line: not orderable, without a price, or not enough stock for the change.Offer another variant or item.
vat_mix_not_allowederror / recoverableas aboveItem <id>: this shop does not sell zero-VAT and VAT items in one orderThe shop does not sell items with and without VAT in one order.Split the purchase into two sessions.
out_of_stockerror / recoverablenone (the line leaves the session)Item <id> is out of stockNo sellable stock and the variant is not on pre-order. The line is removed.Tell the buyer it is sold out; offer an alternative.
quantity_adjustedwarning$.line_items[<i>].quantityOnly <n> available — quantity lowered from <m>The quantity exceeded the stock; it was lowered to the stock. Pre-order variants are never lowered.Show the new quantity and ask whether it is still wanted.
quantity_adjustedwarning$.line_items[<i>].quantityAt most 999 of one item — quantity lowered from <m>Several lines of one variant added up to more than 999.Show the new quantity.

<id> is the item.id as you sent it (trimmed). The messages about one variant — cannot be bought right now, vat_mix_not_allowed and out_of_stock — name the first id you sent for that variant.

Discount messages

With the discount extension negotiated, on every create and PUT of a cart or checkout, a cart's conversion included. Codes are tried in the order you sent them, and the first one the shop accepts is applied.

codetypepathcontentWhenWhat to do / show the buyer
discount_code_combination_disallowedwarning$.discounts.codes[<i>]Code '<code>' cannot be combined with '<applied code>'A code after the one already applied: one code per order.Tell the buyer only one code applies.
discount_code_expiredwarning$.discounts.codes[<i>]Code '<code>' cannot be appliedThe shop's reason says the code has expired.Tell the buyer the code has expired.
discount_code_user_ineligiblewarning$.discounts.codes[<i>]Code '<code>' cannot be appliedThe order is below the code's minimum order total, which is compared with the subtotal without VAT, before the discount.Tell the buyer the code does not apply to this order.
discount_code_invalidwarning$.discounts.codes[<i>]Code '<code>' cannot be appliedAny other refusal: an unknown or switched-off code, a code not active yet, a code at its usage limit, and every code on a session that has never held a line.Ask the buyer to check the code.

On a session whose lines were all removed, codes are judged against a total of 0: a code with a minimum order total gets discount_code_user_ineligible, and any other code the shop accepts is applied without a message but takes nothing off, so discounts.applied stays empty.

<i> counts in the list as the API keeps it: duplicates (compared without regard to case) and blank codes removed. A refused code stays stored and is tried again on the next write; send discounts.codes without it to drop it.

Checkout readiness

Derived anew on every create, PUT, GET and POST …/complete of an open checkout — once the buyer has taken it to the shop, the last answer before the handoff is repeated instead. The fulfillment messages appear only when the fulfillment extension is negotiated.

codeseveritypathcontentWhenWhat to do
missingrecoverable$.line_itemsThe checkout holds no itemThe checkout has no line.PUT line_items.
missingrecoverable$.buyer.emailBuyer email is requiredNo e-mail, or one that is not an e-mail address.PUT the whole buyer with a valid email.
missingrecoverable$.fulfillment.methods[0].destinationsA shipping address is requiredNo destination yet.PUT a destination.
invalid_addressrecoverable$.fulfillment.methods[0].destinations[0]The shipping address needs street, city, postal code and countryThe destination lacks street_address, address_locality, postal_code or address_country.PUT the complete address.
address_undeliverablerecoverable$.fulfillment.methods[0].destinations[0]The shop does not ship to this addressThe address is complete, but none of the shop's shipping methods serves its country for these items.Ask the buyer for another address; otherwise the buyer cannot buy here.
invalid_optionrecoverable$.fulfillment.methods[0].groups[0].selected_option_idThe selected shipping option is not offered for this checkoutThe selected option is no longer offered, for example after the country changed. The session keeps your choice: the answer shows selected_option_id: null and repeats this message until you select an offered option.Select one of the options now offered.
missingrecoverable$.fulfillment.methods[0].groups[0].selected_option_idChoose a shipping optionOptions exist and none is selected.Select an option.
pickup_point_requiredrequires_buyer_input$.fulfillment.methods[0].groups[0]The pickup point is chosen on the shop websiteThe selected option is a pickup-point service (Packeta). It does not block.Tell the buyer they pick the point on the shop.
payment_requiredrequires_buyer_inputnonePayment is completed on the shop website — continue there to place the orderNo recoverable error is left: the checkout is requires_escalation.Give the buyer continue_url.

Closed and handed-over sessions

codetype / severitycontentWhenWhat to do
checkout_completederror / unrecoverableThis checkout is already completedA PUT or cancel on a completed checkout. Nothing changes. (A GET or POST …/complete of it answers messages: [].)Stop; the order is in order.
checkout_cancelederror / unrecoverableThis checkout is canceled or expired — start a new oneA PUT, POST …/complete or cancel on a canceled or expired checkout. (A GET of it answers messages: [].)Create a new checkout if the buyer still wants to buy.
handed_offwarningThe buyer continued on the shop: the items are in the shop’s cart now and this session is no longer changedA PUT of a cart or checkout after the buyer took it to the shop. The answer is the last one before the handoff; your request was not applied.Stop writing; poll the checkout for the order.

These messages can also arrive on a write that raced with the order or with the deadline: the answer is then the session as it is now, closed.

JSONWriting after the handoff
{
  "ucp": {
    "version": "2026-04-08",
    "capabilities": {
      "dev.ucp.shopping.checkout": [
        {
          "version": "2026-04-08"
        }
      ],
      "dev.ucp.shopping.fulfillment": [
        {
          "version": "2026-04-08"
        }
      ],
      "dev.ucp.shopping.discount": [
        {
          "version": "2026-04-08"
        }
      ]
    },
    "payment_handlers": {}
  },
  "id": "3d7b9e2a-6f14-4c58-9b3e-8a2f1c6d4e97",
  "buyer": {
    "email": "jana.novakova@example.com",
    "last_name": "Nováková",
    "first_name": "Jana",
    "phone_number": "+421 905 123 456"
  },
  "links": [
    {
      "url": "https://shop.example.com/obchodne-podmienky",
      "type": "terms_of_service",
      "title": "Obchodné podmienky"
    },
    {
      "url": "https://shop.example.com/ochrana-osobnych-udajov",
      "type": "privacy_policy",
      "title": "Ochrana osobných údajov"
    }
  ],
  "status": "requires_escalation",
  "totals": [
    {
      "type": "subtotal",
      "amount": 71880
    },
    {
      "type": "discount",
      "amount": -5844,
      "display_text": "JESEN10"
    },
    {
      "type": "fulfillment",
      "amount": 490
    },
    {
      "type": "total",
      "amount": 66526
    }
  ],
  "context": {
    "language": "sk",
    "address_country": "SK"
  },
  "currency": "EUR",
  "messages": [
    {
      "code": "payment_required",
      "type": "error",
      "content": "Payment is completed on the shop website — continue there to place the order",
      "severity": "requires_buyer_input"
    },
    {
      "type": "warning",
      "code": "handed_off",
      "content": "The buyer continued on the shop: the items are in the shop’s cart now and this session is no longer changed"
    }
  ],
  "discounts": {
    "codes": [
      "JESEN10"
    ],
    "applied": [
      {
        "code": "JESEN10",
        "title": "JESEN10",
        "amount": 5844,
        "method": "across"
      }
    ]
  },
  "expires_at": "2026-10-27T09:21:30.551Z",
  "line_items": [
    {
      "id": "f6a3c8e1-9d27-4b54-8e6a-2c1f7b9d3a58",
      "item": {
        "id": "1e8a6c4f-2b93-4d71-9f05-6a3c8e2b1d47",
        "price": 69900,
        "title": "Trek Marlin 5",
        "image_url": "https://shop.example.com/api/public/uploads/c4d1e5a2-8b3f-4a6c-9e07-5f2d8a1b3c40/products/trek-marlin-5-modra.webp"
      },
      "totals": [
        {
          "type": "subtotal",
          "amount": 69900
        },
        {
          "type": "total",
          "amount": 69900
        }
      ],
      "quantity": 1
    },
    {
      "id": "2e9d5b7a-4c83-4f16-a9b2-6d8e1c3f7a49",
      "item": {
        "id": "b5f81c3d-6e49-4a2b-8d73-9c0e4f1a6b25",
        "price": 990,
        "title": "Fľaša na vodu 750 ml"
      },
      "totals": [
        {
          "type": "subtotal",
          "amount": 1980
        },
        {
          "type": "total",
          "amount": 1980
        }
      ],
      "quantity": 2
    }
  ],
  "fulfillment": {
    "methods": [
      {
        "id": "shipping",
        "type": "shipping",
        "groups": [
          {
            "id": "default",
            "options": [
              {
                "id": "0c5e8a3f-7b21-4d96-8f4c-9e2a6d1b3c75",
                "title": "GLS kuriér",
                "totals": [
                  {
                    "type": "total",
                    "amount": 490
                  }
                ],
                "description": "Doručenie na adresu do 1–2 pracovných dní"
              },
              {
                "id": "6b2d9f4e-3a17-4c68-9d5b-1f8e3a7c2b40",
                "title": "Packeta – výdajné miesto",
                "totals": [
                  {
                    "type": "total",
                    "amount": 290
                  }
                ],
                "description": "Delivery in 2–3 business days"
              }
            ],
            "line_item_ids": [
              "f6a3c8e1-9d27-4b54-8e6a-2c1f7b9d3a58",
              "2e9d5b7a-4c83-4f16-a9b2-6d8e1c3f7a49"
            ],
            "selected_option_id": "0c5e8a3f-7b21-4d96-8f4c-9e2a6d1b3c75"
          }
        ],
        "destinations": [
          {
            "id": "destination",
            "last_name": "Nováková",
            "first_name": "Jana",
            "postal_code": "811 01",
            "street_address": "Hlavná 12",
            "address_country": "SK",
            "address_locality": "Bratislava"
          }
        ],
        "line_item_ids": [
          "f6a3c8e1-9d27-4b54-8e6a-2c1f7b9d3a58",
          "2e9d5b7a-4c83-4f16-a9b2-6d8e1c3f7a49"
        ],
        "selected_destination_id": "destination"
      }
    ]
  },
  "continue_url": "https://shop.example.com/ucp/continue/9bbd3dedf887f93c6287fa7f4b8de141501155714c3b2050bb83551e36ec780b"
}

A PUT after the buyer opened continue_url but before the order: the last answer, its values unchanged (served from storage, so its keys come in JSONB order), with its messages and a handed_off warning. The request was not applied. Only a PUT shows the handoff; a GET (and complete) returns the values of step 4.

Catalog messages

codetypepathcontentWhenWhat to do
price_filter_ignoredwarning$.filters.pricefilters.price is denominated in <currency>, prices are presented in <currency> and cannot be converted, so the price filter was not appliedSearch or lookup with filters.price in a context.currency the shop has no exchange rate for. Every match is returned.Filter the results yourself, or send the bounds in the currency of the answer.
not_foundinfononethe identifier itself, for example trek-marlin-9Lookup: an id that matched no product or variant the shop shows.Drop the id; the other results stand.

Showing messages to the buyer

content is English and written for developers. Build the text you show the buyer from code and path, in the buyer's language, and keep content for your logs. Three rules cover every message:

  1. Act on recoverable errors yourself, by sending what path names.
  2. Show warnings as the change they describe — "only 2 in stock, quantity lowered" — because the answer already contains the changed state.
  3. Pass requires_buyer_input to the buyer together with continue_url.

Codes may be added in the future. Handle an unknown code by its type and severity, never by failing.

Answers that are not JSON

The web server

The web server in front of every shop answers these itself, before the API sees the request. The body is a short HTML page, not JSON.

HTTPWhenRetry?What to do
301A request to http://. It is redirected to the same URL on https://; a client following it may turn a POST into a GET.—Call https:// from the start.
413A request body over 1 MiB.NoKeep bodies small.
429More than 100 requests a second from your IP, with a burst of 200, counted over all of the web server's shop traffic. No Retry-After.Yes, with backoffSpread requests; see Rate limits.
502The API is down or restarting.Yes, with backoffRetry writes with the same Idempotency-Key.
504The API did not answer within 60 seconds. A write may have been applied.Yes, with backoffRetry writes with the same Idempotency-Key: you get the stored answer, or a 409 while it is still running.
Web server rate limit (HTML)
<html>
<head><title>429 Too Many Requests</title></head>
<body>
<center><h1>429 Too Many Requests</h1></center>
<hr><center>nginx</center>
</body>
</html>

nginx’s own page, above 100 requests/s per client IP (burst 200): one budget for everything that IP sends to any shop host Monokaido serves (UCP calls, pages, images) and to the storefront API. No Retry-After, not JSON. Back off and retry. The footer may carry nginx’s version.

Paths the web server does not send to the API — upper-case /UCP/…, /.well-known/ucp/ with a trailing slash, /ucp without the rest — reach the shop's web pages and answer with HTML as well. Use the paths exactly as published.

The handoff routes

continue_url (GET /ucp/continue/<token>) is opened by the buyer's browser, and its page sends POST /ucp/continue/<token> itself. Your client calls neither, but you may see these answers reported by buyers:

RouteHTTPBodyWhen
GET200HTMLThe handoff page: it moves the lines into the buyer's cart and goes on to the shop.
GET410HTMLThe session is completed, canceled or expired, or the token is not one of this shop's.
GET404HTML (an empty page)The host no longer serves UCP.
GET, POST429JSON rate_limitedMore than 60 requests a minute from the buyer's IP to that route.
GET, POST503JSON service_unavailable, This shop is temporarily unavailableThe shop is suspended or could not be looked up — or, on POST only, the session was busy with your requests.
GET, POST500JSON internal_errorAn unexpected failure.
POST200{"redirect": "/checkout"}, "/cart" or "/"Where the page sends the browser next.
POST400JSON invalid_request, The request is not validThe page sent no usable guest-cart id.
POST404JSON not_found, Not foundThe session can no longer be handed over. (No UCP business on this host when the host serves no UCP.)

The page retries a 5xx, a 429 or a lost connection twice, after 1 and 2 seconds, then sends the browser to the shop's checkout, cart or home page without the lines. If your platform keeps polling a session the buyer is opening, keep at most one request in flight, so the buyer's claim gets its turn.

What to log

The API returns no request id. For every call, log at least the time in UTC, the host, the method and path, the HTTP status, code (or the message codes) and your own correlation id. Send that id as X-Request-ID too: the shop's web server writes it into its access log (it is not returned), so a problem can be found on Monokaido's side. Put nothing secret in it. Cart and checkout ids are credentials: keep them out of logs that other parties can read — see Security and data protection.