UCP conformance
Which parts of UCP 2026-04-08 Monokaido implements, which it does not, and what that means for an integration.
On this page
- What a shop publishes
- Capabilities, extensions and transports
- Protocol rules
- Discovery and the business profile
- Platform profile and negotiation
- Transport
- Authentication, signing and identity
- Checkout and its completion
- Cart
- Catalog
- Fulfillment extension
- Discount extension
- Order capability and webhooks
- Idempotency
- Errors and messages
- Where the specification is ambiguous
- Beyond the specification
- How changes are announced
Monokaido implements the Universal Commerce Protocol version 2026-04-08 —
that version only — over REST. This chapter compares the implementation with
the specification of that version, rule by rule, and says for each rule what
Monokaido does and what your client should do about it.
The comparison was made against the specification pages under
ucp.dev/2026-04-08 and the
official REST schema (rest.openapi.json) as published on 26 October 2026.
What this chapter says about Monokaido is the behaviour of the deployed code;
Concepts and lifecycle explains it in context.
| Status | Meaning |
|---|---|
| Supported | Implemented as the specification describes it. |
| Partial | Implemented with a narrower scope or another reading; the row says exactly how. |
| Not supported | Not implemented. Do not rely on it. |
| Not applicable | The rule cannot apply to this implementation. |
What a shop publishes
Every custom domain that serves UCP answers GET /.well-known/ucp with the
profile below. Shops differ in two things only. The first is the host, written
<shop host> here: the shop's primary domain when that domain serves UCP,
otherwise the domain the request came to; continue_url and
order.permalink_url use the same host. The second is catalog mode: a shop in
catalog mode lists the two catalog capabilities only.
| Capability | version | spec | schema | extends | Declared |
|---|---|---|---|---|---|
dev.ucp.shopping.catalog.search | 2026-04-08 | https://ucp.dev/2026-04-08/specification/catalog/search | https://ucp.dev/2026-04-08/schemas/shopping/catalog_search.json | — | always |
dev.ucp.shopping.catalog.lookup | 2026-04-08 | https://ucp.dev/2026-04-08/specification/catalog/lookup | https://ucp.dev/2026-04-08/schemas/shopping/catalog_lookup.json | — | always |
dev.ucp.shopping.cart | 2026-04-08 | https://ucp.dev/2026-04-08/specification/cart | https://ucp.dev/2026-04-08/schemas/shopping/cart.json | — | not in catalog mode |
dev.ucp.shopping.checkout | 2026-04-08 | https://ucp.dev/2026-04-08/specification/checkout | https://ucp.dev/2026-04-08/schemas/shopping/checkout.json | — | not in catalog mode |
dev.ucp.shopping.fulfillment | 2026-04-08 | https://ucp.dev/2026-04-08/specification/fulfillment | https://ucp.dev/2026-04-08/schemas/shopping/fulfillment.json | "dev.ucp.shopping.checkout" | not in catalog mode |
dev.ucp.shopping.discount | 2026-04-08 | https://ucp.dev/2026-04-08/specification/discount | https://ucp.dev/2026-04-08/schemas/shopping/discount.json | ["dev.ucp.shopping.checkout", "dev.ucp.shopping.cart"] | not in catalog mode |
| Profile key | Value |
|---|---|
ucp.version | "2026-04-08" |
ucp.services | one key, dev.ucp.shopping, with one entry: transport: "rest", endpoint: "https://<shop host>/ucp/v1", version: "2026-04-08", spec: "https://ucp.dev/2026-04-08/specification/overview", schema: "https://ucp.dev/2026-04-08/services/shopping/rest.openapi.json" |
ucp.payment_handlers | {} |
ucp.supported_versions | absent |
signing_keys, keys | absent |
All spec and schema URLs sit on https://ucp.dev/, the origin the
specification binds the dev.ucp.* namespace to.
| Operation | Root capability |
|---|---|
POST /ucp/v1/catalog/search | dev.ucp.shopping.catalog.search |
POST /ucp/v1/catalog/lookup, POST /ucp/v1/catalog/product | dev.ucp.shopping.catalog.lookup |
every /ucp/v1/carts route | dev.ucp.shopping.cart |
every /ucp/v1/checkout-sessions route | dev.ucp.shopping.checkout |
Capabilities, extensions and transports
| Specification item | Status | What it means for you |
|---|---|---|
dev.ucp.shopping.catalog.search | Partial | Text search over the product title (in the answer's language) and the shop's own product codes — a substring match that ignores case but not diacritics — with an optional price filter. A non-empty filters.categories is refused with 400; other filters are ignored. Results come newest first, with no relevance ranking. The query does not match a variant's SKU or GTIN: lookup finds a variant by its SKU, and nothing finds one by GTIN. |
dev.ucp.shopping.catalog.lookup | Partial | catalog/lookup takes up to 20 ids (variant id, variant SKU, product id or handle); catalog/product reads one product by product id, variant id or handle, with an option selection. preferences is ignored: a selection no variant matches is relaxed from the end of selected. |
dev.ucp.shopping.cart | Partial | An update keeps the fields it leaves out, and a checkout made from a cart does not write its changes back to the cart (see Cart). A shop in catalog mode does not declare it; cart calls there answer 200 with capabilities_incompatible. |
dev.ucp.shopping.checkout | Partial | A checkout is never completed through the API: a ready checkout is requires_escalation and the buyer finishes on the shop through continue_url. |
dev.ucp.shopping.fulfillment | Partial | One shipping method with one group of options. No pickup method type, no split shipments. |
dev.ucp.shopping.discount | Partial | One code is applied per session; further codes get discount_code_combination_disallowed. Without the negotiated extension no code is read or applied. |
dev.ucp.shopping.order | Not supported | No GET /orders/{id}, no order webhooks. Poll GET /ucp/v1/checkout-sessions/{id} for status: "completed" and order. |
dev.ucp.common.identity_linking | Not supported | Every session is anonymous. A buyer signed in to the shop in the browser tab that opens continue_url gets the items in their customer cart. |
dev.ucp.shopping.ap2_mandate | Not supported | No autonomous payment: the buyer always finishes on the shop. |
dev.ucp.shopping.buyer_consent | Not supported | The shop's legal links arrive in the checkout's links[]; consent is given on the shop. |
dev.ucp.common.loyalty | Not supported | — |
dev.ucp.shopping.permalink | Not supported | Use continue_url. |
| Payment handlers | Not supported | payment_handlers is always {}. The buyer pays on the shop's own checkout with the shop's own payment methods. |
Signals (dev.ucp.buyer_ip, dev.ucp.user_agent) and attribution | Not supported | Accepted and ignored, like every field the API does not use. No signal message ever asks for one. |
Transport rest | Supported | The only transport. |
Transports mcp, a2a, embedded | Not supported | The profile lists a single REST service. continue_url is a full page that refuses to be shown in a frame, not an embeddable checkout. |
Protocol rules
The first column names the rule and, in brackets, the specification page it comes from.
Discovery and the business profile
| Rule | Status | Monokaido | For your client |
|---|---|---|---|
The profile is at /.well-known/ucp, over HTTPS, and does not redirect (overview) | Supported | Served on every custom domain that serves UCP. Only plain http:// is redirected (301) to https://. | Fetch the https:// address directly. |
Cache-Control is public with max-age of at least 60, never private, no-store or no-cache (overview) | Supported | public, max-age=300 | Cache the profile for up to five minutes. |
version, services and payment_handlers are present, registries even when empty (overview) | Supported | payment_handlers is {} by design. | — |
signing_keys (may) (overview) | Not supported | None published: Monokaido sends no webhooks and signs nothing. | Answers cannot be verified by signature. |
supported_versions for the older versions a business supports (overview) | Not applicable | Monokaido supports one version, so there is no older one to list. | — |
Platform profile and negotiation
| Rule | Status | Monokaido | For your client |
|---|---|---|---|
The platform sends UCP-Agent: profile="…", an RFC 8941 dictionary, on every request (overview, REST binding) | Supported | Read on every /ucp/v1 call by a parser for the subset UCP uses: lower-case keys, profile as a quoted string, other members and parameters ignored, the last profile wins. A missing header, a header without a quoted profile, or one that does not parse as a whole is 400 invalid_profile_url. | Send exactly profile="https://…". |
The profile URL points to /.well-known/ucp (REST binding) | Partial | Not enforced: any https:// URL of at most 1000 characters without whitespace or credentials is accepted, any port, path and query. | Host it at /.well-known/ucp anyway. |
| The business fetches and validates the platform profile unless it has it cached (overview) | Supported | GET with no redirects, 200 only, at most 256 KiB, 5 s, public addresses only. Cached for the profile's max-age clamped to 60 s – 1 h (5 min without one; no-store, no-cache and private are ignored), served stale for up to 1 h while it refreshes; failures cached 60 s. At most 5 profiles that are not cached yet are fetched a minute per client (an IPv4 address or an IPv6 /64), and 60 in all per API server process; beyond that 503 with Retry-After. Only ucp.version and ucp.capabilities are validated. | Serve it fast with Cache-Control: public, max-age=3600. Nothing else in your profile is checked. |
| Intersection: same name, highest common version, prune extensions without an active parent, repeat (overview) | Supported | Parents come from the shop's declaration; an extension with several parents stays while any one is active. Your own extends is ignored. The shop lists each capability at 2026-04-08 only, so a capability your profile lists at other versions only is not active. | Declare checkout to get fulfillment; declare cart or checkout to get discount. |
The response's ucp names the version and the capabilities active for the operation: checkout with discount and fulfillment, cart with discount (overview) | Supported | capabilities lists the root capability and each negotiated extension of it. | Read the extensions there before relying on their fields. |
Request-time validation: the platform's version must be the business's or a key of its supported_versions, otherwise version_unsupported (overview) | Supported | Any version other than 2026-04-08 is refused with 422 version_unsupported. | Declare 2026-04-08. |
| A business SHOULD handle requests from platforms using its version or older (overview) | Not supported | Older published versions, such as 2026-01-23, are refused like any other. | — |
Negotiation errors and their HTTP status: invalid_profile_url 400, profile_unreachable 424, profile_malformed 422, version_unsupported 422, capabilities_incompatible 200 (overview) | Supported | Each carries continue_url to the shop's home page. capabilities_incompatible is a 200 business answer with ucp.status: "error". | Treat capabilities_incompatible as final for that operation on that shop until its profile changes. |
Transport
| Rule | Status | Monokaido | For your client |
|---|---|---|---|
| HTTPS with TLS 1.3 at least (REST binding) | Partial | TLS 1.2 and TLS 1.3 are both accepted. | If your policy demands TLS 1.3, require it in your client; the shop offers it. |
JSON with Content-Type: application/json (overview) | Supported | Every /ucp/v1 answer is JSON. The continue_url page is HTML, by design: it is for the buyer's browser. | Send Content-Type: application/json in UTF-8. |
Request-Id header, marked required on every operation in the REST header table and the official schema (REST binding) | Not supported | Neither read nor returned. No request id of any kind comes back. | Sending it is harmless; log your own correlation id. |
Idempotency-Key header, marked required in the REST header table and on every write in the official schema (REST binding) | Partial | Optional; see Idempotency. | Send it on every write. |
Authentication, signing and identity
| Rule | Status | Monokaido | For your client |
|---|---|---|---|
| Businesses SHOULD authenticate platforms — API key, OAuth bearer token, mutual TLS or HTTP message signatures (overview, REST binding) | Not supported | The API is open, the "Open API" option the REST binding lists. The shop is chosen by the Host header; the session id is the only credential of a session. Credentials you send are ignored. | Keep session ids private. Do not assume another caller cannot reach a session whose id it knows. |
Request signing (RFC 9421 Signature, Signature-Input, Content-Digest) and signed responses (overview, signatures) | Not supported | Signatures you send are ignored; nothing is signed. | — |
Signature and trust errors signature_missing, signature_invalid, key_not_found, digest_mismatch, algorithm_unsupported, profile_not_trusted (overview, signatures) | Not supported | Never emitted. | — |
Identity linking (OAuth 2.0) and the scopes dev.ucp.shopping.checkout:manage, dev.ucp.shopping.cart:manage (identity linking, checkout, cart) | Not supported | Sessions are guest sessions. The buyer signs in, if at all, on the shop. | — |
Checkout and its completion
| Rule | Status | Monokaido | For your client |
|---|---|---|---|
payment_handlers in the checkout's ucp (checkout) | Supported | {} on every checkout answer that is not an error. | — |
| Update is a full replacement: the platform sends the entire checkout (checkout) | Partial | line_items, when sent, replaces every line, and a buyer or context sent replaces the stored one whole (null clears it). A field the request leaves out keeps its stored value instead of being cleared: the discount codes are cleared by discounts.codes: [], and a shipping address stays until another one is sent. | Send the whole resource on every update, as the specification asks. |
Complete checkout places the order and returns the checkout with order (checkout) | Not supported | POST …/complete answers the checkout as it stands. Payment data in the body is ignored. | Never expect an order from complete. Hand the buyer continue_url, then poll GET. |
Statuses incomplete, requires_escalation, ready_for_complete, complete_in_progress, completed, canceled (checkout) | Partial | Four are used. Any recoverable error → incomplete; none → requires_escalation with payment_required. completed only when the buyer opened the checkout's continue_url (or that of the cart it was made from) and then placed an order on the shop from the cart the handoff filled, holding at least one of the checkout's items. ready_for_complete and complete_in_progress never occur. | Treat requires_escalation as "ready: hand over". |
requires_escalation carries continue_url and at least one message needing the buyer (checkout) | Supported | Always payment_required with severity requires_buyer_input, plus pickup_point_required when the chosen method needs a pickup point. | continue_url is a link for the buyer's browser, not for your server. |
continue_url in every non-final answer, absent in final ones, absolute HTTPS (checkout) | Supported | In every cart answer and every open checkout; absent from completed and canceled checkouts. | — |
Order confirmation order: {id, label?, permalink_url} (checkout) | Supported | id is the order's UUID, label its number, permalink_url the shop's confirmation page, which needs no sign-in. The rest of the body is the last quote, not the order. | Do not present the quote's totals as the order's. |
| A completed checkout is immutable (checkout) | Supported | Writes are compare-and-set; nothing reopens a completed checkout. An update of a completed or canceled checkout answers it unchanged with checkout_completed or checkout_canceled. | — |
| Cancel is refused for a completed or canceled checkout (checkout) | Supported | Answered 200 with the resource and the message checkout_completed or checkout_canceled (unrecoverable); ucp.status is absent. An order is never canceled through UCP. | Read the message, not only the HTTP status. |
| The business sends a confirmation e-mail once the checkout is completed (checkout) | Partial | The shop's own order confirmation e-mail, the same as for every order placed on its checkout, goes to the address the buyer entered there. A merchant who switched off its transactional e-mails sends none. | — |
expires_at, RFC 3339; 6 hours when the business sets none (checkout) | Supported | Always set. Carts live 7 days and checkouts 24 hours from their last create or update; a read does not extend them. | Read expires_at; do not assume 6 hours. |
links[] with the legal links (checkout) | Partial | From the shop's checkout consents that have a link: type is privacy_policy when the consent looks like one, otherwise terms_of_service, at most 10, links starting with / made absolute on the shop's host. refund_policy, shipping_policy and faq are never used. [] when the shop has no consent with a link. | Show them to the buyer before the handoff. |
context: address_country, address_region, postal_code, intent, language, currency, eligibility, payment (checkout) | Partial | context.language picks the language and thus the currency of prices. context.currency only denominates filters.price in catalog requests; it never changes the currency of prices. address_country, address_region and postal_code are checked, stored and returned in carts and checkouts, but change neither prices nor shipping options: the shipping address does that. intent, eligibility and payment are ignored, without the eligibility_not_accepted warning the specification suggests. | Choose the currency through the language; send the shipping address for shipping options. |
Cart
| Rule | Status | Monokaido | For your client |
|---|---|---|---|
POST /carts, GET and PUT /carts/{id}, POST /carts/{id}/cancel (cart) | Supported | — | — |
Create answers 201 Created (official REST schema) | Supported | 201 for a new cart or checkout. 200 when a conversion returns the cart's open checkout, and for business errors, which the REST binding answers with 200. | — |
| Update is a full replacement: the platform sends the entire cart (cart) | Partial | line_items is required on every update (400 without it) and replaces every line. A buyer or context sent replaces the stored one whole (null clears it); buyer, context and discounts the request leaves out keep their stored value, and discounts.codes: [] clears the codes. | Send the whole resource on every update. |
Get cart answers not_found for a cart that does not exist, has expired or was canceled (cart) | Supported | As an HTTP 200 business answer with ucp.status: "error", never a 404. | — |
Cancel cart returns the cart as it was; later operations SHOULD answer not_found (cart) | Supported | Every later call on the cart answers not_found. The cancel answer still carries the cart's continue_url, which no longer works. | — |
The cart provides continue_url and estimated totals and may set expires_at (cart) | Supported | A cart's continue_url opens the shop's cart page, or its checkout page when an open checkout was made from the cart: that checkout is then handed over. Cart totals hold no shipping. | — |
Cart to checkout with cart_id: the cart's contents, overlapping checkout fields ignored, an existing incomplete checkout returned (cart) | Partial | cart_id is read only when the cart capability is negotiated, as the specification says; otherwise the request is an ordinary create and needs line_items. Lines, buyer, context and language come from the cart; the request's line_items, buyer and context are ignored. Discount codes in the request replace the cart's when sent. An open checkout of the cart is returned (200) with nothing from the request applied; an expired one is canceled and a new one made. A cart already taken to the shop is refused with handed_off. | Send cart_id without discounts to keep the cart's codes; change the checkout with PUT. |
| The cart's discount codes carry forward to the checkout (discount) | Partial | Only when the conversion request sends no codes. | As above. |
| Changes made during checkout are reflected back to the cart (should) (cart) | Not supported | The checkout gets its own copy of the lines; the cart stays as it was. | Once converted, work on the checkout only. |
Catalog
| Rule | Status | Monokaido | For your client |
|---|---|---|---|
POST /catalog/search, /catalog/lookup and /catalog/product; with lookup, both lookup and product (catalog REST binding) | Supported | — | — |
A search carries at least one input (query, filters), and the business validates it (catalog search) | Partial | Not enforced: a search without query and filters lists the whole catalog, newest first. | Always send query or filters.price. |
| Cursor pagination, default page size 10 (catalog REST binding) | Supported | At most 20 per page; a larger limit is lowered without an error. The cursor carries only the page number. Pages can be short, or empty while has_next_page is true. | Send the same query, filters, limit and context with every cursor; follow has_next_page, not the page size. |
Lookup answers 200; unknown ids give fewer products, optionally an info not_found (catalog REST binding) | Supported | One info message not_found per id that matched nothing. | — |
Too many ids in a batch → 400 request_too_large (catalog lookup, catalog REST binding) | Supported | More than 20 ids. Other caps (100 lines, 10 discount codes) answer 400 invalid_request. | — |
Product not found → 200, ucp.status: "error", not_found unrecoverable (catalog REST binding) | Supported | With continue_url to the shop's home page. | — |
filters.categories (catalog search) | Not supported | A non-empty list is refused with 400 invalid_request. | Search by query and filters.price. |
Fulfillment extension
| Rule | Status | Monokaido | For your client |
|---|---|---|---|
methods[] of type shipping or pickup, with destinations, groups, options and selected_option_id (fulfillment) | Partial | One method, type: "shipping", with one group whose options are the shop's shipping methods, even when your profile declares supports_multi_group. The request's first shipping method is read: its selected (else first) destination and the first group's selected_option_id. The destination's own id comes back as "destination". Options carry id, title, optionally description, and totals. A method that needs a pickup point adds pickup_point_required: the buyer picks the point on the shop. | Send one shipping method with one destination; leave pickup points to the shop. |
Discount extension
| Rule | Status | Monokaido | For your client |
|---|---|---|---|
discounts.codes replaces the stored codes, [] clears them; discounts.applied; rejected codes as warnings with a path; amounts in totals[] and line_items[].totals[] (discount) | Partial | One code is applied — the first the shop accepts. Each further code gets discount_code_combination_disallowed; a rejected code gets discount_code_expired, discount_code_user_ineligible or discount_code_invalid, with path $.discounts.codes[<index>], the index in discounts.codes as the answer returns it (duplicates and blank codes removed). applied holds at most one entry, with method: "across". The amount appears in the top-level totals only, not in line_items[].totals. discount_code_already_applied and discount_code_user_not_logged_in are never sent. | Send the most important code first. |
Order capability and webhooks
| Rule | Status | Monokaido | For your client |
|---|---|---|---|
GET /orders/{id} and signed order webhooks to the platform's webhook_url (order) | Not supported | No order route, no webhooks of any kind. Only a checkout reports an order; a cart without a checkout, handed over through its own continue_url, never does. | Poll the checkout; stop at completed or canceled. |
Idempotency
| Rule | Status | Monokaido | For your client |
|---|---|---|---|
Writes carry an Idempotency-Key (REST header table: required; REST binding prose: should support) | Partial | Optional. Without a key a write runs unprotected: a retried create makes a second session. A key is 1–255 printable ASCII characters without spaces. | Send a fresh key with every write and reuse it on every retry of that write. |
| Results are kept at least 24 hours, 48 recommended (REST binding, signatures) | Supported | At least 25 hours from the key's first use: an hourly job deletes older keys. The recommended 48 hours are not met. | Do not rely on a key more than 24 hours after its first use. |
A duplicate with the same payload gets the stored result; a different payload gets 409 (REST binding) | Supported | 409 idempotency_conflict. A duplicate while the first request still runs also gets 409, with Retry-After: 2. Business answers (200 with ucp.status: "error") are stored and replayed too. A request whose operation fails with a protocol error (400, 503, 500) releases its key. | After a 503 or a network error, retry with the same key. |
| The payload is matched by the SHA-256 of the raw body bytes (signatures) | Partial | Matched by the canonical JSON of the operation, the path id and the body with sorted keys: whitespace and key order do not matter, array order does. | — |
| Keys are unique per client and per operation type (signatures) | Partial | Scoped to the shop and to your exact profile URL, not to the operation: the same key on another operation or another session id is 409 idempotency_conflict. | Keep your profile URL stable, and never reuse a key across writes. |
| A key has at least 128 bits of entropy (signatures) | Not applicable | A rule for the key you generate. Monokaido checks only its format. | Use a UUID v4. |
On a storage failure the business fails closed with 503 (signatures) | Partial | A key that cannot be claimed fails the request with 500 internal_error before anything runs. A result that cannot be stored after the operation ran is logged and the answer still goes out; a retry with the key then gets 409 until the 120-second claim runs out, and after that the operation runs again. | Treat 409 with Retry-After as "retry later". |
Errors and messages
| Rule | Status | Monokaido | For your client |
|---|---|---|---|
Protocol errors as JSON {code, content} with their HTTP status, Retry-After on 429 and 503 (overview, REST binding) | Supported | invalid_request 400 (415 for an unsupported charset or encoding), invalid_profile_url 400, not_found 404, idempotency_conflict 409, request_too_large 400 or 413, profile_malformed and version_unsupported 422, profile_unreachable 424, rate_limited 429, internal_error 500, service_unavailable 503. Retry-After is 60 on a 429 and 1 to 60 on a 503. Profile, version and discovery errors add a continue_url to the shop's home page. Exception: the web server answers some requests itself with HTML — 413 above 1 MiB, 429 above 100 requests a second per IP, 502 and 504 when the API is down or slow. | Handle the HTML answers by status code alone. |
401 and 403 (overview) | Not applicable | 401 is never sent. 403 comes only from the platform's cross-origin write gate, for a write that carries a Monokaido session cookie and comes from a foreign web origin — a server-side agent never does that; its body is invalid_request. | — |
Business outcomes are HTTP 200 with messages[]; ucp.status is an optional discriminator (checkout, overview) | Supported | Successful answers omit ucp.status; answers without a resource set "error". Session and product not_found are business answers. | Read a missing ucp.status as success. |
Message shapes and severities recoverable, requires_buyer_input, requires_buyer_review, unrecoverable (checkout) | Partial | recoverable, requires_buyer_input and unrecoverable are used; requires_buyer_review never. Warnings carry no presentation; no message has content_type. A missing field is reported with the code missing (the specification's example uses field_required; codes are open-ended). | Branch on type and severity; treat codes you do not know by their severity. |
Where the specification is ambiguous
| Topic | What the specification says | What Monokaido implements | For your client |
|---|---|---|---|
The parents of dev.ucp.shopping.discount | The overview's example business profile has "extends": "dev.ucp.shopping.checkout"; the overview's multi-parent example and the discount page declare cart and checkout as parents. | Both parents. Discount is active with the cart alone. | Declare discount whenever you use carts or checkouts. |
The shape of version_unsupported | The error table gives REST status 422 and the REST example is a {code, content, continue_url} body; the example under request-time validation shows an answer with a ucp envelope and a message. | A 422 protocol error, {code, content, continue_url}, without a ucp envelope. | Handle it as a protocol error. |
Idempotency-Key and Request-Id | The REST header table and the official schema mark both as required; the prose says operations that modify state should support idempotency. | The prose: the key is optional, Request-Id is ignored. | Send the key on every write. |
| Cart-to-checkout and discount codes | The cart page asks the business to use the cart's contents and ignore overlapping checkout fields; the discount page asks it to carry the cart's codes forward. | Lines, buyer, context and language from the cart; codes from the request when it sends some, else from the cart; shipping from the request. | Omit discounts from the conversion request to keep the cart's codes. |
When to use requires_escalation | The status of a checkout that needs information the API cannot take, or the buyer's input; the checkout page also says a checkout is finalized by the buyer through a trusted UI unless the AP2 mandates extension is supported. | Every ready checkout, because payment always happens on the shop. It is the last state the API reaches by itself. | Hand over at requires_escalation; do not wait for ready_for_complete. |
An empty links[] | The field is required. | [] when the shop has no checkout consent with a link, or the consents could not be read. | Accept an empty list. |
| The platform profile's own caching | A profile must be served with public and a max-age of at least 60; the fetching rules let a business cache it with a 60-second floor regardless of its Cache-Control and refresh it in the background. | Its max-age is used, clamped to 60 s – 1 h; no-store, no-cache and private are ignored. | Set max-age to what you can live with. |
Beyond the specification
Monokaido adds constraints the specification leaves open. They are part of the contract:
- One shop per host. The shop is chosen by the
Hostheader. A host that does not serve UCP answers404 not_foundwhatever the reason; a suspended company answers503. See Concepts and lifecycle. - Rate limits per client IP and per operation, per minute: 600 reads, 300
writes, 60 handoff requests;
429 rate_limitedwithRetry-After: 60. Answers carryX-RateLimit-Limit,X-RateLimit-RemainingandX-RateLimit-Reset. The web server adds 100 requests a second per IP (burst 200), counted together with that IP's other storefront traffic. Details in Idempotency, concurrency and limits. - Request caps. 100 lines, quantity 999 per line (a larger quantity is lowered to 999), 10 discount codes, 20 lookup ids, page size 20, body 1 MiB.
- One request at a time per session. Writes and checkout reads on one
session queue; a request that cannot get its turn within 10 seconds, or finds
four already waiting, gets
503withRetry-After: 1. - Server-side calls only. There is no CORS for third-party origins, so a web page on another origin cannot call the API.
- Caching. Every answer is
no-storeexcept the business profile; answers also carry anETag. - The handoff.
continue_urlcarries the lines and the applied discount code to the shop's cart, not the buyer's details. See Security and data protection for what is stored and for how long.
How changes are announced
- The business profile is the authoritative statement of what a shop serves at a given moment: the protocol version, the endpoint and the capabilities. It can change when the merchant changes the shop (catalog mode, switching UCP off, its domains), and it is meant to be re-read: it is cacheable for five minutes.
- The protocol version is fixed. The specification allows backward-
compatible changes within a version — new optional fields, new error codes,
new enum values — and requires a new version for a breaking one. Monokaido
does not change what
2026-04-08means: support for another version would appear in the profile'sucp.versionandsupported_versions. - This manual documents the contract. Each page shows the date it was last updated, and the OpenAPI document and the other machine-readable files are published with these pages. The manual offers no subscription, deprecation schedule or service-level commitment.
To notice a change early, compare ucp.version and the list of capabilities
each time you re-read a shop's profile, compare the OpenAPI document with the
copy you built against from time to time, and ignore fields, codes and enum
values you do not know in answers — new ones may appear.