Rýchly štart pre vývojárov
Celý nákup cez UCP krok za krokom — curl, JavaScript a Python, od profilu obchodu po odovzdanie checkoutu kupujúcemu.
Na tejto stránke
- Pred začiatkom
- Obchod so zapnutým UCP
- Profil vašej platformy
- Hlavičky
- Volania
- Príprava pre ukážky
- Krok 1: objavenie obchodu
- Krok 2: vyhľadanie v katalógu
- Krok 3: vytvorenie košíka
- Krok 4: úprava košíka
- Krok 5: z košíka checkout
- Krok 6: kupujúci a doprava
- Krok 7: odovzdanie kupujúcemu
- Krok 8: čakanie na objednávku
- Kompletné skripty
- Čo sa môže pokaziť
Táto kapitola vás prevedie jedným nákupom v obchode na Monokaide od prvého
volania až po objednávku. Prečítate si profil obchodu, vyhľadáte v jeho katalógu,
zostavíte košík, urobíte z neho checkout, doplníte kupujúceho a dopravu,
odovzdáte kupujúcemu continue_url a zistíte, že objednávka vznikla. Pri
každom kroku je presná požiadavka aj s hlavičkami v curl, JavaScripte
(Node 20, vstavaný fetch) a Pythone 3 (requests) a za ňou odpoveď
obchodu. Na konci sú dva kompletné skripty, ktoré
prejdú celý postup aj s opakovaním požiadaviek a čakaním na objednávku.
Príklady používajú vymyslený obchod https://shop.example.com (slovenský
obchod s bicyklami, ceny v eurách) a profil platformy
https://platform.example.com/.well-known/ucp. Odpovede v JSON sú príklady z
referencie API, ktoré priamo z ukážkových dát
vytvoril kód Monokaida. Majú teda presne taký tvar, aký vráti skutočný obchod;
identifikátory, ceny a texty sa líšia od obchodu k obchodu.
Agent / platforma → Obchod (UCP API):
GET /.well-known/ucpNačíta profil obchodu: verziu protokolu, adresu REST API a schopnosti, ktoré obchod ponúka. Netreba žiadnu hlavičku; odpoveď sa smie uchovať v cache päť minút.
Agent / platforma → Obchod (UCP API):
POST /ucp/v1/catalog/searchHľadá v katalógu obchodu (lookup a product čítajú položky podľa id). Každé volanie /ucp/v1 nesie hlavičku UCP-Agent s adresou profilu platformy.
Agent / platforma → Obchod (UCP API):
POST /ucp/v1/cartsZaloží košík, ktorý nacení obchod — jeho ceny, sklad a zľavy. Košík platí 7 dní od poslednej zmeny.
Agent / platforma → Obchod (UCP API):
POST /ucp/v1/checkout-sessionsincompleteVytvorí checkout z košíka (cart_id) alebo priamo z line_items. Kým niečo chýba, má stav incomplete.
Agent / platforma → Obchod (UCP API):
PUT /ucp/v1/checkout-sessions/{id}requires_escalationDoplní e-mail kupujúceho a s rozšírením fulfillment aj adresu a spôsob dopravy. Keď nič nechýba: requires_escalation so správou payment_required a s continue_url.
Agent / platforma → Kupujúci (prehliadač):
continue_urlOdovzdá continue_url kupujúcemu. Monokaido cez UCP platbu neprijíma a POST …/complete checkout nedokončí.
Kupujúci (prehliadač) → Obchod (UCP API):
GET /ucp/continue/{token}Otvorí continue_url na doméne obchodu. Stránka presunie položky a zľavový kód do jeho košíka v tomto prehliadači a otvorí pokladňu obchodu.
Kupujúci (prehliadač) → Obchod (UCP API):
/checkoutV pokladni obchodu dokončí nákup vrátane platby — ako ktorýkoľvek zákazník.
Agent / platforma → Obchod (UCP API):
GET /ucp/v1/checkout-sessions/{id}completedKeď objednávka obsahuje aspoň jednu položku z relácie, ďalšie čítanie checkoutu vráti completed s objednávkou (order). Webhooky neexistujú: stav treba zisťovať čítaním.
continue_url košíka (bez checkoutu) funguje rovnako, len otvorí košík obchodu namiesto pokladne.
Pred začiatkom
Obchod so zapnutým UCP
Potrebujete doménu obchodu, ktorému obchodník UCP zapol
(Zapnutie UCP v obchode). UCP beží len na vlastnej doméne
obchodu s aktívnym certifikátom, nikdy na adrese *.shop.monokaido.com.
Rýchla kontrola:
curl -sS -o /dev/null -w '%{http_code}\n' https://shop.example.com/.well-known/ucp
200 znamená, že obchod UCP poskytuje. 404 znamená, že nie, nech je dôvod
akýkoľvek: odpoveď je zámerne vo všetkých prípadoch rovnaká. 503 znamená,
že obchod práve nemôže odpovedať: skúste to znova po Retry-After.
Testovacie prostredie (sandbox) neexistuje. Košíky a checkouty sa naceňujú
aktuálnymi cenami a skladom obchodu, no nie sú to objednávky: nič sa
neobjedná ani nezaplatí, kým kupujúci neotvorí continue_url a nezadá
objednávku v obchode. Taká objednávka je skutočnou objednávkou obchodu, preto každý test
dohodnite s obchodníkom.
Profil vašej platformy
Každé volanie pod /ucp/v1 uvádza v hlavičke UCP-Agent profil vašej
platformy a obchod si ho stiahne. Na adrese https://, ktorú spravujete,
zverejnite JSON dokument ako tento:
{
"ucp": {
"version": "2026-04-08",
"capabilities": {
"dev.ucp.shopping.catalog.search": [{ "version": "2026-04-08" }],
"dev.ucp.shopping.catalog.lookup": [{ "version": "2026-04-08" }],
"dev.ucp.shopping.cart": [{ "version": "2026-04-08" }],
"dev.ucp.shopping.checkout": [{ "version": "2026-04-08" }],
"dev.ucp.shopping.fulfillment": [{ "version": "2026-04-08" }],
"dev.ucp.shopping.discount": [{ "version": "2026-04-08" }]
}
}
}
Obchod z neho číta len toto: ucp.version, ktorá musí byť presne
2026-04-08, a ucp.capabilities. Schopnosť (capability), ktorú vynecháte,
sa nevyjedná a každá operácia, ktorá ju potrebuje, odpovie
capabilities_incompatible. Bez dev.ucp.shopping.fulfillment checkout
nežiada adresu ani spôsob dopravy; bez dev.ucp.shopping.discount sa
zľavové kódy ignorujú. Ďalšie časti profilu sú dovolené a obchod ich
ignoruje.
Obchod si profil stiahne obyčajným GET a prijme ho, len ak:
- adresa začína
https://, neobsahuje meno ani heslo, nemá v sebe medzery a má najviac 1 000 znakov; - hostiteľ sa preloží na verejnú IP adresu (súkromné, loopback a ďalšie vyhradené rozsahy sa odmietajú);
- odpoveď je HTTP
200(presmerovania sa nesledujú) s telom JSON s veľkosťou najviac 256 KiB a príde do 5 sekúnd.
Profil posielajte s hlavičkou Cache-Control: public, max-age=3600. Obchod
si ho uchová na dobu podľa max-age, obmedzenú na 60 sekúnd až jednu hodinu
(bez max-age päť minút). Keď ten čas uplynie, ďalšia požiadavka ešte dostane
uložený profil a obchod si ho medzitým na pozadí stiahne znova, takže zmena
profilu sa do obchodu dostane krátko po tom čase. Neúspešné stiahnutie
profilu, ktorý obchod ešte nemá, si pamätá 60 sekúnd: keď hosting opravíte,
počkajte minútu. Kľúčom vyrovnávacej pamäte je celá adresa a podľa nej sa
oddeľujú aj vaše kľúče Idempotency-Key, preto ju nemeňte. Každá adresa
profilu, ktorá ešte nie je uložená, znamená jedno stiahnutie a pre jednu IP
adresu klienta sa stiahne najviac 5 nových profilov za minútu, spolu vo
všetkých obchodoch na Monokaide; nad tento počet obchod odpovie 503 s
Retry-After.
Hlavičky
| Hlavička | Kedy ju poslať | Hodnota |
|---|---|---|
UCP-Agent | pri každom volaní pod /ucp/v1 | profile="https://platform.example.com/.well-known/ucp". Úvodzovky sú súčasťou zápisu a profile sa píše malými písmenami. |
Content-Type | pri každej požiadavke s telom | application/json, UTF-8. Telo vo formáte formulára (application/x-www-form-urlencoded) sa spracuje tiež, no jeho hodnoty prídu ako reťazce, takže číselné polia ako quantity sa odmietnu; telo iného typu sa nečíta. |
Idempotency-Key | pri siedmich zápisoch: vytvorenie, úprava a zrušenie košíka; vytvorenie, úprava, dokončenie a zrušenie checkoutu | Nové UUID pre každý logický zápis, pri každom opakovaní toho istého zápisu poslané znova bez zmeny. Prijme sa ľubovoľná hodnota s 1 až 255 viditeľnými znakmi ASCII; iná hodnota sa odmietne s 400. Kľúč sa uchováva najmenej 25 hodín od prvého použitia (spoliehajte sa na 24), zvlášť pre každý obchod a každú adresu profilu. Je nepovinný, no bez neho zopakované vytvorenie založí druhý košík alebo checkout. |
Accept-Language | nepovinná | Použije sa, keď telo neobsahuje context.language alebo ho obchod neponúka. Jazyky sa skúšajú v poradí, v akom sú uvedené; váhy q= sa ignorujú. Ak sa žiadny nezhoduje, odpoveď je v predvolenom jazyku obchodu. Košík aj checkout si jazyk ponechajú, kým ho telo požiadavky nezmení. |
Žiadny API kľúč, OAuth ani cookie: obchod sa určí podľa hostiteľa, ktorého voláte. API volajte zo svojho servera. Pre cudzie pôvody neposiela hlavičky CORS, takže webová stránka na vašej doméne ho z prehliadača nezavolá.
Volania
| Krok | Volanie | Odpoveď |
|---|---|---|
| 1 | GET /.well-known/ucp | profil obchodu |
| 2 | POST /ucp/v1/catalog/search | produkty s variantmi |
| 3 | POST /ucp/v1/carts | 201, nový košík |
| 4 | PUT /ucp/v1/carts/{id} | košík s novým nacenením |
| 5 | POST /ucp/v1/checkout-sessions | 201, checkout v stave incomplete |
| 6 | PUT /ucp/v1/checkout-sessions/{id} | checkout v stave requires_escalation |
| 7 | žiadne | kupujúci otvorí continue_url v prehliadači |
| 8 | GET /ucp/v1/checkout-sessions/{id} | completed aj s objednávkou v order |
Príprava pre ukážky
Ukážky jednotlivých krokov používajú spoločnú prípravu. V curl sú to
premenné shellu (ukážky potrebujú aj jq a uuidgen). V JavaScripte a
Pythone pošle hlavičky malá pomocná funkcia, ktorá vyhodí výnimku pri oboch
druhoch zlyhania: pri chybe protokolu (chybový stav HTTP s poľom code) aj
pri obchodnej chybe (HTTP 200 s ucp.status: "error", keď sa nevrátil
žiadny zdroj). Kroky vložte za ňu v tomto poradí. Pomocné funkcie
požiadavky neopakujú; kompletné skripty áno.
SHOP=https://shop.example.com
AGENT='profile="https://platform.example.com/.well-known/ucp"'
// ucp.mjs — paste the steps below after this, in one ES module (Node 20+, no dependencies)
import { randomUUID } from 'node:crypto';
import { setTimeout as sleep } from 'node:timers/promises';
const SHOP = process.env.SHOP ?? 'https://shop.example.com';
const UCP_AGENT = 'profile="https://platform.example.com/.well-known/ucp"';
async function ucp(method, url, body, idempotencyKey) {
const res = await fetch(url, {
method,
headers: {
'UCP-Agent': UCP_AGENT,
'Accept-Language': 'sk',
...(body && { 'Content-Type': 'application/json' }),
...(idempotencyKey && { 'Idempotency-Key': idempotencyKey }),
},
body: body && JSON.stringify(body),
});
const data = await res.json(); // an HTML error page from the web server would throw here
if (!res.ok) throw new Error(`${res.status} ${data.code}: ${data.content}`); // protocol error
if (data.ucp?.status === 'error') {
throw new Error(`${data.messages[0].code}: ${data.messages[0].content}`); // HTTP 200, but no resource
}
return data;
}
# ucp.py — run the steps below after this, in one session (pip install requests)
import os, time, uuid
import requests
SHOP = os.environ.get("SHOP", "https://shop.example.com")
http = requests.Session()
http.headers.update({
"UCP-Agent": 'profile="https://platform.example.com/.well-known/ucp"',
"Accept-Language": "sk",
})
def ucp(method, url, body=None, idempotency_key=None):
headers = {"Idempotency-Key": idempotency_key} if idempotency_key else {}
r = http.request(method, url, json=body, headers=headers, timeout=65) # json= sets Content-Type
data = r.json() # an HTML error page from the web server would raise here
if not r.ok: # protocol error
raise RuntimeError(f"{r.status_code} {data['code']}: {data['content']}")
if data.get("ucp", {}).get("status") == "error": # HTTP 200, but no resource
raise RuntimeError(f"{data['messages'][0]['code']}: {data['messages'][0]['content']}")
return data
Krok 1: objavenie obchodu
Profil obchodu je verejný: nepotrebuje hlavičku UCP-Agent a obchod
dovoľuje uchovať ho päť minút.
curl -sS -o profile.json -w 'HTTP %{http_code}\n' "$SHOP/.well-known/ucp"
jq . profile.json
API=$(jq -r '.ucp.services["dev.ucp.shopping"][0].endpoint' profile.json) # https://shop.example.com/ucp/v1
const profile = await ucp('GET', `${SHOP}/.well-known/ucp`);
const API = profile.ucp.services['dev.ucp.shopping'][0].endpoint; // https://shop.example.com/ucp/v1
console.log(Object.keys(profile.ucp.capabilities));
profile = ucp("GET", f"{SHOP}/.well-known/ucp")
API = profile["ucp"]["services"]["dev.ucp.shopping"][0]["endpoint"] # https://shop.example.com/ucp/v1
print(list(profile["ucp"]["capabilities"]))
{
"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": {}
}
}GET https://shop.example.com/.well-known/ucp for a shop whose primary domain is shop.example.com, not in catalog mode.
Vyčítajte z neho tri veci:
endpointvucp.services["dev.ucp.shopping"]je základ každého ďalšieho volania. Jeho hostiteľ je primárna doména obchodu, ktorá sa môže líšiť od domény, ktorú ste volali. Použite ho a adresu neskladajte sami.ucp.capabilitieshovorí, čo obchod práve ponúka. Obchod v katalógovom režime uvádza lendev.ucp.shopping.catalog.searchadev.ucp.shopping.catalog.lookup: vyhľadávať v ňom môžete, no každé volanie košíka a checkoutu odpoviecapabilities_incompatible, preto skončite po kroku 2.payment_handlersje vždy{}. Cez UCP neprechádza žiadna platba; kupujúci platí v obchode.
Krok 2: vyhľadanie v katalógu
Hľadajte textom v jazyku obchodu. pagination.limit je predvolene 10 a
väčšia hodnota sa zníži na 20. context.language určuje jazyk textov a tým
aj menu všetkých cien. context.currency menu nemení: hovorí len, v akej
mene je zadaný filter filters.price. Bez neho sa hranice ceny čítajú v mene
odpovede, teda v tej, ktorú určuje jazyk.
curl -sS -o search.json -w 'HTTP %{http_code}\n' -X POST "$API/catalog/search" \
-H "UCP-Agent: $AGENT" \
-H 'Content-Type: application/json' \
-H 'Accept-Language: sk' \
-d '{"query":"marlin","pagination":{"limit":2},"context":{"language":"sk","address_country":"SK","currency":"EUR"}}'
jq '.products[] | {title, variants: [.variants[] | {id, title, price, availability}]}' search.json
VARIANT=$(jq -r '[.products[].variants[] | select(.availability.available)][0].id' search.json)
const search = await ucp('POST', `${API}/catalog/search`, {
query: 'marlin',
pagination: { limit: 2 },
context: { language: 'sk', address_country: 'SK', currency: 'EUR' },
});
const product = search.products[0];
const variant = product.variants.find((v) => v.availability.available);
console.log(product.title, variant.title, variant.price); // Trek Marlin 5 Modrá / M { amount: 69900, currency: 'EUR' }
console.log(search.pagination); // { has_next_page: true, cursor: 'cDoy', total_count: 3 }
search = ucp("POST", f"{API}/catalog/search", {
"query": "marlin",
"pagination": {"limit": 2},
"context": {"language": "sk", "address_country": "SK", "currency": "EUR"},
})
product = search["products"][0]
variant = next(v for v in product["variants"] if v["availability"]["available"])
print(product["title"], variant["title"], variant["price"]) # Trek Marlin 5 Modrá / M {'amount': 69900, 'currency': 'EUR'}
print(search["pagination"]) # {'has_next_page': True, 'cursor': 'cDoy', 'total_count': 3}
Telo požiadavky:
{
"query": "marlin",
"pagination": {
"limit": 2
},
"context": {
"language": "sk",
"address_country": "SK",
"currency": "EUR"
}
}First page of a text search in Slovak. currency here only denominates a price filter (none is sent); unknown fields would be ignored.
Odpoveď:
{
"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
}
}The answer to CatalogSearchRequest. The listing had 3 matches (page 1 of 2), so has_next_page is true and total_count is 3 (no price filter in effect). The first available variant of Trek Marlin 5 is already first; the pre-order variant counts as available; the vendor image keeps its own URL.
Pre ďalšie kroky je dôležité:
variants[].idpatrí do položky košíka. Vyberte variant savailability.available: true(stavin_stockalebopreorder).- Ceny sú celé čísla v najmenších jednotkách meny, s DPH:
69900sEURje 699,00 €.list_priceje pôvodná alebo odporúčaná cena a uvádza sa, len keď je vyššia. - Stránkovanie. Kým je
has_next_pagetrue, pošlite rovnaké telo znova a dopagination.cursordajtecursor, ktorý ste dostali. Kurzor nesie len číslo strany, preto dopyt, filtre a limit musia zostať rovnaké. Strana môže byť kratšia, dokonca prázdna, a pritom ďalšie strany nasledujú: pokračujte, kýmhas_next_pagenie jefalse. - Poradie a zhoda. Produkty prichádzajú od najnovšieho, bez zoradenia
podľa relevancie. Dopyt sa porovnáva s názvom produktu a s kódmi samotného
produktu (kód produktu v obchode a identifikátory produktu od dodávateľa),
nie s popisom, SKU variantov ani kódom EAN. Na veľkosti písmen nezáleží,
na diakritike áno:
modranenájdeModrá. Produkty podľa SKU, handle alebo identifikátora nájdete cezPOST /ucp/v1/catalog/lookup, najviac 20 naraz. - Zoznam, ktorý sa hýbe. Vyhľadávanie číta zoznam produktov obchodu, ktorý
platforma obnovuje na pozadí: novo publikovaný produkt sa môže objaviť až o
niekoľko minút a produkt publikovaný počas stránkovania posunie ďalšie
strany, takže sa produkt môže zopakovať alebo vynechať. Produkty
deduplikujte podľa
id. Lookup a detail produktu čítajú priamo údaje produktu.
Krok 3: vytvorenie košíka
Každá položka uvádza variant v item.id: vždy variants[].id, ktorý ste
vybrali z katalógu. Prijme sa aj identifikátor produktu, položka však potom
kúpi ten aktívny variant produktu, ktorý vznikol najskôr — nemusí to byť
variant, ktorý ukázalo vyhľadávanie, ani variant na sklade; odpoveď uvedie
tento variant. quantity je predvolene 1; nula, záporné alebo desatinné
číslo sa odmietne s 400. Prijme sa najviac 100 položiek a množstvo nad 999
sa bez správy zníži na 999. Dve položky toho istého variantu sa spoja do
jednej so súčtom množstiev. buyer je pri košíku nepovinný. Je to prvý
zápis, preto nesie Idempotency-Key.
KEY=$(uuidgen) # one key per logical write; a retry of this request sends the same one
jq -n --arg variant "$VARIANT" '{
line_items: [
{item: {id: $variant}, quantity: 1},
{item: {id: "4a6e2d9c-8b15-4f37-9c48-1e7a5b3d2f90"}}
],
buyer: {email: "Jana.Novakova@Example.com"},
context: {address_country: "SK", language: "sk"}
}' | curl -sS -o cart.json -w 'HTTP %{http_code}\n' -X POST "$API/carts" \
-H "UCP-Agent: $AGENT" \
-H 'Content-Type: application/json' \
-H 'Accept-Language: sk' \
-H "Idempotency-Key: $KEY" \
--data-binary @-
jq . cart.json
CART=$(jq -r .id cart.json)
let cart = await ucp('POST', `${API}/carts`, {
line_items: [
{ item: { id: variant.id }, quantity: 1 },
{ item: { id: '4a6e2d9c-8b15-4f37-9c48-1e7a5b3d2f90' } }, // a helmet; quantity defaults to 1
],
buyer: { email: 'Jana.Novakova@Example.com' },
context: { address_country: 'SK', language: 'sk' },
}, randomUUID()); // keep the key if you retry this very request
console.log(cart.id, cart.totals, cart.continue_url);
cart = ucp("POST", f"{API}/carts", {
"line_items": [
{"item": {"id": variant["id"]}, "quantity": 1},
{"item": {"id": "4a6e2d9c-8b15-4f37-9c48-1e7a5b3d2f90"}}, # a helmet; quantity defaults to 1
],
"buyer": {"email": "Jana.Novakova@Example.com"},
"context": {"address_country": "SK", "language": "sk"},
}, str(uuid.uuid4())) # keep the key if you retry this very request
print(cart["id"], cart["totals"], cart["continue_url"])
{
"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"
}
}Step 1 of the flow. The helmet line has no quantity (1). The e-mail is lower-cased when stored.
Obchod odpovie 201 a košíkom naceneným podľa vlastných pravidiel:
{
"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"
}The answer to CartCreateRequest: the bike (699.00 €) and the helmet (59.90 €). No messages key (none). discounts is present because the discount extension is negotiated. The helmet has no image, so no image_url. expires_at = now + 7 days.
- Uložte si
idkošíka. Košík žije 7 dní od posledného zápisu; potom každé volanie naň odpovienot_found. totalsobsahujesubtotal, zápornú položkudiscount, keď platí kód, atotal. Ceny položiek sú vitem.price, rovnako v najmenších jednotkách.messagessa objaví, len keď je čo oznámiť. Položku, ktorú obchod nemôže predať, vynechá so správouitem_unavailable,out_of_stockalebovat_mix_not_allowed; zvyšok košíka sa vytvorí.- Ak sa nedá pridať žiadna položka, odpoveďou je HTTP
200sucp.status: "error", správouout_of_stockaleboitem_unavailablea bez identifikátora košíka. - Aj košík má
continue_url, kupujúcemu však odovzdajte checkout: len checkout sa dozvie, že objednávka vznikla.
Krok 4: úprava košíka
PUT nahradí položky košíka. line_items je povinné a je to celý zoznam:
variant, ktorý vynecháte, sa odstráni. Položky sa párujú podľa item.id;
poslané id položky sa ignoruje. Pole, ktoré vynecháte, napríklad buyer
alebo context, si ponechá uloženú hodnotu. Pole, ktoré pošlete, nahradí
uloženú hodnotu ako celok a discounts.codes nahradí zoznam kódov ([] odstráni
všetky).
KEY=$(uuidgen)
jq -n --arg variant "$VARIANT" '{
line_items: [
{item: {id: $variant}, quantity: 1},
{item: {id: "b5f81c3d-6e49-4a2b-8d73-9c0e4f1a6b25"}, quantity: 3}
],
discounts: {codes: ["JESEN10", "LETO5"]}
}' | curl -sS -o cart.json -w 'HTTP %{http_code}\n' -X PUT "$API/carts/$CART" \
-H "UCP-Agent: $AGENT" \
-H 'Content-Type: application/json' \
-H 'Accept-Language: sk' \
-H "Idempotency-Key: $KEY" \
--data-binary @-
jq '{totals, messages}' cart.json
cart = await ucp('PUT', `${API}/carts/${cart.id}`, {
line_items: [ // the whole list: the helmet is left out, so it is removed
{ item: { id: variant.id }, quantity: 1 },
{ item: { id: 'b5f81c3d-6e49-4a2b-8d73-9c0e4f1a6b25' }, quantity: 3 }, // bottles
],
discounts: { codes: ['JESEN10', 'LETO5'] },
}, randomUUID());
for (const m of cart.messages ?? []) console.log(m.type, m.code, m.path, m.content);
cart = ucp("PUT", f"{API}/carts/{cart['id']}", {
"line_items": [ # the whole list: the helmet is left out, so it is removed
{"item": {"id": variant["id"]}, "quantity": 1},
{"item": {"id": "b5f81c3d-6e49-4a2b-8d73-9c0e4f1a6b25"}, "quantity": 3}, # bottles
],
"discounts": {"codes": ["JESEN10", "LETO5"]},
}, str(uuid.uuid4()))
for m in cart.get("messages", []):
print(m["type"], m["code"], m.get("path"), m["content"])
{
"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"
]
}
}Step 2. line_items is a full replacement: the helmet is removed. The line id is accepted and ignored (lines match by variant). Only 2 bottles are in stock. buyer and context are left out, so they are kept.
{
"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"
}The answer to CartUpdateRequest. The bottle line was lowered to the 2 in stock (quantity_adjusted at its index in the answer); JESEN10 (10 % of the net subtotal 584.39 € = 58.44 €) is applied, LETO5 cannot be combined. Subtotal 718.80 € − 58.44 € = 660.36 €. The kept buyer and context are echoed; expires_at moved to now + 7 days.
- Na košík platí jeden zľavový kód. Kódy sa skúšajú v poradí, v akom ich
pošlete; uplatní sa prvý, ktorý obchod prijme, a každý ďalší dostane
varovanie
discount_code_combination_disallowed. Kód, ktorý obchod odmietne, dostane varovaniediscount_code_invalid,discount_code_expiredalebodiscount_code_user_ineligible;pathkaždého varovania ukazuje na kód vdiscounts.codes. Percentuálny kód sa počíta zo sumy bez DPH, rovnako ako v pokladni obchodu, a odpočíta sa od sumy s DPH. - Množstvo nad stav skladu sa zníži a pribudne varovanie
quantity_adjusted. - Položky sa vracajú v poradí obchodu, nie vo vašom. Párujte ich podľa
item.id;pathsprávy, napríklad$.line_items[1].quantity, ukazuje do odpovede, nie do vašej požiadavky. - Každé vytvorenie a úprava nastaví
expires_atna 7 dní odteraz.
Krok 5: z košíka checkout
Identifikátor košíka pošlite na POST /ucp/v1/checkout-sessions. Checkout
prevezme položky košíka, kupujúceho, kontext (aj s jazykom) a zľavové kódy;
vlastné line_items, buyer a context z požiadavky sa nepoužijú. Spolu s
cart_id môžete poslať fulfillment a discounts; tie sa uplatnia na nový
checkout. Samotný košík zostane nezmenený. cart_id sa číta, len keď váš profil
uvádza dev.ucp.shopping.cart; bez neho požiadavka potrebuje line_items.
Bez košíka prijme tá istá cesta priamo line_items (aj ostatné polia)
v tvare z kroku 3.
KEY=$(uuidgen)
curl -sS -o checkout.json -w 'HTTP %{http_code}\n' -X POST "$API/checkout-sessions" \
-H "UCP-Agent: $AGENT" \
-H 'Content-Type: application/json' \
-H 'Accept-Language: sk' \
-H "Idempotency-Key: $KEY" \
-d "{\"cart_id\":\"$CART\"}"
jq '{id, status, messages, options: .fulfillment.methods[0].groups[0].options}' checkout.json
CHECKOUT=$(jq -r .id checkout.json)
OPTION=$(jq -r '.fulfillment.methods[0].groups[0].options[0].id' checkout.json)
let checkout = await ucp('POST', `${API}/checkout-sessions`, { cart_id: cart.id }, randomUUID());
console.log(checkout.status); // incomplete
for (const m of checkout.messages) console.log(m.type, m.severity, m.code, m.path);
checkout = ucp("POST", f"{API}/checkout-sessions", {"cart_id": cart["id"]}, str(uuid.uuid4()))
print(checkout["status"]) # incomplete
for m in checkout["messages"]:
print(m["type"], m.get("severity"), m["code"], m.get("path"))
{
"cart_id": "9a3f6c2e-5b18-4d47-8e9c-1f2a7b4d6e83"
}Step 3: a checkout from the cart of step 2. Its lines, buyer, context and codes come from the cart.
{
"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"
}The answer to CheckoutFromCartRequest. The checkout has its own lines (new line ids), the cart’s buyer, context and codes; JESEN10 is applied again and LETO5 warned about again. No address yet, so both shipping options are offered (no country filter) and none is selected: two recoverable errors keep it incomplete, so there is no payment_required. expires_at = now + 24 h.
- Odpoveď je
201. Tento checkout jeincomplete, lebo adresa ani spôsob dopravy ešte nie sú známe; checkout, ktorému nič nechýba, jerequires_escalationhneď od začiatku. Checkout mámessagesvždy a jeho chybyrecoverablehovoria, čo ešte chýba. - Checkout má vlastné položky s novými identifikátormi.
fulfillment.methods[0].groups[0].optionsuvádza spôsoby dopravy obchodu s poplatkami. Kým adresa nie je známa, nefiltrujú sa podľa krajiny.- Checkout žije 24 hodín od posledného zápisu.
- Košík má najviac jeden otvorený checkout. Na opätovné poslanie toho
istého
cart_idobchod odpovie týmto checkoutom s HTTP200a ignorujefulfillmentajdiscountsopakovanej požiadavky; checkout meňte cezPUT.
Krok 6: kupujúci a doprava
Checkout prejde do requires_escalation, len čo žiadna správa nie je
error so severity: "recoverable". Tieto správy hovoria, čo treba poslať:
code | path | Čo poslať |
|---|---|---|
missing | $.buyer.email | buyer.email. Adresa, ktorá nie je platným e-mailom, sa hlási ako chýbajúca. |
missing | $.fulfillment.methods[0].destinations | adresu doručenia |
invalid_address | $.fulfillment.methods[0].destinations[0] | adresu s street_address, address_locality, postal_code a address_country |
address_undeliverable | $.fulfillment.methods[0].destinations[0] | inú adresu: žiadny spôsob dopravy obchodu tieto položky do tejto krajiny nedoručí |
missing | $.fulfillment.methods[0].groups[0].selected_option_id | jedno z ponúknutých options[].id |
invalid_option | $.fulfillment.methods[0].groups[0].selected_option_id | možnosť z aktuálneho zoznamu |
missing | $.line_items | aspoň jednu položku |
item_unavailable, out_of_stock, vat_mix_not_allowed | žiadna alebo $.line_items[n] | iný tovar alebo line_items bez tejto položky. Nesie ich len odpoveď na požiadavku, ktorá položku poslala: vynechaná položka nemá path, odmietnutá zmena ukazuje na položku, ktorú si checkout ponechal. |
Meno a telefón kupujúceho nie sú povinné.
KEY=$(uuidgen)
jq -n --arg option "$OPTION" '{
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: $option}]
}]},
discounts: {codes: ["JESEN10"]}
}' | curl -sS -o checkout.json -w 'HTTP %{http_code}\n' -X PUT "$API/checkout-sessions/$CHECKOUT" \
-H "UCP-Agent: $AGENT" \
-H 'Content-Type: application/json' \
-H 'Accept-Language: sk' \
-H "Idempotency-Key: $KEY" \
--data-binary @-
jq '{status, totals, messages}' checkout.json
const [option] = checkout.fulfillment.methods[0].groups[0].options; // GLS kuriér
checkout = await ucp('PUT', `${API}/checkout-sessions/${checkout.id}`, {
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: option.id }],
}],
},
discounts: { codes: ['JESEN10'] },
}, randomUUID());
const blocking = checkout.messages.filter((m) => m.type === 'error' && m.severity === 'recoverable');
console.log(checkout.status, blocking); // requires_escalation []
option = checkout["fulfillment"]["methods"][0]["groups"][0]["options"][0] # GLS kuriér
checkout = ucp("PUT", f"{API}/checkout-sessions/{checkout['id']}", {
"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": option["id"]}],
}]},
"discounts": {"codes": ["JESEN10"]},
}, str(uuid.uuid4()))
blocking = [m for m in checkout["messages"] if m["type"] == "error" and m.get("severity") == "recoverable"]
print(checkout["status"], blocking) # requires_escalation []
{
"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"
]
}
}Step 4. buyer replaces the stored buyer, so the e-mail is sent again with the new fields. GLS is selected with an option id from step 3. Sending only JESEN10 drops LETO5 and its warning.
{
"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"
}The answer to CheckoutUpdateRequest (step 4), and to a GET before the buyer has ordered. Nothing the platform can provide is missing: status requires_escalation, payment_required (requires_buyer_input) and continue_url. GLS 4.90 € is the fulfillment total: 718.80 − 58.44 + 4.90 = 665.26 €. The destination id came back as destination. Give continue_url to the buyer now.
Ako sa polia správajú:
buyernahradí uloženého kupujúceho ako celok. Posielajte zakaždým všetky polia:buyerlen s menom zmaže uložený e-mail.fulfillment. Obchod číta prvý spôsob stype: "shipping"(alebo beztype). Z jehodestinationsvezme tú, ktorú uvádzaselected_destination_id, inak prvú. Checkout má jednu adresu; vráti sa s identifikátorom"destination", nech ste jej dali akýkoľvek. Adresu možno nahradiť, nie odstrániť.address_countryposielajte ako kód ISO 3166-1 alpha-2, napríkladSK(rozumie aj kódom alpha-3 a názvom krajín); krajinu, ktorú obchod nerozpozná, odmietne s400.groups[0].selected_option_idvyberie spôsob dopravy;nullvýber zruší.- Ponuka dopravy závisí od adresy. Keď je adresa uložená, ponúknu sa len
spôsoby, ktoré do jej krajiny doručujú. Spôsob vybraný pred zadaním adresy
môže vypadnúť, čo sa prejaví ako
invalid_optionsoselected_option_id: null. Bezpečné poradie je poslať najprv adresu a potom vybrať z možností v tej odpovedi. Príklad robí oboje jednou požiadavkou, čo funguje, keď vybraný spôsob do krajiny adresy doručuje. - Každé vytvorenie a
PUTnastavíexpires_atna 24 hodín odteraz, kým kupujúci neotvorícontinue_url.
Checkout v stave requires_escalation nesie vždy payment_required
(requires_buyer_input): kupujúci platí v obchode. Ak vybraný spôsob
doručuje na výdajné miesto, pickup_point_required (tiež
requires_buyer_input) oznamuje, že výdajné miesto si kupujúci vyberie v
obchode. Ani jedna správa nebráni ďalšiemu kroku.
Krok 7: odovzdanie kupujúcemu
Ukážte kupujúcemu zhrnutie — položky, sumy, dopravu a odkazy obchodu z
links (obchodné podmienky a ochranu osobných údajov, ak ich obchod má
nastavené) — a continue_url mu dajte ako odkaz alebo tlačidlo, ktoré
otvorí vo vlastnom prehliadači. Každú položku pomenujte podľa vlastného
záznamu o variante, ktorý ste vybrali (jeho title, napríklad Modrá / M,
uložený podľa item.id): item.title položky je iba názov produktu. Odkaz
dajte až teraz, pri requires_escalation: keď prvýkrát vloží položky do
košíka kupujúceho, checkout sa zmrazí taký, aký je.
jq -r 'if .status == "requires_escalation" then .continue_url else "not ready: \(.status)" end' checkout.json
if (checkout.status !== 'requires_escalation') throw new Error('Not ready yet — fix the recoverable messages first');
console.log(`Finish the purchase on the shop: ${checkout.continue_url}`);
if checkout["status"] != "requires_escalation":
raise RuntimeError("Not ready yet — fix the recoverable messages first")
print(f"Finish the purchase on the shop: {checkout['continue_url']}")
Keď kupujúci odkaz otvorí, krátka stránka na doméne obchodu pridá položky checkoutu a uplatnený zľavový kód do košíka, ktorý má tento prehliadač v obchode, a otvorí pokladňu obchodu. Tovar, ktorý už v košíku bol, v ňom zostane. Ak je kupujúci v tej istej karte prehliadača prihlásený do obchodu, položky pôjdu do jeho zákazníckeho košíka. Ak už ten košík obsahuje tovar v inej mene, položky sa pridajú v tej mene, ak v nej obchod predáva; inak sa nepridá nič a kupujúci skončí v košíku obchodu. Potom zadá v obchode e-mail, adresu, dopravu a platbu a objednávku odošle.
- Odkaz patrí do prehliadača kupujúceho. Nikdy ho neotvárajte zo svojho servera. Samotné otvorenie len zobrazí stránku; položky sa presunú, až keď sa v prehliadači spustí jej skript, ktorý potrebuje JavaScript a úložisko prehliadača obchodu. Položky skončia v košíku toho prehliadača, ktorý odkaz otvoril: prehliadač vnútri aplikácie má vlastný košík, preto nech kupujúci nákup dokončí v ňom, alebo odkaz otvorí v systémovom prehliadači.
- S odkazom zaobchádzajte ako s tajomstvom. Nie je jednorazový: každý prehliadač, ktorý ho otvorí, dostane položky raz do vlastného košíka a objednávka z takého košíka môže váš checkout dokončiť. Pošlite ho len kupujúcemu.
- Funguje, kým je checkout otvorený, do
expires_at. Potom odpovie HTTP410stránkou, ktorá len odkazuje na úvodnú stránku obchodu. - Keď ho kupujúci otvoril, checkout je zmrazený.
PUTvráti posledný stav s varovanímhanded_offa nič nezmení;GETvyzerá presne ako predtým. Ak chce kupujúci iný nákup, vytvorte nový checkout.
Krok 8: čakanie na objednávku
Webhooky neexistujú: checkout čítajte, kým nie je completed alebo
canceled, a začnite hneď, ako kupujúcemu dáte odkaz.
for wait in 5 10 20 40 60 60 60; do
sleep "$wait"
curl -sS -o checkout.json "$API/checkout-sessions/$CHECKOUT" -H "UCP-Agent: $AGENT"
STATUS=$(jq -r .status checkout.json)
echo "$STATUS"
[ "$STATUS" = completed ] || [ "$STATUS" = canceled ] && break
done
jq '.order' checkout.json
let current = checkout;
for (let wait = 5; ['incomplete', 'requires_escalation'].includes(current.status); wait = Math.min(wait * 2, 60)) {
await sleep(wait * 1000);
current = await ucp('GET', `${API}/checkout-sessions/${checkout.id}`);
}
console.log(current.status, current.order); // completed { id: '8e1c4f7a-…', label: 'SK2600417', permalink_url: '…' }
current, wait = checkout, 5
while current["status"] in ("incomplete", "requires_escalation"):
time.sleep(wait)
wait = min(wait * 2, 60)
current = ucp("GET", f"{API}/checkout-sessions/{checkout['id']}")
print(current["status"], current.get("order")) # completed {'id': '8e1c4f7a-…', 'label': 'SK2600417', …}
{
"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"
}
}GET after the buyer opened continue_url and placed order SK2600417 on the shop (step 6). The body is the last answer the platform received (step 4) without continue_url, with status: "completed", messages: [] and order appended. It does not reflect what the buyer changed on the shop. “Completed” means placed, not paid.
- Jedna požiadavka naraz na jeden checkout. Čítanie checkoutu čaká za
každou inou požiadavkou naň a keď je checkout zaneprázdnený, môže odpovedať
503sRetry-After: 1. Záplava čítaní môže zdržať aj odovzdanie kupujúcemu. - Interval. Každých 5–10 sekúnd v prvých minútach po odovzdaní odkazu a potom raz za minútu úplne stačí. Čítania sú obmedzené na 600 za minútu na IP adresu a operáciu, spolu pre všetky obchody a relácie, ktoré z tej IP voláte; plánujte s polovicou (pozri Limity požiadaviek).
- Termín.
expires_atpredlžujú len vaše zápisy a keď kupujúci odkaz otvoril, už ani tie. Prvé čítanie poexpires_atzruší checkout, ktorý žiadna objednávka nedokončila, takže jedno čítanie po termíne dá konečnú odpoveď. completedznamená odoslaná, nie zaplatená.order.idje identifikátor objednávky v obchode,order.labeljej číslo (chýba, ak ho obchod nemá) aorder.permalink_urlstránka potvrdenia obchodu. Tá sa zostaví z adresy a stav objednávky neukazuje.- Telo je vaša posledná ponuka, nie objednávka.
line_itemsatotalssú to, čo checkout obsahoval, keď kupujúci odišiel; čo zmenil v obchode, sa v ňom neprejaví.
Niektoré odoslané objednávky checkout nedokončia: keď sa kupujúci po
odovzdaní ako hosť prihlási až v pokladni obchodu (objednávka potom vznikne z
iného košíka), keď pred objednaním odstráni všetky položky vášho checkoutu,
alebo keď bol checkout predtým zrušený (vami alebo po expires_at najbližšou
požiadavkou, ktorá ho čítala). Dokončenie navyše nie je zaručené: ak na
strane Monokaida zlyhá, chyba sa iba zapíše do logu. Váš checkout potom
skončí ako canceled. Netvrďte kupujúcemu, že objednávka neexistuje;
rozhodujúce je potvrdenie od obchodu.
Kompletné skripty
Dva skripty nižšie prejdú celý postup v jednom obchode: objavenie,
vyhľadávanie, ktoré pokračuje po kurzore, košík a jeho úpravu, checkout,
kupujúceho a adresu, prvý ponúknutý spôsob dopravy, continue_url a čakanie
na objednávku. Ukazujú ošetrenie chýb, aké skutočná integrácia potrebuje:
- Chyby protokolu (chybový stav HTTP s
codeacontent) skript zastavia, okrem tých, ktoré má zmysel zopakovať. - Obchodné chyby (HTTP
200sucp.status: "error") ho zastavia so správou; ostatné správy sa vypíšu a postup pokračuje. - Jeden
Idempotency-Keyna logický zápis, vytvorený raz a poslaný znova pri každom opakovaní toho zápisu, takže zopakované vytvorenie nikdy nezaloží druhú reláciu. - Opakovanie dodržiava
Retry-Afterpri429a503a pri409, kým tá istá požiadavka ešte beží.500, odpovede, ktoré nie sú JSON (vlastné stránky webového servera pri502,504a pri jeho limite), aj sieťové chyby sa opakujú s exponenciálne rastúcim odstupom.409bezRetry-Afterznamená, že kľúč bol použitý pre inú požiadavku, čo je chyba v kóde, a skript skončí. - Čakanie predlžuje interval z
POLL_SECONDSaž na minútu a končí pricompleted,canceled, termíne checkoutu alebo poMAX_POLL_MINUTES.
| Premenná | Predvolená hodnota | Význam |
|---|---|---|
SHOP | https://shop.example.com | pôvod (origin) obchodu |
PROFILE_URL | https://platform.example.com/.well-known/ucp | profil vašej platformy |
QUERY | marlin | hľadaný text |
LANGUAGE | sk | context.language a Accept-Language |
DISCOUNT_CODE | prázdna | kód, ktorý sa uplatní v kroku 4 |
POLL_SECONDS | 5 | prvý interval čítania v sekundách |
MAX_POLL_MINUTES | 30 | po koľkých minútach čakanie skončí |
Kupujúci a adresa sú ukážkové údaje: nahraďte ich údajmi svojho kupujúceho.
// ucp-quickstart.mjs — one whole UCP purchase in a Monokaido shop.
// Node 20+, no dependencies. SHOP=https://shop.example.com node ucp-quickstart.mjs
import { randomUUID } from 'node:crypto';
import { setTimeout as sleep } from 'node:timers/promises';
const env = process.env;
const SHOP = (env.SHOP ?? 'https://shop.example.com').replace(/\/+$/, '');
const PROFILE_URL = env.PROFILE_URL ?? 'https://platform.example.com/.well-known/ucp';
const QUERY = env.QUERY ?? 'marlin';
const LANGUAGE = env.LANGUAGE ?? 'sk';
const DISCOUNT_CODE = env.DISCOUNT_CODE ?? '';
const POLL_SECONDS = Number(env.POLL_SECONDS ?? 5);
const MAX_POLL_MINUTES = Number(env.MAX_POLL_MINUTES ?? 30);
const BUYER = { first_name: 'Jana', last_name: 'Nováková', email: 'jana.novakova@example.com', phone_number: '+421 905 123 456' };
const ADDRESS = {
id: 'home', first_name: 'Jana', last_name: 'Nováková', street_address: 'Hlavná 12',
address_locality: 'Bratislava', postal_code: '811 01', address_country: 'SK',
};
const RETRYABLE = new Set([429, 500, 502, 503, 504]);
class ProtocolError extends Error {
constructor(status, body) {
super(`HTTP ${status} ${body.code ?? ''}: ${body.content ?? ''}`);
this.status = status;
this.body = body;
}
}
// One logical call. A write gets ONE Idempotency-Key that every retry reuses,
// so a retried create can never make a second cart or checkout.
async function call(method, url, body, { write = false } = {}) {
const key = write ? randomUUID() : undefined;
for (let attempt = 1; ; attempt++) {
let res;
try {
res = await fetch(url, {
method,
headers: {
'UCP-Agent': `profile="${PROFILE_URL}"`,
'Accept-Language': LANGUAGE,
...(body !== undefined && { 'Content-Type': 'application/json' }),
...(key && { 'Idempotency-Key': key }),
},
body: body === undefined ? undefined : JSON.stringify(body),
signal: AbortSignal.timeout(65_000), // the web server gives up after 60 s
});
} catch (err) {
// No answer: the write may or may not have run. The same key makes the retry safe.
if (attempt >= 5) throw err;
await sleep(2 ** attempt * 1000);
continue;
}
const json = (res.headers.get('content-type') ?? '').includes('application/json');
const data = json ? await res.json() : { content: `${res.status} page from the web server` };
if (res.ok) return data;
// 409 with Retry-After = "same request still running"; without it, the key was reused for another request.
const retry = RETRYABLE.has(res.status) || (res.status === 409 && res.headers.has('retry-after'));
if (!retry || attempt >= 5) throw new ProtocolError(res.status, data);
const wait = Number(res.headers.get('retry-after')) || 2 ** attempt;
console.warn(` ${res.status} ${data.code ?? '(not JSON)'} — retrying in ${wait} s${key ? ' with the same key' : ''}`);
await sleep(wait * 1000);
}
}
// HTTP 200 can still mean "nothing came back": ucp.status "error" + messages.
function resource(data, what, { quiet = false } = {}) {
if (data.ucp?.status === 'error') {
const m = data.messages?.[0] ?? {};
throw new Error(`${what}: ${m.code} — ${m.content}`);
}
if (!quiet) for (const m of data.messages ?? []) console.log(` [${m.type}${m.severity ? '/' + m.severity : ''}] ${m.code}: ${m.content}`);
return data;
}
const money = ({ amount, currency }) => `${(amount / 100).toFixed(2)} ${currency}`; // 2 decimals: EUR, CZK, HUF, PLN
const total = (totals, currency) => money({ amount: totals.find((t) => t.type === 'total').amount, currency });
console.log(`1. Discover ${SHOP}`);
const profile = await call('GET', `${SHOP}/.well-known/ucp`);
const API = profile.ucp.services['dev.ucp.shopping'].find((s) => s.transport === 'rest').endpoint;
if (!profile.ucp.capabilities['dev.ucp.shopping.checkout']) {
throw new Error('This shop takes no orders over UCP (catalog mode): only search and lookup are offered.');
}
console.log(`2. Search "${QUERY}"`);
let product, variant, cursor;
for (let page = 1; page <= 5 && !variant; page++) {
const body = { query: QUERY, pagination: { limit: 10, ...(cursor && { cursor }) }, context: { language: LANGUAGE } };
const result = resource(await call('POST', `${API}/catalog/search`, body), 'search');
product = result.products.find((p) => p.variants.some((v) => v.availability.available));
variant = product?.variants.find((v) => v.availability.available);
if (!result.pagination.has_next_page) break; // a short (even empty) page is normal: keep following the cursor
cursor = result.pagination.cursor;
}
if (!variant) throw new Error(`Nothing available for "${QUERY}"`);
console.log(` ${product.title} — ${variant.title}, ${money(variant.price)}`);
console.log('3. Create a cart');
let cart = resource(await call('POST', `${API}/carts`, {
line_items: [{ item: { id: variant.id }, quantity: 1 }],
buyer: { email: BUYER.email },
context: { language: LANGUAGE, address_country: ADDRESS.address_country },
}, { write: true }), 'create cart');
console.log(` cart ${cart.id}: ${total(cart.totals, cart.currency)}`);
console.log('4. Update the cart (PUT replaces line_items: send every line)');
cart = resource(await call('PUT', `${API}/carts/${cart.id}`, {
line_items: cart.line_items.map((l) => ({ item: { id: l.item.id }, quantity: l.quantity })),
...(DISCOUNT_CODE && { discounts: { codes: [DISCOUNT_CODE] } }),
}, { write: true }), 'update cart');
console.log(` ${total(cart.totals, cart.currency)}`);
console.log('5. Turn the cart into a checkout');
let checkout = resource(await call('POST', `${API}/checkout-sessions`, { cart_id: cart.id }, { write: true }), 'create checkout');
const put = async (body) =>
resource(await call('PUT', `${API}/checkout-sessions/${checkout.id}`, body, { write: true }), 'update checkout');
console.log('6. Buyer, address and shipping until requires_escalation');
checkout = await put({ buyer: BUYER, fulfillment: { methods: [{ type: 'shipping', destinations: [ADDRESS], selected_destination_id: 'home' }] } });
for (let round = 1; checkout.status === 'incomplete'; round++) {
const blocking = checkout.messages.filter((m) => m.type === 'error' && m.severity === 'recoverable');
const options = checkout.fulfillment?.methods?.[0]?.groups?.[0]?.options ?? [];
const onlyTheOption = blocking.length > 0 && blocking.every((m) => m.path?.endsWith('.selected_option_id'));
if (round > 3 || !onlyTheOption || options.length === 0) {
throw new Error(`The agent cannot finish this checkout: ${blocking.map((m) => `${m.code} (${m.content})`).join('; ')}`);
}
const option = options[0]; // the shop's own order; a real agent lets the buyer choose
console.log(` shipping: ${option.title}, ${total(option.totals, checkout.currency)}`);
checkout = await put({ fulfillment: { methods: [{ type: 'shipping', groups: [{ selected_option_id: option.id }] }] } });
}
if (checkout.status !== 'requires_escalation') throw new Error(`Unexpected status ${checkout.status}`);
console.log(`7. Ready: ${total(checkout.totals, checkout.currency)}. Give the buyer this link — they pay on the shop:`);
console.log(`\n ${checkout.continue_url}\n`);
for (const link of checkout.links) console.log(` ${link.type}: ${link.url}`);
console.log('8. Waiting for the order (Ctrl+C stops; the checkout stays open until expires_at)');
const deadline = Math.min(Date.parse(checkout.expires_at) + 60_000, Date.now() + MAX_POLL_MINUTES * 60_000);
for (let wait = POLL_SECONDS; ; wait = Math.min(wait * 1.5, 60)) {
await sleep(wait * 1000);
// One request at a time per session: a GET of a checkout queues behind every other request on it.
const now = resource(await call('GET', `${API}/checkout-sessions/${checkout.id}`), 'poll', { quiet: true });
if (now.status === 'completed') {
console.log(` Order ${now.order.label ?? now.order.id} placed (placed, not necessarily paid): ${now.order.permalink_url}`);
break;
}
if (now.status === 'canceled') { console.log(' The checkout was canceled or expired without an order.'); break; }
if (Date.now() > deadline) { console.log(` Still ${now.status}; stopped polling.`); break; }
}
#!/usr/bin/env python3
"""ucp_quickstart.py — one whole UCP purchase in a Monokaido shop.
Python 3.9+ and requests (pip install requests).
SHOP=https://shop.example.com python3 ucp_quickstart.py
"""
import os
import time
import uuid
from datetime import datetime
import requests
SHOP = os.environ.get("SHOP", "https://shop.example.com").rstrip("/")
PROFILE_URL = os.environ.get("PROFILE_URL", "https://platform.example.com/.well-known/ucp")
QUERY = os.environ.get("QUERY", "marlin")
LANGUAGE = os.environ.get("LANGUAGE", "sk")
DISCOUNT_CODE = os.environ.get("DISCOUNT_CODE", "")
POLL_SECONDS = float(os.environ.get("POLL_SECONDS", "5"))
MAX_POLL_MINUTES = float(os.environ.get("MAX_POLL_MINUTES", "30"))
BUYER = {"first_name": "Jana", "last_name": "Nováková",
"email": "jana.novakova@example.com", "phone_number": "+421 905 123 456"}
ADDRESS = {"id": "home", "first_name": "Jana", "last_name": "Nováková", "street_address": "Hlavná 12",
"address_locality": "Bratislava", "postal_code": "811 01", "address_country": "SK"}
RETRYABLE = {429, 500, 502, 503, 504}
http = requests.Session()
http.headers.update({"UCP-Agent": f'profile="{PROFILE_URL}"', "Accept-Language": LANGUAGE})
class ProtocolError(Exception):
def __init__(self, status, body):
super().__init__(f"HTTP {status} {body.get('code', '')}: {body.get('content', '')}")
self.status, self.body = status, body
def call(method, url, body=None, write=False):
"""One logical call. A write gets ONE Idempotency-Key that every retry reuses,
so a retried create can never make a second cart or checkout."""
headers = {"Idempotency-Key": str(uuid.uuid4())} if write else {}
for attempt in range(1, 6):
try:
# json= sets Content-Type: application/json; the web server gives up after 60 s
r = http.request(method, url, json=body, headers=headers, timeout=65)
except requests.RequestException:
# No answer: the write may or may not have run. The same key makes the retry safe.
if attempt == 5:
raise
time.sleep(2 ** attempt)
continue
is_json = "application/json" in r.headers.get("Content-Type", "")
data = r.json() if is_json else {"content": f"{r.status_code} page from the web server"}
if r.ok:
return data
# 409 with Retry-After = "same request still running"; without it, the key was reused for another request.
retry = r.status_code in RETRYABLE or (r.status_code == 409 and "Retry-After" in r.headers)
if not retry or attempt == 5:
raise ProtocolError(r.status_code, data)
wait = int(r.headers.get("Retry-After") or 2 ** attempt)
print(f" {r.status_code} {data.get('code', '(not JSON)')} — retrying in {wait} s"
+ (" with the same key" if write else ""))
time.sleep(wait)
def resource(data, what, quiet=False):
"""HTTP 200 can still mean "nothing came back": ucp.status "error" + messages."""
if data.get("ucp", {}).get("status") == "error":
m = (data.get("messages") or [{}])[0]
raise RuntimeError(f"{what}: {m.get('code')} — {m.get('content')}")
if not quiet:
for m in data.get("messages", []):
severity = f"/{m['severity']}" if m.get("severity") else ""
print(f" [{m['type']}{severity}] {m['code']}: {m['content']}")
return data
def money(amount, currency):
return f"{amount / 100:.2f} {currency}" # 2 decimals: EUR, CZK, HUF, PLN
def total(totals, currency):
return money(next(t["amount"] for t in totals if t["type"] == "total"), currency)
print(f"1. Discover {SHOP}")
profile = call("GET", f"{SHOP}/.well-known/ucp")
API = next(s["endpoint"] for s in profile["ucp"]["services"]["dev.ucp.shopping"] if s["transport"] == "rest")
if "dev.ucp.shopping.checkout" not in profile["ucp"]["capabilities"]:
raise SystemExit("This shop takes no orders over UCP (catalog mode): only search and lookup are offered.")
print(f'2. Search "{QUERY}"')
product = variant = cursor = None
for page in range(5):
pagination = {"limit": 10, **({"cursor": cursor} if cursor else {})}
result = resource(call("POST", f"{API}/catalog/search",
{"query": QUERY, "pagination": pagination, "context": {"language": LANGUAGE}}), "search")
for p in result["products"]:
variant = next((v for v in p["variants"] if v["availability"]["available"]), None)
if variant:
product = p
break
if variant or not result["pagination"]["has_next_page"]:
break # a short (even empty) page is normal: keep following the cursor
cursor = result["pagination"]["cursor"]
if not variant:
raise SystemExit(f'Nothing available for "{QUERY}"')
print(f" {product['title']} — {variant['title']}, {money(**variant['price'])}")
print("3. Create a cart")
cart = resource(call("POST", f"{API}/carts", {
"line_items": [{"item": {"id": variant["id"]}, "quantity": 1}],
"buyer": {"email": BUYER["email"]},
"context": {"language": LANGUAGE, "address_country": ADDRESS["address_country"]},
}, write=True), "create cart")
print(f" cart {cart['id']}: {total(cart['totals'], cart['currency'])}")
print("4. Update the cart (PUT replaces line_items: send every line)")
update = {"line_items": [{"item": {"id": l["item"]["id"]}, "quantity": l["quantity"]} for l in cart["line_items"]]}
if DISCOUNT_CODE:
update["discounts"] = {"codes": [DISCOUNT_CODE]}
cart = resource(call("PUT", f"{API}/carts/{cart['id']}", update, write=True), "update cart")
print(f" {total(cart['totals'], cart['currency'])}")
print("5. Turn the cart into a checkout")
checkout = resource(call("POST", f"{API}/checkout-sessions", {"cart_id": cart["id"]}, write=True), "create checkout")
def put(body):
return resource(call("PUT", f"{API}/checkout-sessions/{checkout['id']}", body, write=True), "update checkout")
print("6. Buyer, address and shipping until requires_escalation")
checkout = put({"buyer": BUYER, "fulfillment": {"methods": [
{"type": "shipping", "destinations": [ADDRESS], "selected_destination_id": "home"}]}})
for round_ in range(1, 5):
if checkout["status"] != "incomplete":
break
blocking = [m for m in checkout["messages"] if m["type"] == "error" and m.get("severity") == "recoverable"]
groups = (checkout.get("fulfillment") or {}).get("methods", [{}])[0].get("groups", [{}])
options = groups[0].get("options", []) if groups else []
only_the_option = blocking and all(m.get("path", "").endswith(".selected_option_id") for m in blocking)
if round_ > 3 or not only_the_option or not options:
raise SystemExit("The agent cannot finish this checkout: "
+ "; ".join(f"{m['code']} ({m['content']})" for m in blocking))
option = options[0] # the shop's own order; a real agent lets the buyer choose
print(f" shipping: {option['title']}, {total(option['totals'], checkout['currency'])}")
checkout = put({"fulfillment": {"methods": [{"type": "shipping", "groups": [{"selected_option_id": option["id"]}]}]}})
if checkout["status"] != "requires_escalation":
raise SystemExit(f"Unexpected status {checkout['status']}")
print(f"7. Ready: {total(checkout['totals'], checkout['currency'])}. Give the buyer this link — they pay on the shop:")
print(f"\n {checkout['continue_url']}\n")
for link in checkout["links"]:
print(f" {link['type']}: {link['url']}")
print("8. Waiting for the order (Ctrl+C stops; the checkout stays open until expires_at)")
expires = datetime.fromisoformat(checkout["expires_at"].replace("Z", "+00:00")).timestamp()
deadline = min(expires + 60, time.time() + MAX_POLL_MINUTES * 60)
wait = POLL_SECONDS
while True:
time.sleep(wait)
# One request at a time per session: a GET of a checkout queues behind every other request on it.
now = resource(call("GET", f"{API}/checkout-sessions/{checkout['id']}"), "poll", quiet=True)
if now["status"] == "completed":
order = now["order"]
print(f" Order {order.get('label', order['id'])} placed (placed, not necessarily paid): {order['permalink_url']}")
break
if now["status"] == "canceled":
print(" The checkout was canceled or expired without an order.")
break
if time.time() > deadline:
print(f" Still {now['status']}; stopped polling.")
break
wait = min(wait * 1.5, 60)
Spustíte ich takto:
SHOP=https://shop.example.com PROFILE_URL=https://platform.example.com/.well-known/ucp node ucp-quickstart.mjs
SHOP=https://shop.example.com PROFILE_URL=https://platform.example.com/.well-known/ucp python3 ucp_quickstart.py
Čo sa môže pokaziť
| Čo vidíte | Prečo | Čo urobiť |
|---|---|---|
404 not_found, „No UCP business on this host“ | Hostiteľ UCP neposkytuje: nie je vlastnou doménou obchodu s overeným DNS a aktívnym certifikátom, UCP je vypnuté, vstavaný obchod je vypnutý, kľúč obchodu je neaktívny alebo mu vypršala platnosť, alebo modul E-shop nie je aktívny. Odpoveď nikdy nepovie, ktorý dôvod platí. | Overte doménu s obchodníkom a volajte hostiteľa z endpoint v profile. |
400 invalid_profile_url | UCP-Agent chýba alebo má zlý tvar. | Pošlite profile="https://…" s úvodzovkami a profile malými písmenami. |
424 profile_unreachable | Obchod si váš profil nestiahol: odpoveď nebola 200, bola presmerovaním, prišla príliš pomaly, mala viac ako 256 KiB alebo adresa je súkromná. | Opravte hosting a počkajte 60 sekúnd: toľko si obchod zlyhanie pamätá. |
422 profile_malformed alebo version_unsupported | Profil nie je profilom UCP alebo jeho ucp.version nie je 2026-04-08. | Zverejnite profil uvedený vyššie. |
200 s capabilities_incompatible | Váš profil schopnosť neuvádza alebo je obchod v katalógovom režime. | Doplňte schopnosť do profilu; pri obchode v katalógovom režime skončite po vyhľadávaní. |
200 s not_found pri košíku alebo checkoute | Identifikátor je neznámy, patrí inému obchodu alebo košík vypršal či bol zrušený. | Začnite znova od kroku 3. |
400 invalid_request | Pole má zlý tvar; content ho pomenuje, napríklad line_items must be an array pri PUT košíka bez položiek. „Idempotency-Key is not valid“ znamená, že kľúč nemá 1 až 255 viditeľných znakov ASCII. | Opravte požiadavku: nevykonala sa. |
409 idempotency_conflict bez Retry-After | Kľúč bol použitý pre inú požiadavku: inú operáciu, iný identifikátor v ceste alebo iné telo. | Pre každý logický zápis použite nový kľúč. |
409 idempotency_conflict s Retry-After: 2 | Prvá požiadavka s týmto kľúčom ešte beží. | Počkajte a pošlite tú istú požiadavku s tým istým kľúčom. |
503 „Another request on this session is being processed“ | Košík alebo checkout drží iná požiadavka. | Zopakujte po Retry-After (1 s) s tým istým kľúčom; na jednu reláciu posielajte jednu požiadavku naraz. |
503 „This shop is temporarily unavailable“ | Obchod je pozastavený alebo sa ho nepodarilo nájsť. | Zopakujte po Retry-After (60 s). |
503 „Too many new platform profiles from this client“ alebo „Platform profile discovery is busy“ | Obchod sťahuje priveľa profilov, ktoré nemá uložené: z vašej IP viac ako 5 nových za minútu alebo priveľa celkovo. | Zopakujte po Retry-After; používajte jednu stálu adresu profilu. |
429 rate_limited (JSON) | Limit operácie na IP adresu je vyčerpaný: 600 čítaní, 300 zápisov za minútu. | Počkajte Retry-After (60 s). |
429, 413, 502 alebo 504 ako stránka HTML | Webový server pred obchodom: viac ako 100 požiadaviek za sekundu, telo nad 1 MiB alebo API nedostupné či pomalšie ako 60 s. | Spomaľte a zopakujte; pri 502 a 504 opakujte zápisy s tým istým kľúčom. Telá držte malé. |
Checkout zostáva incomplete | Zostala chyba recoverable. | Prečítajte messages a pošlite, čo uvádza path (krok 6). |
handed_off | Kupujúci už nákup preniesol do obchodu. | Prestaňte zapisovať a čakajte; pre iný nákup vytvorte nový checkout. |
Checkout nikdy nie je completed | Kupujúci neobjednal, prihlásil sa v pokladni obchodu, odstránil položky alebo bol checkout predtým zrušený; zriedka zlyhalo samotné dokončenie (nie je zaručené). | Skončite pri termíne; rozhodujúce je potvrdenie od obchodu. |
Každý kód aj s tým, čo má klient urobiť, nájdete v kapitole Chyby a správy. Opakovanie požiadaviek, súbežné požiadavky, limity a životnosť relácií sú v kapitole Idempotencia, súbežnosť a limity.