API reference (OpenAPI)
Every UCP endpoint, header, field and response — OpenAPI 3.1 to download for Swagger, Postman and client generators.
On this page
The reference below is generated from the OpenAPI 3.1 document that describes Monokaido's UCP API: every operation, header, field, status code and example. The same document is published for download, so you can import it into your tools or generate a typed client from it. A guided walk through one purchase is in the Developer quickstart.
Base URL and conventions
- One server per shop. Each shop serves the API on its own custom
domain: the business profile at
https://shop.example.com/.well-known/ucp, the REST operations underhttps://shop.example.com/ucp/v1. Take the base from the profile'sendpoint, which names the shop's primary domain. The routes under/ucp/continue/are opened by the buyer's browser, never by an agent. - No authentication. No API key, no OAuth, no cookie: the shop is chosen
by the host you call. Every call under
/ucp/v1names your platform profile inUCP-Agent: profile="https://…". - Headers.
Content-Type: application/jsonon every request with a body;Idempotency-Keyon the seven writes (optional, strongly recommended);Accept-Languageoptional. - JSON in UTF-8. Fields the API does not know are ignored. A field it
reads with the wrong JSON type is refused with
400 invalid_request, andcontentnames the field. A few hints it cannot use are dropped instead: an unreadablecontext.language,context.currencyorcontext.address_country, entries ofdiscounts.codesoridsthat are not strings, aselected_option_idorselected_destination_idthat is not a string, and lineids (the full list: What is adjusted instead of refused). - Key order of JSON objects is not significant, in requests or answers.
Answers built from stored data — a replayed write, a completed, canceled or
handed-over checkout, a stored
buyerorcontext— come back in PostgreSQL's JSONB key order (shorter keys first, then alphabetical). - Money is an integer in the ISO 4217 minor units of its currency,
VAT included. The catalog writes it as an object,
{"amount": 69900, "currency": "EUR"}(699.00 €); carts and checkouts write bare integers initem.priceandtotals[].amountnext to onecurrencyfor the whole session. EUR, CZK, PLN and HUF have two decimals (1 990 Ft is199000), JPY none, KWD three. Adiscounttotal is negative. - Timestamps are ISO 8601 in UTC with milliseconds:
"expires_at": "2026-10-27T09:21:30.551Z". - Ids. Carts and checkouts have lower-case UUID v4 ids; products,
variants, lines, shipping options and orders are UUIDs too. A path
{id}that is not a UUID answers like an unknown session. The token incontinue_urlis 64 hexadecimal characters and is never the session id. - Two kinds of failure. A protocol error is an HTTP error status with
{"code", "content", "continue_url"?}. A business outcome is HTTP200(201for a created cart or checkout) withmessages[], and withucp.status: "error"when no resource came back. An unknown cart, checkout or product is such a200, never a404. A few answers come from the web server in front as HTML. Every code is in Errors and messages. - Caching. Every API answer is
Cache-Control: no-store, except the business profile (public, max-age=300). - Limits per client IP and operation: 600 reads, 300 writes and 60
handoff requests per minute, shared by every shop your IP calls. Answers
within the limit carry
X-RateLimit-Limit,X-RateLimit-RemainingandX-RateLimit-Reset(seconds); a429carriesRetry-After: 60. The web server in front adds 100 requests per second per IP (bursts of up to 200 more) and a body limit of 1 MiB. Details in Idempotency, concurrency and limits.
Downloads
The whole UCP API to import into Swagger Editor, Postman, Insomnia or openapi-generator — by URL or as a file.
https://help.monokaido.com/ucp/ openapi.json The same document in YAML, for tools and reviews that prefer it.
https://help.monokaido.com/ucp/ openapi.yaml Tool definitions for a language model’s function calling, mapped onto the UCP operations.
https://help.monokaido.com/ucp/ tools.json The whole UCP developer guide as one Markdown file, for a language model’s context.
https://help.monokaido.com/ucp/ llms-full.txt The contents of the whole manual for language models, in the llms.txt format.
https://help.monokaido.com/llms.txt
Using the document in your tools
The files are served with Access-Control-Allow-Origin: *, so tools that run
in a browser can import them by URL too. The document's server is
https://{shopDomain} with shop.example.com as the default: point every
tool at the shop you integrate with, and replace the example UCP-Agent
value with your own profile.
Swagger Editor
Open editor.swagger.io, choose File → Import
URL and paste https://help.monokaido.com/ucp/openapi.json. The editor is
good for reading and validating. Its Try it out sends requests from your
browser, which the shop's API does not allow from other origins, so send
requests from Postman, Insomnia, curl or your server instead.
Postman and Insomnia
Use Import and paste the URL https://help.monokaido.com/ucp/openapi.json;
both turn the operations into a collection of requests. Then set the
collection's base URL to the shop's origin and the UCP-Agent header to
your profile. Both send requests from the desktop app, so CORS does not
apply.
A typed TypeScript client
openapi-typescript turns the document into types,
and openapi-fetch uses them for a small typed client:
npm install openapi-fetch
npx openapi-typescript https://help.monokaido.com/ucp/openapi.json -o src/ucp.d.ts
import createClient from 'openapi-fetch';
import type { paths } from './ucp.js';
const shop = createClient<paths>({ baseUrl: 'https://shop.example.com' });
const agent = 'profile="https://platform.example.com/.well-known/ucp"';
const { data, error, response } = await shop.POST('/ucp/v1/catalog/search', {
params: { header: { 'UCP-Agent': agent } },
body: { query: 'marlin', pagination: { limit: 2 }, context: { language: 'sk' } },
});
if (!data) throw new Error(`HTTP ${response.status}: ${JSON.stringify(error)}`);
// HTTP 200 is either the result or a business error with ucp.status "error".
if ('products' in data) {
for (const p of data.products) console.log(p.title, p.price_range.min.amount);
} else {
console.log(data.messages);
}
A 200 answer is typed as the result or a business error, so the type
checker makes you handle both.
openapi-generator
openapi-generator builds clients for many languages. It needs Java; the npm wrapper downloads the generator itself:
# TypeScript (fetch)
npx @openapitools/openapi-generator-cli generate \
-i https://help.monokaido.com/ucp/openapi.json \
-g typescript-fetch -o ./ucp-client
# Python
npx @openapitools/openapi-generator-cli generate \
-i https://help.monokaido.com/ucp/openapi.json \
-g python -o ./ucp-client-py --package-name ucp_client
The generator calls its OpenAPI 3.1 support beta and prints warnings while it
runs; it also renames models whose names clash with the target language (in
TypeScript the Error schema becomes ModelError and RequestContext
becomes ModelRequestContext) and names the inline answer types itself
(GetCart200Response, CreateCheckout200Response for every checkout
operation). Pass the shop's origin as the client's base path: the generated
default is https://shop.example.com. A generated client cannot tell the
business error of an HTTP 200 from a resource by the status: check
ucp.status === "error" on every 200 as well.
Operations
The examples call https://shop.example.com — use the domain of a shop that has UCP switched on instead.
Discovery
The business profile at /.well-known/ucp: the protocol version, the REST endpoint and the capabilities this shop declares. Needs no UCP-Agent header.
Get the business profile
GET/.well-known/ucp
The shop’s UCP business profile: protocol version, the REST endpoint to call, the capabilities it declares and payment_handlers: {}. Fetch it first and use its endpoint (on the shop’s primary domain). Needs no UCP-Agent header and no negotiation.
- Cacheable:
Cache-Control: public, max-age=300(every other UCP answer isno-store). Never redirected. - A shop in catalog mode declares only
dev.ucp.shopping.catalog.searchanddev.ucp.shopping.catalog.lookup. - No
supported_versions(one version only), nosigning_keys(no webhooks are sent, responses are not signed). - The web server maps this path to the API’s
/ucp/well-known, which answers the same./.well-known/ucp/(trailing slash) is not this route.
Merchant checks: Turning UCP on for a shop.
Responses
200The business profile.
Response headers
Cache-ControlstringThe business profile may be cached for 5 minutes by anyone.
always:public, max-age=300ETagstringAn ETag of the body, generated by the API on every response with a body (writes included). The API’s ETag is strong; the web server sends it weak (
W/"…") when it compresses the response, which it does for a client that sendsAccept-Encoding: gzip. AGETthat sends a matchingIf-None-Match(either form) can be answered304 Not Modifiedeven though the answer isno-store.X-RateLimit-LimitintegerRequests this client IP may make to this operation per 60-second window (600 reads, 300 writes, 60 handoff). Set on every answer that passed the rate limiter.
X-RateLimit-RemainingintegerThe operation’s limit minus this client IP’s current count. The counts live in the API server’s memory; each request is meant to leave the count 60 s after it was made, but the rate limiter (
@nestjs/throttler6.5) drops those pending decrements, for every client, whenever any client’s block ends. Read it as an upper bound and keep steady traffic at half a limit or less.minimum: 0X-RateLimit-ResetintegerSeconds left in a 60-second window the limiter keeps per client IP and operation. The count is not reset when that window ends, so this is not the moment the budget comes back.
minimum: 0
SchemaBusinessProfile
{
"ucp": {
"version": "2026-04-08",
"services": {
"dev.ucp.shopping": [
{
"version": "2026-04-08",
"spec": "https://ucp.dev/2026-04-08/specification/overview",
"transport": "rest",
"endpoint": "https://shop.example.com/ucp/v1",
"schema": "https://ucp.dev/2026-04-08/services/shopping/rest.openapi.json"
}
]
},
"capabilities": {
"dev.ucp.shopping.catalog.search": [
{
"version": "2026-04-08",
"spec": "https://ucp.dev/2026-04-08/specification/catalog/search",
"schema": "https://ucp.dev/2026-04-08/schemas/shopping/catalog_search.json"
}
],
"dev.ucp.shopping.catalog.lookup": [
{
"version": "2026-04-08",
"spec": "https://ucp.dev/2026-04-08/specification/catalog/lookup",
"schema": "https://ucp.dev/2026-04-08/schemas/shopping/catalog_lookup.json"
}
],
"dev.ucp.shopping.cart": [
{
"version": "2026-04-08",
"spec": "https://ucp.dev/2026-04-08/specification/cart",
"schema": "https://ucp.dev/2026-04-08/schemas/shopping/cart.json"
}
],
"dev.ucp.shopping.checkout": [
{
"version": "2026-04-08",
"spec": "https://ucp.dev/2026-04-08/specification/checkout",
"schema": "https://ucp.dev/2026-04-08/schemas/shopping/checkout.json"
}
],
"dev.ucp.shopping.fulfillment": [
{
"version": "2026-04-08",
"spec": "https://ucp.dev/2026-04-08/specification/fulfillment",
"schema": "https://ucp.dev/2026-04-08/schemas/shopping/fulfillment.json",
"extends": "dev.ucp.shopping.checkout"
}
],
"dev.ucp.shopping.discount": [
{
"version": "2026-04-08",
"spec": "https://ucp.dev/2026-04-08/specification/discount",
"schema": "https://ucp.dev/2026-04-08/schemas/shopping/discount.json",
"extends": [
"dev.ucp.shopping.checkout",
"dev.ucp.shopping.cart"
]
}
]
},
"payment_handlers": {}
}
}{
"ucp": {
"version": "2026-04-08",
"services": {
"dev.ucp.shopping": [
{
"version": "2026-04-08",
"spec": "https://ucp.dev/2026-04-08/specification/overview",
"transport": "rest",
"endpoint": "https://shop.example.com/ucp/v1",
"schema": "https://ucp.dev/2026-04-08/services/shopping/rest.openapi.json"
}
]
},
"capabilities": {
"dev.ucp.shopping.catalog.search": [
{
"version": "2026-04-08",
"spec": "https://ucp.dev/2026-04-08/specification/catalog/search",
"schema": "https://ucp.dev/2026-04-08/schemas/shopping/catalog_search.json"
}
],
"dev.ucp.shopping.catalog.lookup": [
{
"version": "2026-04-08",
"spec": "https://ucp.dev/2026-04-08/specification/catalog/lookup",
"schema": "https://ucp.dev/2026-04-08/schemas/shopping/catalog_lookup.json"
}
]
},
"payment_handlers": {}
}
}Request with curl
curl 'https://shop.example.com/.well-known/ucp'Catalog
Search, lookup and product detail over the shop’s catalog — the same products, prices and stock states the shop’s own pages show. Search reads the shop’s product listing, a materialised view the platform refreshes in the background after catalog changes, so a new or changed product can take a few minutes to show up there; lookup and product read the product data itself. Reads only; no Idempotency-Key.
Search the catalog
POST/ucp/v1/catalog/search
Full-text search over the products the shop lists, with paging and an optional price filter.
- Matching:
queryis a case-insensitive, diacritic-sensitive (modradoes not findModrá) substring of the product title in the answer’s language, or of the product code / external ids. Descriptions, brand names, variant SKUs and GTINs are not searched — uselookupCatalogfor SKUs and ids. - Order: newest product first. There is no relevance ranking and no sort option.
- What is listed: published, enabled, listed and orderable products the storefront shows (its exclusion filters and blocked categories apply). A product without a priced variant, or with no variant inside the price filter, is dropped after the page is read, so a page can be short — even empty while
has_next_pageis true. Keep followingcursoruntilhas_next_pageis false. - Paging:
pagination.limitdefault 10, at most 20 (larger values are lowered). The cursor encodes only the page number; repeat the query, filters, limit and context with it.total_countis the number of matching listed products and is left out whenever a price filter is in effect. Pages are counted over a listing that can change while you page (a product published meanwhile shifts the later pages): de-duplicate products byid. - Prices: in the currency of the answer’s language;
filters.priceis incontext.currency(seePriceFilter). Search reads the shop’s product listing, a materialised view refreshed in the background after catalog changes: a new or changed product can take a few minutes to show here (lookup and product read the product data itself). - Categories: not supported; a non-empty
filters.categories→ 400.
An absent body — or one of a content type the API does not parse (anything but application/json and application/x-www-form-urlencoded) — is an empty search.
Parameters
UCP-AgentheaderstringrequiredNames your platform profile. Required on every
/ucp/v1/*operation; never read by the profile and handoff routes.Details
Grammar — a subset of an RFC 8941 Dictionary, at most 4096 characters:
- members separated by
,(spaces or tabs around it allowed; a trailing comma is an error); keys lower-case, matching[a-z*][a-z0-9_\-.*]*; - the
profilemember must be an sf-string:profile="https://…", printable ASCII 0x20–0x7E, escapes\"and\\only; - other members (tokens, numbers, booleans, strings) and parameters (
;key=value) are parsed and ignored; whenprofileappears twice the last one wins; - any parse error makes the whole header unusable.
Accepted:
profile="https://platform.example.com/.well-known/ucp",version="2026-04-08", profile="https://platform.example.com/.well-known/ucp";sig=abc. Refused: a missing header,profile=https://…(a token, not a string),Profile="…",a=1 b=2, profile="…".The URL — at most 1000 characters, no whitespace anywhere, starting
https://, no user name or password, a non-empty host. Any port, path and query string are accepted (the spec’s/.well-known/ucppath is not enforced). Failing any rule →400 invalid_profile_url.The fetch —
GETwithAccept: application/jsonandUser-Agent: Monokaido-UCP/1.0 (+https://monokaido.com); no redirects followed; only HTTP 200 accepted; at most 256 KiB; 3 s idle, 5 s total; the host must resolve to a public address (private, loopback, link-local and other reserved ranges are refused). Anything else →424 profile_unreachable. A body that is not JSON, or whoseucp.versionis not aYYYY-MM-DDstring →422 profile_malformed; a valid profile of another version →422 version_unsupported. Onlyucp.versionanducp.capabilities(name →[{"version"}], first 20 entries per name, names likedev.ucp.shopping.cart) are read; everything else in it is ignored.Caching — a fetched profile is cached for its
Cache-Control: max-age, clamped to 60 s – 1 h (5 min without one;no-store,no-cacheandprivateare ignored) and served up to 1 h past that while one background refresh runs. A failed fetch (424, or422 profile_malformed) is remembered for 60 s. A profile of anotherucp.versionis a valid profile and is cached like one, so after you change its version expect422 version_unsupporteduntil that entry is refreshed (itsmax-age, then one more request) — or use a new profile URL. These caches are kept in memory by each API process. Fetching a profile the shop does not know yet is budgeted: 5 new profile URLs per minute per client (IPv4 address, IPv6 /64) and 60 per minute in all →503 service_unavailablewithRetry-After. The full URL string is the cache key, so keep it stable (no per-request query strings).minLength: 1maxLength: 4096example:profile="https://platform.example.com/.well-known/ucp"- members separated by
Accept-LanguageheaderstringFallback for the answer’s language after
context.languagein the body. The first 10 comma-separated tags are tried in header order (q=weights are ignored,*is skipped) against the languages the shop offers: an exact tag (case-insensitive), then the primary subtag (en-US→en-GB); with no match, the shop’s first language. (A storefront without a language list of its own offers every language of the company: then the primary subtag of the first usable tag is used as it is — a language the company has not set up is answered in its default currency, untranslated — and, with no tag at all, the company’s default language.)The language decides the currency of every price and the translations. A cart or checkout keeps the language it was created in: a later request changes it only when its body names a
context.languagedifferent from the stored one, and when that switches the shop language every line is re-added in the new currency and gets a new line id. Once a session has a language, this header alone never switches it. On a storefront without a language list, a session created with no usable tag stores none: each later request is then answered in the language its own tags pick, and a different pick adds the lines again (new line ids). Sendcontext.languageon the create.example:sk-SK,sk;q=0.9,en;q=0.8
Request body
Optional. No body lists every product, newest first.
A catalog search. The whole body is optional: no body (or {}) lists every product. Unknown fields (signals, …) are ignored.
querystring | nullFree text; longer text is cut to 200 characters. Matches, case-insensitively (but not ignoring diacritics) as a substring, the product title in the answer’s language or the product code / external ids — not descriptions, brands, variant SKUs or GTINs (use lookup for SKUs). Absent: every listed product.
maxLength: 200filtersSearchFiltersFilters. Other filters (brand, availability …) are ignored without a message.
paginationobjectPaging.
limitintegerPage size, default 10. A value above 20 is lowered to 20; 0, decimals and strings → 400.
default:10minimum: 1cursorstring | nullpagination.cursorfrom the previous answer. Opaque, at most 40 characters. It carries only the page number: resend the samequery,filters,limitandcontextwith it.maxLength: 40
contextRequestContext | nullBuyer context. Every field is optional; strings longer than the stated length are cut (counted in code points). Unknown fields (
intent,eligibility,payment…) are ignored.
{
"query": "marlin",
"pagination": {
"limit": 2
},
"context": {
"language": "sk",
"address_country": "SK",
"currency": "EUR"
}
}{
"query": "marlin",
"pagination": {
"limit": 2,
"cursor": "cDoy"
},
"context": {
"language": "sk",
"address_country": "SK",
"currency": "EUR"
}
}{
"query": "marlin",
"filters": {
"price": {
"max": 2500000
}
},
"pagination": {
"limit": 2
},
"context": {
"language": "sk",
"currency": "CZK"
}
}Responses
200A page of results (CatalogSearchResponse), or — when your profile does not declare dev.ucp.shopping.catalog.search — the capabilities_incompatible business err…
A page of results (CatalogSearchResponse), or — when your profile does not declare dev.ucp.shopping.catalog.search — the capabilities_incompatible business error.
Response headers
Cache-ControlstringEvery UCP answer (success or error) except the business profile.
always:no-storeETagstringAn ETag of the body, generated by the API on every response with a body (writes included). The API’s ETag is strong; the web server sends it weak (
W/"…") when it compresses the response, which it does for a client that sendsAccept-Encoding: gzip. AGETthat sends a matchingIf-None-Match(either form) can be answered304 Not Modifiedeven though the answer isno-store.X-RateLimit-LimitintegerRequests this client IP may make to this operation per 60-second window (600 reads, 300 writes, 60 handoff). Set on every answer that passed the rate limiter.
X-RateLimit-RemainingintegerThe operation’s limit minus this client IP’s current count. The counts live in the API server’s memory; each request is meant to leave the count 60 s after it was made, but the rate limiter (
@nestjs/throttler6.5) drops those pending decrements, for every client, whenever any client’s block ends. Read it as an upper bound and keep steady traffic at half a limit or less.minimum: 0X-RateLimit-ResetintegerSeconds left in a 60-second window the limiter keeps per client IP and operation. The count is not reset when that window ends, so this is not the moment the budget comes back.
minimum: 0
Exactly one of
- CatalogSearchResponseobject · CatalogSearchResponse
A page of search results.
additionalProperties: falseThe
ucpobject of a successful catalog or cart answer. A missingstatusmeans success.Newest product first (there is no relevance ranking).
Search paging.
messagesarray<Message>Present only when non-empty. Search emits only
price_filter_ignored.minItems: 1
- UcpBusinessErrorResponseobject · UcpBusinessErrorResponse
HTTP 200 with no resource. The cases:
capabilities_incompatible— your profile and this shop share no version of the operation’s root capability (always for carts and checkouts on a shop in catalog mode). Nothing ran; no Idempotency-Key was used.not_found— the product, cart (Cart not found or has expired) or checkout (Checkout session not found) does not exist for this shop, is malformed, or (carts) was canceled or expired. An unknowncart_idonPOST /checkout-sessionsalso readsCheckout session not found.out_of_stock/item_unavailable— a create in which none of the requested items could be added; no session was created.handed_off— converting a cart whose lines the buyer already took to the shop.
Inside a write the last three are stored under the Idempotency-Key like a success (
capabilities_incompatibleis answered before the key is read).additionalProperties: falseThe
ucpobject of a business error. Never haspayment_handlers.capabilitiesis{}forcapabilities_incompatibleand lists the operation’s root (and its negotiated extensions) otherwise.Exactly one error message saying why no resource could be returned.
minItems: 1maxItems: 1continue_urlstring (uri)requiredWhere a buyer could go instead: the shop home (
https://{primary domain}/), orhttps://{primary domain}/cartforhanded_off. Not a handoff link.
{
"ucp": {
"version": "2026-04-08",
"capabilities": {
"dev.ucp.shopping.catalog.search": [
{
"version": "2026-04-08"
}
]
}
},
"products": [
{
"id": "6f1d2b7c-3a84-4e59-b0c2-8d7e1f4a5b62",
"handle": "trek-marlin-5",
"title": "Trek Marlin 5",
"description": {
"plain": "Horský bicykel s hliníkovým rámom Alpha Silver, odpruženou vidlicou a 29\" kolesami.",
"html": "<p>Horský bicykel pre začínajúcich jazdcov.</p><ul><li>Rám Alpha Silver Aluminium</li><li>Vidlica SR Suntour XCT 30, zdvih 100 mm</li><li>Pohon Shimano 2×8</li></ul>"
},
"url": "https://shop.example.com/products/trek-marlin-5",
"price_range": {
"min": {
"amount": 69900,
"currency": "EUR"
},
"max": {
"amount": 71900,
"currency": "EUR"
}
},
"list_price_range": {
"min": {
"amount": 74900,
"currency": "EUR"
},
"max": {
"amount": 74900,
"currency": "EUR"
}
},
"media": [
{
"type": "image",
"url": "https://shop.example.com/api/public/uploads/c4d1e5a2-8b3f-4a6c-9e07-5f2d8a1b3c40/products/trek-marlin-5-modra.webp",
"alt_text": "Trek Marlin 5, modrá, bočný pohľad"
},
{
"type": "image",
"url": "https://images.vendor.example/trek/marlin-5-detail.jpg"
}
],
"options": [
{
"name": "Farba",
"values": [
{
"label": "Modrá"
},
{
"label": "Čierna"
}
]
},
{
"name": "Veľkosť",
"values": [
{
"label": "M"
},
{
"label": "L"
}
]
}
],
"variants": [
{
"id": "1e8a6c4f-2b93-4d71-9f05-6a3c8e2b1d47",
"sku": "MARLIN5-M-MOD",
"barcodes": [
{
"type": "EAN",
"value": "8594012345674"
}
],
"title": "Modrá / M",
"description": {
"plain": "Modrá / M"
},
"url": "https://shop.example.com/products/trek-marlin-5",
"price": {
"amount": 69900,
"currency": "EUR"
},
"list_price": {
"amount": 74900,
"currency": "EUR"
},
"availability": {
"available": true,
"status": "in_stock"
},
"options": [
{
"name": "Farba",
"label": "Modrá"
},
{
"name": "Veľkosť",
"label": "M"
}
]
},
{
"id": "7c2f9b1e-4a68-4e3d-b2c9-5d1f7a3e8b06",
"sku": "MARLIN5-L-MOD",
"title": "Modrá / L",
"description": {
"plain": "Modrá / L"
},
"url": "https://shop.example.com/products/trek-marlin-5",
"price": {
"amount": 69900,
"currency": "EUR"
},
"availability": {
"available": false,
"status": "out_of_stock"
},
"options": [
{
"name": "Farba",
"label": "Modrá"
},
{
"name": "Veľkosť",
"label": "L"
}
]
},
{
"id": "d93b4a7e-1f26-4c85-a0e7-3b9c6d2f4e18",
"sku": "MARLIN5-M-CIE",
"title": "Čierna / M",
"description": {
"plain": "Čierna / M"
},
"url": "https://shop.example.com/products/trek-marlin-5",
"price": {
"amount": 71900,
"currency": "EUR"
},
"availability": {
"available": true,
"status": "preorder"
},
"options": [
{
"name": "Farba",
"label": "Čierna"
},
{
"name": "Veľkosť",
"label": "M"
}
]
}
],
"tags": [
"Trek"
]
},
{
"id": "2f6c8a1d-7e43-4b95-9c2a-4d1e8b6f3a07",
"handle": "trek-marlin-7",
"title": "Trek Marlin 7",
"description": {
"plain": "Trek Marlin 7"
},
"url": "https://shop.example.com/products/trek-marlin-7",
"price_range": {
"min": {
"amount": 104900,
"currency": "EUR"
},
"max": {
"amount": 104900,
"currency": "EUR"
}
},
"media": [
{
"type": "image",
"url": "https://shop.example.com/api/public/uploads/c4d1e5a2-8b3f-4a6c-9e07-5f2d8a1b3c40/products/trek-marlin-7.webp"
}
],
"options": [
{
"name": "Veľkosť",
"values": [
{
"label": "L"
}
]
}
],
"variants": [
{
"id": "c8e2a5f1-3d79-4a16-b4e3-9f6c2d8a1e54",
"sku": "MARLIN7-L",
"title": "L",
"description": {
"plain": "L"
},
"url": "https://shop.example.com/products/trek-marlin-7",
"price": {
"amount": 104900,
"currency": "EUR"
},
"availability": {
"available": true,
"status": "in_stock"
},
"options": [
{
"name": "Veľkosť",
"label": "L"
}
]
}
],
"tags": [
"Trek"
]
}
],
"pagination": {
"has_next_page": true,
"cursor": "cDoy",
"total_count": 3
}
}{
"ucp": {
"version": "2026-04-08",
"capabilities": {
"dev.ucp.shopping.catalog.search": [
{
"version": "2026-04-08"
}
]
}
},
"products": [
{
"id": "6f1d2b7c-3a84-4e59-b0c2-8d7e1f4a5b62",
"handle": "trek-marlin-5",
"title": "Trek Marlin 5",
"description": {
"plain": "Horský bicykel s hliníkovým rámom Alpha Silver, odpruženou vidlicou a 29\" kolesami.",
"html": "<p>Horský bicykel pre začínajúcich jazdcov.</p><ul><li>Rám Alpha Silver Aluminium</li><li>Vidlica SR Suntour XCT 30, zdvih 100 mm</li><li>Pohon Shimano 2×8</li></ul>"
},
"url": "https://shop.example.com/products/trek-marlin-5",
"price_range": {
"min": {
"amount": 69900,
"currency": "EUR"
},
"max": {
"amount": 71900,
"currency": "EUR"
}
},
"list_price_range": {
"min": {
"amount": 74900,
"currency": "EUR"
},
"max": {
"amount": 74900,
"currency": "EUR"
}
},
"media": [
{
"type": "image",
"url": "https://shop.example.com/api/public/uploads/c4d1e5a2-8b3f-4a6c-9e07-5f2d8a1b3c40/products/trek-marlin-5-modra.webp",
"alt_text": "Trek Marlin 5, modrá, bočný pohľad"
},
{
"type": "image",
"url": "https://images.vendor.example/trek/marlin-5-detail.jpg"
}
],
"options": [
{
"name": "Farba",
"values": [
{
"label": "Modrá"
},
{
"label": "Čierna"
}
]
},
{
"name": "Veľkosť",
"values": [
{
"label": "M"
},
{
"label": "L"
}
]
}
],
"variants": [
{
"id": "1e8a6c4f-2b93-4d71-9f05-6a3c8e2b1d47",
"sku": "MARLIN5-M-MOD",
"barcodes": [
{
"type": "EAN",
"value": "8594012345674"
}
],
"title": "Modrá / M",
"description": {
"plain": "Modrá / M"
},
"url": "https://shop.example.com/products/trek-marlin-5",
"price": {
"amount": 69900,
"currency": "EUR"
},
"list_price": {
"amount": 74900,
"currency": "EUR"
},
"availability": {
"available": true,
"status": "in_stock"
},
"options": [
{
"name": "Farba",
"label": "Modrá"
},
{
"name": "Veľkosť",
"label": "M"
}
]
},
{
"id": "7c2f9b1e-4a68-4e3d-b2c9-5d1f7a3e8b06",
"sku": "MARLIN5-L-MOD",
"title": "Modrá / L",
"description": {
"plain": "Modrá / L"
},
"url": "https://shop.example.com/products/trek-marlin-5",
"price": {
"amount": 69900,
"currency": "EUR"
},
"availability": {
"available": false,
"status": "out_of_stock"
},
"options": [
{
"name": "Farba",
"label": "Modrá"
},
{
"name": "Veľkosť",
"label": "L"
}
]
},
{
"id": "d93b4a7e-1f26-4c85-a0e7-3b9c6d2f4e18",
"sku": "MARLIN5-M-CIE",
"title": "Čierna / M",
"description": {
"plain": "Čierna / M"
},
"url": "https://shop.example.com/products/trek-marlin-5",
"price": {
"amount": 71900,
"currency": "EUR"
},
"availability": {
"available": true,
"status": "preorder"
},
"options": [
{
"name": "Farba",
"label": "Čierna"
},
{
"name": "Veľkosť",
"label": "M"
}
]
}
],
"tags": [
"Trek"
]
},
{
"id": "2f6c8a1d-7e43-4b95-9c2a-4d1e8b6f3a07",
"handle": "trek-marlin-7",
"title": "Trek Marlin 7",
"description": {
"plain": "Trek Marlin 7"
},
"url": "https://shop.example.com/products/trek-marlin-7",
"price_range": {
"min": {
"amount": 104900,
"currency": "EUR"
},
"max": {
"amount": 104900,
"currency": "EUR"
}
},
"media": [
{
"type": "image",
"url": "https://shop.example.com/api/public/uploads/c4d1e5a2-8b3f-4a6c-9e07-5f2d8a1b3c40/products/trek-marlin-7.webp"
}
],
"options": [
{
"name": "Veľkosť",
"values": [
{
"label": "L"
}
]
}
],
"variants": [
{
"id": "c8e2a5f1-3d79-4a16-b4e3-9f6c2d8a1e54",
"sku": "MARLIN7-L",
"title": "L",
"description": {
"plain": "L"
},
"url": "https://shop.example.com/products/trek-marlin-7",
"price": {
"amount": 104900,
"currency": "EUR"
},
"availability": {
"available": true,
"status": "in_stock"
},
"options": [
{
"name": "Veľkosť",
"label": "L"
}
]
}
],
"tags": [
"Trek"
]
}
],
"pagination": {
"has_next_page": true,
"cursor": "cDoy",
"total_count": 3
},
"messages": [
{
"type": "warning",
"code": "price_filter_ignored",
"path": "$.filters.price",
"content": "filters.price is denominated in CZK, prices are presented in EUR and cannot be converted, so the price filter was not applied"
}
]
}{
"ucp": {
"version": "2026-04-08",
"status": "error",
"capabilities": {}
},
"messages": [
{
"type": "error",
"code": "capabilities_incompatible",
"content": "The platform and this business share no version of the capability this operation needs",
"severity": "unrecoverable"
}
],
"continue_url": "https://shop.example.com/"
}400Bad Request (Error): invalid_profile_url — the UCP-Agent header is missing or unusable (with continue_url); invalid_request — a field this operation reads has …
Bad Request (Error):
invalid_profile_url— theUCP-Agentheader is missing or unusable (withcontinue_url);invalid_request— a field this operation reads has the wrong JSON type or value;contentnames it. Malformed JSON or a top-level JSON primitive says only "The request is not valid". Unknown fields are ignored, never refused. For this operation:request must be an object(a JSON body that is not an object),request.query must be a string,filters must be an object,filters.price must be an object,filters.price.min|max must be a non-negative integer,filters.categories must be an array of strings,filters.categories is not supported by this shop; search by query (and filters.price) instead,pagination must be an object,pagination.limit must be a positive integer(alsonull),pagination.cursor is not valid,context must be an object,context.<field> must be a string.
Response headers
Cache-ControlstringEvery UCP answer (success or error) except the business profile.
always:no-store
SchemaError
{
"code": "invalid_profile_url",
"content": "The UCP-Agent header must carry profile=\"https://…\"",
"continue_url": "https://shop.example.com/"
}{
"code": "invalid_request",
"content": "filters.categories is not supported by this shop; search by query (and filters.price) instead"
}{
"code": "invalid_request",
"content": "pagination.cursor is not valid"
}{
"code": "invalid_request",
"content": "The request is not valid"
}Request with curl
curl -X POST 'https://shop.example.com/ucp/v1/catalog/search' \
-H 'UCP-Agent: profile="https://platform.example.com/.well-known/ucp"' \
-H 'Accept-Language: sk-SK,sk;q=0.9,en;q=0.8' \
-H 'Content-Type: application/json' \
--data '{
"query": "marlin",
"pagination": {
"limit": 2
},
"context": {
"language": "sk",
"address_country": "SK",
"currency": "EUR"
}
}'Look products up by identifier
POST/ucp/v1/catalog/lookup
Up to 20 identifiers in one call — variant ids, variant SKUs, product ids, product handles — answered from the product data itself, not from the search listing.
- A variant id or SKU matches that variant (
inputs[].match: "exact"); a SKU can match variants of several products. A product id or handle (in any language) matches the product’s featured variant ("featured"). An identifier that matched a variant is not tried as a product. - Each product appears once, in the order its first identifier appears in
ids, and lists only the variants that were asked for; itsprice_range,list_price_rangeandoptionsstill describe all its priced variants (within the price filter, when one is set). - Every identifier that matched nothing — unknown, hidden by the shop, or the asked variant has no price — gets an
infonot_foundmessage whosecontentis the identifier. - Unlike search, a product the shop does not list in its catalog pages (but would show by its URL) is found, and a product that is not orderable is returned with every variant
discontinued. filters.priceworks as in search; an asked variant outside it is reportednot_found.
Parameters
UCP-AgentheaderstringrequiredNames your platform profile. Required on every
/ucp/v1/*operation; never read by the profile and handoff routes.Details
Grammar — a subset of an RFC 8941 Dictionary, at most 4096 characters:
- members separated by
,(spaces or tabs around it allowed; a trailing comma is an error); keys lower-case, matching[a-z*][a-z0-9_\-.*]*; - the
profilemember must be an sf-string:profile="https://…", printable ASCII 0x20–0x7E, escapes\"and\\only; - other members (tokens, numbers, booleans, strings) and parameters (
;key=value) are parsed and ignored; whenprofileappears twice the last one wins; - any parse error makes the whole header unusable.
Accepted:
profile="https://platform.example.com/.well-known/ucp",version="2026-04-08", profile="https://platform.example.com/.well-known/ucp";sig=abc. Refused: a missing header,profile=https://…(a token, not a string),Profile="…",a=1 b=2, profile="…".The URL — at most 1000 characters, no whitespace anywhere, starting
https://, no user name or password, a non-empty host. Any port, path and query string are accepted (the spec’s/.well-known/ucppath is not enforced). Failing any rule →400 invalid_profile_url.The fetch —
GETwithAccept: application/jsonandUser-Agent: Monokaido-UCP/1.0 (+https://monokaido.com); no redirects followed; only HTTP 200 accepted; at most 256 KiB; 3 s idle, 5 s total; the host must resolve to a public address (private, loopback, link-local and other reserved ranges are refused). Anything else →424 profile_unreachable. A body that is not JSON, or whoseucp.versionis not aYYYY-MM-DDstring →422 profile_malformed; a valid profile of another version →422 version_unsupported. Onlyucp.versionanducp.capabilities(name →[{"version"}], first 20 entries per name, names likedev.ucp.shopping.cart) are read; everything else in it is ignored.Caching — a fetched profile is cached for its
Cache-Control: max-age, clamped to 60 s – 1 h (5 min without one;no-store,no-cacheandprivateare ignored) and served up to 1 h past that while one background refresh runs. A failed fetch (424, or422 profile_malformed) is remembered for 60 s. A profile of anotherucp.versionis a valid profile and is cached like one, so after you change its version expect422 version_unsupporteduntil that entry is refreshed (itsmax-age, then one more request) — or use a new profile URL. These caches are kept in memory by each API process. Fetching a profile the shop does not know yet is budgeted: 5 new profile URLs per minute per client (IPv4 address, IPv6 /64) and 60 per minute in all →503 service_unavailablewithRetry-After. The full URL string is the cache key, so keep it stable (no per-request query strings).minLength: 1maxLength: 4096example:profile="https://platform.example.com/.well-known/ucp"- members separated by
Accept-LanguageheaderstringFallback for the answer’s language after
context.languagein the body. The first 10 comma-separated tags are tried in header order (q=weights are ignored,*is skipped) against the languages the shop offers: an exact tag (case-insensitive), then the primary subtag (en-US→en-GB); with no match, the shop’s first language. (A storefront without a language list of its own offers every language of the company: then the primary subtag of the first usable tag is used as it is — a language the company has not set up is answered in its default currency, untranslated — and, with no tag at all, the company’s default language.)The language decides the currency of every price and the translations. A cart or checkout keeps the language it was created in: a later request changes it only when its body names a
context.languagedifferent from the stored one, and when that switches the shop language every line is re-added in the new currency and gets a new line id. Once a session has a language, this header alone never switches it. On a storefront without a language list, a session created with no usable tag stores none: each later request is then answered in the language its own tags pick, and a different pick adds the lines again (new line ids). Sendcontext.languageon the create.example:sk-SK,sk;q=0.9,en;q=0.8
Request bodyrequired
The identifiers.
A batch lookup by identifier. Unknown fields are ignored.
idsarray<string>requiredUp to 20 identifiers, each a variant id (any case), a variant SKU (exact, case-sensitive; several products may share one), a product id or a product handle (in any language). Non-string and blank entries are dropped, exact duplicates removed. Empty or missing → 400
invalid_request; more than 20 → 400request_too_large.minItems: 1maxItems: 20each item:maxLength: 255filtersSearchFiltersFilters. Other filters (brand, availability …) are ignored without a message.
contextRequestContext | nullBuyer context. Every field is optional; strings longer than the stated length are cut (counted in code points). Unknown fields (
intent,eligibility,payment…) are ignored.
{
"ids": [
"7C2F9B1E-4A68-4E3D-B2C9-5D1F7A3E8B06",
"BOTTLE-750-BLK",
"trek-marlin-9",
"a2c94e18-5d37-4b6f-8e21-0f9b3c7d6e15"
],
"context": {
"language": "sk"
}
}Responses
200The products found (CatalogLookupResponse) — HTTP 200 also when none was — or the capabilities_incompatible business error.
Response headers
Cache-ControlstringEvery UCP answer (success or error) except the business profile.
always:no-storeETagstringAn ETag of the body, generated by the API on every response with a body (writes included). The API’s ETag is strong; the web server sends it weak (
W/"…") when it compresses the response, which it does for a client that sendsAccept-Encoding: gzip. AGETthat sends a matchingIf-None-Match(either form) can be answered304 Not Modifiedeven though the answer isno-store.X-RateLimit-LimitintegerRequests this client IP may make to this operation per 60-second window (600 reads, 300 writes, 60 handoff). Set on every answer that passed the rate limiter.
X-RateLimit-RemainingintegerThe operation’s limit minus this client IP’s current count. The counts live in the API server’s memory; each request is meant to leave the count 60 s after it was made, but the rate limiter (
@nestjs/throttler6.5) drops those pending decrements, for every client, whenever any client’s block ends. Read it as an upper bound and keep steady traffic at half a limit or less.minimum: 0X-RateLimit-ResetintegerSeconds left in a 60-second window the limiter keeps per client IP and operation. The count is not reset when that window ends, so this is not the moment the budget comes back.
minimum: 0
Exactly one of
- CatalogLookupResponseobject · CatalogLookupResponse
Lookup results. No pagination.
additionalProperties: falseThe
ucpobject of a successful catalog or cart answer. A missingstatusmeans success.One entry per product, in the order its first identifier appears in
ids; each holds only the matched variants.messagesarray<Message>Present only when non-empty:
price_filter_ignoredfirst, then oneinfonot_foundper identifier that matched nothing, inidsorder.minItems: 1
- UcpBusinessErrorResponseobject · UcpBusinessErrorResponse
HTTP 200 with no resource. The cases:
capabilities_incompatible— your profile and this shop share no version of the operation’s root capability (always for carts and checkouts on a shop in catalog mode). Nothing ran; no Idempotency-Key was used.not_found— the product, cart (Cart not found or has expired) or checkout (Checkout session not found) does not exist for this shop, is malformed, or (carts) was canceled or expired. An unknowncart_idonPOST /checkout-sessionsalso readsCheckout session not found.out_of_stock/item_unavailable— a create in which none of the requested items could be added; no session was created.handed_off— converting a cart whose lines the buyer already took to the shop.
Inside a write the last three are stored under the Idempotency-Key like a success (
capabilities_incompatibleis answered before the key is read).additionalProperties: falseThe
ucpobject of a business error. Never haspayment_handlers.capabilitiesis{}forcapabilities_incompatibleand lists the operation’s root (and its negotiated extensions) otherwise.Exactly one error message saying why no resource could be returned.
minItems: 1maxItems: 1continue_urlstring (uri)requiredWhere a buyer could go instead: the shop home (
https://{primary domain}/), orhttps://{primary domain}/cartforhanded_off. Not a handoff link.
{
"ucp": {
"version": "2026-04-08",
"capabilities": {
"dev.ucp.shopping.catalog.lookup": [
{
"version": "2026-04-08"
}
]
}
},
"products": [
{
"id": "6f1d2b7c-3a84-4e59-b0c2-8d7e1f4a5b62",
"handle": "trek-marlin-5",
"title": "Trek Marlin 5",
"description": {
"plain": "Horský bicykel s hliníkovým rámom Alpha Silver, odpruženou vidlicou a 29\" kolesami.",
"html": "<p>Horský bicykel pre začínajúcich jazdcov.</p><ul><li>Rám Alpha Silver Aluminium</li><li>Vidlica SR Suntour XCT 30, zdvih 100 mm</li><li>Pohon Shimano 2×8</li></ul>"
},
"url": "https://shop.example.com/products/trek-marlin-5",
"price_range": {
"min": {
"amount": 69900,
"currency": "EUR"
},
"max": {
"amount": 71900,
"currency": "EUR"
}
},
"list_price_range": {
"min": {
"amount": 74900,
"currency": "EUR"
},
"max": {
"amount": 74900,
"currency": "EUR"
}
},
"media": [
{
"type": "image",
"url": "https://shop.example.com/api/public/uploads/c4d1e5a2-8b3f-4a6c-9e07-5f2d8a1b3c40/products/trek-marlin-5-modra.webp",
"alt_text": "Trek Marlin 5, modrá, bočný pohľad"
},
{
"type": "image",
"url": "https://images.vendor.example/trek/marlin-5-detail.jpg"
}
],
"options": [
{
"name": "Farba",
"values": [
{
"label": "Modrá"
},
{
"label": "Čierna"
}
]
},
{
"name": "Veľkosť",
"values": [
{
"label": "M"
},
{
"label": "L"
}
]
}
],
"variants": [
{
"id": "7c2f9b1e-4a68-4e3d-b2c9-5d1f7a3e8b06",
"sku": "MARLIN5-L-MOD",
"title": "Modrá / L",
"description": {
"plain": "Modrá / L"
},
"url": "https://shop.example.com/products/trek-marlin-5",
"price": {
"amount": 69900,
"currency": "EUR"
},
"availability": {
"available": false,
"status": "out_of_stock"
},
"options": [
{
"name": "Farba",
"label": "Modrá"
},
{
"name": "Veľkosť",
"label": "L"
}
],
"inputs": [
{
"id": "7C2F9B1E-4A68-4E3D-B2C9-5D1F7A3E8B06",
"match": "exact"
}
]
}
],
"tags": [
"Trek"
]
},
{
"id": "0b7e3f5a-9c12-4d8e-a6f4-2e1c5b9d7a83",
"handle": "flasa-na-vodu-750-ml",
"title": "Fľaša na vodu 750 ml",
"description": {
"plain": "Fľaša bez BPA s uzáverom proti vytečeniu & úchytom.",
"html": "<p>Fľaša bez BPA s <b>uzáverom proti vytečeniu</b> & úchytom.</p>"
},
"url": "https://shop.example.com/products/flasa-na-vodu-750-ml",
"price_range": {
"min": {
"amount": 990,
"currency": "EUR"
},
"max": {
"amount": 990,
"currency": "EUR"
}
},
"options": [
{
"name": "Farba",
"values": [
{
"label": "Čierna"
}
]
}
],
"variants": [
{
"id": "b5f81c3d-6e49-4a2b-8d73-9c0e4f1a6b25",
"sku": "BOTTLE-750-BLK",
"title": "Čierna",
"description": {
"plain": "Čierna"
},
"url": "https://shop.example.com/products/flasa-na-vodu-750-ml",
"price": {
"amount": 990,
"currency": "EUR"
},
"availability": {
"available": true,
"status": "in_stock"
},
"options": [
{
"name": "Farba",
"label": "Čierna"
}
],
"inputs": [
{
"id": "BOTTLE-750-BLK",
"match": "exact"
}
]
}
]
},
{
"id": "a2c94e18-5d37-4b6f-8e21-0f9b3c7d6e15",
"handle": "cyklisticka-prilba-lumen",
"title": "Cyklistická prilba Lumen",
"description": {
"plain": "Ľahká prilba s integrovaným zadným svetlom."
},
"url": "https://shop.example.com/products/cyklisticka-prilba-lumen",
"price_range": {
"min": {
"amount": 5990,
"currency": "EUR"
},
"max": {
"amount": 5990,
"currency": "EUR"
}
},
"variants": [
{
"id": "4a6e2d9c-8b15-4f37-9c48-1e7a5b3d2f90",
"sku": "LUMEN-HELM-M",
"barcodes": [
{
"type": "UPC",
"value": "859401234567"
}
],
"title": "Cyklistická prilba Lumen",
"description": {
"plain": "Cyklistická prilba Lumen"
},
"url": "https://shop.example.com/products/cyklisticka-prilba-lumen",
"price": {
"amount": 5990,
"currency": "EUR"
},
"availability": {
"available": true,
"status": "in_stock"
},
"inputs": [
{
"id": "a2c94e18-5d37-4b6f-8e21-0f9b3c7d6e15",
"match": "featured"
}
]
}
],
"tags": [
"Lumen"
]
}
],
"messages": [
{
"type": "info",
"code": "not_found",
"content": "trek-marlin-9"
}
]
}{
"ucp": {
"version": "2026-04-08",
"status": "error",
"capabilities": {}
},
"messages": [
{
"type": "error",
"code": "capabilities_incompatible",
"content": "The platform and this business share no version of the capability this operation needs",
"severity": "unrecoverable"
}
],
"continue_url": "https://shop.example.com/"
}400Bad Request (Error): invalid_profile_url — the UCP-Agent header is missing or unusable (with continue_url); invalid_request — a field this operation reads has …
Bad Request (Error):
invalid_profile_url— theUCP-Agentheader is missing or unusable (withcontinue_url);invalid_request— a field this operation reads has the wrong JSON type or value;contentnames it. Malformed JSON or a top-level JSON primitive says only "The request is not valid". Unknown fields are ignored, never refused. For this operation:request must be an object(also for no body),ids must be a non-empty array, and the price / category filter refusals of search.request_too_large"ids holds at most 20 identifiers" — more than 20 entries inids(HTTP 400, not 413).
Response headers
Cache-ControlstringEvery UCP answer (success or error) except the business profile.
always:no-store
SchemaError
{
"code": "invalid_profile_url",
"content": "The UCP-Agent header must carry profile=\"https://…\"",
"continue_url": "https://shop.example.com/"
}{
"code": "invalid_request",
"content": "ids must be a non-empty array"
}{
"code": "request_too_large",
"content": "ids holds at most 20 identifiers"
}{
"code": "invalid_request",
"content": "The request is not valid"
}Request with curl
curl -X POST 'https://shop.example.com/ucp/v1/catalog/lookup' \
-H 'UCP-Agent: profile="https://platform.example.com/.well-known/ucp"' \
-H 'Accept-Language: sk-SK,sk;q=0.9,en;q=0.8' \
-H 'Content-Type: application/json' \
--data '{
"ids": [
"7C2F9B1E-4A68-4E3D-B2C9-5D1F7A3E8B06",
"BOTTLE-750-BLK",
"trek-marlin-9",
"a2c94e18-5d37-4b6f-8e21-0f9b3c7d6e15"
],
"context": {
"language": "sk"
}
}'Get one product in detail
POST/ucp/v1/catalog/product
One product with all its priced variants and, for each option value, whether it exists and is available next to the current selection. Use it to drive a variant picker.
idmay be a product id, a product handle or a variant id (SKUs are not accepted here).- The anchor variant is chosen by
selected(all selections must match; when none does, selections are dropped from the end); else the variant named byid; else the featured variant. It comes first invariants, andselectedin the answer repeats its options. filters.priceandpreferencesare ignored. Relaxed selections are not reported.- An unknown or hidden product is a 200 business error
not_found, not an HTTP 404. This operation needsdev.ucp.shopping.catalog.lookup(there is no separate capability).
Parameters
UCP-AgentheaderstringrequiredNames your platform profile. Required on every
/ucp/v1/*operation; never read by the profile and handoff routes.Details
Grammar — a subset of an RFC 8941 Dictionary, at most 4096 characters:
- members separated by
,(spaces or tabs around it allowed; a trailing comma is an error); keys lower-case, matching[a-z*][a-z0-9_\-.*]*; - the
profilemember must be an sf-string:profile="https://…", printable ASCII 0x20–0x7E, escapes\"and\\only; - other members (tokens, numbers, booleans, strings) and parameters (
;key=value) are parsed and ignored; whenprofileappears twice the last one wins; - any parse error makes the whole header unusable.
Accepted:
profile="https://platform.example.com/.well-known/ucp",version="2026-04-08", profile="https://platform.example.com/.well-known/ucp";sig=abc. Refused: a missing header,profile=https://…(a token, not a string),Profile="…",a=1 b=2, profile="…".The URL — at most 1000 characters, no whitespace anywhere, starting
https://, no user name or password, a non-empty host. Any port, path and query string are accepted (the spec’s/.well-known/ucppath is not enforced). Failing any rule →400 invalid_profile_url.The fetch —
GETwithAccept: application/jsonandUser-Agent: Monokaido-UCP/1.0 (+https://monokaido.com); no redirects followed; only HTTP 200 accepted; at most 256 KiB; 3 s idle, 5 s total; the host must resolve to a public address (private, loopback, link-local and other reserved ranges are refused). Anything else →424 profile_unreachable. A body that is not JSON, or whoseucp.versionis not aYYYY-MM-DDstring →422 profile_malformed; a valid profile of another version →422 version_unsupported. Onlyucp.versionanducp.capabilities(name →[{"version"}], first 20 entries per name, names likedev.ucp.shopping.cart) are read; everything else in it is ignored.Caching — a fetched profile is cached for its
Cache-Control: max-age, clamped to 60 s – 1 h (5 min without one;no-store,no-cacheandprivateare ignored) and served up to 1 h past that while one background refresh runs. A failed fetch (424, or422 profile_malformed) is remembered for 60 s. A profile of anotherucp.versionis a valid profile and is cached like one, so after you change its version expect422 version_unsupporteduntil that entry is refreshed (itsmax-age, then one more request) — or use a new profile URL. These caches are kept in memory by each API process. Fetching a profile the shop does not know yet is budgeted: 5 new profile URLs per minute per client (IPv4 address, IPv6 /64) and 60 per minute in all →503 service_unavailablewithRetry-After. The full URL string is the cache key, so keep it stable (no per-request query strings).minLength: 1maxLength: 4096example:profile="https://platform.example.com/.well-known/ucp"- members separated by
Accept-LanguageheaderstringFallback for the answer’s language after
context.languagein the body. The first 10 comma-separated tags are tried in header order (q=weights are ignored,*is skipped) against the languages the shop offers: an exact tag (case-insensitive), then the primary subtag (en-US→en-GB); with no match, the shop’s first language. (A storefront without a language list of its own offers every language of the company: then the primary subtag of the first usable tag is used as it is — a language the company has not set up is answered in its default currency, untranslated — and, with no tag at all, the company’s default language.)The language decides the currency of every price and the translations. A cart or checkout keeps the language it was created in: a later request changes it only when its body names a
context.languagedifferent from the stored one, and when that switches the shop language every line is re-added in the new currency and gets a new line id. Once a session has a language, this header alone never switches it. On a storefront without a language list, a session created with no usable tag stores none: each later request is then answered in the language its own tags pick, and a different pick adds the lines again (new line ids). Sendcontext.languageon the create.example:sk-SK,sk;q=0.9,en;q=0.8
Request bodyrequired
The product and an optional selection.
A product detail request. preferences and other unknown fields are ignored.
idstringrequiredA product id, a product handle or a variant id (SKUs are not accepted here). Required; longer text is cut to 255 characters.
minLength: 1maxLength: 255selectedarray<object> | nullOption values to select (names and labels compared case-insensitively). The variant matching all of them becomes the anchor; when none does, selections are dropped from the end until one matches.
selectedwins over a variant id inid. Entries without both fields are skipped; at most the first 20 are read. Relaxation is not reported.maxItems: 20namestringrequiredOption name.
maxLength: 100labelstringrequiredOption value.
maxLength: 255
filtersSearchFiltersOnly
filters.categoriesis checked (and refused when non-blank);filters.priceis ignored here.contextRequestContext | nullBuyer context. Every field is optional; strings longer than the stated length are cut (counted in code points). Unknown fields (
intent,eligibility,payment…) are ignored.
{
"id": "6f1d2b7c-3a84-4e59-b0c2-8d7e1f4a5b62",
"selected": [
{
"name": "Farba",
"label": "Čierna"
},
{
"name": "Veľkosť",
"label": "L"
}
],
"context": {
"language": "sk"
}
}Responses
200The product (GetProductResponse), or a business error: not_found (unknown or hidden product) or capabilities_incompatible.
Response headers
Cache-ControlstringEvery UCP answer (success or error) except the business profile.
always:no-storeETagstringAn ETag of the body, generated by the API on every response with a body (writes included). The API’s ETag is strong; the web server sends it weak (
W/"…") when it compresses the response, which it does for a client that sendsAccept-Encoding: gzip. AGETthat sends a matchingIf-None-Match(either form) can be answered304 Not Modifiedeven though the answer isno-store.X-RateLimit-LimitintegerRequests this client IP may make to this operation per 60-second window (600 reads, 300 writes, 60 handoff). Set on every answer that passed the rate limiter.
X-RateLimit-RemainingintegerThe operation’s limit minus this client IP’s current count. The counts live in the API server’s memory; each request is meant to leave the count 60 s after it was made, but the rate limiter (
@nestjs/throttler6.5) drops those pending decrements, for every client, whenever any client’s block ends. Read it as an upper bound and keep steady traffic at half a limit or less.minimum: 0X-RateLimit-ResetintegerSeconds left in a 60-second window the limiter keeps per client IP and operation. The count is not reset when that window ends, so this is not the moment the budget comes back.
minimum: 0
Exactly one of
- GetProductResponseobject · GetProductResponse
One product in detail.
additionalProperties: falseThe
ucpobject of a successful catalog or cart answer. A missingstatusmeans success.A product as
getProductreturns it: every priced variant, the selected one first, and per-value availability.
- UcpBusinessErrorResponseobject · UcpBusinessErrorResponse
HTTP 200 with no resource. The cases:
capabilities_incompatible— your profile and this shop share no version of the operation’s root capability (always for carts and checkouts on a shop in catalog mode). Nothing ran; no Idempotency-Key was used.not_found— the product, cart (Cart not found or has expired) or checkout (Checkout session not found) does not exist for this shop, is malformed, or (carts) was canceled or expired. An unknowncart_idonPOST /checkout-sessionsalso readsCheckout session not found.out_of_stock/item_unavailable— a create in which none of the requested items could be added; no session was created.handed_off— converting a cart whose lines the buyer already took to the shop.
Inside a write the last three are stored under the Idempotency-Key like a success (
capabilities_incompatibleis answered before the key is read).additionalProperties: falseThe
ucpobject of a business error. Never haspayment_handlers.capabilitiesis{}forcapabilities_incompatibleand lists the operation’s root (and its negotiated extensions) otherwise.Exactly one error message saying why no resource could be returned.
minItems: 1maxItems: 1continue_urlstring (uri)requiredWhere a buyer could go instead: the shop home (
https://{primary domain}/), orhttps://{primary domain}/cartforhanded_off. Not a handoff link.
{
"ucp": {
"version": "2026-04-08",
"capabilities": {
"dev.ucp.shopping.catalog.lookup": [
{
"version": "2026-04-08"
}
]
}
},
"product": {
"id": "6f1d2b7c-3a84-4e59-b0c2-8d7e1f4a5b62",
"handle": "trek-marlin-5",
"title": "Trek Marlin 5",
"description": {
"plain": "Horský bicykel s hliníkovým rámom Alpha Silver, odpruženou vidlicou a 29\" kolesami.",
"html": "<p>Horský bicykel pre začínajúcich jazdcov.</p><ul><li>Rám Alpha Silver Aluminium</li><li>Vidlica SR Suntour XCT 30, zdvih 100 mm</li><li>Pohon Shimano 2×8</li></ul>"
},
"url": "https://shop.example.com/products/trek-marlin-5",
"price_range": {
"min": {
"amount": 69900,
"currency": "EUR"
},
"max": {
"amount": 71900,
"currency": "EUR"
}
},
"list_price_range": {
"min": {
"amount": 74900,
"currency": "EUR"
},
"max": {
"amount": 74900,
"currency": "EUR"
}
},
"media": [
{
"type": "image",
"url": "https://shop.example.com/api/public/uploads/c4d1e5a2-8b3f-4a6c-9e07-5f2d8a1b3c40/products/trek-marlin-5-modra.webp",
"alt_text": "Trek Marlin 5, modrá, bočný pohľad"
},
{
"type": "image",
"url": "https://images.vendor.example/trek/marlin-5-detail.jpg"
}
],
"options": [
{
"name": "Farba",
"values": [
{
"label": "Modrá",
"exists": true,
"available": true
},
{
"label": "Čierna",
"exists": true,
"available": true
}
]
},
{
"name": "Veľkosť",
"values": [
{
"label": "M",
"exists": true,
"available": true
},
{
"label": "L",
"exists": false,
"available": false
}
]
}
],
"variants": [
{
"id": "d93b4a7e-1f26-4c85-a0e7-3b9c6d2f4e18",
"sku": "MARLIN5-M-CIE",
"title": "Čierna / M",
"description": {
"plain": "Čierna / M"
},
"url": "https://shop.example.com/products/trek-marlin-5",
"price": {
"amount": 71900,
"currency": "EUR"
},
"availability": {
"available": true,
"status": "preorder"
},
"options": [
{
"name": "Farba",
"label": "Čierna"
},
{
"name": "Veľkosť",
"label": "M"
}
]
},
{
"id": "1e8a6c4f-2b93-4d71-9f05-6a3c8e2b1d47",
"sku": "MARLIN5-M-MOD",
"barcodes": [
{
"type": "EAN",
"value": "8594012345674"
}
],
"title": "Modrá / M",
"description": {
"plain": "Modrá / M"
},
"url": "https://shop.example.com/products/trek-marlin-5",
"price": {
"amount": 69900,
"currency": "EUR"
},
"list_price": {
"amount": 74900,
"currency": "EUR"
},
"availability": {
"available": true,
"status": "in_stock"
},
"options": [
{
"name": "Farba",
"label": "Modrá"
},
{
"name": "Veľkosť",
"label": "M"
}
]
},
{
"id": "7c2f9b1e-4a68-4e3d-b2c9-5d1f7a3e8b06",
"sku": "MARLIN5-L-MOD",
"title": "Modrá / L",
"description": {
"plain": "Modrá / L"
},
"url": "https://shop.example.com/products/trek-marlin-5",
"price": {
"amount": 69900,
"currency": "EUR"
},
"availability": {
"available": false,
"status": "out_of_stock"
},
"options": [
{
"name": "Farba",
"label": "Modrá"
},
{
"name": "Veľkosť",
"label": "L"
}
]
}
],
"tags": [
"Trek"
],
"selected": [
{
"name": "Farba",
"label": "Čierna"
},
{
"name": "Veľkosť",
"label": "M"
}
]
}
}{
"ucp": {
"version": "2026-04-08",
"status": "error",
"capabilities": {
"dev.ucp.shopping.catalog.lookup": [
{
"version": "2026-04-08"
}
]
}
},
"messages": [
{
"type": "error",
"code": "not_found",
"content": "Product not found",
"severity": "unrecoverable"
}
],
"continue_url": "https://shop.example.com/"
}{
"ucp": {
"version": "2026-04-08",
"status": "error",
"capabilities": {}
},
"messages": [
{
"type": "error",
"code": "capabilities_incompatible",
"content": "The platform and this business share no version of the capability this operation needs",
"severity": "unrecoverable"
}
],
"continue_url": "https://shop.example.com/"
}400Bad Request (Error): invalid_profile_url — the UCP-Agent header is missing or unusable (with continue_url); invalid_request — a field this operation reads has …
Bad Request (Error):
invalid_profile_url— theUCP-Agentheader is missing or unusable (withcontinue_url);invalid_request— a field this operation reads has the wrong JSON type or value;contentnames it. Malformed JSON or a top-level JSON primitive says only "The request is not valid". Unknown fields are ignored, never refused. For this operation:request must be an object(also for no body),id is required, the category filter refusal.
Response headers
Cache-ControlstringEvery UCP answer (success or error) except the business profile.
always:no-store
SchemaError
{
"code": "invalid_profile_url",
"content": "The UCP-Agent header must carry profile=\"https://…\"",
"continue_url": "https://shop.example.com/"
}{
"code": "invalid_request",
"content": "id is required"
}{
"code": "invalid_request",
"content": "The request is not valid"
}Request with curl
curl -X POST 'https://shop.example.com/ucp/v1/catalog/product' \
-H 'UCP-Agent: profile="https://platform.example.com/.well-known/ucp"' \
-H 'Accept-Language: sk-SK,sk;q=0.9,en;q=0.8' \
-H 'Content-Type: application/json' \
--data '{
"id": "6f1d2b7c-3a84-4e59-b0c2-8d7e1f4a5b62",
"selected": [
{
"name": "Farba",
"label": "Čierna"
},
{
"name": "Veľkosť",
"label": "L"
}
],
"context": {
"language": "sk"
}
}'Cart
Carts (dev.ucp.shopping.cart): a basket priced by the shop, kept 7 days from the last write. A cart can be converted into a checkout (POST /ucp/v1/checkout-sessions with cart_id) or handed to the buyer directly through its continue_url — but only a checkout is ever marked completed by the buyer’s order. Carts carry no status field.
Create a cart
POST/ucp/v1/carts
Creates a cart priced by the shop: the shop’s own prices, VAT, markup rules and stock, in the currency of the session’s language.
- Answers 201 with the cart. Lines that cannot be bought are left out with a message (
item_unavailable,out_of_stock,vat_mix_not_allowed); a quantity above the sellable stock is lowered withquantity_adjusted. A line’s quantity above 999 is lowered to 999 without a message; lines of one item that add up to more than 999 are lowered withquantity_adjusted.line_items: []creates an empty cart. - If lines were sent and none could be added, no cart is created: HTTP 200 business error
out_of_stock(all sold out) oritem_unavailable. - With
dev.ucp.shopping.discountthe codes are applied (one at a time, seeDiscountsRequest) and the answer hasdiscounts. - The cart lives 7 days from its last write. It has a
continue_urlyou can give the buyer — but only a checkout is ever markedcompletedby the buyer’s order, so convert the cart first if you want to follow the outcome. - Send an
Idempotency-Key: a retried create without one makes a second cart.
Parameters
UCP-AgentheaderstringrequiredNames your platform profile. Required on every
/ucp/v1/*operation; never read by the profile and handoff routes.Details
Grammar — a subset of an RFC 8941 Dictionary, at most 4096 characters:
- members separated by
,(spaces or tabs around it allowed; a trailing comma is an error); keys lower-case, matching[a-z*][a-z0-9_\-.*]*; - the
profilemember must be an sf-string:profile="https://…", printable ASCII 0x20–0x7E, escapes\"and\\only; - other members (tokens, numbers, booleans, strings) and parameters (
;key=value) are parsed and ignored; whenprofileappears twice the last one wins; - any parse error makes the whole header unusable.
Accepted:
profile="https://platform.example.com/.well-known/ucp",version="2026-04-08", profile="https://platform.example.com/.well-known/ucp";sig=abc. Refused: a missing header,profile=https://…(a token, not a string),Profile="…",a=1 b=2, profile="…".The URL — at most 1000 characters, no whitespace anywhere, starting
https://, no user name or password, a non-empty host. Any port, path and query string are accepted (the spec’s/.well-known/ucppath is not enforced). Failing any rule →400 invalid_profile_url.The fetch —
GETwithAccept: application/jsonandUser-Agent: Monokaido-UCP/1.0 (+https://monokaido.com); no redirects followed; only HTTP 200 accepted; at most 256 KiB; 3 s idle, 5 s total; the host must resolve to a public address (private, loopback, link-local and other reserved ranges are refused). Anything else →424 profile_unreachable. A body that is not JSON, or whoseucp.versionis not aYYYY-MM-DDstring →422 profile_malformed; a valid profile of another version →422 version_unsupported. Onlyucp.versionanducp.capabilities(name →[{"version"}], first 20 entries per name, names likedev.ucp.shopping.cart) are read; everything else in it is ignored.Caching — a fetched profile is cached for its
Cache-Control: max-age, clamped to 60 s – 1 h (5 min without one;no-store,no-cacheandprivateare ignored) and served up to 1 h past that while one background refresh runs. A failed fetch (424, or422 profile_malformed) is remembered for 60 s. A profile of anotherucp.versionis a valid profile and is cached like one, so after you change its version expect422 version_unsupporteduntil that entry is refreshed (itsmax-age, then one more request) — or use a new profile URL. These caches are kept in memory by each API process. Fetching a profile the shop does not know yet is budgeted: 5 new profile URLs per minute per client (IPv4 address, IPv6 /64) and 60 per minute in all →503 service_unavailablewithRetry-After. The full URL string is the cache key, so keep it stable (no per-request query strings).minLength: 1maxLength: 4096example:profile="https://platform.example.com/.well-known/ucp"- members separated by
Idempotency-KeyheaderstringOptional, strongly recommended. Without it (or with an empty value) the write simply runs: a retried create makes a second session.
- Format: 1–255 printable ASCII characters 0x21–0x7E — no spaces. Anything else (also a header sent twice, which arrives joined with
", ") →400 invalid_request"Idempotency-Key is not valid". A UUID v4 per logical write is a good choice. - Scope: this shop (all its domains) × the exact
UCP-Agentprofile URL string × the key. - Request identity: the operation, the path
{id}as sent (after URL decoding; case included) and the JSON body compared canonically (object key order and whitespace do not matter, array order does). Cancel routes hash{}whatever the body. - Same key, same request, finished → the stored HTTP status and body are replayed — the same values, including a 200 business error, the original
expires_atand theucpobject negotiated the first time, even when the session has changed since. The replay comes from storage, so its object keys come in JSONB order (and itsETagdiffers). - The
UCP-Agentprofile is resolved and negotiated before the key is read: a profile error or a 200capabilities_incompatibleanswer is never stored. - Same key, different request →
409 idempotency_conflict. Same request still running →409withRetry-After: 2. - If the first request fails with a protocol error (
400,503session busy,500…) the key is released: retrying with the same key runs the operation again. - A claim whose request died is taken over by the same request after 120 s. Keys are kept for at least 25 hours after their first use (an hourly job deletes older ones); do not rely on a key more than 24 hours after its first use.
- Not read by catalog operations,
GETroutes, the profile or the handoff routes.
pattern: ^[\x21-\x7E]{1,255}$minLength: 1maxLength: 255example:0c2f8a4e-6b1d-4f37-9a85-3e7d1c9b5f02- Format: 1–255 printable ASCII characters 0x21–0x7E — no spaces. Anything else (also a header sent twice, which arrives joined with
Accept-LanguageheaderstringFallback for the answer’s language after
context.languagein the body. The first 10 comma-separated tags are tried in header order (q=weights are ignored,*is skipped) against the languages the shop offers: an exact tag (case-insensitive), then the primary subtag (en-US→en-GB); with no match, the shop’s first language. (A storefront without a language list of its own offers every language of the company: then the primary subtag of the first usable tag is used as it is — a language the company has not set up is answered in its default currency, untranslated — and, with no tag at all, the company’s default language.)The language decides the currency of every price and the translations. A cart or checkout keeps the language it was created in: a later request changes it only when its body names a
context.languagedifferent from the stored one, and when that switches the shop language every line is re-added in the new currency and gets a new line id. Once a session has a language, this header alone never switches it. On a storefront without a language list, a session created with no usable tag stores none: each later request is then answered in the language its own tags pick, and a different pick adds the lines again (new line ids). Sendcontext.languageon the create.example:sk-SK,sk;q=0.9,en;q=0.8
Request bodyrequired
A new cart. Unknown fields are ignored. Body problems are reported before anything is stored.
The lines. Required;
[]creates an empty cart. If lines are sent and none can be added the answer is a 200 business error and no cart exists.maxItems: 100buyerBuyerRequest | nullReplaces the whole stored buyer: a field you leave out is erased (send the e-mail again when you add a name).
nullclears the buyer; leavingbuyerout keeps it. A non-string value → 400; a longer string is cut to the stated length (counted in code points). Names and phone are never required.contextRequestContext | nullBuyer context. Every field is optional; strings longer than the stated length are cut (counted in code points). Unknown fields (
intent,eligibility,payment…) are ignored.discountsDiscountsRequest | nullDiscount codes (only read when
dev.ucp.shopping.discountis negotiated). Without the extension a code is never applied: one applied earlier is taken off the session’s cart for that answer (the stored codes are kept, and a later request that negotiates the extension applies them again).fulfillmentFulfillmentRequest | nullAccepted and stored, never used for a cart (validated like a checkout’s when the fulfillment extension is negotiated, so a malformed one is still a 400).
{
"line_items": [
{
"item": {
"id": "1e8a6c4f-2b93-4d71-9f05-6a3c8e2b1d47"
},
"quantity": 1
},
{
"item": {
"id": "4a6e2d9c-8b15-4f37-9c48-1e7a5b3d2f90"
}
}
],
"buyer": {
"email": "Jana.Novakova@Example.com"
},
"context": {
"address_country": "SK",
"language": "sk"
}
}{
"line_items": [
{
"item": {
"id": "7c2f9b1e-4a68-4e3d-b2c9-5d1f7a3e8b06"
},
"quantity": 1
}
]
}Responses
200No cart: none of the requested items could be added, or capabilities_incompatible (always a business error; typed as the union of the other cart answers so tha…
No cart: none of the requested items could be added, or capabilities_incompatible (always a business error; typed as the union of the other cart answers so that generated clients read the 201 cart correctly).
Response headers
Cache-ControlstringEvery UCP answer (success or error) except the business profile.
always:no-storeETagstringAn ETag of the body, generated by the API on every response with a body (writes included). The API’s ETag is strong; the web server sends it weak (
W/"…") when it compresses the response, which it does for a client that sendsAccept-Encoding: gzip. AGETthat sends a matchingIf-None-Match(either form) can be answered304 Not Modifiedeven though the answer isno-store.X-RateLimit-LimitintegerRequests this client IP may make to this operation per 60-second window (600 reads, 300 writes, 60 handoff). Set on every answer that passed the rate limiter.
X-RateLimit-RemainingintegerThe operation’s limit minus this client IP’s current count. The counts live in the API server’s memory; each request is meant to leave the count 60 s after it was made, but the rate limiter (
@nestjs/throttler6.5) drops those pending decrements, for every client, whenever any client’s block ends. Read it as an upper bound and keep steady traffic at half a limit or less.minimum: 0X-RateLimit-ResetintegerSeconds left in a 60-second window the limiter keeps per client IP and operation. The count is not reset when that window ends, so this is not the moment the budget comes back.
minimum: 0
Exactly one of
- Cartobject · Cart
A cart. It has no
status: a cancel answers the cart as it was (withcontinue_url), and only a later read showsnot_found.additionalProperties: falseThe
ucpobject of a successful catalog or cart answer. A missingstatusmeans success.idstring (uuid)requiredThe cart id.
In the shop cart’s order, which need not be the order you sent.
ISO 4217 currency code.
pattern: ^[A-Z]{3}$subtotal,discount? andtotal(neverfulfillmenton a cart).minItems: 2maxItems: 3messagesarray<Message>Present only when non-empty. Line and discount messages appear only in the answer to the write that caused them (a cart the buyer took to the shop repeats its last answer, messages included).
minItems: 1buyerBuyerThe stored buyer; empty fields are left out. Absent when nothing is stored.
contextContextThe stored context, echoed as you sent it (after cleaning); empty fields are left out. Absent when nothing is stored.
discountsDiscountsPresent only when
dev.ucp.shopping.discountis negotiated. Thediscounttotal is present whether or not it is.The buyer handoff link,
https://{primary domain}/ucp/continue/{64 hex}. Give it to the BUYER to open in their browser; never call it yourself. It never changes during the session’s life and works while the session is open and unexpired — anincompletecheckout included: the first line that reaches a buyer’s cart freezes the session in the status it has, so give the link only atrequires_escalation. Once a cart was converted, its own link hands over the checkout made from it. Not single-use: every distinct browser (or signed-in customer) that opens it gets the lines once.pattern: ^https://[^/]+/ucp/continue/[0-9a-f]{64}$When the session expires (UTC, ISO 8601 with milliseconds). A write extends it (cart: now + 7 days, checkout: now + 24 h); reads,
completeandcanceldo not. After a handoff nothing extends it.
- UcpBusinessErrorResponseobject · UcpBusinessErrorResponse
HTTP 200 with no resource. The cases:
capabilities_incompatible— your profile and this shop share no version of the operation’s root capability (always for carts and checkouts on a shop in catalog mode). Nothing ran; no Idempotency-Key was used.not_found— the product, cart (Cart not found or has expired) or checkout (Checkout session not found) does not exist for this shop, is malformed, or (carts) was canceled or expired. An unknowncart_idonPOST /checkout-sessionsalso readsCheckout session not found.out_of_stock/item_unavailable— a create in which none of the requested items could be added; no session was created.handed_off— converting a cart whose lines the buyer already took to the shop.
Inside a write the last three are stored under the Idempotency-Key like a success (
capabilities_incompatibleis answered before the key is read).additionalProperties: falseThe
ucpobject of a business error. Never haspayment_handlers.capabilitiesis{}forcapabilities_incompatibleand lists the operation’s root (and its negotiated extensions) otherwise.Exactly one error message saying why no resource could be returned.
minItems: 1maxItems: 1continue_urlstring (uri)requiredWhere a buyer could go instead: the shop home (
https://{primary domain}/), orhttps://{primary domain}/cartforhanded_off. Not a handoff link.
{
"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": "out_of_stock",
"content": "All requested items are currently out of stock",
"severity": "unrecoverable"
}
],
"continue_url": "https://shop.example.com/"
}{
"ucp": {
"version": "2026-04-08",
"status": "error",
"capabilities": {}
},
"messages": [
{
"type": "error",
"code": "capabilities_incompatible",
"content": "The platform and this business share no version of the capability this operation needs",
"severity": "unrecoverable"
}
],
"continue_url": "https://shop.example.com/"
}201The cart was created.
Response headers
Cache-ControlstringEvery UCP answer (success or error) except the business profile.
always:no-storeETagstringAn ETag of the body, generated by the API on every response with a body (writes included). The API’s ETag is strong; the web server sends it weak (
W/"…") when it compresses the response, which it does for a client that sendsAccept-Encoding: gzip. AGETthat sends a matchingIf-None-Match(either form) can be answered304 Not Modifiedeven though the answer isno-store.X-RateLimit-LimitintegerRequests this client IP may make to this operation per 60-second window (600 reads, 300 writes, 60 handoff). Set on every answer that passed the rate limiter.
X-RateLimit-RemainingintegerThe operation’s limit minus this client IP’s current count. The counts live in the API server’s memory; each request is meant to leave the count 60 s after it was made, but the rate limiter (
@nestjs/throttler6.5) drops those pending decrements, for every client, whenever any client’s block ends. Read it as an upper bound and keep steady traffic at half a limit or less.minimum: 0X-RateLimit-ResetintegerSeconds left in a 60-second window the limiter keeps per client IP and operation. The count is not reset when that window ends, so this is not the moment the budget comes back.
minimum: 0
SchemaCart
{
"ucp": {
"version": "2026-04-08",
"capabilities": {
"dev.ucp.shopping.cart": [
{
"version": "2026-04-08"
}
],
"dev.ucp.shopping.discount": [
{
"version": "2026-04-08"
}
]
}
},
"id": "9a3f6c2e-5b18-4d47-8e9c-1f2a7b4d6e83",
"line_items": [
{
"id": "e2b7d4a9-3c61-4f85-9a2e-7d1c5b8f3a46",
"item": {
"id": "1e8a6c4f-2b93-4d71-9f05-6a3c8e2b1d47",
"title": "Trek Marlin 5",
"price": 69900,
"image_url": "https://shop.example.com/api/public/uploads/c4d1e5a2-8b3f-4a6c-9e07-5f2d8a1b3c40/products/trek-marlin-5-modra.webp"
},
"quantity": 1,
"totals": [
{
"type": "subtotal",
"amount": 69900
},
{
"type": "total",
"amount": 69900
}
]
},
{
"id": "5c9e1a7f-2d48-4b36-a8f1-6e3d9c2b7a15",
"item": {
"id": "4a6e2d9c-8b15-4f37-9c48-1e7a5b3d2f90",
"title": "Cyklistická prilba Lumen",
"price": 5990
},
"quantity": 1,
"totals": [
{
"type": "subtotal",
"amount": 5990
},
{
"type": "total",
"amount": 5990
}
]
}
],
"currency": "EUR",
"totals": [
{
"type": "subtotal",
"amount": 75890
},
{
"type": "total",
"amount": 75890
}
],
"buyer": {
"email": "jana.novakova@example.com"
},
"context": {
"address_country": "SK",
"language": "sk"
},
"discounts": {
"codes": [],
"applied": []
},
"continue_url": "https://shop.example.com/ucp/continue/e1f0907c4abcf12564330feef5762f8471cc386a3563ce53cf8342ce6be08b62",
"expires_at": "2026-11-02T09:14:05.118Z"
}400Bad Request (Error): invalid_profile_url — the UCP-Agent header is missing or unusable (with continue_url); invalid_request — a field this operation reads has …
Bad Request (Error):
invalid_profile_url— theUCP-Agentheader is missing or unusable (withcontinue_url);invalid_request— a field this operation reads has the wrong JSON type or value;contentnames it. Malformed JSON or a top-level JSON primitive says only "The request is not valid". Unknown fields are ignored, never refused. For this operation:request must be an object(also for no body),line_items must be an array,line_items holds at most 100 entries,line_items[i] must be an object,line_items[i].item must be an object,line_items[i].item.id is required,line_items[i].quantity must be a positive integer,buyer must be an object,buyer.<field> must be a string,context …,discounts.codes must be an array,discounts.codes holds at most 10 codes, thefulfillment …refusals. Nothing is stored.invalid_request"Idempotency-Key is not valid" — seeIdempotency-Key.
Response headers
Cache-ControlstringEvery UCP answer (success or error) except the business profile.
always:no-store
SchemaError
{
"code": "invalid_profile_url",
"content": "The UCP-Agent header must carry profile=\"https://…\"",
"continue_url": "https://shop.example.com/"
}{
"code": "invalid_request",
"content": "line_items[0].quantity must be a positive integer"
}{
"code": "invalid_request",
"content": "line_items must be an array"
}{
"code": "invalid_request",
"content": "Idempotency-Key is not valid"
}{
"code": "invalid_request",
"content": "The request is not valid"
}Request with curl
curl -X POST 'https://shop.example.com/ucp/v1/carts' \
-H 'UCP-Agent: profile="https://platform.example.com/.well-known/ucp"' \
-H 'Idempotency-Key: 0c2f8a4e-6b1d-4f37-9a85-3e7d1c9b5f02' \
-H 'Accept-Language: sk-SK,sk;q=0.9,en;q=0.8' \
-H 'Content-Type: application/json' \
--data '{
"line_items": [
{
"item": {
"id": "1e8a6c4f-2b93-4d71-9f05-6a3c8e2b1d47"
},
"quantity": 1
},
{
"item": {
"id": "4a6e2d9c-8b15-4f37-9c48-1e7a5b3d2f90"
}
}
],
"buyer": {
"email": "Jana.Novakova@Example.com"
},
"context": {
"address_country": "SK",
"language": "sk"
}
}'Get a cart
GET/ucp/v1/carts/{id}
The cart as it is now. A read stores nothing, does not extend expires_at, and carries no line or discount messages (those come only with the write that caused them). Each line keeps the unit price it got when its variant entered the cart (or was re-added for a new language), unless the storefront has Live prices on cart load switched on (not visible through UCP): then every read and write prices the lines again.
- An unknown, canceled or expired cart is a 200 business error
not_found. - After the buyer took the cart to the shop (its
continue_urlpage added the lines to the buyer’s shop cart), the last answer is returned again — its messages included — with a freshucpobject; nothing on it says so. Line and discount messages of the write that produced that answer come back on every read: do not report them to the buyer again, and do not take them for a sign of the handoff. - This read does not wait for a write in progress on the same cart.
Parameters
idpathstringrequiredThe cart’s
id(a UUID, any letter case). An id that is not a UUID, belongs to another shop, or names a canceled or expired cart is answered with the 200not_foundbusiness error, not a 4xx.pattern: ^[0-9a-fA-F]{8}-[0-9a-fA-F]{4}-[0-9a-fA-F]{4}-[0-9a-fA-F]{4}-[0-9a-fA-F]{12}$example:9a3f6c2e-5b18-4d47-8e9c-1f2a7b4d6e83UCP-AgentheaderstringrequiredNames your platform profile. Required on every
/ucp/v1/*operation; never read by the profile and handoff routes.Details
Grammar — a subset of an RFC 8941 Dictionary, at most 4096 characters:
- members separated by
,(spaces or tabs around it allowed; a trailing comma is an error); keys lower-case, matching[a-z*][a-z0-9_\-.*]*; - the
profilemember must be an sf-string:profile="https://…", printable ASCII 0x20–0x7E, escapes\"and\\only; - other members (tokens, numbers, booleans, strings) and parameters (
;key=value) are parsed and ignored; whenprofileappears twice the last one wins; - any parse error makes the whole header unusable.
Accepted:
profile="https://platform.example.com/.well-known/ucp",version="2026-04-08", profile="https://platform.example.com/.well-known/ucp";sig=abc. Refused: a missing header,profile=https://…(a token, not a string),Profile="…",a=1 b=2, profile="…".The URL — at most 1000 characters, no whitespace anywhere, starting
https://, no user name or password, a non-empty host. Any port, path and query string are accepted (the spec’s/.well-known/ucppath is not enforced). Failing any rule →400 invalid_profile_url.The fetch —
GETwithAccept: application/jsonandUser-Agent: Monokaido-UCP/1.0 (+https://monokaido.com); no redirects followed; only HTTP 200 accepted; at most 256 KiB; 3 s idle, 5 s total; the host must resolve to a public address (private, loopback, link-local and other reserved ranges are refused). Anything else →424 profile_unreachable. A body that is not JSON, or whoseucp.versionis not aYYYY-MM-DDstring →422 profile_malformed; a valid profile of another version →422 version_unsupported. Onlyucp.versionanducp.capabilities(name →[{"version"}], first 20 entries per name, names likedev.ucp.shopping.cart) are read; everything else in it is ignored.Caching — a fetched profile is cached for its
Cache-Control: max-age, clamped to 60 s – 1 h (5 min without one;no-store,no-cacheandprivateare ignored) and served up to 1 h past that while one background refresh runs. A failed fetch (424, or422 profile_malformed) is remembered for 60 s. A profile of anotherucp.versionis a valid profile and is cached like one, so after you change its version expect422 version_unsupporteduntil that entry is refreshed (itsmax-age, then one more request) — or use a new profile URL. These caches are kept in memory by each API process. Fetching a profile the shop does not know yet is budgeted: 5 new profile URLs per minute per client (IPv4 address, IPv6 /64) and 60 per minute in all →503 service_unavailablewithRetry-After. The full URL string is the cache key, so keep it stable (no per-request query strings).minLength: 1maxLength: 4096example:profile="https://platform.example.com/.well-known/ucp"- members separated by
Accept-LanguageheaderstringFallback for the answer’s language after
context.languagein the body. The first 10 comma-separated tags are tried in header order (q=weights are ignored,*is skipped) against the languages the shop offers: an exact tag (case-insensitive), then the primary subtag (en-US→en-GB); with no match, the shop’s first language. (A storefront without a language list of its own offers every language of the company: then the primary subtag of the first usable tag is used as it is — a language the company has not set up is answered in its default currency, untranslated — and, with no tag at all, the company’s default language.)The language decides the currency of every price and the translations. A cart or checkout keeps the language it was created in: a later request changes it only when its body names a
context.languagedifferent from the stored one, and when that switches the shop language every line is re-added in the new currency and gets a new line id. Once a session has a language, this header alone never switches it. On a storefront without a language list, a session created with no usable tag stores none: each later request is then answered in the language its own tags pick, and a different pick adds the lines again (new line ids). Sendcontext.languageon the create.example:sk-SK,sk;q=0.9,en;q=0.8
Responses
200The cart, or a business error: not_found or capabilities_incompatible.
Response headers
Cache-ControlstringEvery UCP answer (success or error) except the business profile.
always:no-storeETagstringAn ETag of the body, generated by the API on every response with a body (writes included). The API’s ETag is strong; the web server sends it weak (
W/"…") when it compresses the response, which it does for a client that sendsAccept-Encoding: gzip. AGETthat sends a matchingIf-None-Match(either form) can be answered304 Not Modifiedeven though the answer isno-store.X-RateLimit-LimitintegerRequests this client IP may make to this operation per 60-second window (600 reads, 300 writes, 60 handoff). Set on every answer that passed the rate limiter.
X-RateLimit-RemainingintegerThe operation’s limit minus this client IP’s current count. The counts live in the API server’s memory; each request is meant to leave the count 60 s after it was made, but the rate limiter (
@nestjs/throttler6.5) drops those pending decrements, for every client, whenever any client’s block ends. Read it as an upper bound and keep steady traffic at half a limit or less.minimum: 0X-RateLimit-ResetintegerSeconds left in a 60-second window the limiter keeps per client IP and operation. The count is not reset when that window ends, so this is not the moment the budget comes back.
minimum: 0
Exactly one of
- Cartobject · Cart
A cart. It has no
status: a cancel answers the cart as it was (withcontinue_url), and only a later read showsnot_found.additionalProperties: falseThe
ucpobject of a successful catalog or cart answer. A missingstatusmeans success.idstring (uuid)requiredThe cart id.
In the shop cart’s order, which need not be the order you sent.
ISO 4217 currency code.
pattern: ^[A-Z]{3}$subtotal,discount? andtotal(neverfulfillmenton a cart).minItems: 2maxItems: 3messagesarray<Message>Present only when non-empty. Line and discount messages appear only in the answer to the write that caused them (a cart the buyer took to the shop repeats its last answer, messages included).
minItems: 1buyerBuyerThe stored buyer; empty fields are left out. Absent when nothing is stored.
contextContextThe stored context, echoed as you sent it (after cleaning); empty fields are left out. Absent when nothing is stored.
discountsDiscountsPresent only when
dev.ucp.shopping.discountis negotiated. Thediscounttotal is present whether or not it is.The buyer handoff link,
https://{primary domain}/ucp/continue/{64 hex}. Give it to the BUYER to open in their browser; never call it yourself. It never changes during the session’s life and works while the session is open and unexpired — anincompletecheckout included: the first line that reaches a buyer’s cart freezes the session in the status it has, so give the link only atrequires_escalation. Once a cart was converted, its own link hands over the checkout made from it. Not single-use: every distinct browser (or signed-in customer) that opens it gets the lines once.pattern: ^https://[^/]+/ucp/continue/[0-9a-f]{64}$When the session expires (UTC, ISO 8601 with milliseconds). A write extends it (cart: now + 7 days, checkout: now + 24 h); reads,
completeandcanceldo not. After a handoff nothing extends it.
- UcpBusinessErrorResponseobject · UcpBusinessErrorResponse
HTTP 200 with no resource. The cases:
capabilities_incompatible— your profile and this shop share no version of the operation’s root capability (always for carts and checkouts on a shop in catalog mode). Nothing ran; no Idempotency-Key was used.not_found— the product, cart (Cart not found or has expired) or checkout (Checkout session not found) does not exist for this shop, is malformed, or (carts) was canceled or expired. An unknowncart_idonPOST /checkout-sessionsalso readsCheckout session not found.out_of_stock/item_unavailable— a create in which none of the requested items could be added; no session was created.handed_off— converting a cart whose lines the buyer already took to the shop.
Inside a write the last three are stored under the Idempotency-Key like a success (
capabilities_incompatibleis answered before the key is read).additionalProperties: falseThe
ucpobject of a business error. Never haspayment_handlers.capabilitiesis{}forcapabilities_incompatibleand lists the operation’s root (and its negotiated extensions) otherwise.Exactly one error message saying why no resource could be returned.
minItems: 1maxItems: 1continue_urlstring (uri)requiredWhere a buyer could go instead: the shop home (
https://{primary domain}/), orhttps://{primary domain}/cartforhanded_off. Not a handoff link.
{
"ucp": {
"version": "2026-04-08",
"capabilities": {
"dev.ucp.shopping.cart": [
{
"version": "2026-04-08"
}
],
"dev.ucp.shopping.discount": [
{
"version": "2026-04-08"
}
]
}
},
"id": "9a3f6c2e-5b18-4d47-8e9c-1f2a7b4d6e83",
"line_items": [
{
"id": "e2b7d4a9-3c61-4f85-9a2e-7d1c5b8f3a46",
"item": {
"id": "1e8a6c4f-2b93-4d71-9f05-6a3c8e2b1d47",
"title": "Trek Marlin 5",
"price": 69900,
"image_url": "https://shop.example.com/api/public/uploads/c4d1e5a2-8b3f-4a6c-9e07-5f2d8a1b3c40/products/trek-marlin-5-modra.webp"
},
"quantity": 1,
"totals": [
{
"type": "subtotal",
"amount": 69900
},
{
"type": "total",
"amount": 69900
}
]
},
{
"id": "8f4a2c6e-1b93-4d57-b6e8-3a9c7d1f5e24",
"item": {
"id": "b5f81c3d-6e49-4a2b-8d73-9c0e4f1a6b25",
"title": "Fľaša na vodu 750 ml",
"price": 990
},
"quantity": 2,
"totals": [
{
"type": "subtotal",
"amount": 1980
},
{
"type": "total",
"amount": 1980
}
]
}
],
"currency": "EUR",
"totals": [
{
"type": "subtotal",
"amount": 71880
},
{
"type": "discount",
"amount": -5844,
"display_text": "JESEN10"
},
{
"type": "total",
"amount": 66036
}
],
"buyer": {
"email": "jana.novakova@example.com"
},
"context": {
"language": "sk",
"address_country": "SK"
},
"discounts": {
"codes": [
"JESEN10",
"LETO5"
],
"applied": [
{
"code": "JESEN10",
"title": "JESEN10",
"amount": 5844,
"method": "across"
}
]
},
"continue_url": "https://shop.example.com/ucp/continue/e1f0907c4abcf12564330feef5762f8471cc386a3563ce53cf8342ce6be08b62",
"expires_at": "2026-11-02T09:16:47.402Z"
}{
"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/"
}{
"ucp": {
"version": "2026-04-08",
"status": "error",
"capabilities": {}
},
"messages": [
{
"type": "error",
"code": "capabilities_incompatible",
"content": "The platform and this business share no version of the capability this operation needs",
"severity": "unrecoverable"
}
],
"continue_url": "https://shop.example.com/"
}400Bad Request (Error): invalid_profile_url — the UCP-Agent header is missing or unusable (with continue_url);
Bad Request (Error):
invalid_profile_url— theUCP-Agentheader is missing or unusable (withcontinue_url);
Response headers
Cache-ControlstringEvery UCP answer (success or error) except the business profile.
always:no-store
SchemaError
{
"code": "invalid_profile_url",
"content": "The UCP-Agent header must carry profile=\"https://…\"",
"continue_url": "https://shop.example.com/"
}Request with curl
curl 'https://shop.example.com/ucp/v1/carts/9a3f6c2e-5b18-4d47-8e9c-1f2a7b4d6e83' \
-H 'UCP-Agent: profile="https://platform.example.com/.well-known/ucp"' \
-H 'Accept-Language: sk-SK,sk;q=0.9,en;q=0.8'Update a cart
PUT/ucp/v1/carts/{id}
Replaces the cart’s lines and, when sent, its buyer, context and discount codes; fields you leave out keep their value. line_items is required and is a full replacement (a line you do not list is removed).
- Every successful write extends
expires_atto now + 7 days. buyerreplaces the whole stored buyer (a field you leave out is erased);nullclears it.- A
context.languagedifferent from the stored one that picks another shop language re-adds every line in it (its currency, new line ids). - After the buyer took the cart to the shop, a PUT changes nothing: the last answer comes back with a
handed_offwarning (no validation of the body’s lines either). - Requests on one cart are serialised (see 503).
Parameters
idpathstringrequiredThe cart’s
id(a UUID, any letter case). An id that is not a UUID, belongs to another shop, or names a canceled or expired cart is answered with the 200not_foundbusiness error, not a 4xx.pattern: ^[0-9a-fA-F]{8}-[0-9a-fA-F]{4}-[0-9a-fA-F]{4}-[0-9a-fA-F]{4}-[0-9a-fA-F]{12}$example:9a3f6c2e-5b18-4d47-8e9c-1f2a7b4d6e83UCP-AgentheaderstringrequiredNames your platform profile. Required on every
/ucp/v1/*operation; never read by the profile and handoff routes.Details
Grammar — a subset of an RFC 8941 Dictionary, at most 4096 characters:
- members separated by
,(spaces or tabs around it allowed; a trailing comma is an error); keys lower-case, matching[a-z*][a-z0-9_\-.*]*; - the
profilemember must be an sf-string:profile="https://…", printable ASCII 0x20–0x7E, escapes\"and\\only; - other members (tokens, numbers, booleans, strings) and parameters (
;key=value) are parsed and ignored; whenprofileappears twice the last one wins; - any parse error makes the whole header unusable.
Accepted:
profile="https://platform.example.com/.well-known/ucp",version="2026-04-08", profile="https://platform.example.com/.well-known/ucp";sig=abc. Refused: a missing header,profile=https://…(a token, not a string),Profile="…",a=1 b=2, profile="…".The URL — at most 1000 characters, no whitespace anywhere, starting
https://, no user name or password, a non-empty host. Any port, path and query string are accepted (the spec’s/.well-known/ucppath is not enforced). Failing any rule →400 invalid_profile_url.The fetch —
GETwithAccept: application/jsonandUser-Agent: Monokaido-UCP/1.0 (+https://monokaido.com); no redirects followed; only HTTP 200 accepted; at most 256 KiB; 3 s idle, 5 s total; the host must resolve to a public address (private, loopback, link-local and other reserved ranges are refused). Anything else →424 profile_unreachable. A body that is not JSON, or whoseucp.versionis not aYYYY-MM-DDstring →422 profile_malformed; a valid profile of another version →422 version_unsupported. Onlyucp.versionanducp.capabilities(name →[{"version"}], first 20 entries per name, names likedev.ucp.shopping.cart) are read; everything else in it is ignored.Caching — a fetched profile is cached for its
Cache-Control: max-age, clamped to 60 s – 1 h (5 min without one;no-store,no-cacheandprivateare ignored) and served up to 1 h past that while one background refresh runs. A failed fetch (424, or422 profile_malformed) is remembered for 60 s. A profile of anotherucp.versionis a valid profile and is cached like one, so after you change its version expect422 version_unsupporteduntil that entry is refreshed (itsmax-age, then one more request) — or use a new profile URL. These caches are kept in memory by each API process. Fetching a profile the shop does not know yet is budgeted: 5 new profile URLs per minute per client (IPv4 address, IPv6 /64) and 60 per minute in all →503 service_unavailablewithRetry-After. The full URL string is the cache key, so keep it stable (no per-request query strings).minLength: 1maxLength: 4096example:profile="https://platform.example.com/.well-known/ucp"- members separated by
Idempotency-KeyheaderstringOptional, strongly recommended. Without it (or with an empty value) the write simply runs: a retried create makes a second session.
- Format: 1–255 printable ASCII characters 0x21–0x7E — no spaces. Anything else (also a header sent twice, which arrives joined with
", ") →400 invalid_request"Idempotency-Key is not valid". A UUID v4 per logical write is a good choice. - Scope: this shop (all its domains) × the exact
UCP-Agentprofile URL string × the key. - Request identity: the operation, the path
{id}as sent (after URL decoding; case included) and the JSON body compared canonically (object key order and whitespace do not matter, array order does). Cancel routes hash{}whatever the body. - Same key, same request, finished → the stored HTTP status and body are replayed — the same values, including a 200 business error, the original
expires_atand theucpobject negotiated the first time, even when the session has changed since. The replay comes from storage, so its object keys come in JSONB order (and itsETagdiffers). - The
UCP-Agentprofile is resolved and negotiated before the key is read: a profile error or a 200capabilities_incompatibleanswer is never stored. - Same key, different request →
409 idempotency_conflict. Same request still running →409withRetry-After: 2. - If the first request fails with a protocol error (
400,503session busy,500…) the key is released: retrying with the same key runs the operation again. - A claim whose request died is taken over by the same request after 120 s. Keys are kept for at least 25 hours after their first use (an hourly job deletes older ones); do not rely on a key more than 24 hours after its first use.
- Not read by catalog operations,
GETroutes, the profile or the handoff routes.
pattern: ^[\x21-\x7E]{1,255}$minLength: 1maxLength: 255example:0c2f8a4e-6b1d-4f37-9a85-3e7d1c9b5f02- Format: 1–255 printable ASCII characters 0x21–0x7E — no spaces. Anything else (also a header sent twice, which arrives joined with
Accept-LanguageheaderstringFallback for the answer’s language after
context.languagein the body. The first 10 comma-separated tags are tried in header order (q=weights are ignored,*is skipped) against the languages the shop offers: an exact tag (case-insensitive), then the primary subtag (en-US→en-GB); with no match, the shop’s first language. (A storefront without a language list of its own offers every language of the company: then the primary subtag of the first usable tag is used as it is — a language the company has not set up is answered in its default currency, untranslated — and, with no tag at all, the company’s default language.)The language decides the currency of every price and the translations. A cart or checkout keeps the language it was created in: a later request changes it only when its body names a
context.languagedifferent from the stored one, and when that switches the shop language every line is re-added in the new currency and gets a new line id. Once a session has a language, this header alone never switches it. On a storefront without a language list, a session created with no usable tag stores none: each later request is then answered in the language its own tags pick, and a different pick adds the lines again (new line ids). Sendcontext.languageon the create.example:sk-SK,sk;q=0.9,en;q=0.8
Request bodyrequired
A cart update. Fields you leave out keep their stored value (except that line_items is required). Every write extends expires_at to 7 days from now.
Required, full replacement: lines not listed are removed. Missing → 400
line_items must be an array.maxItems: 100buyerBuyerRequest | nullReplaces the whole stored buyer: a field you leave out is erased (send the e-mail again when you add a name).
nullclears the buyer; leavingbuyerout keeps it. A non-string value → 400; a longer string is cut to the stated length (counted in code points). Names and phone are never required.contextRequestContext | nullBuyer context. Every field is optional; strings longer than the stated length are cut (counted in code points). Unknown fields (
intent,eligibility,payment…) are ignored.discountsDiscountsRequest | nullDiscount codes (only read when
dev.ucp.shopping.discountis negotiated). Without the extension a code is never applied: one applied earlier is taken off the session’s cart for that answer (the stored codes are kept, and a later request that negotiates the extension applies them again).fulfillmentFulfillmentRequest | nullAccepted and stored, never used for a cart (validated like a checkout’s when the fulfillment extension is negotiated, so a malformed one is still a 400).
{
"line_items": [
{
"id": "e2b7d4a9-3c61-4f85-9a2e-7d1c5b8f3a46",
"item": {
"id": "1e8a6c4f-2b93-4d71-9f05-6a3c8e2b1d47"
},
"quantity": 1
},
{
"item": {
"id": "b5f81c3d-6e49-4a2b-8d73-9c0e4f1a6b25"
},
"quantity": 3
}
],
"discounts": {
"codes": [
"JESEN10",
"LETO5"
]
}
}Responses
200The updated cart (messages lists what was adjusted or refused); the unchanged cart with handed_off after a handoff; or a business error: not_found, capabilitie…
The updated cart (messages lists what was adjusted or refused); the unchanged cart with handed_off after a handoff; or a business error: not_found, capabilities_incompatible.
Response headers
Cache-ControlstringEvery UCP answer (success or error) except the business profile.
always:no-storeETagstringAn ETag of the body, generated by the API on every response with a body (writes included). The API’s ETag is strong; the web server sends it weak (
W/"…") when it compresses the response, which it does for a client that sendsAccept-Encoding: gzip. AGETthat sends a matchingIf-None-Match(either form) can be answered304 Not Modifiedeven though the answer isno-store.X-RateLimit-LimitintegerRequests this client IP may make to this operation per 60-second window (600 reads, 300 writes, 60 handoff). Set on every answer that passed the rate limiter.
X-RateLimit-RemainingintegerThe operation’s limit minus this client IP’s current count. The counts live in the API server’s memory; each request is meant to leave the count 60 s after it was made, but the rate limiter (
@nestjs/throttler6.5) drops those pending decrements, for every client, whenever any client’s block ends. Read it as an upper bound and keep steady traffic at half a limit or less.minimum: 0X-RateLimit-ResetintegerSeconds left in a 60-second window the limiter keeps per client IP and operation. The count is not reset when that window ends, so this is not the moment the budget comes back.
minimum: 0
Exactly one of
- Cartobject · Cart
A cart. It has no
status: a cancel answers the cart as it was (withcontinue_url), and only a later read showsnot_found.additionalProperties: falseThe
ucpobject of a successful catalog or cart answer. A missingstatusmeans success.idstring (uuid)requiredThe cart id.
In the shop cart’s order, which need not be the order you sent.
ISO 4217 currency code.
pattern: ^[A-Z]{3}$subtotal,discount? andtotal(neverfulfillmenton a cart).minItems: 2maxItems: 3messagesarray<Message>Present only when non-empty. Line and discount messages appear only in the answer to the write that caused them (a cart the buyer took to the shop repeats its last answer, messages included).
minItems: 1buyerBuyerThe stored buyer; empty fields are left out. Absent when nothing is stored.
contextContextThe stored context, echoed as you sent it (after cleaning); empty fields are left out. Absent when nothing is stored.
discountsDiscountsPresent only when
dev.ucp.shopping.discountis negotiated. Thediscounttotal is present whether or not it is.The buyer handoff link,
https://{primary domain}/ucp/continue/{64 hex}. Give it to the BUYER to open in their browser; never call it yourself. It never changes during the session’s life and works while the session is open and unexpired — anincompletecheckout included: the first line that reaches a buyer’s cart freezes the session in the status it has, so give the link only atrequires_escalation. Once a cart was converted, its own link hands over the checkout made from it. Not single-use: every distinct browser (or signed-in customer) that opens it gets the lines once.pattern: ^https://[^/]+/ucp/continue/[0-9a-f]{64}$When the session expires (UTC, ISO 8601 with milliseconds). A write extends it (cart: now + 7 days, checkout: now + 24 h); reads,
completeandcanceldo not. After a handoff nothing extends it.
- UcpBusinessErrorResponseobject · UcpBusinessErrorResponse
HTTP 200 with no resource. The cases:
capabilities_incompatible— your profile and this shop share no version of the operation’s root capability (always for carts and checkouts on a shop in catalog mode). Nothing ran; no Idempotency-Key was used.not_found— the product, cart (Cart not found or has expired) or checkout (Checkout session not found) does not exist for this shop, is malformed, or (carts) was canceled or expired. An unknowncart_idonPOST /checkout-sessionsalso readsCheckout session not found.out_of_stock/item_unavailable— a create in which none of the requested items could be added; no session was created.handed_off— converting a cart whose lines the buyer already took to the shop.
Inside a write the last three are stored under the Idempotency-Key like a success (
capabilities_incompatibleis answered before the key is read).additionalProperties: falseThe
ucpobject of a business error. Never haspayment_handlers.capabilitiesis{}forcapabilities_incompatibleand lists the operation’s root (and its negotiated extensions) otherwise.Exactly one error message saying why no resource could be returned.
minItems: 1maxItems: 1continue_urlstring (uri)requiredWhere a buyer could go instead: the shop home (
https://{primary domain}/), orhttps://{primary domain}/cartforhanded_off. Not a handoff link.
{
"ucp": {
"version": "2026-04-08",
"capabilities": {
"dev.ucp.shopping.cart": [
{
"version": "2026-04-08"
}
],
"dev.ucp.shopping.discount": [
{
"version": "2026-04-08"
}
]
}
},
"id": "9a3f6c2e-5b18-4d47-8e9c-1f2a7b4d6e83",
"line_items": [
{
"id": "e2b7d4a9-3c61-4f85-9a2e-7d1c5b8f3a46",
"item": {
"id": "1e8a6c4f-2b93-4d71-9f05-6a3c8e2b1d47",
"title": "Trek Marlin 5",
"price": 69900,
"image_url": "https://shop.example.com/api/public/uploads/c4d1e5a2-8b3f-4a6c-9e07-5f2d8a1b3c40/products/trek-marlin-5-modra.webp"
},
"quantity": 1,
"totals": [
{
"type": "subtotal",
"amount": 69900
},
{
"type": "total",
"amount": 69900
}
]
},
{
"id": "8f4a2c6e-1b93-4d57-b6e8-3a9c7d1f5e24",
"item": {
"id": "b5f81c3d-6e49-4a2b-8d73-9c0e4f1a6b25",
"title": "Fľaša na vodu 750 ml",
"price": 990
},
"quantity": 2,
"totals": [
{
"type": "subtotal",
"amount": 1980
},
{
"type": "total",
"amount": 1980
}
]
}
],
"currency": "EUR",
"totals": [
{
"type": "subtotal",
"amount": 71880
},
{
"type": "discount",
"amount": -5844,
"display_text": "JESEN10"
},
{
"type": "total",
"amount": 66036
}
],
"messages": [
{
"type": "warning",
"code": "quantity_adjusted",
"content": "Only 2 available — quantity lowered from 3",
"path": "$.line_items[1].quantity"
},
{
"type": "warning",
"code": "discount_code_combination_disallowed",
"path": "$.discounts.codes[1]",
"content": "Code 'LETO5' cannot be combined with 'JESEN10'"
}
],
"buyer": {
"email": "jana.novakova@example.com"
},
"context": {
"language": "sk",
"address_country": "SK"
},
"discounts": {
"codes": [
"JESEN10",
"LETO5"
],
"applied": [
{
"code": "JESEN10",
"title": "JESEN10",
"amount": 5844,
"method": "across"
}
]
},
"continue_url": "https://shop.example.com/ucp/continue/e1f0907c4abcf12564330feef5762f8471cc386a3563ce53cf8342ce6be08b62",
"expires_at": "2026-11-02T09:16:47.402Z"
}{
"ucp": {
"version": "2026-04-08",
"capabilities": {
"dev.ucp.shopping.cart": [
{
"version": "2026-04-08"
}
],
"dev.ucp.shopping.discount": [
{
"version": "2026-04-08"
}
]
}
},
"id": "9a3f6c2e-5b18-4d47-8e9c-1f2a7b4d6e83",
"buyer": {
"email": "jana.novakova@example.com"
},
"totals": [
{
"type": "subtotal",
"amount": 71880
},
{
"type": "discount",
"amount": -5844,
"display_text": "JESEN10"
},
{
"type": "total",
"amount": 66036
}
],
"context": {
"language": "sk",
"address_country": "SK"
},
"currency": "EUR",
"messages": [
{
"code": "quantity_adjusted",
"path": "$.line_items[1].quantity",
"type": "warning",
"content": "Only 2 available — quantity lowered from 3"
},
{
"code": "discount_code_combination_disallowed",
"path": "$.discounts.codes[1]",
"type": "warning",
"content": "Code 'LETO5' cannot be combined with 'JESEN10'"
},
{
"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",
"LETO5"
],
"applied": [
{
"code": "JESEN10",
"title": "JESEN10",
"amount": 5844,
"method": "across"
}
]
},
"expires_at": "2026-11-02T09:16:47.402Z",
"line_items": [
{
"id": "e2b7d4a9-3c61-4f85-9a2e-7d1c5b8f3a46",
"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": "8f4a2c6e-1b93-4d57-b6e8-3a9c7d1f5e24",
"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
}
],
"continue_url": "https://shop.example.com/ucp/continue/e1f0907c4abcf12564330feef5762f8471cc386a3563ce53cf8342ce6be08b62"
}{
"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/"
}{
"ucp": {
"version": "2026-04-08",
"status": "error",
"capabilities": {}
},
"messages": [
{
"type": "error",
"code": "capabilities_incompatible",
"content": "The platform and this business share no version of the capability this operation needs",
"severity": "unrecoverable"
}
],
"continue_url": "https://shop.example.com/"
}400Bad Request (Error): invalid_profile_url — the UCP-Agent header is missing or unusable (with continue_url); invalid_request — a field this operation reads has …
Bad Request (Error):
invalid_profile_url— theUCP-Agentheader is missing or unusable (withcontinue_url);invalid_request— a field this operation reads has the wrong JSON type or value;contentnames it. Malformed JSON or a top-level JSON primitive says only "The request is not valid". Unknown fields are ignored, never refused. As forcreateCart;line_itemsmissing →line_items must be an array. The cart is not changed.invalid_request"Idempotency-Key is not valid" — seeIdempotency-Key.
Response headers
Cache-ControlstringEvery UCP answer (success or error) except the business profile.
always:no-store
SchemaError
{
"code": "invalid_profile_url",
"content": "The UCP-Agent header must carry profile=\"https://…\"",
"continue_url": "https://shop.example.com/"
}{
"code": "invalid_request",
"content": "line_items must be an array"
}{
"code": "invalid_request",
"content": "line_items[0].quantity must be a positive integer"
}{
"code": "invalid_request",
"content": "Idempotency-Key is not valid"
}{
"code": "invalid_request",
"content": "The request is not valid"
}Request with curl
curl -X PUT 'https://shop.example.com/ucp/v1/carts/9a3f6c2e-5b18-4d47-8e9c-1f2a7b4d6e83' \
-H 'UCP-Agent: profile="https://platform.example.com/.well-known/ucp"' \
-H 'Idempotency-Key: 0c2f8a4e-6b1d-4f37-9a85-3e7d1c9b5f02' \
-H 'Accept-Language: sk-SK,sk;q=0.9,en;q=0.8' \
-H 'Content-Type: application/json' \
--data '{
"line_items": [
{
"id": "e2b7d4a9-3c61-4f85-9a2e-7d1c5b8f3a46",
"item": {
"id": "1e8a6c4f-2b93-4d71-9f05-6a3c8e2b1d47"
},
"quantity": 1
},
{
"item": {
"id": "b5f81c3d-6e49-4a2b-8d73-9c0e4f1a6b25"
},
"quantity": 3
}
],
"discounts": {
"codes": [
"JESEN10",
"LETO5"
]
}
}'Cancel a cart
POST/ucp/v1/carts/{id}/cancel
Cancels the cart. The answer is the cart as it was — with continue_url and the stored expires_at, because a cart has no status. From then on every operation on it answers not_found, and its continue_url shows the “gone” page.
- Any request body is ignored (the Idempotency-Key hashes
{}). - Canceling a cart does not cancel a checkout made from it.
Parameters
idpathstringrequiredThe cart’s
id(a UUID, any letter case). An id that is not a UUID, belongs to another shop, or names a canceled or expired cart is answered with the 200not_foundbusiness error, not a 4xx.pattern: ^[0-9a-fA-F]{8}-[0-9a-fA-F]{4}-[0-9a-fA-F]{4}-[0-9a-fA-F]{4}-[0-9a-fA-F]{12}$example:9a3f6c2e-5b18-4d47-8e9c-1f2a7b4d6e83UCP-AgentheaderstringrequiredNames your platform profile. Required on every
/ucp/v1/*operation; never read by the profile and handoff routes.Details
Grammar — a subset of an RFC 8941 Dictionary, at most 4096 characters:
- members separated by
,(spaces or tabs around it allowed; a trailing comma is an error); keys lower-case, matching[a-z*][a-z0-9_\-.*]*; - the
profilemember must be an sf-string:profile="https://…", printable ASCII 0x20–0x7E, escapes\"and\\only; - other members (tokens, numbers, booleans, strings) and parameters (
;key=value) are parsed and ignored; whenprofileappears twice the last one wins; - any parse error makes the whole header unusable.
Accepted:
profile="https://platform.example.com/.well-known/ucp",version="2026-04-08", profile="https://platform.example.com/.well-known/ucp";sig=abc. Refused: a missing header,profile=https://…(a token, not a string),Profile="…",a=1 b=2, profile="…".The URL — at most 1000 characters, no whitespace anywhere, starting
https://, no user name or password, a non-empty host. Any port, path and query string are accepted (the spec’s/.well-known/ucppath is not enforced). Failing any rule →400 invalid_profile_url.The fetch —
GETwithAccept: application/jsonandUser-Agent: Monokaido-UCP/1.0 (+https://monokaido.com); no redirects followed; only HTTP 200 accepted; at most 256 KiB; 3 s idle, 5 s total; the host must resolve to a public address (private, loopback, link-local and other reserved ranges are refused). Anything else →424 profile_unreachable. A body that is not JSON, or whoseucp.versionis not aYYYY-MM-DDstring →422 profile_malformed; a valid profile of another version →422 version_unsupported. Onlyucp.versionanducp.capabilities(name →[{"version"}], first 20 entries per name, names likedev.ucp.shopping.cart) are read; everything else in it is ignored.Caching — a fetched profile is cached for its
Cache-Control: max-age, clamped to 60 s – 1 h (5 min without one;no-store,no-cacheandprivateare ignored) and served up to 1 h past that while one background refresh runs. A failed fetch (424, or422 profile_malformed) is remembered for 60 s. A profile of anotherucp.versionis a valid profile and is cached like one, so after you change its version expect422 version_unsupporteduntil that entry is refreshed (itsmax-age, then one more request) — or use a new profile URL. These caches are kept in memory by each API process. Fetching a profile the shop does not know yet is budgeted: 5 new profile URLs per minute per client (IPv4 address, IPv6 /64) and 60 per minute in all →503 service_unavailablewithRetry-After. The full URL string is the cache key, so keep it stable (no per-request query strings).minLength: 1maxLength: 4096example:profile="https://platform.example.com/.well-known/ucp"- members separated by
Idempotency-KeyheaderstringOptional, strongly recommended. Without it (or with an empty value) the write simply runs: a retried create makes a second session.
- Format: 1–255 printable ASCII characters 0x21–0x7E — no spaces. Anything else (also a header sent twice, which arrives joined with
", ") →400 invalid_request"Idempotency-Key is not valid". A UUID v4 per logical write is a good choice. - Scope: this shop (all its domains) × the exact
UCP-Agentprofile URL string × the key. - Request identity: the operation, the path
{id}as sent (after URL decoding; case included) and the JSON body compared canonically (object key order and whitespace do not matter, array order does). Cancel routes hash{}whatever the body. - Same key, same request, finished → the stored HTTP status and body are replayed — the same values, including a 200 business error, the original
expires_atand theucpobject negotiated the first time, even when the session has changed since. The replay comes from storage, so its object keys come in JSONB order (and itsETagdiffers). - The
UCP-Agentprofile is resolved and negotiated before the key is read: a profile error or a 200capabilities_incompatibleanswer is never stored. - Same key, different request →
409 idempotency_conflict. Same request still running →409withRetry-After: 2. - If the first request fails with a protocol error (
400,503session busy,500…) the key is released: retrying with the same key runs the operation again. - A claim whose request died is taken over by the same request after 120 s. Keys are kept for at least 25 hours after their first use (an hourly job deletes older ones); do not rely on a key more than 24 hours after its first use.
- Not read by catalog operations,
GETroutes, the profile or the handoff routes.
pattern: ^[\x21-\x7E]{1,255}$minLength: 1maxLength: 255example:0c2f8a4e-6b1d-4f37-9a85-3e7d1c9b5f02- Format: 1–255 printable ASCII characters 0x21–0x7E — no spaces. Anything else (also a header sent twice, which arrives joined with
Accept-LanguageheaderstringFallback for the answer’s language after
context.languagein the body. The first 10 comma-separated tags are tried in header order (q=weights are ignored,*is skipped) against the languages the shop offers: an exact tag (case-insensitive), then the primary subtag (en-US→en-GB); with no match, the shop’s first language. (A storefront without a language list of its own offers every language of the company: then the primary subtag of the first usable tag is used as it is — a language the company has not set up is answered in its default currency, untranslated — and, with no tag at all, the company’s default language.)The language decides the currency of every price and the translations. A cart or checkout keeps the language it was created in: a later request changes it only when its body names a
context.languagedifferent from the stored one, and when that switches the shop language every line is re-added in the new currency and gets a new line id. Once a session has a language, this header alone never switches it. On a storefront without a language list, a session created with no usable tag stores none: each later request is then answered in the language its own tags pick, and a different pick adds the lines again (new line ids). Sendcontext.languageon the create.example:sk-SK,sk;q=0.9,en;q=0.8
Responses
200The cart as it was when canceled, or a business error: not_found (unknown, already canceled or expired), capabilities_incompatible.
Response headers
Cache-ControlstringEvery UCP answer (success or error) except the business profile.
always:no-storeETagstringAn ETag of the body, generated by the API on every response with a body (writes included). The API’s ETag is strong; the web server sends it weak (
W/"…") when it compresses the response, which it does for a client that sendsAccept-Encoding: gzip. AGETthat sends a matchingIf-None-Match(either form) can be answered304 Not Modifiedeven though the answer isno-store.X-RateLimit-LimitintegerRequests this client IP may make to this operation per 60-second window (600 reads, 300 writes, 60 handoff). Set on every answer that passed the rate limiter.
X-RateLimit-RemainingintegerThe operation’s limit minus this client IP’s current count. The counts live in the API server’s memory; each request is meant to leave the count 60 s after it was made, but the rate limiter (
@nestjs/throttler6.5) drops those pending decrements, for every client, whenever any client’s block ends. Read it as an upper bound and keep steady traffic at half a limit or less.minimum: 0X-RateLimit-ResetintegerSeconds left in a 60-second window the limiter keeps per client IP and operation. The count is not reset when that window ends, so this is not the moment the budget comes back.
minimum: 0
Exactly one of
- Cartobject · Cart
A cart. It has no
status: a cancel answers the cart as it was (withcontinue_url), and only a later read showsnot_found.additionalProperties: falseThe
ucpobject of a successful catalog or cart answer. A missingstatusmeans success.idstring (uuid)requiredThe cart id.
In the shop cart’s order, which need not be the order you sent.
ISO 4217 currency code.
pattern: ^[A-Z]{3}$subtotal,discount? andtotal(neverfulfillmenton a cart).minItems: 2maxItems: 3messagesarray<Message>Present only when non-empty. Line and discount messages appear only in the answer to the write that caused them (a cart the buyer took to the shop repeats its last answer, messages included).
minItems: 1buyerBuyerThe stored buyer; empty fields are left out. Absent when nothing is stored.
contextContextThe stored context, echoed as you sent it (after cleaning); empty fields are left out. Absent when nothing is stored.
discountsDiscountsPresent only when
dev.ucp.shopping.discountis negotiated. Thediscounttotal is present whether or not it is.The buyer handoff link,
https://{primary domain}/ucp/continue/{64 hex}. Give it to the BUYER to open in their browser; never call it yourself. It never changes during the session’s life and works while the session is open and unexpired — anincompletecheckout included: the first line that reaches a buyer’s cart freezes the session in the status it has, so give the link only atrequires_escalation. Once a cart was converted, its own link hands over the checkout made from it. Not single-use: every distinct browser (or signed-in customer) that opens it gets the lines once.pattern: ^https://[^/]+/ucp/continue/[0-9a-f]{64}$When the session expires (UTC, ISO 8601 with milliseconds). A write extends it (cart: now + 7 days, checkout: now + 24 h); reads,
completeandcanceldo not. After a handoff nothing extends it.
- UcpBusinessErrorResponseobject · UcpBusinessErrorResponse
HTTP 200 with no resource. The cases:
capabilities_incompatible— your profile and this shop share no version of the operation’s root capability (always for carts and checkouts on a shop in catalog mode). Nothing ran; no Idempotency-Key was used.not_found— the product, cart (Cart not found or has expired) or checkout (Checkout session not found) does not exist for this shop, is malformed, or (carts) was canceled or expired. An unknowncart_idonPOST /checkout-sessionsalso readsCheckout session not found.out_of_stock/item_unavailable— a create in which none of the requested items could be added; no session was created.handed_off— converting a cart whose lines the buyer already took to the shop.
Inside a write the last three are stored under the Idempotency-Key like a success (
capabilities_incompatibleis answered before the key is read).additionalProperties: falseThe
ucpobject of a business error. Never haspayment_handlers.capabilitiesis{}forcapabilities_incompatibleand lists the operation’s root (and its negotiated extensions) otherwise.Exactly one error message saying why no resource could be returned.
minItems: 1maxItems: 1continue_urlstring (uri)requiredWhere a buyer could go instead: the shop home (
https://{primary domain}/), orhttps://{primary domain}/cartforhanded_off. Not a handoff link.
{
"ucp": {
"version": "2026-04-08",
"capabilities": {
"dev.ucp.shopping.cart": [
{
"version": "2026-04-08"
}
],
"dev.ucp.shopping.discount": [
{
"version": "2026-04-08"
}
]
}
},
"id": "9a3f6c2e-5b18-4d47-8e9c-1f2a7b4d6e83",
"line_items": [
{
"id": "e2b7d4a9-3c61-4f85-9a2e-7d1c5b8f3a46",
"item": {
"id": "1e8a6c4f-2b93-4d71-9f05-6a3c8e2b1d47",
"title": "Trek Marlin 5",
"price": 69900,
"image_url": "https://shop.example.com/api/public/uploads/c4d1e5a2-8b3f-4a6c-9e07-5f2d8a1b3c40/products/trek-marlin-5-modra.webp"
},
"quantity": 1,
"totals": [
{
"type": "subtotal",
"amount": 69900
},
{
"type": "total",
"amount": 69900
}
]
},
{
"id": "8f4a2c6e-1b93-4d57-b6e8-3a9c7d1f5e24",
"item": {
"id": "b5f81c3d-6e49-4a2b-8d73-9c0e4f1a6b25",
"title": "Fľaša na vodu 750 ml",
"price": 990
},
"quantity": 2,
"totals": [
{
"type": "subtotal",
"amount": 1980
},
{
"type": "total",
"amount": 1980
}
]
}
],
"currency": "EUR",
"totals": [
{
"type": "subtotal",
"amount": 71880
},
{
"type": "discount",
"amount": -5844,
"display_text": "JESEN10"
},
{
"type": "total",
"amount": 66036
}
],
"buyer": {
"email": "jana.novakova@example.com"
},
"context": {
"language": "sk",
"address_country": "SK"
},
"discounts": {
"codes": [
"JESEN10",
"LETO5"
],
"applied": [
{
"code": "JESEN10",
"title": "JESEN10",
"amount": 5844,
"method": "across"
}
]
},
"continue_url": "https://shop.example.com/ucp/continue/e1f0907c4abcf12564330feef5762f8471cc386a3563ce53cf8342ce6be08b62",
"expires_at": "2026-11-02T09:16:47.402Z"
}{
"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/"
}{
"ucp": {
"version": "2026-04-08",
"status": "error",
"capabilities": {}
},
"messages": [
{
"type": "error",
"code": "capabilities_incompatible",
"content": "The platform and this business share no version of the capability this operation needs",
"severity": "unrecoverable"
}
],
"continue_url": "https://shop.example.com/"
}400Bad Request (Error): invalid_profile_url — the UCP-Agent header is missing or unusable (with continue_url); invalid_request "The request is not valid" — a body…
Bad Request (Error):
invalid_profile_url— theUCP-Agentheader is missing or unusable (withcontinue_url);invalid_request"The request is not valid" — a body sent as JSON that does not parse (the body is ignored, but it is parsed before routing);invalid_request"Idempotency-Key is not valid" — seeIdempotency-Key.
Response headers
Cache-ControlstringEvery UCP answer (success or error) except the business profile.
always:no-store
SchemaError
{
"code": "invalid_profile_url",
"content": "The UCP-Agent header must carry profile=\"https://…\"",
"continue_url": "https://shop.example.com/"
}{
"code": "invalid_request",
"content": "Idempotency-Key is not valid"
}{
"code": "invalid_request",
"content": "The request is not valid"
}Request with curl
curl -X POST 'https://shop.example.com/ucp/v1/carts/9a3f6c2e-5b18-4d47-8e9c-1f2a7b4d6e83/cancel' \
-H 'UCP-Agent: profile="https://platform.example.com/.well-known/ucp"' \
-H 'Idempotency-Key: 0c2f8a4e-6b1d-4f37-9a85-3e7d1c9b5f02' \
-H 'Accept-Language: sk-SK,sk;q=0.9,en;q=0.8'Checkout
Checkout sessions (dev.ucp.shopping.checkout, with the fulfillment and discount extensions): buyer, shipping address and option, codes. Status incomplete → requires_escalation (ready; the buyer pays on the shop) → completed (the buyer placed the order) or canceled. Kept 24 h from the last write.
Create a checkout session
POST/ucp/v1/checkout-sessions
Creates a checkout session — directly from line_items, or by converting a cart (cart_id, when dev.ucp.shopping.cart is negotiated).
- A new checkout answers 201. Its
statusisincompletewhile anything the platform can provide is missing (every gap is arecoverableerror with apath) or this write refused a line (item_unavailable,out_of_stock,vat_mix_not_allowedarerecoverableerrors too, without apathwhen the line is not in the answer; the next read no longer repeats them), elserequires_escalationwithpayment_required. - Ready needs: at least one line, a valid
buyer.email, and — withdev.ucp.shopping.fulfillment— a destination with street, city, postal code and a recognised country that the shop ships to, plus aselected_option_idamong the offered options. Names and phone are optional. A Packeta option addspickup_point_required(chosen by the buyer on the shop) but does not block readiness. - From a cart: lines, buyer, context and language come from the cart (this request’s
line_items,buyerandcontextare ignored;buyerandcontextare still checked);fulfillmentfrom this request; discount codes from this request when sent, else the cart’s. The checkout gets its own lines (new line ids); the cart is left as it was. A cart has at most one open checkout: converting it again answers that checkout with 200 and applies nothing from the request (update the checkout instead). A cart the buyer already took to the shop → 200handed_off. - If the cart’s open checkout has expired, it is canceled and a new one is created (201).
- If lines were sent and none could be added, no checkout is created (200
out_of_stock/item_unavailable). - The checkout lives 24 h from its last write. Send an
Idempotency-Key: a retried create without one makes a second checkout.
Parameters
UCP-AgentheaderstringrequiredNames your platform profile. Required on every
/ucp/v1/*operation; never read by the profile and handoff routes.Details
Grammar — a subset of an RFC 8941 Dictionary, at most 4096 characters:
- members separated by
,(spaces or tabs around it allowed; a trailing comma is an error); keys lower-case, matching[a-z*][a-z0-9_\-.*]*; - the
profilemember must be an sf-string:profile="https://…", printable ASCII 0x20–0x7E, escapes\"and\\only; - other members (tokens, numbers, booleans, strings) and parameters (
;key=value) are parsed and ignored; whenprofileappears twice the last one wins; - any parse error makes the whole header unusable.
Accepted:
profile="https://platform.example.com/.well-known/ucp",version="2026-04-08", profile="https://platform.example.com/.well-known/ucp";sig=abc. Refused: a missing header,profile=https://…(a token, not a string),Profile="…",a=1 b=2, profile="…".The URL — at most 1000 characters, no whitespace anywhere, starting
https://, no user name or password, a non-empty host. Any port, path and query string are accepted (the spec’s/.well-known/ucppath is not enforced). Failing any rule →400 invalid_profile_url.The fetch —
GETwithAccept: application/jsonandUser-Agent: Monokaido-UCP/1.0 (+https://monokaido.com); no redirects followed; only HTTP 200 accepted; at most 256 KiB; 3 s idle, 5 s total; the host must resolve to a public address (private, loopback, link-local and other reserved ranges are refused). Anything else →424 profile_unreachable. A body that is not JSON, or whoseucp.versionis not aYYYY-MM-DDstring →422 profile_malformed; a valid profile of another version →422 version_unsupported. Onlyucp.versionanducp.capabilities(name →[{"version"}], first 20 entries per name, names likedev.ucp.shopping.cart) are read; everything else in it is ignored.Caching — a fetched profile is cached for its
Cache-Control: max-age, clamped to 60 s – 1 h (5 min without one;no-store,no-cacheandprivateare ignored) and served up to 1 h past that while one background refresh runs. A failed fetch (424, or422 profile_malformed) is remembered for 60 s. A profile of anotherucp.versionis a valid profile and is cached like one, so after you change its version expect422 version_unsupporteduntil that entry is refreshed (itsmax-age, then one more request) — or use a new profile URL. These caches are kept in memory by each API process. Fetching a profile the shop does not know yet is budgeted: 5 new profile URLs per minute per client (IPv4 address, IPv6 /64) and 60 per minute in all →503 service_unavailablewithRetry-After. The full URL string is the cache key, so keep it stable (no per-request query strings).minLength: 1maxLength: 4096example:profile="https://platform.example.com/.well-known/ucp"- members separated by
Idempotency-KeyheaderstringOptional, strongly recommended. Without it (or with an empty value) the write simply runs: a retried create makes a second session.
- Format: 1–255 printable ASCII characters 0x21–0x7E — no spaces. Anything else (also a header sent twice, which arrives joined with
", ") →400 invalid_request"Idempotency-Key is not valid". A UUID v4 per logical write is a good choice. - Scope: this shop (all its domains) × the exact
UCP-Agentprofile URL string × the key. - Request identity: the operation, the path
{id}as sent (after URL decoding; case included) and the JSON body compared canonically (object key order and whitespace do not matter, array order does). Cancel routes hash{}whatever the body. - Same key, same request, finished → the stored HTTP status and body are replayed — the same values, including a 200 business error, the original
expires_atand theucpobject negotiated the first time, even when the session has changed since. The replay comes from storage, so its object keys come in JSONB order (and itsETagdiffers). - The
UCP-Agentprofile is resolved and negotiated before the key is read: a profile error or a 200capabilities_incompatibleanswer is never stored. - Same key, different request →
409 idempotency_conflict. Same request still running →409withRetry-After: 2. - If the first request fails with a protocol error (
400,503session busy,500…) the key is released: retrying with the same key runs the operation again. - A claim whose request died is taken over by the same request after 120 s. Keys are kept for at least 25 hours after their first use (an hourly job deletes older ones); do not rely on a key more than 24 hours after its first use.
- Not read by catalog operations,
GETroutes, the profile or the handoff routes.
pattern: ^[\x21-\x7E]{1,255}$minLength: 1maxLength: 255example:0c2f8a4e-6b1d-4f37-9a85-3e7d1c9b5f02- Format: 1–255 printable ASCII characters 0x21–0x7E — no spaces. Anything else (also a header sent twice, which arrives joined with
Accept-LanguageheaderstringFallback for the answer’s language after
context.languagein the body. The first 10 comma-separated tags are tried in header order (q=weights are ignored,*is skipped) against the languages the shop offers: an exact tag (case-insensitive), then the primary subtag (en-US→en-GB); with no match, the shop’s first language. (A storefront without a language list of its own offers every language of the company: then the primary subtag of the first usable tag is used as it is — a language the company has not set up is answered in its default currency, untranslated — and, with no tag at all, the company’s default language.)The language decides the currency of every price and the translations. A cart or checkout keeps the language it was created in: a later request changes it only when its body names a
context.languagedifferent from the stored one, and when that switches the shop language every line is re-added in the new currency and gets a new line id. Once a session has a language, this header alone never switches it. On a storefront without a language list, a session created with no usable tag stores none: each later request is then answered in the language its own tags pick, and a different pick adds the lines again (new line ids). Sendcontext.languageon the create.example:sk-SK,sk;q=0.9,en;q=0.8
Request bodyrequired
A new checkout session, directly from lines or from a cart. Send line_items, or cart_id to convert a cart (read only when dev.ucp.shopping.cart is negotiated). Without a usable cart_id, line_items is required: a missing one is 400 invalid_request "line_items must be an array". Unknown fields are ignored.
cart_idstringConvert this cart (only when
dev.ucp.shopping.cartis negotiated; otherwise it is ignored andline_itemsis required). The checkout takes the cart’s lines, buyer, context and language —line_items,buyerandcontextin this request are ignored (buyerandcontextare still checked: a malformed one is a 400) — itsfulfillmentfrom this request, and its discount codes from this request whendiscounts.codesis sent (else the cart’s). The cart itself is not changed. One open checkout per cart: when the cart already has one, that checkout is answered with HTTP 200 and this request’s fields are not applied. A cart whose lines the buyer already took to the shop → 200handed_off. An unknown id → 200not_found("Checkout session not found").line_itemsarray<LineItemRequest>The lines (direct create). Required unless
cart_idconverts a cart;[]creates an empty,incompletecheckout.maxItems: 100buyerBuyerRequest | nullReplaces the whole stored buyer: a field you leave out is erased (send the e-mail again when you add a name).
nullclears the buyer; leavingbuyerout keeps it. A non-string value → 400; a longer string is cut to the stated length (counted in code points). Names and phone are never required.contextRequestContext | nullBuyer context. Every field is optional; strings longer than the stated length are cut (counted in code points). Unknown fields (
intent,eligibility,payment…) are ignored.fulfillmentFulfillmentRequest | nullShipping (only read when
dev.ucp.shopping.fulfillmentis negotiated; ignored on carts — a cart stores it but never uses it, and a conversion takesfulfillmentfrom the conversion request).nullor a body withoutfulfillmentkeeps what the session holds.discountsDiscountsRequest | nullDiscount codes (only read when
dev.ucp.shopping.discountis negotiated). Without the extension a code is never applied: one applied earlier is taken off the session’s cart for that answer (the stored codes are kept, and a later request that negotiates the extension applies them again).
{
"cart_id": "9a3f6c2e-5b18-4d47-8e9c-1f2a7b4d6e83"
}{
"line_items": [
{
"item": {
"id": "4a6e2d9c-8b15-4f37-9c48-1e7a5b3d2f90"
},
"quantity": 1
}
],
"buyer": {
"first_name": "Peter",
"last_name": "Horváth",
"email": "peter.horvath@example.com"
},
"context": {
"address_country": "SK",
"language": "sk"
},
"fulfillment": {
"methods": [
{
"type": "shipping",
"destinations": [
{
"id": "office",
"street_address": "Námestie SNP 5",
"address_locality": "Banská Bystrica",
"postal_code": "974 01",
"address_country": "SK",
"first_name": "Peter",
"last_name": "Horváth"
}
],
"selected_destination_id": "office"
}
]
}
}Responses
200The cart’s existing open checkout (nothing from this request applied), or a business error: handed_off, not_found (unknown cart_id, reported as "Checkout sessi…
The cart’s existing open checkout (nothing from this request applied), or a business error: handed_off, not_found (unknown cart_id, reported as "Checkout session not found"), out_of_stock / item_unavailable (nothing could be added), capabilities_incompatible.
Response headers
Cache-ControlstringEvery UCP answer (success or error) except the business profile.
always:no-storeETagstringAn ETag of the body, generated by the API on every response with a body (writes included). The API’s ETag is strong; the web server sends it weak (
W/"…") when it compresses the response, which it does for a client that sendsAccept-Encoding: gzip. AGETthat sends a matchingIf-None-Match(either form) can be answered304 Not Modifiedeven though the answer isno-store.X-RateLimit-LimitintegerRequests this client IP may make to this operation per 60-second window (600 reads, 300 writes, 60 handoff). Set on every answer that passed the rate limiter.
X-RateLimit-RemainingintegerThe operation’s limit minus this client IP’s current count. The counts live in the API server’s memory; each request is meant to leave the count 60 s after it was made, but the rate limiter (
@nestjs/throttler6.5) drops those pending decrements, for every client, whenever any client’s block ends. Read it as an upper bound and keep steady traffic at half a limit or less.minimum: 0X-RateLimit-ResetintegerSeconds left in a 60-second window the limiter keeps per client IP and operation. The count is not reset when that window ends, so this is not the moment the budget comes back.
minimum: 0
Exactly one of
- Checkoutobject · Checkout
A checkout session.
Once
completedorcanceled— and while the buyer’s handoff is done but the order not yet placed — the body is the last answer the platform received, with the currentstatus(andorder): it is not re-priced, and it does not show what the buyer changed on the shop. Only the lines and the applied discount code travel to the shop; the buyer enters address, shipping and payment there again.additionalProperties: falseThe
ucpobject of every checkout answer that returns a checkout (any status).idstring (uuid)requiredThe checkout session id.
incomplete— at least onerecoverableerror is listed (no line, no valid e-mail, no or incomplete address, no shipping to it, no or an unoffered option, a line error in this answer).requires_escalation— nothing the platform can provide is missing;payment_requiredis listed and the buyer must continue on the shop throughcontinue_url.completed— the buyer placed an order on the shop from the cart the handoff filled; seeorder. It means placed, not paid. Set shortly after the order (once the shop has started the payment and handed the order to its ERP) and best effort: a failure is only logged, so an order can exist for a checkout that never readscompleted.canceled— canceled byPOST …/cancel, or expired (an open checkout read or written afterexpires_atturnscanceled).
ready_for_completeandcomplete_in_progressare never used: the API never completes a checkout.one of:incompleterequires_escalationcompletedcanceledISO 4217 currency code.
pattern: ^[A-Z]{3}$In the shop cart’s order.
subtotal,discount?,fulfillment?,total.minItems: 2maxItems: 4Always present, possibly empty. Order: line messages and discount messages (only in the answer to the write that caused them), then
missing/invalid_address/address_undeliverable/invalid_option,pickup_point_required, andpayment_requiredlast. Empty oncompletedandcanceledreads.Always present: at most 10, possibly empty.
maxItems: 10buyerBuyerThe stored buyer; empty fields are left out. Absent when nothing is stored.
contextContextThe stored context, echoed as you sent it (after cleaning); empty fields are left out. Absent when nothing is stored.
fulfillmentFulfillmentPresent only when
dev.ucp.shopping.fulfillmentis negotiated. Oneshippingmethod with one group; there is nopickupmethod — a pickup point (Packeta) is chosen by the buyer on the shop.discountsDiscountsPresent only when
dev.ucp.shopping.discountis negotiated. Thediscounttotal is present whether or not it is.continue_urlstring (uri) · ContinueUrlPresent while the checkout is
incompleteorrequires_escalation; absent oncecompletedorcanceled.pattern: ^https://[^/]+/ucp/continue/[0-9a-f]{64}$When the session expires (UTC, ISO 8601 with milliseconds). A write extends it (cart: now + 7 days, checkout: now + 24 h); reads,
completeandcanceldo not. After a handoff nothing extends it.orderOrderPresent once
completed.
- UcpBusinessErrorResponseobject · UcpBusinessErrorResponse
HTTP 200 with no resource. The cases:
capabilities_incompatible— your profile and this shop share no version of the operation’s root capability (always for carts and checkouts on a shop in catalog mode). Nothing ran; no Idempotency-Key was used.not_found— the product, cart (Cart not found or has expired) or checkout (Checkout session not found) does not exist for this shop, is malformed, or (carts) was canceled or expired. An unknowncart_idonPOST /checkout-sessionsalso readsCheckout session not found.out_of_stock/item_unavailable— a create in which none of the requested items could be added; no session was created.handed_off— converting a cart whose lines the buyer already took to the shop.
Inside a write the last three are stored under the Idempotency-Key like a success (
capabilities_incompatibleis answered before the key is read).additionalProperties: falseThe
ucpobject of a business error. Never haspayment_handlers.capabilitiesis{}forcapabilities_incompatibleand lists the operation’s root (and its negotiated extensions) otherwise.Exactly one error message saying why no resource could be returned.
minItems: 1maxItems: 1continue_urlstring (uri)requiredWhere a buyer could go instead: the shop home (
https://{primary domain}/), orhttps://{primary domain}/cartforhanded_off. Not a handoff link.
{
"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",
"status": "incomplete",
"currency": "EUR",
"line_items": [
{
"id": "f6a3c8e1-9d27-4b54-8e6a-2c1f7b9d3a58",
"item": {
"id": "1e8a6c4f-2b93-4d71-9f05-6a3c8e2b1d47",
"title": "Trek Marlin 5",
"price": 69900,
"image_url": "https://shop.example.com/api/public/uploads/c4d1e5a2-8b3f-4a6c-9e07-5f2d8a1b3c40/products/trek-marlin-5-modra.webp"
},
"quantity": 1,
"totals": [
{
"type": "subtotal",
"amount": 69900
},
{
"type": "total",
"amount": 69900
}
]
},
{
"id": "2e9d5b7a-4c83-4f16-a9b2-6d8e1c3f7a49",
"item": {
"id": "b5f81c3d-6e49-4a2b-8d73-9c0e4f1a6b25",
"title": "Fľaša na vodu 750 ml",
"price": 990
},
"quantity": 2,
"totals": [
{
"type": "subtotal",
"amount": 1980
},
{
"type": "total",
"amount": 1980
}
]
}
],
"totals": [
{
"type": "subtotal",
"amount": 71880
},
{
"type": "discount",
"amount": -5844,
"display_text": "JESEN10"
},
{
"type": "total",
"amount": 66036
}
],
"messages": [
{
"type": "error",
"code": "missing",
"path": "$.fulfillment.methods[0].destinations",
"content": "A shipping address is required",
"severity": "recoverable"
},
{
"type": "error",
"code": "missing",
"path": "$.fulfillment.methods[0].groups[0].selected_option_id",
"content": "Choose a shipping option",
"severity": "recoverable"
}
],
"links": [
{
"type": "terms_of_service",
"url": "https://shop.example.com/obchodne-podmienky",
"title": "Obchodné podmienky"
},
{
"type": "privacy_policy",
"url": "https://shop.example.com/ochrana-osobnych-udajov",
"title": "Ochrana osobných údajov"
}
],
"buyer": {
"email": "jana.novakova@example.com"
},
"context": {
"language": "sk",
"address_country": "SK"
},
"fulfillment": {
"methods": [
{
"id": "shipping",
"type": "shipping",
"line_item_ids": [
"f6a3c8e1-9d27-4b54-8e6a-2c1f7b9d3a58",
"2e9d5b7a-4c83-4f16-a9b2-6d8e1c3f7a49"
],
"groups": [
{
"id": "default",
"line_item_ids": [
"f6a3c8e1-9d27-4b54-8e6a-2c1f7b9d3a58",
"2e9d5b7a-4c83-4f16-a9b2-6d8e1c3f7a49"
],
"options": [
{
"id": "0c5e8a3f-7b21-4d96-8f4c-9e2a6d1b3c75",
"title": "GLS kuriér",
"description": "Doručenie na adresu do 1–2 pracovných dní",
"totals": [
{
"type": "total",
"amount": 490
}
]
},
{
"id": "6b2d9f4e-3a17-4c68-9d5b-1f8e3a7c2b40",
"title": "Packeta – výdajné miesto",
"description": "Delivery in 2–3 business days",
"totals": [
{
"type": "total",
"amount": 290
}
]
}
],
"selected_option_id": null
}
]
}
]
},
"discounts": {
"codes": [
"JESEN10",
"LETO5"
],
"applied": [
{
"code": "JESEN10",
"title": "JESEN10",
"amount": 5844,
"method": "across"
}
]
},
"continue_url": "https://shop.example.com/ucp/continue/9bbd3dedf887f93c6287fa7f4b8de141501155714c3b2050bb83551e36ec780b",
"expires_at": "2026-10-27T09:18:12.930Z"
}{
"ucp": {
"version": "2026-04-08",
"status": "error",
"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"
}
]
}
},
"messages": [
{
"type": "error",
"code": "handed_off",
"content": "The buyer continued on the shop: the items are in the shop’s cart — finish the purchase there",
"severity": "unrecoverable"
}
],
"continue_url": "https://shop.example.com/cart"
}{
"ucp": {
"version": "2026-04-08",
"status": "error",
"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"
}
]
}
},
"messages": [
{
"type": "error",
"code": "not_found",
"content": "Checkout session not found",
"severity": "unrecoverable"
}
],
"continue_url": "https://shop.example.com/"
}{
"ucp": {
"version": "2026-04-08",
"status": "error",
"capabilities": {}
},
"messages": [
{
"type": "error",
"code": "capabilities_incompatible",
"content": "The platform and this business share no version of the capability this operation needs",
"severity": "unrecoverable"
}
],
"continue_url": "https://shop.example.com/"
}201A new checkout session.
Response headers
Cache-ControlstringEvery UCP answer (success or error) except the business profile.
always:no-storeETagstringAn ETag of the body, generated by the API on every response with a body (writes included). The API’s ETag is strong; the web server sends it weak (
W/"…") when it compresses the response, which it does for a client that sendsAccept-Encoding: gzip. AGETthat sends a matchingIf-None-Match(either form) can be answered304 Not Modifiedeven though the answer isno-store.X-RateLimit-LimitintegerRequests this client IP may make to this operation per 60-second window (600 reads, 300 writes, 60 handoff). Set on every answer that passed the rate limiter.
X-RateLimit-RemainingintegerThe operation’s limit minus this client IP’s current count. The counts live in the API server’s memory; each request is meant to leave the count 60 s after it was made, but the rate limiter (
@nestjs/throttler6.5) drops those pending decrements, for every client, whenever any client’s block ends. Read it as an upper bound and keep steady traffic at half a limit or less.minimum: 0X-RateLimit-ResetintegerSeconds left in a 60-second window the limiter keeps per client IP and operation. The count is not reset when that window ends, so this is not the moment the budget comes back.
minimum: 0
SchemaCheckout
{
"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",
"status": "incomplete",
"currency": "EUR",
"line_items": [
{
"id": "f6a3c8e1-9d27-4b54-8e6a-2c1f7b9d3a58",
"item": {
"id": "1e8a6c4f-2b93-4d71-9f05-6a3c8e2b1d47",
"title": "Trek Marlin 5",
"price": 69900,
"image_url": "https://shop.example.com/api/public/uploads/c4d1e5a2-8b3f-4a6c-9e07-5f2d8a1b3c40/products/trek-marlin-5-modra.webp"
},
"quantity": 1,
"totals": [
{
"type": "subtotal",
"amount": 69900
},
{
"type": "total",
"amount": 69900
}
]
},
{
"id": "2e9d5b7a-4c83-4f16-a9b2-6d8e1c3f7a49",
"item": {
"id": "b5f81c3d-6e49-4a2b-8d73-9c0e4f1a6b25",
"title": "Fľaša na vodu 750 ml",
"price": 990
},
"quantity": 2,
"totals": [
{
"type": "subtotal",
"amount": 1980
},
{
"type": "total",
"amount": 1980
}
]
}
],
"totals": [
{
"type": "subtotal",
"amount": 71880
},
{
"type": "discount",
"amount": -5844,
"display_text": "JESEN10"
},
{
"type": "total",
"amount": 66036
}
],
"messages": [
{
"type": "warning",
"code": "discount_code_combination_disallowed",
"path": "$.discounts.codes[1]",
"content": "Code 'LETO5' cannot be combined with 'JESEN10'"
},
{
"type": "error",
"code": "missing",
"path": "$.fulfillment.methods[0].destinations",
"content": "A shipping address is required",
"severity": "recoverable"
},
{
"type": "error",
"code": "missing",
"path": "$.fulfillment.methods[0].groups[0].selected_option_id",
"content": "Choose a shipping option",
"severity": "recoverable"
}
],
"links": [
{
"type": "terms_of_service",
"url": "https://shop.example.com/obchodne-podmienky",
"title": "Obchodné podmienky"
},
{
"type": "privacy_policy",
"url": "https://shop.example.com/ochrana-osobnych-udajov",
"title": "Ochrana osobných údajov"
}
],
"buyer": {
"email": "jana.novakova@example.com"
},
"context": {
"language": "sk",
"address_country": "SK"
},
"fulfillment": {
"methods": [
{
"id": "shipping",
"type": "shipping",
"line_item_ids": [
"f6a3c8e1-9d27-4b54-8e6a-2c1f7b9d3a58",
"2e9d5b7a-4c83-4f16-a9b2-6d8e1c3f7a49"
],
"groups": [
{
"id": "default",
"line_item_ids": [
"f6a3c8e1-9d27-4b54-8e6a-2c1f7b9d3a58",
"2e9d5b7a-4c83-4f16-a9b2-6d8e1c3f7a49"
],
"options": [
{
"id": "0c5e8a3f-7b21-4d96-8f4c-9e2a6d1b3c75",
"title": "GLS kuriér",
"description": "Doručenie na adresu do 1–2 pracovných dní",
"totals": [
{
"type": "total",
"amount": 490
}
]
},
{
"id": "6b2d9f4e-3a17-4c68-9d5b-1f8e3a7c2b40",
"title": "Packeta – výdajné miesto",
"description": "Delivery in 2–3 business days",
"totals": [
{
"type": "total",
"amount": 290
}
]
}
],
"selected_option_id": null
}
]
}
]
},
"discounts": {
"codes": [
"JESEN10",
"LETO5"
],
"applied": [
{
"code": "JESEN10",
"title": "JESEN10",
"amount": 5844,
"method": "across"
}
]
},
"continue_url": "https://shop.example.com/ucp/continue/9bbd3dedf887f93c6287fa7f4b8de141501155714c3b2050bb83551e36ec780b",
"expires_at": "2026-10-27T09:18:12.930Z"
}{
"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": "b7d2e9a4-1c56-4e83-9f2b-6a4d8c1e3f75",
"status": "incomplete",
"currency": "EUR",
"line_items": [
{
"id": "9c4f7a2e-6d18-4b39-a5c1-8e3f2d6b9a07",
"item": {
"id": "4a6e2d9c-8b15-4f37-9c48-1e7a5b3d2f90",
"title": "Cyklistická prilba Lumen",
"price": 5990
},
"quantity": 1,
"totals": [
{
"type": "subtotal",
"amount": 5990
},
{
"type": "total",
"amount": 5990
}
]
}
],
"totals": [
{
"type": "subtotal",
"amount": 5990
},
{
"type": "total",
"amount": 5990
}
],
"messages": [
{
"type": "error",
"code": "missing",
"path": "$.fulfillment.methods[0].groups[0].selected_option_id",
"content": "Choose a shipping option",
"severity": "recoverable"
}
],
"links": [
{
"type": "terms_of_service",
"url": "https://shop.example.com/obchodne-podmienky",
"title": "Obchodné podmienky"
},
{
"type": "privacy_policy",
"url": "https://shop.example.com/ochrana-osobnych-udajov",
"title": "Ochrana osobných údajov"
}
],
"buyer": {
"first_name": "Peter",
"last_name": "Horváth",
"email": "peter.horvath@example.com"
},
"context": {
"address_country": "SK",
"language": "sk"
},
"fulfillment": {
"methods": [
{
"id": "shipping",
"type": "shipping",
"line_item_ids": [
"9c4f7a2e-6d18-4b39-a5c1-8e3f2d6b9a07"
],
"destinations": [
{
"id": "destination",
"street_address": "Námestie SNP 5",
"address_locality": "Banská Bystrica",
"postal_code": "974 01",
"address_country": "SK",
"first_name": "Peter",
"last_name": "Horváth"
}
],
"selected_destination_id": "destination",
"groups": [
{
"id": "default",
"line_item_ids": [
"9c4f7a2e-6d18-4b39-a5c1-8e3f2d6b9a07"
],
"options": [
{
"id": "0c5e8a3f-7b21-4d96-8f4c-9e2a6d1b3c75",
"title": "GLS kuriér",
"description": "Doručenie na adresu do 1–2 pracovných dní",
"totals": [
{
"type": "total",
"amount": 490
}
]
},
{
"id": "6b2d9f4e-3a17-4c68-9d5b-1f8e3a7c2b40",
"title": "Packeta – výdajné miesto",
"description": "Delivery in 2–3 business days",
"totals": [
{
"type": "total",
"amount": 290
}
]
}
],
"selected_option_id": null
}
]
}
]
},
"discounts": {
"codes": [],
"applied": []
},
"continue_url": "https://shop.example.com/ucp/continue/44e9cdc7dee1005696a98525ce10c6090ab8a7eb3654a490c538c5caf3989e39",
"expires_at": "2026-10-27T10:02:44.207Z"
}400Bad Request (Error): invalid_profile_url — the UCP-Agent header is missing or unusable (with continue_url); invalid_request — a field this operation reads has …
Bad Request (Error):
invalid_profile_url— theUCP-Agentheader is missing or unusable (withcontinue_url);invalid_request— a field this operation reads has the wrong JSON type or value;contentnames it. Malformed JSON or a top-level JSON primitive says only "The request is not valid". Unknown fields are ignored, never refused. For this operation: as forcreateCart(without a usablecart_id,line_itemsis required), plusfulfillment must be an object,fulfillment.methods must be an array,fulfillment.methods[i] must be an object,fulfillment.methods[].destinations must be an array,fulfillment.methods[].destinations[i] must be an object,fulfillment.methods[].groups[0] must be an object,fulfillment.methods[].destinations[i].address_country is not a recognised country; send an ISO 3166-1 alpha-2 code such as "SK". Nothing is stored.invalid_request"Idempotency-Key is not valid" — seeIdempotency-Key.
Response headers
Cache-ControlstringEvery UCP answer (success or error) except the business profile.
always:no-store
SchemaError
{
"code": "invalid_profile_url",
"content": "The UCP-Agent header must carry profile=\"https://…\"",
"continue_url": "https://shop.example.com/"
}{
"code": "invalid_request",
"content": "line_items[0].quantity must be a positive integer"
}{
"code": "invalid_request",
"content": "fulfillment.methods[].destinations[0].address_country is not a recognised country; send an ISO 3166-1 alpha-2 code such as \"SK\""
}{
"code": "invalid_request",
"content": "Idempotency-Key is not valid"
}{
"code": "invalid_request",
"content": "The request is not valid"
}Request with curl
curl -X POST 'https://shop.example.com/ucp/v1/checkout-sessions' \
-H 'UCP-Agent: profile="https://platform.example.com/.well-known/ucp"' \
-H 'Idempotency-Key: 0c2f8a4e-6b1d-4f37-9a85-3e7d1c9b5f02' \
-H 'Accept-Language: sk-SK,sk;q=0.9,en;q=0.8' \
-H 'Content-Type: application/json' \
--data '{
"cart_id": "9a3f6c2e-5b18-4d47-8e9c-1f2a7b4d6e83"
}'Get a checkout session
GET/ucp/v1/checkout-sessions/{id}
The checkout as it is now. Poll this to learn the outcome after giving the buyer continue_url: stop at status: "completed" (read order) or "canceled".
- An open checkout is read again from its backing cart: its status is re-derived (
incomplete⇄requires_escalation), the shipping options are quoted again, and the answer is stored. So a GET waits for writes on the same session and can answer 503 withRetry-After: 1. Keep one request per session in flight; poll every few seconds, not faster. - A read leaves the lines and the codes alone: no stock check, no code applied again, and each line keeps its unit price — unless the storefront has Live prices on cart load switched on (not visible through UCP), which prices the lines again on every read and write.
- A GET does not extend
expires_at. The first request on an open checkout at or afterexpires_at(a read, a write, or a conversion of its cart) turns itcanceledfor good; an order placed before that request still completes it. - Line and discount messages are not repeated on a read.
- A completed or canceled checkout returns the last answer it gave, with the current
status,messages: [], nocontinue_urland, once completed,order. An open one the buyer already took to the shop returns its last answer as it was (messages included: the line and discount messages of the write that produced it come back on every read — do not report them again). Neither is re-priced. A GET cannot tell whether the buyer opened the link. - An unknown id is a 200 business error
not_found.
Parameters
idpathstringrequiredThe checkout session’s
id(a UUID, any letter case). An id that is not a UUID or belongs to another shop is answered with the 200not_foundbusiness error, not a 4xx. Completed and canceled checkouts stay readable until 30 days afterexpires_at.pattern: ^[0-9a-fA-F]{8}-[0-9a-fA-F]{4}-[0-9a-fA-F]{4}-[0-9a-fA-F]{4}-[0-9a-fA-F]{12}$example:3d7b9e2a-6f14-4c58-9b3e-8a2f1c6d4e97UCP-AgentheaderstringrequiredNames your platform profile. Required on every
/ucp/v1/*operation; never read by the profile and handoff routes.Details
Grammar — a subset of an RFC 8941 Dictionary, at most 4096 characters:
- members separated by
,(spaces or tabs around it allowed; a trailing comma is an error); keys lower-case, matching[a-z*][a-z0-9_\-.*]*; - the
profilemember must be an sf-string:profile="https://…", printable ASCII 0x20–0x7E, escapes\"and\\only; - other members (tokens, numbers, booleans, strings) and parameters (
;key=value) are parsed and ignored; whenprofileappears twice the last one wins; - any parse error makes the whole header unusable.
Accepted:
profile="https://platform.example.com/.well-known/ucp",version="2026-04-08", profile="https://platform.example.com/.well-known/ucp";sig=abc. Refused: a missing header,profile=https://…(a token, not a string),Profile="…",a=1 b=2, profile="…".The URL — at most 1000 characters, no whitespace anywhere, starting
https://, no user name or password, a non-empty host. Any port, path and query string are accepted (the spec’s/.well-known/ucppath is not enforced). Failing any rule →400 invalid_profile_url.The fetch —
GETwithAccept: application/jsonandUser-Agent: Monokaido-UCP/1.0 (+https://monokaido.com); no redirects followed; only HTTP 200 accepted; at most 256 KiB; 3 s idle, 5 s total; the host must resolve to a public address (private, loopback, link-local and other reserved ranges are refused). Anything else →424 profile_unreachable. A body that is not JSON, or whoseucp.versionis not aYYYY-MM-DDstring →422 profile_malformed; a valid profile of another version →422 version_unsupported. Onlyucp.versionanducp.capabilities(name →[{"version"}], first 20 entries per name, names likedev.ucp.shopping.cart) are read; everything else in it is ignored.Caching — a fetched profile is cached for its
Cache-Control: max-age, clamped to 60 s – 1 h (5 min without one;no-store,no-cacheandprivateare ignored) and served up to 1 h past that while one background refresh runs. A failed fetch (424, or422 profile_malformed) is remembered for 60 s. A profile of anotherucp.versionis a valid profile and is cached like one, so after you change its version expect422 version_unsupporteduntil that entry is refreshed (itsmax-age, then one more request) — or use a new profile URL. These caches are kept in memory by each API process. Fetching a profile the shop does not know yet is budgeted: 5 new profile URLs per minute per client (IPv4 address, IPv6 /64) and 60 per minute in all →503 service_unavailablewithRetry-After. The full URL string is the cache key, so keep it stable (no per-request query strings).minLength: 1maxLength: 4096example:profile="https://platform.example.com/.well-known/ucp"- members separated by
Accept-LanguageheaderstringFallback for the answer’s language after
context.languagein the body. The first 10 comma-separated tags are tried in header order (q=weights are ignored,*is skipped) against the languages the shop offers: an exact tag (case-insensitive), then the primary subtag (en-US→en-GB); with no match, the shop’s first language. (A storefront without a language list of its own offers every language of the company: then the primary subtag of the first usable tag is used as it is — a language the company has not set up is answered in its default currency, untranslated — and, with no tag at all, the company’s default language.)The language decides the currency of every price and the translations. A cart or checkout keeps the language it was created in: a later request changes it only when its body names a
context.languagedifferent from the stored one, and when that switches the shop language every line is re-added in the new currency and gets a new line id. Once a session has a language, this header alone never switches it. On a storefront without a language list, a session created with no usable tag stores none: each later request is then answered in the language its own tags pick, and a different pick adds the lines again (new line ids). Sendcontext.languageon the create.example:sk-SK,sk;q=0.9,en;q=0.8
Responses
200The checkout (any status), or a business error: not_found, capabilities_incompatible.
Response headers
Cache-ControlstringEvery UCP answer (success or error) except the business profile.
always:no-storeETagstringAn ETag of the body, generated by the API on every response with a body (writes included). The API’s ETag is strong; the web server sends it weak (
W/"…") when it compresses the response, which it does for a client that sendsAccept-Encoding: gzip. AGETthat sends a matchingIf-None-Match(either form) can be answered304 Not Modifiedeven though the answer isno-store.X-RateLimit-LimitintegerRequests this client IP may make to this operation per 60-second window (600 reads, 300 writes, 60 handoff). Set on every answer that passed the rate limiter.
X-RateLimit-RemainingintegerThe operation’s limit minus this client IP’s current count. The counts live in the API server’s memory; each request is meant to leave the count 60 s after it was made, but the rate limiter (
@nestjs/throttler6.5) drops those pending decrements, for every client, whenever any client’s block ends. Read it as an upper bound and keep steady traffic at half a limit or less.minimum: 0X-RateLimit-ResetintegerSeconds left in a 60-second window the limiter keeps per client IP and operation. The count is not reset when that window ends, so this is not the moment the budget comes back.
minimum: 0
Exactly one of
- Checkoutobject · Checkout
A checkout session.
Once
completedorcanceled— and while the buyer’s handoff is done but the order not yet placed — the body is the last answer the platform received, with the currentstatus(andorder): it is not re-priced, and it does not show what the buyer changed on the shop. Only the lines and the applied discount code travel to the shop; the buyer enters address, shipping and payment there again.additionalProperties: falseThe
ucpobject of every checkout answer that returns a checkout (any status).idstring (uuid)requiredThe checkout session id.
incomplete— at least onerecoverableerror is listed (no line, no valid e-mail, no or incomplete address, no shipping to it, no or an unoffered option, a line error in this answer).requires_escalation— nothing the platform can provide is missing;payment_requiredis listed and the buyer must continue on the shop throughcontinue_url.completed— the buyer placed an order on the shop from the cart the handoff filled; seeorder. It means placed, not paid. Set shortly after the order (once the shop has started the payment and handed the order to its ERP) and best effort: a failure is only logged, so an order can exist for a checkout that never readscompleted.canceled— canceled byPOST …/cancel, or expired (an open checkout read or written afterexpires_atturnscanceled).
ready_for_completeandcomplete_in_progressare never used: the API never completes a checkout.one of:incompleterequires_escalationcompletedcanceledISO 4217 currency code.
pattern: ^[A-Z]{3}$In the shop cart’s order.
subtotal,discount?,fulfillment?,total.minItems: 2maxItems: 4Always present, possibly empty. Order: line messages and discount messages (only in the answer to the write that caused them), then
missing/invalid_address/address_undeliverable/invalid_option,pickup_point_required, andpayment_requiredlast. Empty oncompletedandcanceledreads.Always present: at most 10, possibly empty.
maxItems: 10buyerBuyerThe stored buyer; empty fields are left out. Absent when nothing is stored.
contextContextThe stored context, echoed as you sent it (after cleaning); empty fields are left out. Absent when nothing is stored.
fulfillmentFulfillmentPresent only when
dev.ucp.shopping.fulfillmentis negotiated. Oneshippingmethod with one group; there is nopickupmethod — a pickup point (Packeta) is chosen by the buyer on the shop.discountsDiscountsPresent only when
dev.ucp.shopping.discountis negotiated. Thediscounttotal is present whether or not it is.continue_urlstring (uri) · ContinueUrlPresent while the checkout is
incompleteorrequires_escalation; absent oncecompletedorcanceled.pattern: ^https://[^/]+/ucp/continue/[0-9a-f]{64}$When the session expires (UTC, ISO 8601 with milliseconds). A write extends it (cart: now + 7 days, checkout: now + 24 h); reads,
completeandcanceldo not. After a handoff nothing extends it.orderOrderPresent once
completed.
- UcpBusinessErrorResponseobject · UcpBusinessErrorResponse
HTTP 200 with no resource. The cases:
capabilities_incompatible— your profile and this shop share no version of the operation’s root capability (always for carts and checkouts on a shop in catalog mode). Nothing ran; no Idempotency-Key was used.not_found— the product, cart (Cart not found or has expired) or checkout (Checkout session not found) does not exist for this shop, is malformed, or (carts) was canceled or expired. An unknowncart_idonPOST /checkout-sessionsalso readsCheckout session not found.out_of_stock/item_unavailable— a create in which none of the requested items could be added; no session was created.handed_off— converting a cart whose lines the buyer already took to the shop.
Inside a write the last three are stored under the Idempotency-Key like a success (
capabilities_incompatibleis answered before the key is read).additionalProperties: falseThe
ucpobject of a business error. Never haspayment_handlers.capabilitiesis{}forcapabilities_incompatibleand lists the operation’s root (and its negotiated extensions) otherwise.Exactly one error message saying why no resource could be returned.
minItems: 1maxItems: 1continue_urlstring (uri)requiredWhere a buyer could go instead: the shop home (
https://{primary domain}/), orhttps://{primary domain}/cartforhanded_off. Not a handoff link.
{
"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",
"status": "requires_escalation",
"currency": "EUR",
"line_items": [
{
"id": "f6a3c8e1-9d27-4b54-8e6a-2c1f7b9d3a58",
"item": {
"id": "1e8a6c4f-2b93-4d71-9f05-6a3c8e2b1d47",
"title": "Trek Marlin 5",
"price": 69900,
"image_url": "https://shop.example.com/api/public/uploads/c4d1e5a2-8b3f-4a6c-9e07-5f2d8a1b3c40/products/trek-marlin-5-modra.webp"
},
"quantity": 1,
"totals": [
{
"type": "subtotal",
"amount": 69900
},
{
"type": "total",
"amount": 69900
}
]
},
{
"id": "2e9d5b7a-4c83-4f16-a9b2-6d8e1c3f7a49",
"item": {
"id": "b5f81c3d-6e49-4a2b-8d73-9c0e4f1a6b25",
"title": "Fľaša na vodu 750 ml",
"price": 990
},
"quantity": 2,
"totals": [
{
"type": "subtotal",
"amount": 1980
},
{
"type": "total",
"amount": 1980
}
]
}
],
"totals": [
{
"type": "subtotal",
"amount": 71880
},
{
"type": "discount",
"amount": -5844,
"display_text": "JESEN10"
},
{
"type": "fulfillment",
"amount": 490
},
{
"type": "total",
"amount": 66526
}
],
"messages": [
{
"type": "error",
"code": "payment_required",
"content": "Payment is completed on the shop website — continue there to place the order",
"severity": "requires_buyer_input"
}
],
"links": [
{
"type": "terms_of_service",
"url": "https://shop.example.com/obchodne-podmienky",
"title": "Obchodné podmienky"
},
{
"type": "privacy_policy",
"url": "https://shop.example.com/ochrana-osobnych-udajov",
"title": "Ochrana osobných údajov"
}
],
"buyer": {
"first_name": "Jana",
"last_name": "Nováková",
"email": "jana.novakova@example.com",
"phone_number": "+421 905 123 456"
},
"context": {
"language": "sk",
"address_country": "SK"
},
"fulfillment": {
"methods": [
{
"id": "shipping",
"type": "shipping",
"line_item_ids": [
"f6a3c8e1-9d27-4b54-8e6a-2c1f7b9d3a58",
"2e9d5b7a-4c83-4f16-a9b2-6d8e1c3f7a49"
],
"destinations": [
{
"id": "destination",
"street_address": "Hlavná 12",
"address_locality": "Bratislava",
"postal_code": "811 01",
"address_country": "SK",
"first_name": "Jana",
"last_name": "Nováková"
}
],
"selected_destination_id": "destination",
"groups": [
{
"id": "default",
"line_item_ids": [
"f6a3c8e1-9d27-4b54-8e6a-2c1f7b9d3a58",
"2e9d5b7a-4c83-4f16-a9b2-6d8e1c3f7a49"
],
"options": [
{
"id": "0c5e8a3f-7b21-4d96-8f4c-9e2a6d1b3c75",
"title": "GLS kuriér",
"description": "Doručenie na adresu do 1–2 pracovných dní",
"totals": [
{
"type": "total",
"amount": 490
}
]
},
{
"id": "6b2d9f4e-3a17-4c68-9d5b-1f8e3a7c2b40",
"title": "Packeta – výdajné miesto",
"description": "Delivery in 2–3 business days",
"totals": [
{
"type": "total",
"amount": 290
}
]
}
],
"selected_option_id": "0c5e8a3f-7b21-4d96-8f4c-9e2a6d1b3c75"
}
]
}
]
},
"discounts": {
"codes": [
"JESEN10"
],
"applied": [
{
"code": "JESEN10",
"title": "JESEN10",
"amount": 5844,
"method": "across"
}
]
},
"continue_url": "https://shop.example.com/ucp/continue/9bbd3dedf887f93c6287fa7f4b8de141501155714c3b2050bb83551e36ec780b",
"expires_at": "2026-10-27T09:21:30.551Z"
}{
"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",
"status": "incomplete",
"currency": "EUR",
"line_items": [
{
"id": "f6a3c8e1-9d27-4b54-8e6a-2c1f7b9d3a58",
"item": {
"id": "1e8a6c4f-2b93-4d71-9f05-6a3c8e2b1d47",
"title": "Trek Marlin 5",
"price": 69900,
"image_url": "https://shop.example.com/api/public/uploads/c4d1e5a2-8b3f-4a6c-9e07-5f2d8a1b3c40/products/trek-marlin-5-modra.webp"
},
"quantity": 1,
"totals": [
{
"type": "subtotal",
"amount": 69900
},
{
"type": "total",
"amount": 69900
}
]
},
{
"id": "2e9d5b7a-4c83-4f16-a9b2-6d8e1c3f7a49",
"item": {
"id": "b5f81c3d-6e49-4a2b-8d73-9c0e4f1a6b25",
"title": "Fľaša na vodu 750 ml",
"price": 990
},
"quantity": 2,
"totals": [
{
"type": "subtotal",
"amount": 1980
},
{
"type": "total",
"amount": 1980
}
]
}
],
"totals": [
{
"type": "subtotal",
"amount": 71880
},
{
"type": "discount",
"amount": -5844,
"display_text": "JESEN10"
},
{
"type": "total",
"amount": 66036
}
],
"messages": [
{
"type": "error",
"code": "missing",
"path": "$.fulfillment.methods[0].destinations",
"content": "A shipping address is required",
"severity": "recoverable"
},
{
"type": "error",
"code": "missing",
"path": "$.fulfillment.methods[0].groups[0].selected_option_id",
"content": "Choose a shipping option",
"severity": "recoverable"
}
],
"links": [
{
"type": "terms_of_service",
"url": "https://shop.example.com/obchodne-podmienky",
"title": "Obchodné podmienky"
},
{
"type": "privacy_policy",
"url": "https://shop.example.com/ochrana-osobnych-udajov",
"title": "Ochrana osobných údajov"
}
],
"buyer": {
"email": "jana.novakova@example.com"
},
"context": {
"language": "sk",
"address_country": "SK"
},
"fulfillment": {
"methods": [
{
"id": "shipping",
"type": "shipping",
"line_item_ids": [
"f6a3c8e1-9d27-4b54-8e6a-2c1f7b9d3a58",
"2e9d5b7a-4c83-4f16-a9b2-6d8e1c3f7a49"
],
"groups": [
{
"id": "default",
"line_item_ids": [
"f6a3c8e1-9d27-4b54-8e6a-2c1f7b9d3a58",
"2e9d5b7a-4c83-4f16-a9b2-6d8e1c3f7a49"
],
"options": [
{
"id": "0c5e8a3f-7b21-4d96-8f4c-9e2a6d1b3c75",
"title": "GLS kuriér",
"description": "Doručenie na adresu do 1–2 pracovných dní",
"totals": [
{
"type": "total",
"amount": 490
}
]
},
{
"id": "6b2d9f4e-3a17-4c68-9d5b-1f8e3a7c2b40",
"title": "Packeta – výdajné miesto",
"description": "Delivery in 2–3 business days",
"totals": [
{
"type": "total",
"amount": 290
}
]
}
],
"selected_option_id": null
}
]
}
]
},
"discounts": {
"codes": [
"JESEN10",
"LETO5"
],
"applied": [
{
"code": "JESEN10",
"title": "JESEN10",
"amount": 5844,
"method": "across"
}
]
},
"continue_url": "https://shop.example.com/ucp/continue/9bbd3dedf887f93c6287fa7f4b8de141501155714c3b2050bb83551e36ec780b",
"expires_at": "2026-10-27T09:18:12.930Z"
}{
"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": "completed",
"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": [],
"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"
}
]
},
"order": {
"id": "8e1c4f7a-2d95-4b36-a7e8-5c3f9b1d6a24",
"label": "SK2600417",
"permalink_url": "https://shop.example.com/order-confirmation?orderNumber=SK2600417&orderId=8e1c4f7a-2d95-4b36-a7e8-5c3f9b1d6a24"
}
}{
"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",
"status": "canceled",
"currency": "EUR",
"line_items": [
{
"id": "f6a3c8e1-9d27-4b54-8e6a-2c1f7b9d3a58",
"item": {
"id": "1e8a6c4f-2b93-4d71-9f05-6a3c8e2b1d47",
"title": "Trek Marlin 5",
"price": 69900,
"image_url": "https://shop.example.com/api/public/uploads/c4d1e5a2-8b3f-4a6c-9e07-5f2d8a1b3c40/products/trek-marlin-5-modra.webp"
},
"quantity": 1,
"totals": [
{
"type": "subtotal",
"amount": 69900
},
{
"type": "total",
"amount": 69900
}
]
},
{
"id": "2e9d5b7a-4c83-4f16-a9b2-6d8e1c3f7a49",
"item": {
"id": "b5f81c3d-6e49-4a2b-8d73-9c0e4f1a6b25",
"title": "Fľaša na vodu 750 ml",
"price": 990
},
"quantity": 2,
"totals": [
{
"type": "subtotal",
"amount": 1980
},
{
"type": "total",
"amount": 1980
}
]
}
],
"totals": [
{
"type": "subtotal",
"amount": 71880
},
{
"type": "discount",
"amount": -5844,
"display_text": "JESEN10"
},
{
"type": "fulfillment",
"amount": 490
},
{
"type": "total",
"amount": 66526
}
],
"messages": [],
"links": [
{
"type": "terms_of_service",
"url": "https://shop.example.com/obchodne-podmienky",
"title": "Obchodné podmienky"
},
{
"type": "privacy_policy",
"url": "https://shop.example.com/ochrana-osobnych-udajov",
"title": "Ochrana osobných údajov"
}
],
"buyer": {
"email": "jana.novakova@example.com",
"last_name": "Nováková",
"first_name": "Jana",
"phone_number": "+421 905 123 456"
},
"context": {
"language": "sk",
"address_country": "SK"
},
"fulfillment": {
"methods": [
{
"id": "shipping",
"type": "shipping",
"line_item_ids": [
"f6a3c8e1-9d27-4b54-8e6a-2c1f7b9d3a58",
"2e9d5b7a-4c83-4f16-a9b2-6d8e1c3f7a49"
],
"destinations": [
{
"id": "destination",
"last_name": "Nováková",
"first_name": "Jana",
"postal_code": "811 01",
"street_address": "Hlavná 12",
"address_country": "SK",
"address_locality": "Bratislava"
}
],
"selected_destination_id": "destination",
"groups": [
{
"id": "default",
"line_item_ids": [
"f6a3c8e1-9d27-4b54-8e6a-2c1f7b9d3a58",
"2e9d5b7a-4c83-4f16-a9b2-6d8e1c3f7a49"
],
"options": [
{
"id": "0c5e8a3f-7b21-4d96-8f4c-9e2a6d1b3c75",
"title": "GLS kuriér",
"description": "Doručenie na adresu do 1–2 pracovných dní",
"totals": [
{
"type": "total",
"amount": 490
}
]
},
{
"id": "6b2d9f4e-3a17-4c68-9d5b-1f8e3a7c2b40",
"title": "Packeta – výdajné miesto",
"description": "Delivery in 2–3 business days",
"totals": [
{
"type": "total",
"amount": 290
}
]
}
],
"selected_option_id": "0c5e8a3f-7b21-4d96-8f4c-9e2a6d1b3c75"
}
]
}
]
},
"discounts": {
"codes": [
"JESEN10"
],
"applied": [
{
"code": "JESEN10",
"title": "JESEN10",
"amount": 5844,
"method": "across"
}
]
},
"expires_at": "2026-10-27T09:21:30.551Z"
}{
"ucp": {
"version": "2026-04-08",
"status": "error",
"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"
}
]
}
},
"messages": [
{
"type": "error",
"code": "not_found",
"content": "Checkout session not found",
"severity": "unrecoverable"
}
],
"continue_url": "https://shop.example.com/"
}{
"ucp": {
"version": "2026-04-08",
"status": "error",
"capabilities": {}
},
"messages": [
{
"type": "error",
"code": "capabilities_incompatible",
"content": "The platform and this business share no version of the capability this operation needs",
"severity": "unrecoverable"
}
],
"continue_url": "https://shop.example.com/"
}400Bad Request (Error): invalid_profile_url — the UCP-Agent header is missing or unusable (with continue_url);
Bad Request (Error):
invalid_profile_url— theUCP-Agentheader is missing or unusable (withcontinue_url);
Response headers
Cache-ControlstringEvery UCP answer (success or error) except the business profile.
always:no-store
SchemaError
{
"code": "invalid_profile_url",
"content": "The UCP-Agent header must carry profile=\"https://…\"",
"continue_url": "https://shop.example.com/"
}Request with curl
curl 'https://shop.example.com/ucp/v1/checkout-sessions/3d7b9e2a-6f14-4c58-9b3e-8a2f1c6d4e97' \
-H 'UCP-Agent: profile="https://platform.example.com/.well-known/ucp"' \
-H 'Accept-Language: sk-SK,sk;q=0.9,en;q=0.8'Update a checkout session
PUT/ucp/v1/checkout-sessions/{id}
Sets the buyer, the shipping destination and option, discount codes or the lines; fields you leave out keep their value. Answers the checkout with its status re-derived, the shipping options quoted again and, with the discount extension, the codes applied again.
line_items(optional) is a full replacement, checked like a create (stock, availability, the quantity caps); without it the lines are not checked again.buyerreplaces the whole stored buyer. A destination can be replaced but not removed.selected_option_id: nullclears the shipping choice.- A line keeps the unit price it got when its variant entered the session, also when its quantity changes. A new
context.languagere-adds every line at that language’s current prices. A storefront with Live prices on cart load on (not visible through UCP) prices the lines again on every read and write; elsewhere a checkout kept open by updates keeps its old prices — leave a line out of one update and send it again in the next to price it anew. - Every update of an open checkout extends
expires_atto now + 24 h — also{}. - On a completed or canceled checkout nothing changes: it comes back with
checkout_completed/checkout_canceled. - After the buyer opened
continue_url(but before the order), nothing changes: the last answer comes back with ahanded_offwarning. - On an unknown, closed or handed-over checkout the body’s fields are not read (only a body that is not an object is refused): such a 200 says nothing about whether the body was valid.
Parameters
idpathstringrequiredThe checkout session’s
id(a UUID, any letter case). An id that is not a UUID or belongs to another shop is answered with the 200not_foundbusiness error, not a 4xx. Completed and canceled checkouts stay readable until 30 days afterexpires_at.pattern: ^[0-9a-fA-F]{8}-[0-9a-fA-F]{4}-[0-9a-fA-F]{4}-[0-9a-fA-F]{4}-[0-9a-fA-F]{12}$example:3d7b9e2a-6f14-4c58-9b3e-8a2f1c6d4e97UCP-AgentheaderstringrequiredNames your platform profile. Required on every
/ucp/v1/*operation; never read by the profile and handoff routes.Details
Grammar — a subset of an RFC 8941 Dictionary, at most 4096 characters:
- members separated by
,(spaces or tabs around it allowed; a trailing comma is an error); keys lower-case, matching[a-z*][a-z0-9_\-.*]*; - the
profilemember must be an sf-string:profile="https://…", printable ASCII 0x20–0x7E, escapes\"and\\only; - other members (tokens, numbers, booleans, strings) and parameters (
;key=value) are parsed and ignored; whenprofileappears twice the last one wins; - any parse error makes the whole header unusable.
Accepted:
profile="https://platform.example.com/.well-known/ucp",version="2026-04-08", profile="https://platform.example.com/.well-known/ucp";sig=abc. Refused: a missing header,profile=https://…(a token, not a string),Profile="…",a=1 b=2, profile="…".The URL — at most 1000 characters, no whitespace anywhere, starting
https://, no user name or password, a non-empty host. Any port, path and query string are accepted (the spec’s/.well-known/ucppath is not enforced). Failing any rule →400 invalid_profile_url.The fetch —
GETwithAccept: application/jsonandUser-Agent: Monokaido-UCP/1.0 (+https://monokaido.com); no redirects followed; only HTTP 200 accepted; at most 256 KiB; 3 s idle, 5 s total; the host must resolve to a public address (private, loopback, link-local and other reserved ranges are refused). Anything else →424 profile_unreachable. A body that is not JSON, or whoseucp.versionis not aYYYY-MM-DDstring →422 profile_malformed; a valid profile of another version →422 version_unsupported. Onlyucp.versionanducp.capabilities(name →[{"version"}], first 20 entries per name, names likedev.ucp.shopping.cart) are read; everything else in it is ignored.Caching — a fetched profile is cached for its
Cache-Control: max-age, clamped to 60 s – 1 h (5 min without one;no-store,no-cacheandprivateare ignored) and served up to 1 h past that while one background refresh runs. A failed fetch (424, or422 profile_malformed) is remembered for 60 s. A profile of anotherucp.versionis a valid profile and is cached like one, so after you change its version expect422 version_unsupporteduntil that entry is refreshed (itsmax-age, then one more request) — or use a new profile URL. These caches are kept in memory by each API process. Fetching a profile the shop does not know yet is budgeted: 5 new profile URLs per minute per client (IPv4 address, IPv6 /64) and 60 per minute in all →503 service_unavailablewithRetry-After. The full URL string is the cache key, so keep it stable (no per-request query strings).minLength: 1maxLength: 4096example:profile="https://platform.example.com/.well-known/ucp"- members separated by
Idempotency-KeyheaderstringOptional, strongly recommended. Without it (or with an empty value) the write simply runs: a retried create makes a second session.
- Format: 1–255 printable ASCII characters 0x21–0x7E — no spaces. Anything else (also a header sent twice, which arrives joined with
", ") →400 invalid_request"Idempotency-Key is not valid". A UUID v4 per logical write is a good choice. - Scope: this shop (all its domains) × the exact
UCP-Agentprofile URL string × the key. - Request identity: the operation, the path
{id}as sent (after URL decoding; case included) and the JSON body compared canonically (object key order and whitespace do not matter, array order does). Cancel routes hash{}whatever the body. - Same key, same request, finished → the stored HTTP status and body are replayed — the same values, including a 200 business error, the original
expires_atand theucpobject negotiated the first time, even when the session has changed since. The replay comes from storage, so its object keys come in JSONB order (and itsETagdiffers). - The
UCP-Agentprofile is resolved and negotiated before the key is read: a profile error or a 200capabilities_incompatibleanswer is never stored. - Same key, different request →
409 idempotency_conflict. Same request still running →409withRetry-After: 2. - If the first request fails with a protocol error (
400,503session busy,500…) the key is released: retrying with the same key runs the operation again. - A claim whose request died is taken over by the same request after 120 s. Keys are kept for at least 25 hours after their first use (an hourly job deletes older ones); do not rely on a key more than 24 hours after its first use.
- Not read by catalog operations,
GETroutes, the profile or the handoff routes.
pattern: ^[\x21-\x7E]{1,255}$minLength: 1maxLength: 255example:0c2f8a4e-6b1d-4f37-9a85-3e7d1c9b5f02- Format: 1–255 printable ASCII characters 0x21–0x7E — no spaces. Anything else (also a header sent twice, which arrives joined with
Accept-LanguageheaderstringFallback for the answer’s language after
context.languagein the body. The first 10 comma-separated tags are tried in header order (q=weights are ignored,*is skipped) against the languages the shop offers: an exact tag (case-insensitive), then the primary subtag (en-US→en-GB); with no match, the shop’s first language. (A storefront without a language list of its own offers every language of the company: then the primary subtag of the first usable tag is used as it is — a language the company has not set up is answered in its default currency, untranslated — and, with no tag at all, the company’s default language.)The language decides the currency of every price and the translations. A cart or checkout keeps the language it was created in: a later request changes it only when its body names a
context.languagedifferent from the stored one, and when that switches the shop language every line is re-added in the new currency and gets a new line id. Once a session has a language, this header alone never switches it. On a storefront without a language list, a session created with no usable tag stores none: each later request is then answered in the language its own tags pick, and a different pick adds the lines again (new line ids). Sendcontext.languageon the create.example:sk-SK,sk;q=0.9,en;q=0.8
Request bodyrequired
A checkout update. Fields you leave out keep their stored value; {} is valid: it re-derives the status, quotes the shipping options again, applies the stored discount codes again (with the discount extension) and extends expires_at; it does not check the lines again. Every update of an open checkout extends expires_at to 24 h from now.
line_itemsarray<LineItemRequest>Optional. When sent, full replacement; absent keeps the lines.
maxItems: 100buyerBuyerRequest | nullReplaces the whole stored buyer: a field you leave out is erased (send the e-mail again when you add a name).
nullclears the buyer; leavingbuyerout keeps it. A non-string value → 400; a longer string is cut to the stated length (counted in code points). Names and phone are never required.contextRequestContext | nullBuyer context. Every field is optional; strings longer than the stated length are cut (counted in code points). Unknown fields (
intent,eligibility,payment…) are ignored.fulfillmentFulfillmentRequest | nullShipping (only read when
dev.ucp.shopping.fulfillmentis negotiated; ignored on carts — a cart stores it but never uses it, and a conversion takesfulfillmentfrom the conversion request).nullor a body withoutfulfillmentkeeps what the session holds.discountsDiscountsRequest | nullDiscount codes (only read when
dev.ucp.shopping.discountis negotiated). Without the extension a code is never applied: one applied earlier is taken off the session’s cart for that answer (the stored codes are kept, and a later request that negotiates the extension applies them again).
{
"buyer": {
"first_name": "Jana",
"last_name": "Nováková",
"email": "jana.novakova@example.com",
"phone_number": "+421 905 123 456"
},
"fulfillment": {
"methods": [
{
"type": "shipping",
"destinations": [
{
"id": "home",
"street_address": "Hlavná 12",
"address_locality": "Bratislava",
"postal_code": "811 01",
"address_country": "SK",
"first_name": "Jana",
"last_name": "Nováková"
}
],
"selected_destination_id": "home",
"groups": [
{
"selected_option_id": "0c5e8a3f-7b21-4d96-8f4c-9e2a6d1b3c75"
}
]
}
]
},
"discounts": {
"codes": [
"JESEN10"
]
}
}Responses
200The checkout after the update; the unchanged checkout with handed_off, checkout_completed or checkout_canceled; or a business error: not_found, capabilities_in…
The checkout after the update; the unchanged checkout with handed_off, checkout_completed or checkout_canceled; or a business error: not_found, capabilities_incompatible.
Response headers
Cache-ControlstringEvery UCP answer (success or error) except the business profile.
always:no-storeETagstringAn ETag of the body, generated by the API on every response with a body (writes included). The API’s ETag is strong; the web server sends it weak (
W/"…") when it compresses the response, which it does for a client that sendsAccept-Encoding: gzip. AGETthat sends a matchingIf-None-Match(either form) can be answered304 Not Modifiedeven though the answer isno-store.X-RateLimit-LimitintegerRequests this client IP may make to this operation per 60-second window (600 reads, 300 writes, 60 handoff). Set on every answer that passed the rate limiter.
X-RateLimit-RemainingintegerThe operation’s limit minus this client IP’s current count. The counts live in the API server’s memory; each request is meant to leave the count 60 s after it was made, but the rate limiter (
@nestjs/throttler6.5) drops those pending decrements, for every client, whenever any client’s block ends. Read it as an upper bound and keep steady traffic at half a limit or less.minimum: 0X-RateLimit-ResetintegerSeconds left in a 60-second window the limiter keeps per client IP and operation. The count is not reset when that window ends, so this is not the moment the budget comes back.
minimum: 0
Exactly one of
- Checkoutobject · Checkout
A checkout session.
Once
completedorcanceled— and while the buyer’s handoff is done but the order not yet placed — the body is the last answer the platform received, with the currentstatus(andorder): it is not re-priced, and it does not show what the buyer changed on the shop. Only the lines and the applied discount code travel to the shop; the buyer enters address, shipping and payment there again.additionalProperties: falseThe
ucpobject of every checkout answer that returns a checkout (any status).idstring (uuid)requiredThe checkout session id.
incomplete— at least onerecoverableerror is listed (no line, no valid e-mail, no or incomplete address, no shipping to it, no or an unoffered option, a line error in this answer).requires_escalation— nothing the platform can provide is missing;payment_requiredis listed and the buyer must continue on the shop throughcontinue_url.completed— the buyer placed an order on the shop from the cart the handoff filled; seeorder. It means placed, not paid. Set shortly after the order (once the shop has started the payment and handed the order to its ERP) and best effort: a failure is only logged, so an order can exist for a checkout that never readscompleted.canceled— canceled byPOST …/cancel, or expired (an open checkout read or written afterexpires_atturnscanceled).
ready_for_completeandcomplete_in_progressare never used: the API never completes a checkout.one of:incompleterequires_escalationcompletedcanceledISO 4217 currency code.
pattern: ^[A-Z]{3}$In the shop cart’s order.
subtotal,discount?,fulfillment?,total.minItems: 2maxItems: 4Always present, possibly empty. Order: line messages and discount messages (only in the answer to the write that caused them), then
missing/invalid_address/address_undeliverable/invalid_option,pickup_point_required, andpayment_requiredlast. Empty oncompletedandcanceledreads.Always present: at most 10, possibly empty.
maxItems: 10buyerBuyerThe stored buyer; empty fields are left out. Absent when nothing is stored.
contextContextThe stored context, echoed as you sent it (after cleaning); empty fields are left out. Absent when nothing is stored.
fulfillmentFulfillmentPresent only when
dev.ucp.shopping.fulfillmentis negotiated. Oneshippingmethod with one group; there is nopickupmethod — a pickup point (Packeta) is chosen by the buyer on the shop.discountsDiscountsPresent only when
dev.ucp.shopping.discountis negotiated. Thediscounttotal is present whether or not it is.continue_urlstring (uri) · ContinueUrlPresent while the checkout is
incompleteorrequires_escalation; absent oncecompletedorcanceled.pattern: ^https://[^/]+/ucp/continue/[0-9a-f]{64}$When the session expires (UTC, ISO 8601 with milliseconds). A write extends it (cart: now + 7 days, checkout: now + 24 h); reads,
completeandcanceldo not. After a handoff nothing extends it.orderOrderPresent once
completed.
- UcpBusinessErrorResponseobject · UcpBusinessErrorResponse
HTTP 200 with no resource. The cases:
capabilities_incompatible— your profile and this shop share no version of the operation’s root capability (always for carts and checkouts on a shop in catalog mode). Nothing ran; no Idempotency-Key was used.not_found— the product, cart (Cart not found or has expired) or checkout (Checkout session not found) does not exist for this shop, is malformed, or (carts) was canceled or expired. An unknowncart_idonPOST /checkout-sessionsalso readsCheckout session not found.out_of_stock/item_unavailable— a create in which none of the requested items could be added; no session was created.handed_off— converting a cart whose lines the buyer already took to the shop.
Inside a write the last three are stored under the Idempotency-Key like a success (
capabilities_incompatibleis answered before the key is read).additionalProperties: falseThe
ucpobject of a business error. Never haspayment_handlers.capabilitiesis{}forcapabilities_incompatibleand lists the operation’s root (and its negotiated extensions) otherwise.Exactly one error message saying why no resource could be returned.
minItems: 1maxItems: 1continue_urlstring (uri)requiredWhere a buyer could go instead: the shop home (
https://{primary domain}/), orhttps://{primary domain}/cartforhanded_off. Not a handoff link.
{
"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",
"status": "requires_escalation",
"currency": "EUR",
"line_items": [
{
"id": "f6a3c8e1-9d27-4b54-8e6a-2c1f7b9d3a58",
"item": {
"id": "1e8a6c4f-2b93-4d71-9f05-6a3c8e2b1d47",
"title": "Trek Marlin 5",
"price": 69900,
"image_url": "https://shop.example.com/api/public/uploads/c4d1e5a2-8b3f-4a6c-9e07-5f2d8a1b3c40/products/trek-marlin-5-modra.webp"
},
"quantity": 1,
"totals": [
{
"type": "subtotal",
"amount": 69900
},
{
"type": "total",
"amount": 69900
}
]
},
{
"id": "2e9d5b7a-4c83-4f16-a9b2-6d8e1c3f7a49",
"item": {
"id": "b5f81c3d-6e49-4a2b-8d73-9c0e4f1a6b25",
"title": "Fľaša na vodu 750 ml",
"price": 990
},
"quantity": 2,
"totals": [
{
"type": "subtotal",
"amount": 1980
},
{
"type": "total",
"amount": 1980
}
]
}
],
"totals": [
{
"type": "subtotal",
"amount": 71880
},
{
"type": "discount",
"amount": -5844,
"display_text": "JESEN10"
},
{
"type": "fulfillment",
"amount": 490
},
{
"type": "total",
"amount": 66526
}
],
"messages": [
{
"type": "error",
"code": "payment_required",
"content": "Payment is completed on the shop website — continue there to place the order",
"severity": "requires_buyer_input"
}
],
"links": [
{
"type": "terms_of_service",
"url": "https://shop.example.com/obchodne-podmienky",
"title": "Obchodné podmienky"
},
{
"type": "privacy_policy",
"url": "https://shop.example.com/ochrana-osobnych-udajov",
"title": "Ochrana osobných údajov"
}
],
"buyer": {
"first_name": "Jana",
"last_name": "Nováková",
"email": "jana.novakova@example.com",
"phone_number": "+421 905 123 456"
},
"context": {
"language": "sk",
"address_country": "SK"
},
"fulfillment": {
"methods": [
{
"id": "shipping",
"type": "shipping",
"line_item_ids": [
"f6a3c8e1-9d27-4b54-8e6a-2c1f7b9d3a58",
"2e9d5b7a-4c83-4f16-a9b2-6d8e1c3f7a49"
],
"destinations": [
{
"id": "destination",
"street_address": "Hlavná 12",
"address_locality": "Bratislava",
"postal_code": "811 01",
"address_country": "SK",
"first_name": "Jana",
"last_name": "Nováková"
}
],
"selected_destination_id": "destination",
"groups": [
{
"id": "default",
"line_item_ids": [
"f6a3c8e1-9d27-4b54-8e6a-2c1f7b9d3a58",
"2e9d5b7a-4c83-4f16-a9b2-6d8e1c3f7a49"
],
"options": [
{
"id": "0c5e8a3f-7b21-4d96-8f4c-9e2a6d1b3c75",
"title": "GLS kuriér",
"description": "Doručenie na adresu do 1–2 pracovných dní",
"totals": [
{
"type": "total",
"amount": 490
}
]
},
{
"id": "6b2d9f4e-3a17-4c68-9d5b-1f8e3a7c2b40",
"title": "Packeta – výdajné miesto",
"description": "Delivery in 2–3 business days",
"totals": [
{
"type": "total",
"amount": 290
}
]
}
],
"selected_option_id": "0c5e8a3f-7b21-4d96-8f4c-9e2a6d1b3c75"
}
]
}
]
},
"discounts": {
"codes": [
"JESEN10"
],
"applied": [
{
"code": "JESEN10",
"title": "JESEN10",
"amount": 5844,
"method": "across"
}
]
},
"continue_url": "https://shop.example.com/ucp/continue/9bbd3dedf887f93c6287fa7f4b8de141501155714c3b2050bb83551e36ec780b",
"expires_at": "2026-10-27T09:21:30.551Z"
}{
"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"
}{
"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": "completed",
"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": [
{
"type": "error",
"code": "checkout_completed",
"content": "This checkout is already completed",
"severity": "unrecoverable"
}
],
"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"
}
]
},
"order": {
"id": "8e1c4f7a-2d95-4b36-a7e8-5c3f9b1d6a24",
"label": "SK2600417",
"permalink_url": "https://shop.example.com/order-confirmation?orderNumber=SK2600417&orderId=8e1c4f7a-2d95-4b36-a7e8-5c3f9b1d6a24"
}
}{
"ucp": {
"version": "2026-04-08",
"status": "error",
"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"
}
]
}
},
"messages": [
{
"type": "error",
"code": "not_found",
"content": "Checkout session not found",
"severity": "unrecoverable"
}
],
"continue_url": "https://shop.example.com/"
}{
"ucp": {
"version": "2026-04-08",
"status": "error",
"capabilities": {}
},
"messages": [
{
"type": "error",
"code": "capabilities_incompatible",
"content": "The platform and this business share no version of the capability this operation needs",
"severity": "unrecoverable"
}
],
"continue_url": "https://shop.example.com/"
}400Bad Request (Error): invalid_profile_url — the UCP-Agent header is missing or unusable (with continue_url); invalid_request — a field this operation reads has …
Bad Request (Error):
invalid_profile_url— theUCP-Agentheader is missing or unusable (withcontinue_url);invalid_request— a field this operation reads has the wrong JSON type or value;contentnames it. Malformed JSON or a top-level JSON primitive says only "The request is not valid". Unknown fields are ignored, never refused. As forcreateCheckout. The checkout is not changed.invalid_request"Idempotency-Key is not valid" — seeIdempotency-Key.
Response headers
Cache-ControlstringEvery UCP answer (success or error) except the business profile.
always:no-store
SchemaError
{
"code": "invalid_profile_url",
"content": "The UCP-Agent header must carry profile=\"https://…\"",
"continue_url": "https://shop.example.com/"
}{
"code": "invalid_request",
"content": "fulfillment.methods[].destinations[0].address_country is not a recognised country; send an ISO 3166-1 alpha-2 code such as \"SK\""
}{
"code": "invalid_request",
"content": "line_items[0].quantity must be a positive integer"
}{
"code": "invalid_request",
"content": "Idempotency-Key is not valid"
}{
"code": "invalid_request",
"content": "The request is not valid"
}Request with curl
curl -X PUT 'https://shop.example.com/ucp/v1/checkout-sessions/3d7b9e2a-6f14-4c58-9b3e-8a2f1c6d4e97' \
-H 'UCP-Agent: profile="https://platform.example.com/.well-known/ucp"' \
-H 'Idempotency-Key: 0c2f8a4e-6b1d-4f37-9a85-3e7d1c9b5f02' \
-H 'Accept-Language: sk-SK,sk;q=0.9,en;q=0.8' \
-H 'Content-Type: application/json' \
--data '{
"buyer": {
"first_name": "Jana",
"last_name": "Nováková",
"email": "jana.novakova@example.com",
"phone_number": "+421 905 123 456"
},
"fulfillment": {
"methods": [
{
"type": "shipping",
"destinations": [
{
"id": "home",
"street_address": "Hlavná 12",
"address_locality": "Bratislava",
"postal_code": "811 01",
"address_country": "SK",
"first_name": "Jana",
"last_name": "Nováková"
}
],
"selected_destination_id": "home",
"groups": [
{
"selected_option_id": "0c5e8a3f-7b21-4d96-8f4c-9e2a6d1b3c75"
}
]
}
]
},
"discounts": {
"codes": [
"JESEN10"
]
}
}'Complete a checkout session (no payment, no order)
POST/ucp/v1/checkout-sessions/{id}/complete
Never places an order and never takes a payment. UCP’s complete step is answered with the checkout as it stands: requires_escalation with payment_required and continue_url once it is ready (incomplete with its gaps otherwise), or completed with order once the buyer has ordered on the shop. It never makes a checkout completed: like getCheckout it re-derives incomplete ⇄ requires_escalation of an open checkout from what is stored (the shipping options quoted again, the lines and codes not checked again) and stores that answer, and it does not extend expires_at.
To finish a purchase, give continue_url to the buyer and poll getCheckout. Any payment data in the body is ignored and not stored.
Parameters
idpathstringrequiredThe checkout session’s
id(a UUID, any letter case). An id that is not a UUID or belongs to another shop is answered with the 200not_foundbusiness error, not a 4xx. Completed and canceled checkouts stay readable until 30 days afterexpires_at.pattern: ^[0-9a-fA-F]{8}-[0-9a-fA-F]{4}-[0-9a-fA-F]{4}-[0-9a-fA-F]{4}-[0-9a-fA-F]{12}$example:3d7b9e2a-6f14-4c58-9b3e-8a2f1c6d4e97UCP-AgentheaderstringrequiredNames your platform profile. Required on every
/ucp/v1/*operation; never read by the profile and handoff routes.Details
Grammar — a subset of an RFC 8941 Dictionary, at most 4096 characters:
- members separated by
,(spaces or tabs around it allowed; a trailing comma is an error); keys lower-case, matching[a-z*][a-z0-9_\-.*]*; - the
profilemember must be an sf-string:profile="https://…", printable ASCII 0x20–0x7E, escapes\"and\\only; - other members (tokens, numbers, booleans, strings) and parameters (
;key=value) are parsed and ignored; whenprofileappears twice the last one wins; - any parse error makes the whole header unusable.
Accepted:
profile="https://platform.example.com/.well-known/ucp",version="2026-04-08", profile="https://platform.example.com/.well-known/ucp";sig=abc. Refused: a missing header,profile=https://…(a token, not a string),Profile="…",a=1 b=2, profile="…".The URL — at most 1000 characters, no whitespace anywhere, starting
https://, no user name or password, a non-empty host. Any port, path and query string are accepted (the spec’s/.well-known/ucppath is not enforced). Failing any rule →400 invalid_profile_url.The fetch —
GETwithAccept: application/jsonandUser-Agent: Monokaido-UCP/1.0 (+https://monokaido.com); no redirects followed; only HTTP 200 accepted; at most 256 KiB; 3 s idle, 5 s total; the host must resolve to a public address (private, loopback, link-local and other reserved ranges are refused). Anything else →424 profile_unreachable. A body that is not JSON, or whoseucp.versionis not aYYYY-MM-DDstring →422 profile_malformed; a valid profile of another version →422 version_unsupported. Onlyucp.versionanducp.capabilities(name →[{"version"}], first 20 entries per name, names likedev.ucp.shopping.cart) are read; everything else in it is ignored.Caching — a fetched profile is cached for its
Cache-Control: max-age, clamped to 60 s – 1 h (5 min without one;no-store,no-cacheandprivateare ignored) and served up to 1 h past that while one background refresh runs. A failed fetch (424, or422 profile_malformed) is remembered for 60 s. A profile of anotherucp.versionis a valid profile and is cached like one, so after you change its version expect422 version_unsupporteduntil that entry is refreshed (itsmax-age, then one more request) — or use a new profile URL. These caches are kept in memory by each API process. Fetching a profile the shop does not know yet is budgeted: 5 new profile URLs per minute per client (IPv4 address, IPv6 /64) and 60 per minute in all →503 service_unavailablewithRetry-After. The full URL string is the cache key, so keep it stable (no per-request query strings).minLength: 1maxLength: 4096example:profile="https://platform.example.com/.well-known/ucp"- members separated by
Idempotency-KeyheaderstringOptional, strongly recommended. Without it (or with an empty value) the write simply runs: a retried create makes a second session.
- Format: 1–255 printable ASCII characters 0x21–0x7E — no spaces. Anything else (also a header sent twice, which arrives joined with
", ") →400 invalid_request"Idempotency-Key is not valid". A UUID v4 per logical write is a good choice. - Scope: this shop (all its domains) × the exact
UCP-Agentprofile URL string × the key. - Request identity: the operation, the path
{id}as sent (after URL decoding; case included) and the JSON body compared canonically (object key order and whitespace do not matter, array order does). Cancel routes hash{}whatever the body. - Same key, same request, finished → the stored HTTP status and body are replayed — the same values, including a 200 business error, the original
expires_atand theucpobject negotiated the first time, even when the session has changed since. The replay comes from storage, so its object keys come in JSONB order (and itsETagdiffers). - The
UCP-Agentprofile is resolved and negotiated before the key is read: a profile error or a 200capabilities_incompatibleanswer is never stored. - Same key, different request →
409 idempotency_conflict. Same request still running →409withRetry-After: 2. - If the first request fails with a protocol error (
400,503session busy,500…) the key is released: retrying with the same key runs the operation again. - A claim whose request died is taken over by the same request after 120 s. Keys are kept for at least 25 hours after their first use (an hourly job deletes older ones); do not rely on a key more than 24 hours after its first use.
- Not read by catalog operations,
GETroutes, the profile or the handoff routes.
pattern: ^[\x21-\x7E]{1,255}$minLength: 1maxLength: 255example:0c2f8a4e-6b1d-4f37-9a85-3e7d1c9b5f02- Format: 1–255 printable ASCII characters 0x21–0x7E — no spaces. Anything else (also a header sent twice, which arrives joined with
Accept-LanguageheaderstringFallback for the answer’s language after
context.languagein the body. The first 10 comma-separated tags are tried in header order (q=weights are ignored,*is skipped) against the languages the shop offers: an exact tag (case-insensitive), then the primary subtag (en-US→en-GB); with no match, the shop’s first language. (A storefront without a language list of its own offers every language of the company: then the primary subtag of the first usable tag is used as it is — a language the company has not set up is answered in its default currency, untranslated — and, with no tag at all, the company’s default language.)The language decides the currency of every price and the translations. A cart or checkout keeps the language it was created in: a later request changes it only when its body names a
context.languagedifferent from the stored one, and when that switches the shop language every line is re-added in the new currency and gets a new line id. Once a session has a language, this header alone never switches it. On a storefront without a language list, a session created with no usable tag stores none: each later request is then answered in the language its own tags pick, and a different pick adds the lines again (new line ids). Sendcontext.languageon the create.example:sk-SK,sk;q=0.9,en;q=0.8
Request body
Ignored.
Ignored. Any payment, payment_data or AP2 field is ignored and never stored: no payment is processed through UCP. The body still counts for the Idempotency-Key’s request identity.
<any other key>any
{}Responses
200The checkout as it stands (a canceled one with checkout_canceled), or a business error: not_found, capabilities_incompatible.
Response headers
Cache-ControlstringEvery UCP answer (success or error) except the business profile.
always:no-storeETagstringAn ETag of the body, generated by the API on every response with a body (writes included). The API’s ETag is strong; the web server sends it weak (
W/"…") when it compresses the response, which it does for a client that sendsAccept-Encoding: gzip. AGETthat sends a matchingIf-None-Match(either form) can be answered304 Not Modifiedeven though the answer isno-store.X-RateLimit-LimitintegerRequests this client IP may make to this operation per 60-second window (600 reads, 300 writes, 60 handoff). Set on every answer that passed the rate limiter.
X-RateLimit-RemainingintegerThe operation’s limit minus this client IP’s current count. The counts live in the API server’s memory; each request is meant to leave the count 60 s after it was made, but the rate limiter (
@nestjs/throttler6.5) drops those pending decrements, for every client, whenever any client’s block ends. Read it as an upper bound and keep steady traffic at half a limit or less.minimum: 0X-RateLimit-ResetintegerSeconds left in a 60-second window the limiter keeps per client IP and operation. The count is not reset when that window ends, so this is not the moment the budget comes back.
minimum: 0
Exactly one of
- Checkoutobject · Checkout
A checkout session.
Once
completedorcanceled— and while the buyer’s handoff is done but the order not yet placed — the body is the last answer the platform received, with the currentstatus(andorder): it is not re-priced, and it does not show what the buyer changed on the shop. Only the lines and the applied discount code travel to the shop; the buyer enters address, shipping and payment there again.additionalProperties: falseThe
ucpobject of every checkout answer that returns a checkout (any status).idstring (uuid)requiredThe checkout session id.
incomplete— at least onerecoverableerror is listed (no line, no valid e-mail, no or incomplete address, no shipping to it, no or an unoffered option, a line error in this answer).requires_escalation— nothing the platform can provide is missing;payment_requiredis listed and the buyer must continue on the shop throughcontinue_url.completed— the buyer placed an order on the shop from the cart the handoff filled; seeorder. It means placed, not paid. Set shortly after the order (once the shop has started the payment and handed the order to its ERP) and best effort: a failure is only logged, so an order can exist for a checkout that never readscompleted.canceled— canceled byPOST …/cancel, or expired (an open checkout read or written afterexpires_atturnscanceled).
ready_for_completeandcomplete_in_progressare never used: the API never completes a checkout.one of:incompleterequires_escalationcompletedcanceledISO 4217 currency code.
pattern: ^[A-Z]{3}$In the shop cart’s order.
subtotal,discount?,fulfillment?,total.minItems: 2maxItems: 4Always present, possibly empty. Order: line messages and discount messages (only in the answer to the write that caused them), then
missing/invalid_address/address_undeliverable/invalid_option,pickup_point_required, andpayment_requiredlast. Empty oncompletedandcanceledreads.Always present: at most 10, possibly empty.
maxItems: 10buyerBuyerThe stored buyer; empty fields are left out. Absent when nothing is stored.
contextContextThe stored context, echoed as you sent it (after cleaning); empty fields are left out. Absent when nothing is stored.
fulfillmentFulfillmentPresent only when
dev.ucp.shopping.fulfillmentis negotiated. Oneshippingmethod with one group; there is nopickupmethod — a pickup point (Packeta) is chosen by the buyer on the shop.discountsDiscountsPresent only when
dev.ucp.shopping.discountis negotiated. Thediscounttotal is present whether or not it is.continue_urlstring (uri) · ContinueUrlPresent while the checkout is
incompleteorrequires_escalation; absent oncecompletedorcanceled.pattern: ^https://[^/]+/ucp/continue/[0-9a-f]{64}$When the session expires (UTC, ISO 8601 with milliseconds). A write extends it (cart: now + 7 days, checkout: now + 24 h); reads,
completeandcanceldo not. After a handoff nothing extends it.orderOrderPresent once
completed.
- UcpBusinessErrorResponseobject · UcpBusinessErrorResponse
HTTP 200 with no resource. The cases:
capabilities_incompatible— your profile and this shop share no version of the operation’s root capability (always for carts and checkouts on a shop in catalog mode). Nothing ran; no Idempotency-Key was used.not_found— the product, cart (Cart not found or has expired) or checkout (Checkout session not found) does not exist for this shop, is malformed, or (carts) was canceled or expired. An unknowncart_idonPOST /checkout-sessionsalso readsCheckout session not found.out_of_stock/item_unavailable— a create in which none of the requested items could be added; no session was created.handed_off— converting a cart whose lines the buyer already took to the shop.
Inside a write the last three are stored under the Idempotency-Key like a success (
capabilities_incompatibleis answered before the key is read).additionalProperties: falseThe
ucpobject of a business error. Never haspayment_handlers.capabilitiesis{}forcapabilities_incompatibleand lists the operation’s root (and its negotiated extensions) otherwise.Exactly one error message saying why no resource could be returned.
minItems: 1maxItems: 1continue_urlstring (uri)requiredWhere a buyer could go instead: the shop home (
https://{primary domain}/), orhttps://{primary domain}/cartforhanded_off. Not a handoff link.
{
"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",
"status": "requires_escalation",
"currency": "EUR",
"line_items": [
{
"id": "f6a3c8e1-9d27-4b54-8e6a-2c1f7b9d3a58",
"item": {
"id": "1e8a6c4f-2b93-4d71-9f05-6a3c8e2b1d47",
"title": "Trek Marlin 5",
"price": 69900,
"image_url": "https://shop.example.com/api/public/uploads/c4d1e5a2-8b3f-4a6c-9e07-5f2d8a1b3c40/products/trek-marlin-5-modra.webp"
},
"quantity": 1,
"totals": [
{
"type": "subtotal",
"amount": 69900
},
{
"type": "total",
"amount": 69900
}
]
},
{
"id": "2e9d5b7a-4c83-4f16-a9b2-6d8e1c3f7a49",
"item": {
"id": "b5f81c3d-6e49-4a2b-8d73-9c0e4f1a6b25",
"title": "Fľaša na vodu 750 ml",
"price": 990
},
"quantity": 2,
"totals": [
{
"type": "subtotal",
"amount": 1980
},
{
"type": "total",
"amount": 1980
}
]
}
],
"totals": [
{
"type": "subtotal",
"amount": 71880
},
{
"type": "discount",
"amount": -5844,
"display_text": "JESEN10"
},
{
"type": "fulfillment",
"amount": 490
},
{
"type": "total",
"amount": 66526
}
],
"messages": [
{
"type": "error",
"code": "payment_required",
"content": "Payment is completed on the shop website — continue there to place the order",
"severity": "requires_buyer_input"
}
],
"links": [
{
"type": "terms_of_service",
"url": "https://shop.example.com/obchodne-podmienky",
"title": "Obchodné podmienky"
},
{
"type": "privacy_policy",
"url": "https://shop.example.com/ochrana-osobnych-udajov",
"title": "Ochrana osobných údajov"
}
],
"buyer": {
"email": "jana.novakova@example.com",
"last_name": "Nováková",
"first_name": "Jana",
"phone_number": "+421 905 123 456"
},
"context": {
"language": "sk",
"address_country": "SK"
},
"fulfillment": {
"methods": [
{
"id": "shipping",
"type": "shipping",
"line_item_ids": [
"f6a3c8e1-9d27-4b54-8e6a-2c1f7b9d3a58",
"2e9d5b7a-4c83-4f16-a9b2-6d8e1c3f7a49"
],
"destinations": [
{
"id": "destination",
"last_name": "Nováková",
"first_name": "Jana",
"postal_code": "811 01",
"street_address": "Hlavná 12",
"address_country": "SK",
"address_locality": "Bratislava"
}
],
"selected_destination_id": "destination",
"groups": [
{
"id": "default",
"line_item_ids": [
"f6a3c8e1-9d27-4b54-8e6a-2c1f7b9d3a58",
"2e9d5b7a-4c83-4f16-a9b2-6d8e1c3f7a49"
],
"options": [
{
"id": "0c5e8a3f-7b21-4d96-8f4c-9e2a6d1b3c75",
"title": "GLS kuriér",
"description": "Doručenie na adresu do 1–2 pracovných dní",
"totals": [
{
"type": "total",
"amount": 490
}
]
},
{
"id": "6b2d9f4e-3a17-4c68-9d5b-1f8e3a7c2b40",
"title": "Packeta – výdajné miesto",
"description": "Delivery in 2–3 business days",
"totals": [
{
"type": "total",
"amount": 290
}
]
}
],
"selected_option_id": "0c5e8a3f-7b21-4d96-8f4c-9e2a6d1b3c75"
}
]
}
]
},
"discounts": {
"codes": [
"JESEN10"
],
"applied": [
{
"code": "JESEN10",
"title": "JESEN10",
"amount": 5844,
"method": "across"
}
]
},
"continue_url": "https://shop.example.com/ucp/continue/9bbd3dedf887f93c6287fa7f4b8de141501155714c3b2050bb83551e36ec780b",
"expires_at": "2026-10-27T09:21:30.551Z"
}{
"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": "completed",
"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": [],
"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"
}
]
},
"order": {
"id": "8e1c4f7a-2d95-4b36-a7e8-5c3f9b1d6a24",
"label": "SK2600417",
"permalink_url": "https://shop.example.com/order-confirmation?orderNumber=SK2600417&orderId=8e1c4f7a-2d95-4b36-a7e8-5c3f9b1d6a24"
}
}{
"ucp": {
"version": "2026-04-08",
"status": "error",
"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"
}
]
}
},
"messages": [
{
"type": "error",
"code": "not_found",
"content": "Checkout session not found",
"severity": "unrecoverable"
}
],
"continue_url": "https://shop.example.com/"
}{
"ucp": {
"version": "2026-04-08",
"status": "error",
"capabilities": {}
},
"messages": [
{
"type": "error",
"code": "capabilities_incompatible",
"content": "The platform and this business share no version of the capability this operation needs",
"severity": "unrecoverable"
}
],
"continue_url": "https://shop.example.com/"
}400Bad Request (Error): invalid_profile_url — the UCP-Agent header is missing or unusable (with continue_url); invalid_request "The request is not valid" — a body…
Bad Request (Error):
invalid_profile_url— theUCP-Agentheader is missing or unusable (withcontinue_url);invalid_request"The request is not valid" — a body sent as JSON that does not parse.invalid_request"Idempotency-Key is not valid" — seeIdempotency-Key.
Response headers
Cache-ControlstringEvery UCP answer (success or error) except the business profile.
always:no-store
SchemaError
{
"code": "invalid_profile_url",
"content": "The UCP-Agent header must carry profile=\"https://…\"",
"continue_url": "https://shop.example.com/"
}{
"code": "invalid_request",
"content": "Idempotency-Key is not valid"
}{
"code": "invalid_request",
"content": "The request is not valid"
}Request with curl
curl -X POST 'https://shop.example.com/ucp/v1/checkout-sessions/3d7b9e2a-6f14-4c58-9b3e-8a2f1c6d4e97/complete' \
-H 'UCP-Agent: profile="https://platform.example.com/.well-known/ucp"' \
-H 'Idempotency-Key: 0c2f8a4e-6b1d-4f37-9a85-3e7d1c9b5f02' \
-H 'Accept-Language: sk-SK,sk;q=0.9,en;q=0.8' \
-H 'Content-Type: application/json' \
--data '{}'Cancel a checkout session
POST/ucp/v1/checkout-sessions/{id}/cancel
Cancels an open checkout: status: "canceled", messages: [], no continue_url, the stored expires_at. Its continue_url then shows the “gone” page and a later order can no longer complete it.
- A completed checkout is not changed (the order is never canceled through UCP): it comes back with
checkout_completed. A canceled one comes back withcheckout_canceled. - Any request body is ignored (the Idempotency-Key hashes
{}).
Parameters
idpathstringrequiredThe checkout session’s
id(a UUID, any letter case). An id that is not a UUID or belongs to another shop is answered with the 200not_foundbusiness error, not a 4xx. Completed and canceled checkouts stay readable until 30 days afterexpires_at.pattern: ^[0-9a-fA-F]{8}-[0-9a-fA-F]{4}-[0-9a-fA-F]{4}-[0-9a-fA-F]{4}-[0-9a-fA-F]{12}$example:3d7b9e2a-6f14-4c58-9b3e-8a2f1c6d4e97UCP-AgentheaderstringrequiredNames your platform profile. Required on every
/ucp/v1/*operation; never read by the profile and handoff routes.Details
Grammar — a subset of an RFC 8941 Dictionary, at most 4096 characters:
- members separated by
,(spaces or tabs around it allowed; a trailing comma is an error); keys lower-case, matching[a-z*][a-z0-9_\-.*]*; - the
profilemember must be an sf-string:profile="https://…", printable ASCII 0x20–0x7E, escapes\"and\\only; - other members (tokens, numbers, booleans, strings) and parameters (
;key=value) are parsed and ignored; whenprofileappears twice the last one wins; - any parse error makes the whole header unusable.
Accepted:
profile="https://platform.example.com/.well-known/ucp",version="2026-04-08", profile="https://platform.example.com/.well-known/ucp";sig=abc. Refused: a missing header,profile=https://…(a token, not a string),Profile="…",a=1 b=2, profile="…".The URL — at most 1000 characters, no whitespace anywhere, starting
https://, no user name or password, a non-empty host. Any port, path and query string are accepted (the spec’s/.well-known/ucppath is not enforced). Failing any rule →400 invalid_profile_url.The fetch —
GETwithAccept: application/jsonandUser-Agent: Monokaido-UCP/1.0 (+https://monokaido.com); no redirects followed; only HTTP 200 accepted; at most 256 KiB; 3 s idle, 5 s total; the host must resolve to a public address (private, loopback, link-local and other reserved ranges are refused). Anything else →424 profile_unreachable. A body that is not JSON, or whoseucp.versionis not aYYYY-MM-DDstring →422 profile_malformed; a valid profile of another version →422 version_unsupported. Onlyucp.versionanducp.capabilities(name →[{"version"}], first 20 entries per name, names likedev.ucp.shopping.cart) are read; everything else in it is ignored.Caching — a fetched profile is cached for its
Cache-Control: max-age, clamped to 60 s – 1 h (5 min without one;no-store,no-cacheandprivateare ignored) and served up to 1 h past that while one background refresh runs. A failed fetch (424, or422 profile_malformed) is remembered for 60 s. A profile of anotherucp.versionis a valid profile and is cached like one, so after you change its version expect422 version_unsupporteduntil that entry is refreshed (itsmax-age, then one more request) — or use a new profile URL. These caches are kept in memory by each API process. Fetching a profile the shop does not know yet is budgeted: 5 new profile URLs per minute per client (IPv4 address, IPv6 /64) and 60 per minute in all →503 service_unavailablewithRetry-After. The full URL string is the cache key, so keep it stable (no per-request query strings).minLength: 1maxLength: 4096example:profile="https://platform.example.com/.well-known/ucp"- members separated by
Idempotency-KeyheaderstringOptional, strongly recommended. Without it (or with an empty value) the write simply runs: a retried create makes a second session.
- Format: 1–255 printable ASCII characters 0x21–0x7E — no spaces. Anything else (also a header sent twice, which arrives joined with
", ") →400 invalid_request"Idempotency-Key is not valid". A UUID v4 per logical write is a good choice. - Scope: this shop (all its domains) × the exact
UCP-Agentprofile URL string × the key. - Request identity: the operation, the path
{id}as sent (after URL decoding; case included) and the JSON body compared canonically (object key order and whitespace do not matter, array order does). Cancel routes hash{}whatever the body. - Same key, same request, finished → the stored HTTP status and body are replayed — the same values, including a 200 business error, the original
expires_atand theucpobject negotiated the first time, even when the session has changed since. The replay comes from storage, so its object keys come in JSONB order (and itsETagdiffers). - The
UCP-Agentprofile is resolved and negotiated before the key is read: a profile error or a 200capabilities_incompatibleanswer is never stored. - Same key, different request →
409 idempotency_conflict. Same request still running →409withRetry-After: 2. - If the first request fails with a protocol error (
400,503session busy,500…) the key is released: retrying with the same key runs the operation again. - A claim whose request died is taken over by the same request after 120 s. Keys are kept for at least 25 hours after their first use (an hourly job deletes older ones); do not rely on a key more than 24 hours after its first use.
- Not read by catalog operations,
GETroutes, the profile or the handoff routes.
pattern: ^[\x21-\x7E]{1,255}$minLength: 1maxLength: 255example:0c2f8a4e-6b1d-4f37-9a85-3e7d1c9b5f02- Format: 1–255 printable ASCII characters 0x21–0x7E — no spaces. Anything else (also a header sent twice, which arrives joined with
Accept-LanguageheaderstringFallback for the answer’s language after
context.languagein the body. The first 10 comma-separated tags are tried in header order (q=weights are ignored,*is skipped) against the languages the shop offers: an exact tag (case-insensitive), then the primary subtag (en-US→en-GB); with no match, the shop’s first language. (A storefront without a language list of its own offers every language of the company: then the primary subtag of the first usable tag is used as it is — a language the company has not set up is answered in its default currency, untranslated — and, with no tag at all, the company’s default language.)The language decides the currency of every price and the translations. A cart or checkout keeps the language it was created in: a later request changes it only when its body names a
context.languagedifferent from the stored one, and when that switches the shop language every line is re-added in the new currency and gets a new line id. Once a session has a language, this header alone never switches it. On a storefront without a language list, a session created with no usable tag stores none: each later request is then answered in the language its own tags pick, and a different pick adds the lines again (new line ids). Sendcontext.languageon the create.example:sk-SK,sk;q=0.9,en;q=0.8
Responses
200The canceled checkout; a closed one with checkout_completed / checkout_canceled; or a business error: not_found, capabilities_incompatible.
Response headers
Cache-ControlstringEvery UCP answer (success or error) except the business profile.
always:no-storeETagstringAn ETag of the body, generated by the API on every response with a body (writes included). The API’s ETag is strong; the web server sends it weak (
W/"…") when it compresses the response, which it does for a client that sendsAccept-Encoding: gzip. AGETthat sends a matchingIf-None-Match(either form) can be answered304 Not Modifiedeven though the answer isno-store.X-RateLimit-LimitintegerRequests this client IP may make to this operation per 60-second window (600 reads, 300 writes, 60 handoff). Set on every answer that passed the rate limiter.
X-RateLimit-RemainingintegerThe operation’s limit minus this client IP’s current count. The counts live in the API server’s memory; each request is meant to leave the count 60 s after it was made, but the rate limiter (
@nestjs/throttler6.5) drops those pending decrements, for every client, whenever any client’s block ends. Read it as an upper bound and keep steady traffic at half a limit or less.minimum: 0X-RateLimit-ResetintegerSeconds left in a 60-second window the limiter keeps per client IP and operation. The count is not reset when that window ends, so this is not the moment the budget comes back.
minimum: 0
Exactly one of
- Checkoutobject · Checkout
A checkout session.
Once
completedorcanceled— and while the buyer’s handoff is done but the order not yet placed — the body is the last answer the platform received, with the currentstatus(andorder): it is not re-priced, and it does not show what the buyer changed on the shop. Only the lines and the applied discount code travel to the shop; the buyer enters address, shipping and payment there again.additionalProperties: falseThe
ucpobject of every checkout answer that returns a checkout (any status).idstring (uuid)requiredThe checkout session id.
incomplete— at least onerecoverableerror is listed (no line, no valid e-mail, no or incomplete address, no shipping to it, no or an unoffered option, a line error in this answer).requires_escalation— nothing the platform can provide is missing;payment_requiredis listed and the buyer must continue on the shop throughcontinue_url.completed— the buyer placed an order on the shop from the cart the handoff filled; seeorder. It means placed, not paid. Set shortly after the order (once the shop has started the payment and handed the order to its ERP) and best effort: a failure is only logged, so an order can exist for a checkout that never readscompleted.canceled— canceled byPOST …/cancel, or expired (an open checkout read or written afterexpires_atturnscanceled).
ready_for_completeandcomplete_in_progressare never used: the API never completes a checkout.one of:incompleterequires_escalationcompletedcanceledISO 4217 currency code.
pattern: ^[A-Z]{3}$In the shop cart’s order.
subtotal,discount?,fulfillment?,total.minItems: 2maxItems: 4Always present, possibly empty. Order: line messages and discount messages (only in the answer to the write that caused them), then
missing/invalid_address/address_undeliverable/invalid_option,pickup_point_required, andpayment_requiredlast. Empty oncompletedandcanceledreads.Always present: at most 10, possibly empty.
maxItems: 10buyerBuyerThe stored buyer; empty fields are left out. Absent when nothing is stored.
contextContextThe stored context, echoed as you sent it (after cleaning); empty fields are left out. Absent when nothing is stored.
fulfillmentFulfillmentPresent only when
dev.ucp.shopping.fulfillmentis negotiated. Oneshippingmethod with one group; there is nopickupmethod — a pickup point (Packeta) is chosen by the buyer on the shop.discountsDiscountsPresent only when
dev.ucp.shopping.discountis negotiated. Thediscounttotal is present whether or not it is.continue_urlstring (uri) · ContinueUrlPresent while the checkout is
incompleteorrequires_escalation; absent oncecompletedorcanceled.pattern: ^https://[^/]+/ucp/continue/[0-9a-f]{64}$When the session expires (UTC, ISO 8601 with milliseconds). A write extends it (cart: now + 7 days, checkout: now + 24 h); reads,
completeandcanceldo not. After a handoff nothing extends it.orderOrderPresent once
completed.
- UcpBusinessErrorResponseobject · UcpBusinessErrorResponse
HTTP 200 with no resource. The cases:
capabilities_incompatible— your profile and this shop share no version of the operation’s root capability (always for carts and checkouts on a shop in catalog mode). Nothing ran; no Idempotency-Key was used.not_found— the product, cart (Cart not found or has expired) or checkout (Checkout session not found) does not exist for this shop, is malformed, or (carts) was canceled or expired. An unknowncart_idonPOST /checkout-sessionsalso readsCheckout session not found.out_of_stock/item_unavailable— a create in which none of the requested items could be added; no session was created.handed_off— converting a cart whose lines the buyer already took to the shop.
Inside a write the last three are stored under the Idempotency-Key like a success (
capabilities_incompatibleis answered before the key is read).additionalProperties: falseThe
ucpobject of a business error. Never haspayment_handlers.capabilitiesis{}forcapabilities_incompatibleand lists the operation’s root (and its negotiated extensions) otherwise.Exactly one error message saying why no resource could be returned.
minItems: 1maxItems: 1continue_urlstring (uri)requiredWhere a buyer could go instead: the shop home (
https://{primary domain}/), orhttps://{primary domain}/cartforhanded_off. Not a handoff link.
{
"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",
"status": "canceled",
"currency": "EUR",
"line_items": [
{
"id": "f6a3c8e1-9d27-4b54-8e6a-2c1f7b9d3a58",
"item": {
"id": "1e8a6c4f-2b93-4d71-9f05-6a3c8e2b1d47",
"title": "Trek Marlin 5",
"price": 69900,
"image_url": "https://shop.example.com/api/public/uploads/c4d1e5a2-8b3f-4a6c-9e07-5f2d8a1b3c40/products/trek-marlin-5-modra.webp"
},
"quantity": 1,
"totals": [
{
"type": "subtotal",
"amount": 69900
},
{
"type": "total",
"amount": 69900
}
]
},
{
"id": "2e9d5b7a-4c83-4f16-a9b2-6d8e1c3f7a49",
"item": {
"id": "b5f81c3d-6e49-4a2b-8d73-9c0e4f1a6b25",
"title": "Fľaša na vodu 750 ml",
"price": 990
},
"quantity": 2,
"totals": [
{
"type": "subtotal",
"amount": 1980
},
{
"type": "total",
"amount": 1980
}
]
}
],
"totals": [
{
"type": "subtotal",
"amount": 71880
},
{
"type": "discount",
"amount": -5844,
"display_text": "JESEN10"
},
{
"type": "fulfillment",
"amount": 490
},
{
"type": "total",
"amount": 66526
}
],
"messages": [],
"links": [
{
"type": "terms_of_service",
"url": "https://shop.example.com/obchodne-podmienky",
"title": "Obchodné podmienky"
},
{
"type": "privacy_policy",
"url": "https://shop.example.com/ochrana-osobnych-udajov",
"title": "Ochrana osobných údajov"
}
],
"buyer": {
"email": "jana.novakova@example.com",
"last_name": "Nováková",
"first_name": "Jana",
"phone_number": "+421 905 123 456"
},
"context": {
"language": "sk",
"address_country": "SK"
},
"fulfillment": {
"methods": [
{
"id": "shipping",
"type": "shipping",
"line_item_ids": [
"f6a3c8e1-9d27-4b54-8e6a-2c1f7b9d3a58",
"2e9d5b7a-4c83-4f16-a9b2-6d8e1c3f7a49"
],
"destinations": [
{
"id": "destination",
"last_name": "Nováková",
"first_name": "Jana",
"postal_code": "811 01",
"street_address": "Hlavná 12",
"address_country": "SK",
"address_locality": "Bratislava"
}
],
"selected_destination_id": "destination",
"groups": [
{
"id": "default",
"line_item_ids": [
"f6a3c8e1-9d27-4b54-8e6a-2c1f7b9d3a58",
"2e9d5b7a-4c83-4f16-a9b2-6d8e1c3f7a49"
],
"options": [
{
"id": "0c5e8a3f-7b21-4d96-8f4c-9e2a6d1b3c75",
"title": "GLS kuriér",
"description": "Doručenie na adresu do 1–2 pracovných dní",
"totals": [
{
"type": "total",
"amount": 490
}
]
},
{
"id": "6b2d9f4e-3a17-4c68-9d5b-1f8e3a7c2b40",
"title": "Packeta – výdajné miesto",
"description": "Delivery in 2–3 business days",
"totals": [
{
"type": "total",
"amount": 290
}
]
}
],
"selected_option_id": "0c5e8a3f-7b21-4d96-8f4c-9e2a6d1b3c75"
}
]
}
]
},
"discounts": {
"codes": [
"JESEN10"
],
"applied": [
{
"code": "JESEN10",
"title": "JESEN10",
"amount": 5844,
"method": "across"
}
]
},
"expires_at": "2026-10-27T09:21:30.551Z"
}{
"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": "completed",
"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": [
{
"type": "error",
"code": "checkout_completed",
"content": "This checkout is already completed",
"severity": "unrecoverable"
}
],
"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"
}
]
},
"order": {
"id": "8e1c4f7a-2d95-4b36-a7e8-5c3f9b1d6a24",
"label": "SK2600417",
"permalink_url": "https://shop.example.com/order-confirmation?orderNumber=SK2600417&orderId=8e1c4f7a-2d95-4b36-a7e8-5c3f9b1d6a24"
}
}{
"ucp": {
"version": "2026-04-08",
"status": "error",
"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"
}
]
}
},
"messages": [
{
"type": "error",
"code": "not_found",
"content": "Checkout session not found",
"severity": "unrecoverable"
}
],
"continue_url": "https://shop.example.com/"
}{
"ucp": {
"version": "2026-04-08",
"status": "error",
"capabilities": {}
},
"messages": [
{
"type": "error",
"code": "capabilities_incompatible",
"content": "The platform and this business share no version of the capability this operation needs",
"severity": "unrecoverable"
}
],
"continue_url": "https://shop.example.com/"
}400Bad Request (Error): invalid_profile_url — the UCP-Agent header is missing or unusable (with continue_url); invalid_request "The request is not valid" — a body…
Bad Request (Error):
invalid_profile_url— theUCP-Agentheader is missing or unusable (withcontinue_url);invalid_request"The request is not valid" — a body sent as JSON that does not parse (the body is ignored, but it is parsed before routing);invalid_request"Idempotency-Key is not valid" — seeIdempotency-Key.
Response headers
Cache-ControlstringEvery UCP answer (success or error) except the business profile.
always:no-store
SchemaError
{
"code": "invalid_profile_url",
"content": "The UCP-Agent header must carry profile=\"https://…\"",
"continue_url": "https://shop.example.com/"
}{
"code": "invalid_request",
"content": "Idempotency-Key is not valid"
}{
"code": "invalid_request",
"content": "The request is not valid"
}Request with curl
curl -X POST 'https://shop.example.com/ucp/v1/checkout-sessions/3d7b9e2a-6f14-4c58-9b3e-8a2f1c6d4e97/cancel' \
-H 'UCP-Agent: profile="https://platform.example.com/.well-known/ucp"' \
-H 'Idempotency-Key: 0c2f8a4e-6b1d-4f37-9a85-3e7d1c9b5f02' \
-H 'Accept-Language: sk-SK,sk;q=0.9,en;q=0.8'Buyer handoff
continue_url (https://{shopDomain}/ucp/continue/{token}) is opened by the buyer’s browser, never by an agent. The page posts to the same URL from the buyer’s browser, which adds the session’s lines and discount code to the shop cart that browser shows and continues to /checkout or /cart (/ on a shop in catalog mode). These two routes are documented for completeness and for platforms that render the link; agents must never call them.
The buyer’s handoff page (browser only)
GET/ucp/continue/{token}
Opened by the buyer’s browser — agents and platform back-ends must never call this route. Documented so that platforms rendering continue_url know what the buyer will see.
An HTML page on the shop’s own domain. Its script reads the browser’s storefront guest-cart id (localStorage["sf-cart"], created if missing) and the tab’s storefront sign-in (sessionStorage["sf-auth"]), posts them to the same URL, and continues with location.replace to the path the claim returns (/checkout, /cart or /); without JavaScript, a link to the fallback path. It retries a 5xx / 429 twice (after 1 s and 2 s), then goes to the fallback.
- Opening the page changes nothing (link previews and prefetchers are harmless); only the page’s own POST adds the lines.
- It needs JavaScript and the shop’s browser storage. The lines land in the cart of the browser (and storage partition) that opened the link: an in-app browser or WebView keeps a cart of its own. Where storage is blocked they land in a cart no page of the shop shows, and the session is frozen all the same.
- Works while the session is open (
incomplete/requires_escalationcheckout, or a cart) and unexpired; afterwards 410. A host without UCP answers 404. Both are HTML. - Rate-limit, outage and unexpected errors are answered as UCP JSON, not HTML.
- Headers from the API:
Cache-Control: no-store,Referrer-Policy: no-referrer,X-Robots-Tag: noindex, nofollowand a strictContent-Security-Policy. The shop’s web server adds its own security headers, among them a secondReferrer-Policy: strict-origin-when-cross-origin, which a browser applies over the first (the last valid value wins). The page’s<meta name="referrer" content="no-referrer">overrides the header again, so no request the page makes carries aReferer.
What happens on the shop: Concepts and lifecycle.
Parameters
tokenpathstringrequiredThe session’s continue token: exactly 64 lower-case hexadecimal characters, the last path segment of
continue_url. It is a secret of its own, never the session id, and never changes. Anything else is treated as an unknown token.pattern: ^[0-9a-f]{64}$example:9bbd3dedf887f93c6287fa7f4b8de141501155714c3b2050bb83551e36ec780b
Responses
200The handoff page.
Response headers
Cache-ControlstringEvery UCP answer (success or error) except the business profile.
always:no-storeContent-Security-PolicystringThe handoff page’s policy: only its own inline script (by hash), same-origin
fetch, inline styles. The shop’s web server may add its own policy as a second header.example:default-src 'none'; script-src 'sha256-VYn/7oWaIZE1N3eYOOLF7hjVtQcwMb7njm95xAMr…Referrer-PolicystringWhat a browser receives on the handoff page, from two layers: the API sends
no-referrer, and the shop’s web server adds its generalstrict-origin-when-cross-originas a secondReferrer-Policyfield after it. A browser applies the last valid value, so the header alone givesstrict-origin-when-cross-origin. The page’s<meta name="referrer" content="no-referrer">, applied after the header, is what keeps the token out of everyReferer.example:no-referrer, strict-origin-when-cross-originX-Robots-TagstringThe handoff page is never indexed.
always:noindex, nofollow
The handoff page.
<!doctype html>
<html>
<head>
<meta charset="utf-8">
<meta name="referrer" content="no-referrer">
<meta name="robots" content="noindex, nofollow">
<meta name="viewport" content="width=device-width, initial-scale=1">
<title>Horské bicykle s.r.o.</title>
<style>body{font-family:system-ui,-apple-system,"Segoe UI",sans-serif;display:flex;min-height:100vh;align-items:center;justify-content:center;margin:0;color:#1f2937;background:#f8fafc}main{text-align:center;padding:24px}a{color:inherit}</style>
</head>
<body>
<main>
<p><strong>Horské bicykle s.r.o.</strong></p>
<p>…</p>
<noscript><p><a href="/checkout">Horské bicykle s.r.o.</a></p></noscript>
</main>
<script type="application/json" id="ucp-handoff">{"claimUrl":"/ucp/continue/9bbd3dedf887f93c6287fa7f4b8de141501155714c3b2050bb83551e36ec780b","fallback":"/checkout"}</script>
<script>(function () {
var data = JSON.parse(document.getElementById('ucp-handoff').textContent);
var KEY = 'sf-cart';
var AUTH_KEY = 'sf-auth';
var MAX_ATTEMPTS = 3;
function uuid() {
if (window.crypto && crypto.randomUUID) return crypto.randomUUID();
return 'xxxxxxxx-xxxx-4xxx-yxxx-xxxxxxxxxxxx'.replace(/[xy]/g, function (c) {
var r = (Math.random() * 16) | 0;
return (c === 'x' ? r : (r & 0x3) | 0x8).toString(16);
});
}
var stored = null;
try { stored = JSON.parse(localStorage.getItem(KEY) || 'null'); } catch (e) {}
var state = stored && typeof stored === 'object' && stored.state && typeof stored.state === 'object' ? stored.state : {};
var sessionId = typeof state.sessionId === 'string' && state.sessionId ? state.sessionId : uuid();
var next = { state: Object.assign({}, state, { sessionId: sessionId, hasCart: true }), version: stored && typeof stored.version === 'number' ? stored.version : 0 };
try { localStorage.setItem(KEY, JSON.stringify(next)); } catch (e) {}
var token = null;
try {
var auth = JSON.parse(sessionStorage.getItem(AUTH_KEY) || 'null');
var t = auth && auth.state && auth.state.accessToken;
if (typeof t === 'string' && /^[A-Za-z0-9_.-]{16,4096}$/.test(t)) token = t;
} catch (e) {}
function go(path) { window.location.replace(path || data.fallback); }
function retry(attempt) {
if (attempt >= MAX_ATTEMPTS) { go(null); return; }
setTimeout(function () { claim(attempt + 1); }, 1000 * attempt);
}
function claim(attempt) {
var headers = { 'Content-Type': 'application/json' };
if (token) headers.Authorization = 'Bearer ' + token;
fetch(data.claimUrl, {
method: 'POST',
headers: headers,
credentials: 'omit',
body: JSON.stringify({ sessionId: sessionId })
}).then(function (r) {
if (r.ok) {
return r.json().then(function (j) {
go(j && typeof j.redirect === 'string' && /^[/](?:[A-Za-z0-9_-]+(?:[/][A-Za-z0-9_-]+)*)?$/.test(j.redirect) ? j.redirect : null);
}, function () { go(null); });
}
if (r.status >= 500 || r.status === 429) { retry(attempt); return; }
go(null);
}, function () { retry(attempt); });
}
claim(1);
})();</script>
</body>
</html>404The host serves no UCP (switched off, domain no longer verified …): an empty “gone” page.
Response headers
Cache-ControlstringEvery UCP answer (success or error) except the business profile.
always:no-storeContent-Security-PolicystringThe handoff page’s policy: only its own inline script (by hash), same-origin
fetch, inline styles. The shop’s web server may add its own policy as a second header.example:default-src 'none'; script-src 'sha256-VYn/7oWaIZE1N3eYOOLF7hjVtQcwMb7njm95xAMr…Referrer-PolicystringWhat a browser receives on the handoff page, from two layers: the API sends
no-referrer, and the shop’s web server adds its generalstrict-origin-when-cross-originas a secondReferrer-Policyfield after it. A browser applies the last valid value, so the header alone givesstrict-origin-when-cross-origin. The page’s<meta name="referrer" content="no-referrer">, applied after the header, is what keeps the token out of everyReferer.example:no-referrer, strict-origin-when-cross-originX-Robots-TagstringThe handoff page is never indexed.
always:noindex, nofollow
The “gone” page with an empty shop name.
<!doctype html>
<html>
<head>
<meta charset="utf-8">
<meta name="referrer" content="no-referrer">
<meta name="robots" content="noindex, nofollow">
<meta name="viewport" content="width=device-width, initial-scale=1">
<title></title>
<style>body{font-family:system-ui,-apple-system,"Segoe UI",sans-serif;display:flex;min-height:100vh;align-items:center;justify-content:center;margin:0;color:#1f2937;background:#f8fafc}main{text-align:center;padding:24px}a{color:inherit}</style>
</head>
<body>
<main>
<p><strong></strong></p>
<p><a href="/"></a></p>
</main>
</body>
</html>410Gone — unknown or malformed token, or the session is completed, canceled or expired: a page with a link to the shop’s home.
Response headers
Cache-ControlstringEvery UCP answer (success or error) except the business profile.
always:no-storeContent-Security-PolicystringThe handoff page’s policy: only its own inline script (by hash), same-origin
fetch, inline styles. The shop’s web server may add its own policy as a second header.example:default-src 'none'; script-src 'sha256-VYn/7oWaIZE1N3eYOOLF7hjVtQcwMb7njm95xAMr…Referrer-PolicystringWhat a browser receives on the handoff page, from two layers: the API sends
no-referrer, and the shop’s web server adds its generalstrict-origin-when-cross-originas a secondReferrer-Policyfield after it. A browser applies the last valid value, so the header alone givesstrict-origin-when-cross-origin. The page’s<meta name="referrer" content="no-referrer">, applied after the header, is what keeps the token out of everyReferer.example:no-referrer, strict-origin-when-cross-originX-Robots-TagstringThe handoff page is never indexed.
always:noindex, nofollow
The “gone” page.
<!doctype html>
<html>
<head>
<meta charset="utf-8">
<meta name="referrer" content="no-referrer">
<meta name="robots" content="noindex, nofollow">
<meta name="viewport" content="width=device-width, initial-scale=1">
<title>Horské bicykle s.r.o.</title>
<style>body{font-family:system-ui,-apple-system,"Segoe UI",sans-serif;display:flex;min-height:100vh;align-items:center;justify-content:center;margin:0;color:#1f2937;background:#f8fafc}main{text-align:center;padding:24px}a{color:inherit}</style>
</head>
<body>
<main>
<p><strong>Horské bicykle s.r.o.</strong></p>
<p><a href="/">Horské bicykle s.r.o.</a></p>
</main>
</body>
</html>Request with curl
curl 'https://shop.example.com/ucp/continue/9bbd3dedf887f93c6287fa7f4b8de141501155714c3b2050bb83551e36ec780b'Claim the handoff (the page’s own request, browser only)
POST/ucp/continue/{token}
Opened by the buyer’s browser — agents and platform back-ends must never call this route. Documented so that platforms rendering continue_url know what the buyer will see.
Adds the session’s lines (and its applied discount code) to the shop cart this browser shows — the signed-in customer’s cart when a valid storefront token of this shop is sent as Authorization: Bearer <token> (the optional storefrontCustomerToken security scheme), else the guest cart named by sessionId — through the shop’s normal add-to-cart rules, then names the path to continue to.
- Lines are added to what the cart holds (same variant: quantities summed, capped at 999); a line the shop refuses (sold out since, over stock, unpriced) is skipped silently. The browser’s cart is never emptied: when it already holds items in another currency the lines go in that currency, or — if they cannot — nothing is added (redirect
/cart). - Only lines and the code travel; buyer data, address and shipping choice do not. The buyer enters them again on the shop’s checkout.
- Once per browser cart (or customer) and session state: a reload or a second click adds nothing. Another browser or another customer gets the lines once too. A cart link whose cart has an open checkout hands over that checkout instead.
- After a successful handoff the session is frozen for the agent (writes answer
handed_off), and an order placed from that browser cart that contains at least one of the session’s variants completes the checkout. - The claim queues behind requests on the same session; when it cannot get its turn the answer is the generic 503 with
Retry-After: 60.
Parameters
tokenpathstringrequiredThe session’s continue token: exactly 64 lower-case hexadecimal characters, the last path segment of
continue_url. It is a secret of its own, never the session id, and never changes. Anything else is treated as an unknown token.pattern: ^[0-9a-f]{64}$example:9bbd3dedf887f93c6287fa7f4b8de141501155714c3b2050bb83551e36ec780b
Request bodyrequired
Sent by the handoff page’s own script.
sessionIdstringrequiredThe browser’s storefront guest-cart id (
localStorage["sf-cart"].state.sessionId; the page creates one when there is none). 8–128 characters ofA-Z a-z 0-9 _ -, not starting withucp_. It is not the UCP session id.pattern: ^(?!ucp_)[A-Za-z0-9_-]{8,128}$
{
"sessionId": "5f0c2b1e-8d4a-4f6b-9c3e-2a1b0c9d8e7f"
}Responses
200Where the browser goes next. Also answered, with nothing added, when this browser already claimed this session state, when every line was refused, or on a shop…
Where the browser goes next. Also answered, with nothing added, when this browser already claimed this session state, when every line was refused, or on a shop in catalog mode (/).
Response headers
Cache-ControlstringEvery UCP answer (success or error) except the business profile.
always:no-storeETagstringAn ETag of the body, generated by the API on every response with a body (writes included). The API’s ETag is strong; the web server sends it weak (
W/"…") when it compresses the response, which it does for a client that sendsAccept-Encoding: gzip. AGETthat sends a matchingIf-None-Match(either form) can be answered304 Not Modifiedeven though the answer isno-store.X-RateLimit-LimitintegerRequests this client IP may make to this operation per 60-second window (600 reads, 300 writes, 60 handoff). Set on every answer that passed the rate limiter.
X-RateLimit-RemainingintegerThe operation’s limit minus this client IP’s current count. The counts live in the API server’s memory; each request is meant to leave the count 60 s after it was made, but the rate limiter (
@nestjs/throttler6.5) drops those pending decrements, for every client, whenever any client’s block ends. Read it as an upper bound and keep steady traffic at half a limit or less.minimum: 0X-RateLimit-ResetintegerSeconds left in a 60-second window the limiter keeps per client IP and operation. The count is not reset when that window ends, so this is not the moment the budget comes back.
minimum: 0
SchemaHandoffClaimResponse
{
"redirect": "/checkout"
}400invalid_request "The request is not valid": sessionId missing or not acceptable (no JSON body counts as missing), or a body sent as JSON that does not parse.
Response headers
Cache-ControlstringEvery UCP answer (success or error) except the business profile.
always:no-store
SchemaError
{
"code": "invalid_request",
"content": "The request is not valid"
}{
"code": "invalid_request",
"content": "The request is not valid"
}404not_found: "No UCP business on this host", or "Not found" when the token is unknown or malformed or the session can no longer be handed over (completed, cancel…
not_found: "No UCP business on this host", or "Not found" when the token is unknown or malformed or the session can no longer be handed over (completed, canceled, expired).
Response headers
Cache-ControlstringEvery UCP answer (success or error) except the business profile.
always:no-store
SchemaError
{
"code": "not_found",
"content": "No UCP business on this host"
}{
"code": "not_found",
"content": "Not found"
}503service_unavailable "This shop is temporarily unavailable", Retry-After: 60: the session is busy with another request (nothing was added; the page retries), or…
service_unavailable "This shop is temporarily unavailable", Retry-After: 60: the session is busy with another request (nothing was added; the page retries), or the company is deactivated / billing-locked, or the shop lookup failed.
Response headers
Cache-ControlstringEvery UCP answer (success or error) except the business profile.
always:no-storeRetry-AfterintegerSeconds to wait before retrying: 60 after
429 rate_limitedand the generic503; 1 when another request holds the session; 2 when the same Idempotency-Key is still running; 10 when profile discovery is busy; the rest of the current minute (at least 1) when your client used its 5 new profiles this minute.minimum: 1
SchemaError
{
"code": "service_unavailable",
"content": "This shop is temporarily unavailable"
}Request with curl
curl -X POST 'https://shop.example.com/ucp/continue/9bbd3dedf887f93c6287fa7f4b8de141501155714c3b2050bb83551e36ec780b' \
-H 'Content-Type: application/json' \
--data '{
"sessionId": "5f0c2b1e-8d4a-4f6b-9c3e-2a1b0c9d8e7f"
}'Shared responses
Responses several operations return (protocol errors, limits, unavailability). An operation lists each as one line with its status; here they are in full — headers, schema and examples.
NotModified
Not Modified — only when the request sent If-None-Match equal to the current answer’s ETag (strong as the API generates it on every response, or the weak W/"…" form the web server sends with a compressed answer). No body. Clients that do not send If-None-Match never see it.
Response headers
ETagstringAn ETag of the body, generated by the API on every response with a body (writes included). The API’s ETag is strong; the web server sends it weak (
W/"…") when it compresses the response, which it does for a client that sendsAccept-Encoding: gzip. AGETthat sends a matchingIf-None-Match(either form) can be answered304 Not Modifiedeven though the answer isno-store.
No body
NoBusinessOnHost
not_found. Either the host serves no UCP — an unknown host, a *.shop.monokaido.com subdomain, a domain without verified DNS or an active certificate, UCP switched off, the built-in storefront off, an inactive or expired storefront key, or the e-shop module off; one answer for all, on purpose — or the path or method does not exist under /ucp/ (an unknown method is 404, not 405). A domain that has no certificate of its own yet never gets this answer: its TLS handshake fails first. A missing cart, checkout or product is NOT this: it is a 200 business error.
Response headers
Cache-ControlstringEvery UCP answer (success or error) except the business profile.
always:no-store
SchemaError
{
"code": "not_found",
"content": "No UCP business on this host"
}{
"code": "not_found",
"content": "Not found"
}IdempotencyConflict
idempotency_conflict. The Idempotency-Key was used for a different request (409, no Retry-After), or the same request is still running (409, Retry-After: 2). Writes only.
Response headers
Cache-ControlstringEvery UCP answer (success or error) except the business profile.
always:no-storeRetry-AfterintegerSeconds to wait before retrying: 60 after
429 rate_limitedand the generic503; 1 when another request holds the session; 2 when the same Idempotency-Key is still running; 10 when profile discovery is busy; the rest of the current minute (at least 1) when your client used its 5 new profiles this minute.minimum: 1
SchemaError
{
"code": "idempotency_conflict",
"content": "This Idempotency-Key was used for a different request"
}{
"code": "idempotency_conflict",
"content": "A request with this Idempotency-Key is still being processed"
}PayloadTooLarge
Payload Too Large, from one of two layers:
- The web server (
text/html): a request body over 1 MiB, whatever its content type. Not produced by the API: nginx, the web server in front of the shop, answers it with its own HTML page. NoCache-Control: no-store. - The API (
application/json,request_too_large"Request body too large"): a body over the API’s own limits once decompressed — 2 MiB for JSON, 100 KiB or more than 1 000 fields forapplication/x-www-form-urlencoded. A JSON body reaches this through the shop’s domain only when it is sent compressed (Content-Encoding), since the web server limits the body as sent to 1 MiB.
Bodies of this API are far below either limit: 100 line items take a few kilobytes.
Response headers
Cache-ControlstringOn the API’s JSON answer only.
always:no-store
text/html
nginx error page.
<html>
<head><title>413 Request Entity Too Large</title></head>
<body>
<center><h1>413 Request Entity Too Large</h1></center>
<hr><center>nginx</center>
</body>
</html>
application/json
SchemaError
{
"code": "request_too_large",
"content": "Request body too large"
}UnsupportedMediaType
invalid_request "The request is not valid" with HTTP 415: the body declares a charset its parser does not read (JSON: charset= must start with utf-; form-encoded: utf-8 or iso-8859-1), or a Content-Encoding other than gzip, deflate, br or identity. Send Content-Type: application/json (UTF-8). A body of another content type is not read at all.
Response headers
Cache-ControlstringEvery UCP answer (success or error) except the business profile.
always:no-store
SchemaError
{
"code": "invalid_request",
"content": "The request is not valid"
}ProfileProblem
Unprocessable Content — your platform profile: profile_malformed (not JSON, no ucp object, no YYYY-MM-DD ucp.version; remembered for 60 s) or version_unsupported (ucp.version is not 2026-04-08). Both carry continue_url (the shop home). Fix the profile. A profile_malformed failure is remembered for 60 s for that exact URL; a profile of another version is a valid profile and stays cached for its max-age (60 s – 1 h, 5 min without one), so version_unsupported persists until that entry is refreshed — or change the profile URL.
Response headers
Cache-ControlstringEvery UCP answer (success or error) except the business profile.
always:no-store
SchemaError
{
"code": "profile_malformed",
"content": "The platform profile is not a valid UCP profile",
"continue_url": "https://shop.example.com/"
}{
"code": "version_unsupported",
"content": "Protocol version 2026-01-11 is not supported. This business supports version 2026-04-08.",
"continue_url": "https://shop.example.com/"
}ProfileUnreachable
Failed Dependency — profile_unreachable: your platform profile could not be fetched (network error, timeout, a redirect, a status other than 200, over 256 KiB, a non-public address). Remembered for 60 s for that exact URL.
Response headers
Cache-ControlstringEvery UCP answer (success or error) except the business profile.
always:no-store
SchemaError
{
"code": "profile_unreachable",
"content": "Unable to fetch the platform profile",
"continue_url": "https://shop.example.com/"
}RateLimited
Too Many Requests, from one of two layers:
- The API (
application/json,rate_limited,Retry-After: 60): this client IP’s count for the operation passed its limit (seeX-RateLimit-Remaining) — 600 for reads, 300 for writes, 60 for the handoff routes, each operation counted separately, across every Monokaido shop. - The web server (
text/html): 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 and images alike) and to the storefront API, not per shop. Not produced by the API: nginx, the web server in front of the shop, answers it with its own HTML page. NoRetry-After.
Response headers
Cache-ControlstringEvery UCP answer (success or error) except the business profile.
always:no-storeRetry-AfterintegerSeconds to wait before retrying: 60 after
429 rate_limitedand the generic503; 1 when another request holds the session; 2 when the same Idempotency-Key is still running; 10 when profile discovery is busy; the rest of the current minute (at least 1) when your client used its 5 new profiles this minute.minimum: 1
application/json
SchemaError
{
"code": "rate_limited",
"content": "Too many requests"
}text/html
nginx error page.
<html>
<head><title>429 Too Many Requests</title></head>
<body>
<center><h1>429 Too Many Requests</h1></center>
<hr><center>nginx</center>
</body>
</html>
InternalError
internal_error: an unexpected failure (logged on the server; no detail is sent). A failed write released its Idempotency-Key: retry with the same key.
Response headers
Cache-ControlstringEvery UCP answer (success or error) except the business profile.
always:no-store
SchemaError
{
"code": "internal_error",
"content": "Unexpected server error"
}BadGateway
Bad Gateway — the API is down or restarting. Not produced by the API: nginx, the web server in front of the shop, answers it with its own HTML page.
nginx error page.
<html>
<head><title>502 Bad Gateway</title></head>
<body>
<center><h1>502 Bad Gateway</h1></center>
<hr><center>nginx</center>
</body>
</html>
GatewayTimeout
Gateway Timeout — the API did not answer within 60 s. Not produced by the API: nginx, the web server in front of the shop, answers it with its own HTML page. A write may have been applied: retry it with the same Idempotency-Key.
nginx error page.
<html>
<head><title>504 Gateway Time-out</title></head>
<body>
<center><h1>504 Gateway Time-out</h1></center>
<hr><center>nginx</center>
</body>
</html>
CrossOriginRejected
Forbidden — invalid_request "The request is not valid" from the platform’s cross-origin write gate: the POST / PUT carried a Monokaido session cookie (accessToken, refreshToken, platformAccessToken, platformRefreshToken) and was not sent by a page the platform trusts: a Sec-Fetch-Site other than same-origin / none without a trusted Origin, or — without Sec-Fetch-Site — an Origin that is neither trusted nor on the requested host (Origin: null included). A server-side client that sends no such cookie never gets it.
Response headers
Cache-ControlstringEvery UCP answer (success or error) except the business profile.
always:no-store
SchemaError
{
"code": "invalid_request",
"content": "The request is not valid"
}Schemas
Every named schema of the document, once. The types the operations above link to lead here.
UcpVersion
The UCP protocol version. This shop implements exactly one.
2026-04-08CapabilityName
A capability this business can declare. catalog.search, catalog.lookup, cart and checkout are roots; fulfillment extends checkout; discount extends checkout and cart.
dev.ucp.shopping.catalog.searchdev.ucp.shopping.catalog.lookupdev.ucp.shopping.cartdev.ucp.shopping.checkoutdev.ucp.shopping.fulfillmentdev.ucp.shopping.discountCapabilityVersion
A negotiated capability: only its version.
additionalProperties: falseThe UCP protocol version. This shop implements exactly one.
always:2026-04-08
ResponseCapabilities
The capabilities active for this answer: the operation’s root capability and every negotiated extension of it.
- Catalog search:
dev.ucp.shopping.catalog.search. - Catalog lookup and product:
dev.ucp.shopping.catalog.lookup. - Carts:
dev.ucp.shopping.cart, plusdev.ucp.shopping.discountwhen negotiated. - Checkouts:
dev.ucp.shopping.checkout, plusdev.ucp.shopping.fulfillmentanddev.ucp.shopping.discountwhen negotiated.
An extension your profile did not declare is absent. Empty ({}) only in the capabilities_incompatible answer.
additionalProperties: falsedev.ucp.shopping.catalog.searcharray<CapabilityVersion>minItems: 1maxItems: 1dev.ucp.shopping.catalog.lookuparray<CapabilityVersion>minItems: 1maxItems: 1dev.ucp.shopping.cartarray<CapabilityVersion>minItems: 1maxItems: 1dev.ucp.shopping.checkoutarray<CapabilityVersion>minItems: 1maxItems: 1dev.ucp.shopping.fulfillmentarray<CapabilityVersion>minItems: 1maxItems: 1dev.ucp.shopping.discountarray<CapabilityVersion>minItems: 1maxItems: 1
ResponseMetadata
The ucp object of a successful catalog or cart answer. A missing status means success.
additionalProperties: falseThe UCP protocol version. This shop implements exactly one.
always:2026-04-08The capabilities active for this answer: the operation’s root capability and every negotiated extension of it.
- Catalog search:
dev.ucp.shopping.catalog.search. - Catalog lookup and product:
dev.ucp.shopping.catalog.lookup. - Carts:
dev.ucp.shopping.cart, plusdev.ucp.shopping.discountwhen negotiated. - Checkouts:
dev.ucp.shopping.checkout, plusdev.ucp.shopping.fulfillmentanddev.ucp.shopping.discountwhen negotiated.
An extension your profile did not declare is absent. Empty (
{}) only in thecapabilities_incompatibleanswer.- Catalog search:
CheckoutResponseMetadata
The ucp object of every checkout answer that returns a checkout (any status).
additionalProperties: falseThe UCP protocol version. This shop implements exactly one.
always:2026-04-08The capabilities active for this answer: the operation’s root capability and every negotiated extension of it.
- Catalog search:
dev.ucp.shopping.catalog.search. - Catalog lookup and product:
dev.ucp.shopping.catalog.lookup. - Carts:
dev.ucp.shopping.cart, plusdev.ucp.shopping.discountwhen negotiated. - Checkouts:
dev.ucp.shopping.checkout, plusdev.ucp.shopping.fulfillmentanddev.ucp.shopping.discountwhen negotiated.
An extension your profile did not declare is absent. Empty (
{}) only in thecapabilities_incompatibleanswer.- Catalog search:
payment_handlersobjectrequiredAlways
{}: no payment is taken through UCP. The buyer pays on the shop’s own checkout, reached throughcontinue_url, with the shop’s own payment methods.maxProperties: 0
ErrorResponseMetadata
The ucp object of a business error. Never has payment_handlers. capabilities is {} for capabilities_incompatible and lists the operation’s root (and its negotiated extensions) otherwise.
additionalProperties: falseThe UCP protocol version. This shop implements exactly one.
always:2026-04-08statusstringrequiredPresent only on a business error: the body carries no resource.
always:errorThe capabilities active for this answer: the operation’s root capability and every negotiated extension of it.
- Catalog search:
dev.ucp.shopping.catalog.search. - Catalog lookup and product:
dev.ucp.shopping.catalog.lookup. - Carts:
dev.ucp.shopping.cart, plusdev.ucp.shopping.discountwhen negotiated. - Checkouts:
dev.ucp.shopping.checkout, plusdev.ucp.shopping.fulfillmentanddev.ucp.shopping.discountwhen negotiated.
An extension your profile did not declare is absent. Empty (
{}) only in thecapabilities_incompatibleanswer.- Catalog search:
MessageError
An error message. Always carries a severity.
additionalProperties: falsetypestringrequiredalways:errorcodestringrequiredMachine-readable reason. Codes this shop emits:
recoverable—missing(path$.line_items,$.buyer.email,$.fulfillment.methods[0].destinationsor$.fulfillment.methods[0].groups[0].selected_option_id),invalid_address,address_undeliverable,invalid_option,item_unavailable,out_of_stock,vat_mix_not_allowed. Any of them keeps a checkoutincomplete.requires_buyer_input—payment_required(always on arequires_escalationcheckout),pickup_point_required(the selected shipping option needs a pickup point, which the buyer picks on the shop; it does not blockrequires_escalation).unrecoverable—not_found,capabilities_incompatible,handed_off,checkout_completed,checkout_canceled, andout_of_stock/item_unavailablewhen a create could add none of the requested items.
Treat an unknown code by its
typeandseverity.contentstringrequiredA sentence in English for logs and developers. Not localised; do not parse it.
pathstringJSONPath into the RETURNED resource (not the request), e.g.
$.line_items[1].quantity. Line items are returned in the shop cart’s order, which need not be your order: match lines byidoritem.id, never by position. Absent when the message is about the whole request or about a line that is not in the answer.pattern: ^\$severitystringrequiredrecoverable: the platform can fix it with another request.requires_buyer_input: only the buyer can, on the shop (throughcontinue_url).unrecoverable: retrying the same request cannot help.requires_buyer_reviewis part of the UCP vocabulary but never emitted by this shop.one of:recoverablerequires_buyer_inputrequires_buyer_reviewunrecoverable
MessageWarning
A warning: the request was processed, something was adjusted or refused. Never has a severity.
additionalProperties: falsetypestringrequiredalways:warningcodestringrequiredCodes this shop emits:
quantity_adjusted(a quantity was lowered: to the sellable stock, or to 999 when several lines of one item add up to more — a single line above 999 is lowered to 999 without a message),discount_code_combination_disallowed,discount_code_expired,discount_code_user_ineligible,discount_code_invalid(path$.discounts.codes[i]),handed_off(the buyer already continued on the shop; nothing was changed),price_filter_ignored(path$.filters.price— the request field: no returned resource carries the filter).contentstringrequiredA sentence in English for logs and developers. Not localised; do not parse it.
pathstringJSONPath into the RETURNED resource (not the request), e.g.
$.line_items[1].quantity. Line items are returned in the shop cart’s order, which need not be your order: match lines byidoritem.id, never by position. Absent when the message is about the whole request or about a line that is not in the answer.pattern: ^\$
MessageInfo
An informational message. Never has a severity or a path.
additionalProperties: falsetypestringrequiredalways:infocodestringrequiredThis shop emits
not_found(catalog lookup: an identifier that matched nothing).contentstringrequiredFor
not_found: the identifier as you sent it (after trimming).
Message
A business message. Discriminated by type.
Exactly one of
- MessageErrorobject · MessageError
An error message. Always carries a
severity.additionalProperties: falsetypestringrequiredalways:errorcodestringrequiredMachine-readable reason. Codes this shop emits:
recoverable—missing(path$.line_items,$.buyer.email,$.fulfillment.methods[0].destinationsor$.fulfillment.methods[0].groups[0].selected_option_id),invalid_address,address_undeliverable,invalid_option,item_unavailable,out_of_stock,vat_mix_not_allowed. Any of them keeps a checkoutincomplete.requires_buyer_input—payment_required(always on arequires_escalationcheckout),pickup_point_required(the selected shipping option needs a pickup point, which the buyer picks on the shop; it does not blockrequires_escalation).unrecoverable—not_found,capabilities_incompatible,handed_off,checkout_completed,checkout_canceled, andout_of_stock/item_unavailablewhen a create could add none of the requested items.
Treat an unknown code by its
typeandseverity.contentstringrequiredA sentence in English for logs and developers. Not localised; do not parse it.
pathstringJSONPath into the RETURNED resource (not the request), e.g.
$.line_items[1].quantity. Line items are returned in the shop cart’s order, which need not be your order: match lines byidoritem.id, never by position. Absent when the message is about the whole request or about a line that is not in the answer.pattern: ^\$severitystringrequiredrecoverable: the platform can fix it with another request.requires_buyer_input: only the buyer can, on the shop (throughcontinue_url).unrecoverable: retrying the same request cannot help.requires_buyer_reviewis part of the UCP vocabulary but never emitted by this shop.one of:recoverablerequires_buyer_inputrequires_buyer_reviewunrecoverable
- MessageWarningobject · MessageWarning
A warning: the request was processed, something was adjusted or refused. Never has a
severity.additionalProperties: falsetypestringrequiredalways:warningcodestringrequiredCodes this shop emits:
quantity_adjusted(a quantity was lowered: to the sellable stock, or to 999 when several lines of one item add up to more — a single line above 999 is lowered to 999 without a message),discount_code_combination_disallowed,discount_code_expired,discount_code_user_ineligible,discount_code_invalid(path$.discounts.codes[i]),handed_off(the buyer already continued on the shop; nothing was changed),price_filter_ignored(path$.filters.price— the request field: no returned resource carries the filter).contentstringrequiredA sentence in English for logs and developers. Not localised; do not parse it.
pathstringJSONPath into the RETURNED resource (not the request), e.g.
$.line_items[1].quantity. Line items are returned in the shop cart’s order, which need not be your order: match lines byidoritem.id, never by position. Absent when the message is about the whole request or about a line that is not in the answer.pattern: ^\$
- MessageInfoobject · MessageInfo
An informational message. Never has a
severityor apath.additionalProperties: falsetypestringrequiredalways:infocodestringrequiredThis shop emits
not_found(catalog lookup: an identifier that matched nothing).contentstringrequiredFor
not_found: the identifier as you sent it (after trimming).
Error
A protocol error: the request could not be processed. It carries no ucp object and is always Cache-Control: no-store.
additionalProperties: falsecodestringrequiredThe protocol error code (status in brackets):
invalid_profile_url(400),invalid_request(400; 403 from the cross-origin write gate; 415),request_too_large(400 for more than 20 lookup ids; 413 for a body over the API’s limits once decompressed: JSON 2 MiB, form-encoded 100 KiB or more than 1 000 fields — a body over 1 MiB as sent is refused by the shop’s web server first, with an HTML 413),not_found(404),idempotency_conflict(409),profile_malformedandversion_unsupported(422),profile_unreachable(424),rate_limited(429),internal_error(500),service_unavailable(503).one of:invalid_profile_urlprofile_unreachableprofile_malformedversion_unsupportedidempotency_conflictrequest_too_largeinvalid_requestnot_foundrate_limitedservice_unavailableinternal_errorcontentstringrequiredA sentence in English. For
invalid_requestraised by a body check it names the field, e.g.line_items[0].quantity must be a positive integer; errors raised before the body is read (malformed JSON, an unsupported charset) say onlyThe request is not valid.continue_urlstring (uri)The shop’s home page (
https://{primary domain}/). Present only on the discovery errors:invalid_profile_url,profile_unreachable,profile_malformed,version_unsupportedand the discovery-budgetservice_unavailable.
CurrencyCode
ISO 4217 currency code.
pattern: ^[A-Z]{3}$Money
An amount with its currency.
additionalProperties: falseamountintegerrequiredISO 4217 minor units (EUR: cents; JPY: whole yen; HUF: fillér, so 1 990 Ft = 199000). VAT-inclusive whenever the shop knows the gross price, with the shop’s markup rules, conversion and rounding applied — the price its own product page shows.
ISO 4217 currency code.
pattern: ^[A-Z]{3}$
PriceRange
ProductDescription
The product’s description.
additionalProperties: falseplainstringrequiredPlain text: the short description, else the description, with tags removed and common entities decoded; the product title when both are empty. At most 5 000 characters.
maxLength: 5000htmlstringThe stored (translated) description HTML, trimmed, at most 50 000 characters (cut, possibly mid-tag). Not sanitised by UCP — sanitise before rendering. Absent when the product has no description.
maxLength: 50000
VariantDescription
The variant’s description.
additionalProperties: falseplainstringrequiredThe same text as the variant’s
title.
Media
A product image, in the shop’s gallery order (main image first).
additionalProperties: falsetypestringrequiredAlways
image.always:imageurlstringrequiredAbsolute URL. The shop’s own copies are served from
https://{primary domain}/api/public/…; vendor images keep the vendor’s URL as stored (some vendor hosts refuse unknown clients).pattern: ^[Hh][Tt][Tt][Pp][Ss]?://alt_textstringThe image’s (translated) alternative text, when set.
Barcode
The variant’s GTIN.
additionalProperties: falsetypestringrequiredUPCwhen the stored GTIN has exactly 12 characters,EANotherwise (GTIN-8, -13 and -14 all read asEAN).one of:EANUPCvaluestringrequiredThe GTIN as stored, trimmed.
Availability
Stock state, as the shop’s own stock badge reads it.
additionalProperties: falseavailablebooleanrequiredWhether the variant can be bought now:
trueforin_stockandpreorder,falseforout_of_stockanddiscontinued.statusstringrequireddiscontinued: the product is not orderable.preorder: a warehouse marks it pre-order (no stock limit).in_stock: on-hand minus reserved stock is above 0.out_of_stock: otherwise. Stock counts are not published.one of:in_stockpreorderout_of_stockdiscontinued
SelectedOption
One option value of a variant.
additionalProperties: falsenamestringrequiredOption name, e.g.
Veľkosť.labelstringrequiredOption value, e.g.
M.
VariantInput
Which requested identifier this variant answers.
additionalProperties: falseidstringrequiredThe identifier from your
ids, as you sent it (after trimming; upper-case UUIDs stay upper-case).matchstringrequiredexact: the identifier is this variant’s id or SKU.featured: it named the product (id or handle) and this is its featured variant.one of:exactfeatured
Variant
A purchasable variant. Only active variants with a price in the shop’s region are listed.
additionalProperties: falseidstring (uuid)requiredThe variant id — the value to put in
line_items[].item.id.skustringThe variant’s SKU, as the shop stores it. Not guaranteed unique — variants of different products may share one. Absent when empty.
barcodesarray<Barcode>Present only when a GTIN is stored.
minItems: 1maxItems: 1titlestringrequiredThe option labels joined by
/(e.g.Modrá / M); the product title for a variant without options.The variant’s description.
urlstring (uri)requiredThe product’s page on the shop (no variant parameter).
An amount with its currency.
list_priceMoneyThe compare-at price, only when it is higher than
price. Shown as the shop shows it; the shop may store it without VAT, so treat it as indicative.Stock state, as the shop’s own stock badge reads it.
optionsarray<SelectedOption>The variant’s option values (after the shop’s value mapping). Absent when it has none.
minItems: 1inputsarray<VariantInput>Catalog lookup only: the identifiers this variant answers.
minItems: 1
ProductOption
An option of the product and the values its listed variants have.
additionalProperties: falsenamestringrequiredOption name.
valuesarray<object>requiredValues in first-seen order over the listed variants.
minItems: 1labelstringrequiredOption value.
ProductDetailOption
An option with availability signals relative to selected.
additionalProperties: falsenamestringrequiredOption name.
valuesarray<object>requiredValues in first-seen order.
minItems: 1labelstringrequiredOption value.
existsbooleanrequiredSome variant has this value together with every other option value of the selected variant.
availablebooleanrequiredSuch a variant is
availability.available.
Product
A product as search and lookup return it. Fields not published: categories, ratings, stock counts, delivery times, sales-unit data.
In lookup answers variants holds only the variants your identifiers matched (each with inputs), while price_range, list_price_range and options still describe all of the product’s priced variants (within the price filter, when one is set).
additionalProperties: falseidstring (uuid)requiredThe product id. Stable for the product’s life.
handlestringThe product’s URL slug in the answer’s language. Absent when the product has none.
titlestringrequiredThe product title in the answer’s language (prefix and suffix included).
The product’s description.
urlstring (uri)requiredThe product page on the shop’s primary domain:
https://{primary domain}/products/{handle, else id}.Over the listed variants’
price.list_price_rangePriceRangeOver the variants’
list_price; absent when none has one.mediaarray<Media>Up to 20 images; absent when there is none.
minItems: 1maxItems: 20optionsarray<ProductOption>The product’s options; absent when variants have no options.
minItems: 1Priced variants (within the price filter, if any). The first available variant comes first; the others follow in the order the shop’s catalog returns them, which is not guaranteed to be stable.
minItems: 1tagsarray<string>Only the brand name, when the product has a brand.
minItems: 1maxItems: 1
ProductDetail
A product as getProduct returns it: every priced variant, the selected one first, and per-value availability.
additionalProperties: falseidstring (uuid)requiredThe product id. Stable for the product’s life.
handlestringThe product’s URL slug in the answer’s language. Absent when the product has none.
titlestringrequiredThe product title in the answer’s language (prefix and suffix included).
The product’s description.
urlstring (uri)requiredThe product page on the shop’s primary domain:
https://{primary domain}/products/{handle, else id}.Over the listed variants’
price.list_price_rangePriceRangeOver the variants’
list_price; absent when none has one.mediaarray<Media>Up to 20 images; absent when there is none.
minItems: 1maxItems: 20optionsarray<ProductDetailOption>The product’s options with availability relative to
selected; absent when variants have no options.minItems: 1All priced variants. The selected (anchor) variant comes first.
minItems: 1tagsarray<string>Only the brand name, when the product has a brand.
minItems: 1maxItems: 1selectedarray<SelectedOption>The option values of the selected variant (
variants[0]) — not necessarily what you sent. Absent when it has no options.minItems: 1
RequestContext
Buyer context. Every field is optional; strings longer than the stated length are cut (counted in code points). Unknown fields (intent, eligibility, payment …) are ignored.
address_countrystring | nullThe buyer’s country: ISO 3166-1 alpha-2 (
SK;UKreads asGB), alpha-3 (SVK) or an English, Slovak, Czech, Hungarian, German … country name. Not recognised → silently dropped. Stored and echoed only: it does not change prices or shipping (shipping uses the destination’s country).maxLength: 60address_regionstring | nullRegion. Stored and echoed only.
maxLength: 100postal_codestring | nullPostal code. Stored and echoed only.
maxLength: 20languagestring | nullPreferred language, a BCP 47-like tag matching
^[A-Za-z]{2,3}(-[A-Za-z0-9]{1,8})*$and at most 35 characters (otherwise silently dropped). Tried beforeAccept-Language; see that header for the matching rules. It decides the currency.maxLength: 100currencystring | nullISO 4217 code (
^[A-Za-z]{3}$, upper-cased; otherwise silently dropped). Does not change the currency of prices, which follows the language. In catalog requests it only says which currencyfilters.priceis in (without it, the bounds are in the answer’s currency).maxLength: 10
PriceFilter
Keeps a variant when min ≤ price.amount ≤ max (the final VAT-inclusive amount); a product stays while any variant does. The bounds are in context.currency, else (no context.currency, or one the API cannot read) in the currency of the answer, which the language decides. In the answer’s currency → used as sent; another currency → converted at the shop’s exchange rate (min rounded down, max up), or — with no rate — ignored with a price_filter_ignored warning.
Decimals and negative numbers → 400 invalid_request. min > max matches nothing.
mininteger | nullLowest price, inclusive, in minor units of
context.currency(else of the answer’s currency).minimum: 0maxinteger | nullHighest price, inclusive, in minor units of
context.currency(else of the answer’s currency).minimum: 0
SearchFilters
Filters. Other filters (brand, availability …) are ignored without a message.
pricePriceFilterKeeps a variant when
min ≤ price.amount ≤ max(the final VAT-inclusive amount); a product stays while any variant does. The bounds are incontext.currency, else (nocontext.currency, or one the API cannot read) in the currency of the answer, which the language decides. In the answer’s currency → used as sent; another currency → converted at the shop’s exchange rate (min rounded down, max up), or — with no rate — ignored with aprice_filter_ignoredwarning.Decimals and negative numbers →
400 invalid_request.min > maxmatches nothing.categoriesarray<string> | nullNot supported. A list holding any non-blank value →
400 invalid_request"filters.categories is not supported by this shop; search by query (and filters.price) instead". An empty list reads as absent.each item:pattern: ^\s*$
CatalogSearchRequest
A catalog search. The whole body is optional: no body (or {}) lists every product. Unknown fields (signals, …) are ignored.
querystring | nullFree text; longer text is cut to 200 characters. Matches, case-insensitively (but not ignoring diacritics) as a substring, the product title in the answer’s language or the product code / external ids — not descriptions, brands, variant SKUs or GTINs (use lookup for SKUs). Absent: every listed product.
maxLength: 200filtersSearchFiltersFilters. Other filters (brand, availability …) are ignored without a message.
paginationobjectPaging.
limitintegerPage size, default 10. A value above 20 is lowered to 20; 0, decimals and strings → 400.
default:10minimum: 1cursorstring | nullpagination.cursorfrom the previous answer. Opaque, at most 40 characters. It carries only the page number: resend the samequery,filters,limitandcontextwith it.maxLength: 40
contextRequestContext | nullBuyer context. Every field is optional; strings longer than the stated length are cut (counted in code points). Unknown fields (
intent,eligibility,payment…) are ignored.
CatalogLookupRequest
A batch lookup by identifier. Unknown fields are ignored.
idsarray<string>requiredUp to 20 identifiers, each a variant id (any case), a variant SKU (exact, case-sensitive; several products may share one), a product id or a product handle (in any language). Non-string and blank entries are dropped, exact duplicates removed. Empty or missing → 400
invalid_request; more than 20 → 400request_too_large.minItems: 1maxItems: 20each item:maxLength: 255filtersSearchFiltersFilters. Other filters (brand, availability …) are ignored without a message.
contextRequestContext | nullBuyer context. Every field is optional; strings longer than the stated length are cut (counted in code points). Unknown fields (
intent,eligibility,payment…) are ignored.
GetProductRequest
A product detail request. preferences and other unknown fields are ignored.
idstringrequiredA product id, a product handle or a variant id (SKUs are not accepted here). Required; longer text is cut to 255 characters.
minLength: 1maxLength: 255selectedarray<object> | nullOption values to select (names and labels compared case-insensitively). The variant matching all of them becomes the anchor; when none does, selections are dropped from the end until one matches.
selectedwins over a variant id inid. Entries without both fields are skipped; at most the first 20 are read. Relaxation is not reported.maxItems: 20namestringrequiredOption name.
maxLength: 100labelstringrequiredOption value.
maxLength: 255
filtersSearchFiltersOnly
filters.categoriesis checked (and refused when non-blank);filters.priceis ignored here.contextRequestContext | nullBuyer context. Every field is optional; strings longer than the stated length are cut (counted in code points). Unknown fields (
intent,eligibility,payment…) are ignored.
CatalogPagination
Search paging.
additionalProperties: falsehas_next_pagebooleanrequiredWhether another page may follow. Pages can be short or even empty while this is
true(products without a priced variant, or outside the price filter, are dropped after the page is read): keep followingcursoruntil it isfalse.cursorstringSend it as
pagination.cursorfor the next page. Present only whenhas_next_pageistrue.maxLength: 40total_countintegerNumber of listed products matching
query, counted before the per-variant price checks. Left out whenever a price filter is in effect.minimum: 0
CatalogSearchResponse
A page of search results.
additionalProperties: falseThe
ucpobject of a successful catalog or cart answer. A missingstatusmeans success.Newest product first (there is no relevance ranking).
Search paging.
messagesarray<Message>Present only when non-empty. Search emits only
price_filter_ignored.minItems: 1
CatalogLookupResponse
Lookup results. No pagination.
additionalProperties: falseThe
ucpobject of a successful catalog or cart answer. A missingstatusmeans success.One entry per product, in the order its first identifier appears in
ids; each holds only the matched variants.messagesarray<Message>Present only when non-empty:
price_filter_ignoredfirst, then oneinfonot_foundper identifier that matched nothing, inidsorder.minItems: 1
GetProductResponse
One product in detail.
additionalProperties: falseThe
ucpobject of a successful catalog or cart answer. A missingstatusmeans success.A product as
getProductreturns it: every priced variant, the selected one first, and per-value availability.
LineItemRequest
One requested line.
idstring | nullA line id from an earlier answer. Accepted and ignored (whatever its type): lines are matched by
item.id.maxLength: 100itemobjectrequiredWhat to buy.
idstringrequiredA variant id from the catalog (longer text is cut to 100 characters). A product id is accepted too and means the product’s active variant created first — not necessarily the featured, available or priced variant the catalog shows (the answer then names that variant). Ids are checked against the company’s published products, not against this storefront’s catalog: its exclusion filters do not apply, so send only variant ids this shop’s catalog returned. Anything else — a SKU, an unknown id — is not a 400: the line is left out with an
item_unavailablemessage.minLength: 1maxLength: 100
quantityintegerHow many, default 1. 0, negatives, decimals and strings → 400. A value above 999 is lowered to 999 without a message; lines of the same variant are summed, and a sum above 999 is lowered with a
quantity_adjustedwarning. A quantity above the sellable stock is lowered withquantity_adjusted; a sold-out variant is left out without_of_stock. Pre-order variants are never limited.default:1minimum: 1
BuyerRequest
Replaces the whole stored buyer: a field you leave out is erased (send the e-mail again when you add a name). null clears the buyer; leaving buyer out keeps it. A non-string value → 400; a longer string is cut to the stated length (counted in code points). Names and phone are never required.
first_namestring | nullAt most 100 characters.
maxLength: 100last_namestring | nullAt most 100 characters.
maxLength: 100emailstring | nullAt most 254 characters. Lower-cased when it looks like an address (
^[^@\s"]{1,64}@[^@\s"]{1,255}$); any other string is stored as sent, and a checkout then reportsmissingat$.buyer.email.maxLength: 254phone_numberstring | nullAt most 40 characters.
maxLength: 40
PostalAddressRequest
A shipping address. Strings longer than stated are cut; a non-string → 400.
idstring | nullYour id for this address, used only to pick it with
selected_destination_id. Not echoed: the answer always saysdestination.maxLength: 100street_addressstring | nullStreet and number. Required for a ready checkout.
maxLength: 255extended_addressstring | nullApartment, floor …
maxLength: 255address_localitystring | nullCity. Required for a ready checkout.
maxLength: 100address_regionstring | nullRegion.
maxLength: 100postal_codestring | nullPostal code. Required for a ready checkout.
maxLength: 20address_countrystring | nullCountry: alpha-2, alpha-3 or a country name. Required for a ready checkout. On the chosen destination an unrecognised value → 400
invalid_request"…address_country is not a recognised country; send an ISO 3166-1 alpha-2 code such as "SK"".maxLength: 60first_namestring | nullRecipient first name.
maxLength: 100last_namestring | nullRecipient last name.
maxLength: 100phone_numberstring | nullRecipient phone.
maxLength: 40
FulfillmentRequest
Shipping (only read when dev.ucp.shopping.fulfillment is negotiated; ignored on carts — a cart stores it but never uses it, and a conversion takes fulfillment from the conversion request). null or a body without fulfillment keeps what the session holds.
methodsarray<object> | nullOnly the first method (of the first 10) whose
typeisshippingor absent is read;pickupand other types are ignored.typestring | nullshipping(or absent). Other types are ignored.destinationsarray<PostalAddressRequest> | nullAddresses (the first 10 are read). The one named by
selected_destination_id, else the first, becomes the session’s shipping address and replaces the stored one. A stored address cannot be removed, only replaced.selected_destination_idstring | nullThe
idof the address to use. A non-string, or one that names no address, is ignored: the first address is used.maxLength: 100groupsarray<object>Only
groups[0]is read (a non-array is ignored).selected_option_idstring | nullAn option
idfromfulfillment.methods[0].groups[0].optionsof an earlier answer.nullclears the choice; absent — or an empty string or a non-string — keeps it. A choice that is not (or no longer) offered stays stored, reads asnulland addsinvalid_option.maxLength: 100
DiscountsRequest
Discount codes (only read when dev.ucp.shopping.discount is negotiated). Without the extension a code is never applied: one applied earlier is taken off the session’s cart for that answer (the stored codes are kept, and a later request that negotiates the extension applies them again).
codesarray<string> | nullDiscount codes, replacing the stored list;
[]clears them,nullor absent keeps them. At most 10 (counted before duplicates are removed; more → 400), each cut to 50 characters; blanks and non-strings dropped; duplicates removed case-insensitively. The shop applies one code: the first it accepts. Every later code getsdiscount_code_combination_disallowed; a refused code getsdiscount_code_expired,discount_code_user_ineligible(below the code’s minimum order total, which is compared with the subtotal without VAT, before the discount) ordiscount_code_invalid. On a session that has never held a line every code reads asdiscount_code_invalid; the stored codes are tried again on every create and update (not on cancel).maxItems: 10each item:maxLength: 50
CartCreateRequest
A new cart. Unknown fields are ignored. Body problems are reported before anything is stored.
The lines. Required;
[]creates an empty cart. If lines are sent and none can be added the answer is a 200 business error and no cart exists.maxItems: 100buyerBuyerRequest | nullReplaces the whole stored buyer: a field you leave out is erased (send the e-mail again when you add a name).
nullclears the buyer; leavingbuyerout keeps it. A non-string value → 400; a longer string is cut to the stated length (counted in code points). Names and phone are never required.contextRequestContext | nullBuyer context. Every field is optional; strings longer than the stated length are cut (counted in code points). Unknown fields (
intent,eligibility,payment…) are ignored.discountsDiscountsRequest | nullDiscount codes (only read when
dev.ucp.shopping.discountis negotiated). Without the extension a code is never applied: one applied earlier is taken off the session’s cart for that answer (the stored codes are kept, and a later request that negotiates the extension applies them again).fulfillmentFulfillmentRequest | nullAccepted and stored, never used for a cart (validated like a checkout’s when the fulfillment extension is negotiated, so a malformed one is still a 400).
CartUpdateRequest
A cart update. Fields you leave out keep their stored value (except that line_items is required). Every write extends expires_at to 7 days from now.
Required, full replacement: lines not listed are removed. Missing → 400
line_items must be an array.maxItems: 100buyerBuyerRequest | nullReplaces the whole stored buyer: a field you leave out is erased (send the e-mail again when you add a name).
nullclears the buyer; leavingbuyerout keeps it. A non-string value → 400; a longer string is cut to the stated length (counted in code points). Names and phone are never required.contextRequestContext | nullBuyer context. Every field is optional; strings longer than the stated length are cut (counted in code points). Unknown fields (
intent,eligibility,payment…) are ignored.discountsDiscountsRequest | nullDiscount codes (only read when
dev.ucp.shopping.discountis negotiated). Without the extension a code is never applied: one applied earlier is taken off the session’s cart for that answer (the stored codes are kept, and a later request that negotiates the extension applies them again).fulfillmentFulfillmentRequest | nullAccepted and stored, never used for a cart (validated like a checkout’s when the fulfillment extension is negotiated, so a malformed one is still a 400).
CheckoutCreateRequest
A new checkout session, directly from lines or from a cart. Send line_items, or cart_id to convert a cart (read only when dev.ucp.shopping.cart is negotiated). Without a usable cart_id, line_items is required: a missing one is 400 invalid_request "line_items must be an array". Unknown fields are ignored.
cart_idstringConvert this cart (only when
dev.ucp.shopping.cartis negotiated; otherwise it is ignored andline_itemsis required). The checkout takes the cart’s lines, buyer, context and language —line_items,buyerandcontextin this request are ignored (buyerandcontextare still checked: a malformed one is a 400) — itsfulfillmentfrom this request, and its discount codes from this request whendiscounts.codesis sent (else the cart’s). The cart itself is not changed. One open checkout per cart: when the cart already has one, that checkout is answered with HTTP 200 and this request’s fields are not applied. A cart whose lines the buyer already took to the shop → 200handed_off. An unknown id → 200not_found("Checkout session not found").line_itemsarray<LineItemRequest>The lines (direct create). Required unless
cart_idconverts a cart;[]creates an empty,incompletecheckout.maxItems: 100buyerBuyerRequest | nullReplaces the whole stored buyer: a field you leave out is erased (send the e-mail again when you add a name).
nullclears the buyer; leavingbuyerout keeps it. A non-string value → 400; a longer string is cut to the stated length (counted in code points). Names and phone are never required.contextRequestContext | nullBuyer context. Every field is optional; strings longer than the stated length are cut (counted in code points). Unknown fields (
intent,eligibility,payment…) are ignored.fulfillmentFulfillmentRequest | nullShipping (only read when
dev.ucp.shopping.fulfillmentis negotiated; ignored on carts — a cart stores it but never uses it, and a conversion takesfulfillmentfrom the conversion request).nullor a body withoutfulfillmentkeeps what the session holds.discountsDiscountsRequest | nullDiscount codes (only read when
dev.ucp.shopping.discountis negotiated). Without the extension a code is never applied: one applied earlier is taken off the session’s cart for that answer (the stored codes are kept, and a later request that negotiates the extension applies them again).
CheckoutUpdateRequest
A checkout update. Fields you leave out keep their stored value; {} is valid: it re-derives the status, quotes the shipping options again, applies the stored discount codes again (with the discount extension) and extends expires_at; it does not check the lines again. Every update of an open checkout extends expires_at to 24 h from now.
line_itemsarray<LineItemRequest>Optional. When sent, full replacement; absent keeps the lines.
maxItems: 100buyerBuyerRequest | nullReplaces the whole stored buyer: a field you leave out is erased (send the e-mail again when you add a name).
nullclears the buyer; leavingbuyerout keeps it. A non-string value → 400; a longer string is cut to the stated length (counted in code points). Names and phone are never required.contextRequestContext | nullBuyer context. Every field is optional; strings longer than the stated length are cut (counted in code points). Unknown fields (
intent,eligibility,payment…) are ignored.fulfillmentFulfillmentRequest | nullShipping (only read when
dev.ucp.shopping.fulfillmentis negotiated; ignored on carts — a cart stores it but never uses it, and a conversion takesfulfillmentfrom the conversion request).nullor a body withoutfulfillmentkeeps what the session holds.discountsDiscountsRequest | nullDiscount codes (only read when
dev.ucp.shopping.discountis negotiated). Without the extension a code is never applied: one applied earlier is taken off the session’s cart for that answer (the stored codes are kept, and a later request that negotiates the extension applies them again).
CheckoutCompleteRequest
Ignored. Any payment, payment_data or AP2 field is ignored and never stored: no payment is processed through UCP. The body still counts for the Idempotency-Key’s request identity.
<any other key>any
LineItemTotal
A line amount. subtotal and total are equal: discounts are not allocated to lines.
additionalProperties: falsetypestringrequiredone of:subtotaltotalamountintegerrequiredThe line’s total (unit price × quantity). Integer in ISO 4217 minor units of the session’s
currency(EUR: cents), VAT included.minimum: 0
LineItem
A line of the shop cart that prices the session.
additionalProperties: falseidstring (uuid)requiredThe line id. Stable while the line exists; new after a language change, and different in a checkout made from a cart.
itemobjectrequiredThe variant on this line.
additionalProperties: falseidstring (uuid)requiredThe variant id — also when the request named a product id.
titlestringrequiredThe product title in the session’s language (the variant id when there is none) — never the variant’s title: two lines of one product read the same. Identify the variant by
item.id(keep thetitleandoptionsthe catalog showed for it, or look it up).priceintegerrequiredUnit price. Integer in ISO 4217 minor units of the session’s
currency(EUR: cents), VAT included.minimum: 0image_urlstringThe product’s main image, absolute. Absent when there is none.
pattern: ^[Hh][Tt][Tt][Pp][Ss]?://
quantityintegerrequiredQuantity after adjustments (stock, the 999 cap), in the unit the shop sells — one package for goods sold in packages. UCP publishes no package content.
minimum: 1maximum: 999[subtotal, total].minItems: 2maxItems: 2
Total
One total. Order: subtotal, discount?, fulfillment?, total; every entry except total sums to total. Payment-method surcharges are chosen on the shop and never quoted.
additionalProperties: falsetypestringrequiredsubtotal: sum of the line totals.discount(negative; only when a code is applied with an amount above 0): what the shop deducts — a percentage code is a percentage of the subtotal WITHOUT VAT, deducted from this VAT-inclusive subtotal, exactly as the shop’s checkout charges it.fulfillment(checkouts, only once a valid shipping option is selected; may be 0): the shipping fee.total:subtotal + discount + fulfillment.one of:subtotaldiscountfulfillmenttotalamountintegerrequiredThe amount. Integer in ISO 4217 minor units of the session’s
currency(EUR: cents), VAT included.display_textstringOn
discount: the applied code, upper-cased.
Buyer
The stored buyer; empty fields are left out. Absent when nothing is stored.
minProperties: 1additionalProperties: falsefirst_namestringAs stored.
last_namestringAs stored.
emailstringAs stored (lower-cased when it looks like an address).
phone_numberstringAs stored.
Context
The stored context, echoed as you sent it (after cleaning); empty fields are left out. Absent when nothing is stored.
minProperties: 1additionalProperties: falseaddress_countrystringAlpha-2 country code.
pattern: ^[A-Z]{2}$address_regionstringAs sent.
postal_codestringAs sent.
languagestringThe language you sent — not necessarily the one the session uses (that is not published; the currency and titles show it).
currencystringAs sent (upper-cased). Not the currency of the prices.
pattern: ^[A-Z]{3}$
AppliedDiscount
The code the shop applied.
additionalProperties: falsecodestringrequiredThe applied code, upper-cased.
titlestringrequiredThe same as
code.amountintegerrequiredThe deduction, positive. Integer in ISO 4217 minor units of the session’s
currency(EUR: cents), VAT included.minimum: 1methodstringrequiredAlways
across(spread over the whole cart).always:across
Discounts
Present only when dev.ucp.shopping.discount is negotiated. The discount total is present whether or not it is.
additionalProperties: falsecodesarray<string>requiredThe codes you asked for, as stored (your spelling, duplicates removed).
maxItems: 10At most one entry: the session’s cart holds one code.
maxItems: 1
ShippingDestination
The stored shipping address; empty fields are left out.
additionalProperties: falseidstringrequiredAlways
destination(your own id is not echoed).always:destinationstreet_addressstringAs sent.
extended_addressstringAs sent.
address_localitystringAs sent.
address_regionstringAs sent.
postal_codestringAs sent.
address_countrystringAlpha-2 country code.
pattern: ^[A-Z]{2}$first_namestringAs sent.
last_namestringAs sent.
phone_numberstringAs sent.
FulfillmentOption
A shipping option: one of the shop’s active shipping methods, filtered by the destination country (no destination → no country filter) and by the methods’ product rules. No carrier code, delivery date or pickup flag is published.
additionalProperties: falseidstring (uuid)requiredThe shipping method’s id — the value for
selected_option_id.titlestringrequiredThe method’s name in the session’s language.
descriptionstringThe method’s description in the session’s language, else its own description (which can be in another language of the shop), else the English
Delivery in {min}–{max} business days(orDelivery in {n} business days) from its delivery estimate; absent when none exists. At most 300 characters.maxLength: 300totalsarray<object>requiredOne entry.
minItems: 1maxItems: 1typestringrequiredalways:totalamountintegerrequiredThe shipping fee for this checkout — the same rule the shop’s checkout charges (0 once the cart’s subtotal without VAT reaches the method’s free-shipping threshold). Integer in ISO 4217 minor units of the session’s
currency(EUR: cents), VAT included.minimum: 0
FulfillmentGroup
The single shipping group.
additionalProperties: falseidstringrequiredalways:defaultline_item_idsarray<string (uuid)>requiredEvery line of the checkout.
each item:format: uuidIn the shop’s order. Empty while the checkout has no line or the shop does not ship to the destination.
selected_option_idstring (uuid) | nullrequiredThe selected option, only when it is among
options; otherwisenull.
FulfillmentMethod
The single shipping method covering every line.
additionalProperties: falseidstringrequiredalways:shippingtypestringrequiredalways:shippingline_item_idsarray<string (uuid)>requiredEvery line of the checkout.
each item:format: uuiddestinationsarray<ShippingDestination>Present once an address is stored.
minItems: 1maxItems: 1selected_destination_idstringPresent once an address is stored.
always:destinationExactly one group.
minItems: 1maxItems: 1
Fulfillment
Present only when dev.ucp.shopping.fulfillment is negotiated. One shipping method with one group; there is no pickup method — a pickup point (Packeta) is chosen by the buyer on the shop.
additionalProperties: falseExactly one method.
minItems: 1maxItems: 1
Link
A legal link from the shop’s checkout consents.
additionalProperties: falsetypestringrequiredprivacy_policywhen the consent’s label, link text or URL looks like a privacy one (privacy, GDPR, osobn…, súkrom…, adatvédel…, Datenschutz, ochrana …); otherwiseterms_of_service.one of:terms_of_serviceprivacy_policyurlstringrequiredAbsolute; a relative shop link is made absolute on the primary domain.
pattern: ^[Hh][Tt][Tt][Pp][Ss]?://titlestringThe consent’s link text, else its label, at most 200 characters; absent when empty.
maxLength: 200
Order
The order the buyer placed on the shop.
additionalProperties: falseidstring (uuid)requiredThe order id (the parent order when the shop split it by VAT).
labelstringThe shop’s order number. Absent when the order has none.
maxLength: 50permalink_urlstring (uri)requiredThe shop’s order confirmation page,
https://{primary domain}/order-confirmation?orderNumber=…&orderId=…(orderNumberonly when there is one). Renders without a sign-in; shows the number, not the order’s status.
ContinueUrl
The buyer handoff link, https://{primary domain}/ucp/continue/{64 hex}. Give it to the BUYER to open in their browser; never call it yourself. It never changes during the session’s life and works while the session is open and unexpired — an incomplete checkout included: the first line that reaches a buyer’s cart freezes the session in the status it has, so give the link only at requires_escalation. Once a cart was converted, its own link hands over the checkout made from it. Not single-use: every distinct browser (or signed-in customer) that opens it gets the lines once.
pattern: ^https://[^/]+/ucp/continue/[0-9a-f]{64}$ExpiresAt
When the session expires (UTC, ISO 8601 with milliseconds). A write extends it (cart: now + 7 days, checkout: now + 24 h); reads, complete and cancel do not. After a handoff nothing extends it.
Cart
A cart. It has no status: a cancel answers the cart as it was (with continue_url), and only a later read shows not_found.
additionalProperties: falseThe
ucpobject of a successful catalog or cart answer. A missingstatusmeans success.idstring (uuid)requiredThe cart id.
In the shop cart’s order, which need not be the order you sent.
ISO 4217 currency code.
pattern: ^[A-Z]{3}$subtotal,discount? andtotal(neverfulfillmenton a cart).minItems: 2maxItems: 3messagesarray<Message>Present only when non-empty. Line and discount messages appear only in the answer to the write that caused them (a cart the buyer took to the shop repeats its last answer, messages included).
minItems: 1buyerBuyerThe stored buyer; empty fields are left out. Absent when nothing is stored.
contextContextThe stored context, echoed as you sent it (after cleaning); empty fields are left out. Absent when nothing is stored.
discountsDiscountsPresent only when
dev.ucp.shopping.discountis negotiated. Thediscounttotal is present whether or not it is.The buyer handoff link,
https://{primary domain}/ucp/continue/{64 hex}. Give it to the BUYER to open in their browser; never call it yourself. It never changes during the session’s life and works while the session is open and unexpired — anincompletecheckout included: the first line that reaches a buyer’s cart freezes the session in the status it has, so give the link only atrequires_escalation. Once a cart was converted, its own link hands over the checkout made from it. Not single-use: every distinct browser (or signed-in customer) that opens it gets the lines once.pattern: ^https://[^/]+/ucp/continue/[0-9a-f]{64}$When the session expires (UTC, ISO 8601 with milliseconds). A write extends it (cart: now + 7 days, checkout: now + 24 h); reads,
completeandcanceldo not. After a handoff nothing extends it.
CheckoutStatus
incomplete— at least onerecoverableerror is listed (no line, no valid e-mail, no or incomplete address, no shipping to it, no or an unoffered option, a line error in this answer).requires_escalation— nothing the platform can provide is missing;payment_requiredis listed and the buyer must continue on the shop throughcontinue_url.completed— the buyer placed an order on the shop from the cart the handoff filled; seeorder. It means placed, not paid. Set shortly after the order (once the shop has started the payment and handed the order to its ERP) and best effort: a failure is only logged, so an order can exist for a checkout that never readscompleted.canceled— canceled byPOST …/cancel, or expired (an open checkout read or written afterexpires_atturnscanceled).
ready_for_complete and complete_in_progress are never used: the API never completes a checkout.
incompleterequires_escalationcompletedcanceledCheckout
A checkout session.
Once completed or canceled — and while the buyer’s handoff is done but the order not yet placed — the body is the last answer the platform received, with the current status (and order): it is not re-priced, and it does not show what the buyer changed on the shop. Only the lines and the applied discount code travel to the shop; the buyer enters address, shipping and payment there again.
additionalProperties: falseThe
ucpobject of every checkout answer that returns a checkout (any status).idstring (uuid)requiredThe checkout session id.
incomplete— at least onerecoverableerror is listed (no line, no valid e-mail, no or incomplete address, no shipping to it, no or an unoffered option, a line error in this answer).requires_escalation— nothing the platform can provide is missing;payment_requiredis listed and the buyer must continue on the shop throughcontinue_url.completed— the buyer placed an order on the shop from the cart the handoff filled; seeorder. It means placed, not paid. Set shortly after the order (once the shop has started the payment and handed the order to its ERP) and best effort: a failure is only logged, so an order can exist for a checkout that never readscompleted.canceled— canceled byPOST …/cancel, or expired (an open checkout read or written afterexpires_atturnscanceled).
ready_for_completeandcomplete_in_progressare never used: the API never completes a checkout.one of:incompleterequires_escalationcompletedcanceledISO 4217 currency code.
pattern: ^[A-Z]{3}$In the shop cart’s order.
subtotal,discount?,fulfillment?,total.minItems: 2maxItems: 4Always present, possibly empty. Order: line messages and discount messages (only in the answer to the write that caused them), then
missing/invalid_address/address_undeliverable/invalid_option,pickup_point_required, andpayment_requiredlast. Empty oncompletedandcanceledreads.Always present: at most 10, possibly empty.
maxItems: 10buyerBuyerThe stored buyer; empty fields are left out. Absent when nothing is stored.
contextContextThe stored context, echoed as you sent it (after cleaning); empty fields are left out. Absent when nothing is stored.
fulfillmentFulfillmentPresent only when
dev.ucp.shopping.fulfillmentis negotiated. Oneshippingmethod with one group; there is nopickupmethod — a pickup point (Packeta) is chosen by the buyer on the shop.discountsDiscountsPresent only when
dev.ucp.shopping.discountis negotiated. Thediscounttotal is present whether or not it is.continue_urlstring (uri) · ContinueUrlPresent while the checkout is
incompleteorrequires_escalation; absent oncecompletedorcanceled.pattern: ^https://[^/]+/ucp/continue/[0-9a-f]{64}$When the session expires (UTC, ISO 8601 with milliseconds). A write extends it (cart: now + 7 days, checkout: now + 24 h); reads,
completeandcanceldo not. After a handoff nothing extends it.orderOrderPresent once
completed.
BusinessProfile
The business profile. Apart from the capabilities a shop in catalog mode leaves out, the only value that differs between shops is the host in endpoint (the primary domain).
additionalProperties: falseucpobjectrequiredThe profile. There is no
supported_versions, nosigning_keys, no business name or logo.additionalProperties: falseThe UCP protocol version. This shop implements exactly one.
always:2026-04-08servicesobjectrequiredOne service.
additionalProperties: falsedev.ucp.shoppingarray<object>requiredminItems: 1maxItems: 1The UCP protocol version. This shop implements exactly one.
always:2026-04-08specstring (uri)requiredhttps://ucp.dev/2026-04-08/specification/overview.transportstringrequiredREST is the only transport (no MCP, A2A or embedded).
always:restendpointstring (uri)requiredThe REST base URL,
https://{primary domain}/ucp/v1. Call this.schemastring (uri)requiredhttps://ucp.dev/2026-04-08/services/shopping/rest.openapi.json(the protocol’s own OpenAPI).
capabilitiesobjectrequiredWhat this shop declares. A shop in catalog mode (it takes no orders) declares only the two catalog capabilities.
additionalProperties: falsedev.ucp.shopping.catalog.searcharray<object>requiredminItems: 1maxItems: 1The UCP protocol version. This shop implements exactly one.
always:2026-04-08specstring (uri)requiredhttps://ucp.dev/2026-04-08/specification/…for this capability.schemastring (uri)requiredhttps://ucp.dev/2026-04-08/schemas/shopping/….jsonfor this capability.
dev.ucp.shopping.catalog.lookuparray<object>requiredminItems: 1maxItems: 1The UCP protocol version. This shop implements exactly one.
always:2026-04-08specstring (uri)requiredhttps://ucp.dev/2026-04-08/specification/…for this capability.schemastring (uri)requiredhttps://ucp.dev/2026-04-08/schemas/shopping/….jsonfor this capability.
dev.ucp.shopping.cartarray<object>minItems: 1maxItems: 1The UCP protocol version. This shop implements exactly one.
always:2026-04-08specstring (uri)requiredhttps://ucp.dev/2026-04-08/specification/…for this capability.schemastring (uri)requiredhttps://ucp.dev/2026-04-08/schemas/shopping/….jsonfor this capability.
dev.ucp.shopping.checkoutarray<object>minItems: 1maxItems: 1The UCP protocol version. This shop implements exactly one.
always:2026-04-08specstring (uri)requiredhttps://ucp.dev/2026-04-08/specification/…for this capability.schemastring (uri)requiredhttps://ucp.dev/2026-04-08/schemas/shopping/….jsonfor this capability.
dev.ucp.shopping.fulfillmentarray<object>minItems: 1maxItems: 1The UCP protocol version. This shop implements exactly one.
always:2026-04-08specstring (uri)requiredhttps://ucp.dev/2026-04-08/specification/…for this capability.schemastring (uri)requiredhttps://ucp.dev/2026-04-08/schemas/shopping/….jsonfor this capability.extendsstringrequiredThe parent capability.
always:dev.ucp.shopping.checkout
dev.ucp.shopping.discountarray<object>minItems: 1maxItems: 1The UCP protocol version. This shop implements exactly one.
always:2026-04-08specstring (uri)requiredhttps://ucp.dev/2026-04-08/specification/…for this capability.schemastring (uri)requiredhttps://ucp.dev/2026-04-08/schemas/shopping/….jsonfor this capability.The parent capabilities:
["dev.ucp.shopping.checkout","dev.ucp.shopping.cart"].each item · one of:dev.ucp.shopping.catalog.searchdev.ucp.shopping.catalog.lookupdev.ucp.shopping.cartdev.ucp.shopping.checkoutdev.ucp.shopping.fulfillmentdev.ucp.shopping.discount
payment_handlersobjectrequiredAlways
{}: the buyer pays on the shop’s own checkout.maxProperties: 0
HandoffClaimRequest
Sent by the handoff page’s own script.
sessionIdstringrequiredThe browser’s storefront guest-cart id (
localStorage["sf-cart"].state.sessionId; the page creates one when there is none). 8–128 characters ofA-Z a-z 0-9 _ -, not starting withucp_. It is not the UCP session id.pattern: ^(?!ucp_)[A-Za-z0-9_-]{8,128}$
HandoffClaimResponse
Where the buyer’s browser goes next.
additionalProperties: falseredirectstringrequiredSame-origin path the page continues to:
/checkoutfor a checkout (also when a cart link handed over the cart’s open checkout),/cartfor a cart (and when nothing could be added because the browser’s cart holds items in another currency),/on a shop in catalog mode.one of:/checkout/cart/
UcpBusinessErrorResponse
HTTP 200 with no resource. The cases:
capabilities_incompatible— your profile and this shop share no version of the operation’s root capability (always for carts and checkouts on a shop in catalog mode). Nothing ran; no Idempotency-Key was used.not_found— the product, cart (Cart not found or has expired) or checkout (Checkout session not found) does not exist for this shop, is malformed, or (carts) was canceled or expired. An unknowncart_idonPOST /checkout-sessionsalso readsCheckout session not found.out_of_stock/item_unavailable— a create in which none of the requested items could be added; no session was created.handed_off— converting a cart whose lines the buyer already took to the shop.
Inside a write the last three are stored under the Idempotency-Key like a success (capabilities_incompatible is answered before the key is read).
additionalProperties: falseThe
ucpobject of a business error. Never haspayment_handlers.capabilitiesis{}forcapabilities_incompatibleand lists the operation’s root (and its negotiated extensions) otherwise.Exactly one error message saying why no resource could be returned.
minItems: 1maxItems: 1continue_urlstring (uri)requiredWhere a buyer could go instead: the shop home (
https://{primary domain}/), orhttps://{primary domain}/cartforhanded_off. Not a handoff link.