Idempotencia, súbežnosť a limity
Idempotency-Key, opakovanie požiadaviek, súbežné zápisy, limity požiadaviek, platnosť relácií a cache.
Na tejto stránke
- Idempotency-Key
- Ktorých operácií sa týka
- Kľúč
- Rozsah a odtlačok
- Čo dostane opakovanie
- Ako dlho sa kľúče uchovávajú
- Bez kľúča
- Súbežnosť
- Na jednu reláciu jedna požiadavka naraz
- Zápis, ktorý prehrá preteky
- Jeden otvorený checkout na košík
- Opakovanie požiadaviek
- Limity požiadaviek
- API
- Webový server
- Nové profily platformy
- Plánovanie kapacity
- Časové limity a veľkosti
- Cache
- Platnosť relácií
- Čakanie na objednávku
- Kontrolný zoznam pre produkciu
Siete strácajú odpovede, servery sa reštartujú a agenti bežia paralelne. Táto kapitola je zmluvou pre takéto chvíle: ako zaistiť, aby sa zápis dal bezpečne zopakovať, čo sa stane, keď sa jednej relácie dotknú dve požiadavky, ktoré chyby a ako často opakovať, presné limity požiadaviek a časové limity, čo sa ukladá do cache, ako dlho relácia žije a ako čakať na objednávku kupujúceho bez plytvania limitmi. Samotné kódy vysvetľuje kapitola Chyby a správy.
Príklady používajú obchod https://shop.example.com a platformu, ktorej profil
je na adrese https://platform.example.com/.well-known/ucp.
Idempotency-Key
Zápis poslaný dvakrát — lebo odpoveď neprišla alebo sa reštartoval worker —
nesmie vytvoriť dva košíky. Hlavička Idempotency-Key to zaistí: obchod
vykoná prvú požiadavku, uloží jej odpoveď a na každé zopakovanie tej istej
požiadavky odpovie uloženou odpoveďou.
Ktorých operácií sa týka
Hlavičku číta iba týchto sedem zápisov. Ostatné operácie ju ignorujú.
| Operácia | Cesta | Čo patrí do odtlačku požiadavky |
|---|---|---|
| Vytvorenie košíka | POST /ucp/v1/carts | telo |
| Zmena košíka | PUT /ucp/v1/carts/{id} | id v ceste a telo |
| Zrušenie košíka | POST /ucp/v1/carts/{id}/cancel | id v ceste (telo sa ignoruje) |
| Vytvorenie checkoutu | POST /ucp/v1/checkout-sessions | telo |
| Zmena checkoutu | PUT /ucp/v1/checkout-sessions/{id} | id v ceste a telo |
| Dokončenie checkoutu | POST /ucp/v1/checkout-sessions/{id}/complete | id v ceste a telo |
| Zrušenie checkoutu | POST /ucp/v1/checkout-sessions/{id}/cancel | id v ceste (telo sa ignoruje) |
Operácie katalógu, GET košíka či checkoutu, profil obchodu a cesty
odovzdania ju nečítajú nikdy: nemenia nič, čo by opakovanie mohlo zdvojiť.
Kľúč
- Formát: 1–255 tlačiteľných znakov ASCII od
!po~— žiadne medzery, žiadne znaky mimo ASCII (ani diakritika). Na čokoľvek iné obchod odpovie400 invalid_requestIdempotency-Key is not valida nič sa nevykoná. Hlavičku pošlite raz: hlavička poslaná dvakrát dorazí spojená reťazcom", ", ktorý obsahuje medzeru. - Jeden kľúč na jeden logický zápis. Vygenerujte ho vo chvíli, keď sa rozhodnete zápis urobiť — dobrou voľbou je UUID verzie 4 — a ten istý kľúč s tým istým telom posielajte pri každom zopakovaní tohto zápisu. Iný zápis dostane iný kľúč, aj keď má náhodou rovnaké telo.
- Uložte ho k čakajúcemu zápisu. Ak sa váš worker medzi pokusmi môže reštartovať, uložte kľúč spolu s operáciou ešte pred prvým pokusom, aby opakovanie po reštarte poslalo ten istý kľúč.
- Hlavička je nepovinná. Bez nej, alebo s prázdnou hodnotou, sa zápis jednoducho vykoná. Pozrite Bez kľúča.
KEY=$(uuidgen) # generated once for this write; every retry below reuses it
curl -sS -X POST https://shop.example.com/ucp/v1/carts \
-H 'UCP-Agent: profile="https://platform.example.com/.well-known/ucp"' \
-H "Idempotency-Key: $KEY" \
-H 'Content-Type: application/json' \
-d '{"line_items":[{"item":{"id":"1e8a6c4f-2b93-4d71-9f05-6a3c8e2b1d47"},"quantity":1}],"context":{"language":"sk"}}'
import { randomUUID } from 'node:crypto';
// Persist { key, body } with the pending write before the first attempt.
const pending = { key: randomUUID(), body: { line_items: [{ item: { id: VARIANT_ID }, quantity: 1 }] } };
import uuid
# Persist key and body with the pending write before the first attempt.
pending = {"key": str(uuid.uuid4()), "body": {"line_items": [{"item": {"id": VARIANT_ID}, "quantity": 1}]}}
Rozsah a odtlačok
Uložený kľúč patrí jednému obchodu, presnej adrese vášho profilu —
reťazcu v UCP-Agent, porovnanému znak po znaku — a samotnému kľúču. Iná
platforma, ktorá náhodou pošle rovnaký kľúč, vašu odpoveď nikdy neuvidí a ten
istý kľúč v dvoch rôznych obchodoch predstavuje dva rôzne kľúče. Všetky domény jedného
obchodu zdieľajú jeho kľúče.
Z požiadavky, pre ktorú bol kľúč použitý prvýkrát, sa uloží odtlačok: operácia, id v ceste presne tak, ako ste ho poslali, a telo. Telo sa porovnáva ako JSON: na poradí kľúčov objektov a na medzerách nezáleží, na poradí prvkov poľa áno. Id v ceste sa porovnáva ako reťazec, takže to isté id zapísané veľkými písmenami je iná požiadavka.
Adresu profilu nemeňte: opakovanie pod inou adresou profilu — aj s iným query stringom — je prvým použitím kľúča.
Čo dostane opakovanie
| Situácia | Odpoveď |
|---|---|
| Prvé použitie kľúča | Operácia sa vykoná. Jej odpoveď sa uloží. |
| Tá istá požiadavka znova, prvá skončila | Uložený stav HTTP a telo — 201 zostane 201 a vytvorená relácia si ponechá svoje id. Operácia sa znova nevykoná. |
| Tá istá požiadavka znova, prvá ešte beží | 409 idempotency_conflict, A request with this Idempotency-Key is still being processed, Retry-After: 2. Počkajte a pošlite ju znova. |
| Ten istý kľúč pre inú operáciu, iné id v ceste alebo iné telo | 409 idempotency_conflict, This Idempotency-Key was used for a different request, bez Retry-After. Chyba v klientovi: použite nový kľúč. |
Prvá požiadavka skončila chybou protokolu (400, 503 obsadená relácia, 500 …) | Kľúč sa spolu s chybou uvoľnil. Opakovanie operáciu vykoná znova. |
Prvá požiadavka skončila obchodnou chybou (200 s ucp.status: "error", napríklad not_found alebo všetko vypredané) | Táto odpoveď sa uloží a zopakuje ako každá iná. Nový pokus potrebuje nový kľúč. |
| Proces prvej požiadavky zanikol alebo sa jej odpoveď nepodarilo uložiť | Kľúč zostane zabraný najviac 120 sekúnd; dovtedy opakovanie dostane 409 s Retry-After: 2. Potom kľúč prevezme tá istá požiadavka a vykoná sa znova. |
Požiadavka bola odmietnutá skôr, než sa kľúč prečítal: telo, ktoré API nevedelo prečítať (400 The request is not valid, 413, 415), 403, 429, 404 alebo 503 pre obchod, chyby UCP-Agent a profilu, version_unsupported, capabilities_incompatible | Kľúč sa nepoužil. Ďalší pokus je jeho prvým použitím. |
Zopakovaná odpoveď je odpoveďou prvej požiadavky, nie čerstvým čítaním: jej
expires_at, ceny aj správy sú z tej chvíle. Kľúče jej objektov JSON môžu byť
zoradené inak ako v prvej odpovedi (na poradí kľúčov nezáleží), preto sa môže
líšiť aj jej ETag. Ak po zopakovaní potrebujete aktuálny stav,
reláciu si načítajte cez GET. Odpoveď nijako neoznačuje, že ide o
zopakovanie.
Ako dlho sa kľúče uchovávajú
Kľúč sa uchováva 25 hodín od chvíle, keď bol zabraný. Staršie kľúče maže úloha, ktorá beží každú hodinu, takže kľúč zmizne medzi 25 a zhruba 26 hodinami po zabratí. Potom je ten istý kľúč opäť prvým použitím. Kľúč nikdy nepoužívajte pre iný zápis a nespoliehajte sa na ochranu opakovania, ktoré príde viac ako 24 hodín po prvom pokuse.
Bez kľúča
Zápis bez Idempotency-Key sa vykoná pri každom doručení:
| Operácia | Zopakovaná bez kľúča |
|---|---|
POST /ucp/v1/carts, POST /ucp/v1/checkout-sessions s line_items | Druhá relácia. Prvá zostane, kým jej neuplynie platnosť — košík 7 dní, checkout 24 hodín — a jej údaje ešte 30 dní potom. Nedá sa znova nájsť. |
POST /ucp/v1/checkout-sessions s cart_id | Znova sa vráti otvorený checkout košíka (200). |
PUT košíka alebo checkoutu | Uplatní sa znova: rovnaký stav, nanovo nacenený, platnosť znova predĺžená. |
| Zrušenie košíka | Na opakovanie obchod odpovie 200 not_found: košík je už zrušený. |
| Zrušenie checkoutu | Opakovanie vráti zrušený checkout so správou checkout_canceled. |
| Dokončenie checkoutu | Znova sa odpovie podľa aktuálneho stavu checkoutu. Dokončenie nikdy nevytvorí objednávku, takže opakovanie nič nepokazí. |
Kľúč posielajte pri každom zápise. Nič nestojí a zopakované vytvorenie je najčastejší spôsob, ako stratiť prehľad o relácii kupujúceho.
Súbežnosť
Na jednu reláciu jedna požiadavka naraz
Každá požiadavka, ktorá môže reláciu zmeniť, čaká, kým príde na rad: požiadavky na jeden košík alebo checkout bežia jedna po druhej v poradí, v akom prídu na server API.
| Čaká na rad relácie | Nečaká |
|---|---|
PUT a zrušenie košíka; GET, PUT, dokončenie a zrušenie checkoutu; prevod košíka na checkout (čaká na rad košíka); odovzdanie kupujúcemu | Vytvorenie košíka, vytvorenie checkoutu z line_items, GET košíka, katalóg |
Aj GET checkoutu čaká na rad, pretože čítanie môže uložiť zmenený stav
(checkout po uplynutí platnosti zruší čítanie, ktoré ho nájde).
| Limit | Hodnota | Po jeho prekročení |
|---|---|---|
| Čakanie na rad | 10 sekúnd od chvíle, keď požiadavka začne čakať | 503 service_unavailable, Another request on this session is being processed — retry shortly, Retry-After: 1 |
| Požiadavky čakajúce za tou, ktorá beží | 4 | Ďalšia dostane rovnaké 503 okamžite |
Operácia za takým 503 sa nikdy nevykonala a jej Idempotency-Key sa
uvoľnil: po sekunde pošlite tú istú požiadavku s tým istým kľúčom. Limit fronty
platí pre každý proces servera API; medzi procesmi pravidlo „jedna naraz“
zaisťuje zámok v databáze.
Na jednu reláciu posielajte naraz najviac jednu požiadavku. Paralelné zápisy do jednej relácie nič nezrýchlia — aj tak pobežia jeden po druhom — a ich nával môže zablokovať odovzdanie kupujúcemu, ktoré čaká na ten istý rad.
Zápis, ktorý prehrá preteky
Checkout menia bez čakania na jeho rad dve veci: objednávka kupujúceho v obchode, ktorá ho dokončí, a nový prevod jeho košíka, ktorý ho zruší, keď mu uplynula platnosť. Každý zápis do relácie sa preto uplatní, iba ak sa relácia od chvíle, keď ju požiadavka prečítala, nezmenila. Keď zápis tieto preteky prehrá:
| Čo sa medzitým stalo | Dostanete |
|---|---|
| Checkout dokončila objednávka alebo bol zrušený | Checkout tak, ako je teraz — completed s order alebo canceled. PUT alebo zrušenie nesie aj správu checkout_completed alebo checkout_canceled; GET alebo dokončenie nesie pri zrušenom checkoute checkout_canceled a pri dokončenom žiadnu správu. Vaša zmena sa neuplatnila. |
| Košík bol zrušený | 200 not_found, Cart not found or has expired. |
| Relácia sa zmenila inak a je stále otvorená | 503, Retry-After: 1. Zopakujte s tým istým kľúčom. |
Relácie nemajú číslo verzie, ktoré by ste mohli posielať: neexistuje
If-Match, ani nie je potrebný — porovnanie robí server.
Jeden otvorený checkout na košík
Košík má najviac jeden otvorený checkout. Dva prevody toho istého košíka v tej
istej chvíli vrátia ten istý checkout: jeden so 201, druhý so 200. Ak ho
chcete zmeniť, pošlite naň PUT; ďalší prevod košíka nič nezmení.
Opakovanie požiadaviek
| Odpoveď | Opakovať? | Čakať | Ten istý Idempotency-Key? |
|---|---|---|---|
| Žiadna odpoveď: vypršal časový limit, prerušené spojenie | Áno | S rastúcim odstupom | Áno. Zápis sa mohol vykonať; vďaka kľúču dostane opakovanie uloženú odpoveď. |
502 alebo 504 (HTML z webového servera) | Áno | S rastúcim odstupom | Áno. Po 504 môže prvá požiadavka ešte bežať: kým neskončí, dostanete 409 s Retry-After: 2. |
503 Another request on this session is being processed — retry shortly | Áno | Retry-After (1 s) | Áno |
503 This shop is temporarily unavailable | Niekoľkokrát | Retry-After (60 s) | Áno |
503 Too many new platform profiles from this client, retry shortly alebo Platform profile discovery is busy, retry shortly | Áno | Retry-After | Áno |
409 A request with this Idempotency-Key is still being processed | Áno | Retry-After (2 s) | Áno, aj s tým istým telom |
429 rate_limited (JSON) | Áno | Retry-After (60 s) | Áno |
429 z webového servera (HTML) | Áno | S rastúcim odstupom, od 1 s | Áno |
500 internal_error | Niekoľkokrát | S rastúcim odstupom | Áno. Kľúč sa uvoľnil, takže opakovanie operáciu vykoná znova. |
424 profile_unreachable, 422 profile_malformed | Až po oprave profilu | Aspoň 60 s: tak dlho si obchod zlyhanie pamätá | — |
Každé iné 4xx: 400, 403, 404, 409 bez Retry-After, 413, 415, 422 version_unsupported | Nie | — | — |
200 s ucp.status: "error" | Nie: tá istá požiadavka dostane tú istú odpoveď | — | Nový pokus so zmenenou požiadavkou potrebuje nový kľúč. |
Na jedno logické volanie použite najviac 5 pokusov, exponenciálny odstup 1,
2, 4, 8 sekúnd s náhodným rozptylom a stropom 30 sekúnd, a hodnotu
Retry-After namiesto odstupu vždy, keď ju odpoveď nesie. Pri každom pokuse
pošlite to isté telo — ten istý JSON; poradie kľúčov a medzery sa môžu líšiť,
poradie prvkov poľa nie — inak obchod odpovie 409.
Zápis, ktorého pokusy sa všetky skončili bez jednoznačnej odpovede — chyby
siete, 502, 504, 409 s Retry-After —, sa mohol vykonať a reláciu potom
nemáte ako nájsť, lebo API ich nevypisuje. Nezahadzujte ho: jeho kľúč a telo
si nechajte pri rozpracovanom zápise a tú istú požiadavku pošlite neskôr
znova. Kým prvý pokus ešte beží, opakovanie dostane 409 s Retry-After: 2;
keď skončí, jeho uloženú odpoveď; a ak zanikol, tá istá požiadavka kľúč
prevezme 120 sekúnd po jeho zabraní a zápis vykoná. Ak sa prvý pokus do API
vôbec nedostal, opakovanie je prvým použitím kľúča. Nový kľúč by naopak
založil druhú reláciu.
import { randomUUID } from 'node:crypto';
import { setTimeout as sleep } from 'node:timers/promises';
const RETRYABLE = new Set([429, 500, 502, 503, 504]);
const backoff = (attempt) => Math.min(30_000, 1000 * 2 ** (attempt - 1)) * (0.5 + Math.random() / 2);
// One logical UCP call. Every retry reuses the same Idempotency-Key and body.
export async function ucpCall(method, url, { profileUrl, body, key, maxAttempts = 5 } = {}) {
const write = method !== 'GET' && !url.includes('/catalog/');
key ??= write ? randomUUID() : undefined;
for (let attempt = 1; ; attempt++) {
let res;
try {
res = await fetch(url, {
method,
redirect: 'error', // the API never redirects; http:// is a mistake, not a hop
headers: {
'UCP-Agent': `profile="${profileUrl}"`,
...(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) {
if (attempt >= maxAttempts) throw err; // no answer: the same key makes the retry safe
await sleep(backoff(attempt));
continue;
}
const stillRunning = res.status === 409 && res.headers.has('retry-after');
if ((RETRYABLE.has(res.status) || stillRunning) && attempt < maxAttempts) {
const retryAfter = Number(res.headers.get('retry-after'));
await sleep(retryAfter > 0 ? retryAfter * 1000 : backoff(attempt));
continue;
}
return res; // final: classify it as in Errors and messages
}
}
import random, time, uuid
import requests
RETRYABLE = {429, 500, 502, 503, 504}
def backoff(attempt):
return min(30, 2 ** (attempt - 1)) * random.uniform(0.5, 1.0)
def ucp_call(method, url, profile_url, body=None, key=None, max_attempts=5):
"""One logical UCP call. Every retry reuses the same Idempotency-Key and body."""
write = method != "GET" and "/catalog/" not in url
key = key or (str(uuid.uuid4()) if write else None)
headers = {"UCP-Agent": f'profile="{profile_url}"'}
if key:
headers["Idempotency-Key"] = key
for attempt in range(1, max_attempts + 1):
try:
r = requests.request(method, url, json=body, headers=headers,
timeout=65, allow_redirects=False)
except requests.RequestException:
if attempt == max_attempts:
raise # no answer: the same key makes the retry safe
time.sleep(backoff(attempt))
continue
still_running = r.status_code == 409 and "Retry-After" in r.headers
if (r.status_code in RETRYABLE or still_running) and attempt < max_attempts:
retry_after = r.headers.get("Retry-After", "")
time.sleep(int(retry_after) if retry_after.isdigit() else backoff(attempt))
continue
return r # final: classify it as in Errors and messages
429 alebo 503 pre obchod znamená minútu čakania. Konverzačný agent by mal
kupujúcemu povedať, že čaká, alebo to vzdať a vrátiť sa neskôr, namiesto toho,
aby potichu stál.
Limity požiadaviek
API
Každá operácia má vlastný limit na IP adresu klienta za 60 sekúnd:
| Operácie | Limit na IP adresu a operáciu |
|---|---|
Profil obchodu (GET /.well-known/ucp); vyhľadávanie, lookup a detail produktu v katalógu; GET košíka; GET checkoutu | po 600 za minútu |
| Vytvorenie, zmena a zrušenie košíka; vytvorenie, zmena, dokončenie a zrušenie checkoutu | po 300 za minútu |
Stránka odovzdania (GET /ucp/continue/<token>) a jej požiadavka (POST) | po 60 za minútu |
- Každá operácia sa počíta zvlášť: 600 vyhľadávaní a 600 čítaní checkoutu v tej istej minúte sú obe v limite.
- Kľúčom je vaša IP adresa, nie obchod a nie váš profil: jedna výstupná IP
adresa zdieľa limit každej operácie naprieč všetkými obchodmi na
Monokaide. Ide o adresu, ktorú vidí webový server;
X-Forwarded-For, ktorý pošlete, sa nepoužíva. - Každá požiadavka sa počíta 60 sekúnd — v zásade. Požiadavka, ktorá by
limit prekročila, sa odmietne a začne pre vašu IP adresu 60-sekundovú
blokádu danej operácie; každá požiadavka počas blokády dostane
429 rate_limitedsRetry-After: 60a po skončení blokády sa počíta od nuly. - Počet neklesá vždy. Počty sa držia v pamäti servera API (reštart ich
vymaže). Každá požiadavka má zo súčtu odísť 60 sekúnd po tom, ako prišla,
obmedzovač, ktorý API používa (
@nestjs/throttler6.5), však tieto čakajúce odpočty zahodí — všetkým klientom naraz — vždy, keď niektorému klientovi skončí blokáda. Odvtedy váš počet iba rastie, kým nedosiahne limit a nevynuluje ho vaša vlastná blokáda429. Limit preto berte ako rozpočet, nie ako rýchlosť: bežnú prevádzku držte najviac na polovici limitu a po429počkajte celéRetry-After. - Hlavičky. Každá odpoveď, ktorá prešla obmedzovačom, nesie
X-RateLimit-Limit(limit operácie),X-RateLimit-Remaining(limit mínus váš aktuálny počet) aX-RateLimit-Reset(sekundy, ktoré zostávajú z 60-sekundového okna, ktoré obmedzovač vedie pre IP adresu a operáciu; na konci okna sa počet nevynuluje, takže nehovorí, kedy sa rozpočet vráti). SledujteX-RateLimit-Remaininga spomaľte dávno predtým, než klesne na nulu. Odpoveď429nesie namiesto nichRetry-After.
Limity odovzdania sa počítajú pre IP adresu kupujúceho, nie vašu. Kupujúci za jednou zdieľanou adresou — napríklad vo firemnej sieti — zdieľajú limit 60 požiadaviek za minútu.
Webový server
Pred API webový server povoľuje 100 požiadaviek za sekundu na IP adresu s
nárazovou rezervou 200, počítaných zo všetkej návštevnosti obchodov z tejto adresy:
volania UCP všetkých obchodov aj vlastné stránky a obrázky obchodov. Nad tým
príde vlastná HTML odpoveď 429 webového servera, bez Retry-After.
Spomaľte a opakujte s rastúcim odstupom, počnúc jednou sekundou.
Nové profily platformy
Sťahovanie profilu platformy, ktorý server obchodu ešte nepozná, je
obmedzené: 5 nových adries profilu za minútu na klienta (jedna adresa IPv4
alebo jedna sieť IPv6 /64) a spolu 60 za minútu. Nad tým príde 503 s
Retry-After. Profil, ktorý server už pozná, sa nepočíta nikdy, takže jednej
stálej adresy profilu sa limit netýka.
Plánovanie kapacity
| Vzor | Požiadaviek za minútu | Polovica limitu sa dosiahne pri |
|---|---|---|
| Zisťovanie stavu jedného checkoutu každých 5 sekúnd | 12 čítaní checkoutu | zhruba 25 naraz sledovaných checkoutoch z jednej IP adresy |
| Zisťovanie stavu jedného checkoutu raz za minútu | 1 čítanie checkoutu | zhruba 300 checkoutoch |
| Zostavenie relácie: vytvorenie, dve zmeny, prevod, dve zmeny | 6 zápisov v 4 rôznych operáciách | hlboko pod 150 na operáciu |
Keďže počet nemusí klesať (pozri vyššie), tieto čísla platia iba vtedy, keď
vaša prevádzka zostáva výrazne pod limitom; vzor, ktorý dlho beží na plnom
limite, dostáva blokády 429.
Ak jedna výstupná IP adresa obsluhuje naraz veľa kupujúcich, rozložte prevádzku na viac adries alebo predĺžte interval zisťovania stavu (pozrite Čakanie na objednávku).
Časové limity a veľkosti
| Čo | Hodnota | Potom |
|---|---|---|
| Webový server čaká na API | 60 sekúnd | 504 (HTML). Zápis sa ešte môže dokončiť: zopakujte ho s tým istým kľúčom. |
| Požiadavka čaká, kým príde na rad jej relácia | 10 sekúnd | 503, Retry-After: 1 |
Nedokončené zabratie Idempotency-Key | 120 sekúnd | Kľúč prevezme tá istá požiadavka. |
| Obchod sťahuje váš profil | 3 sekundy bez dát, 5 sekúnd spolu | 424 profile_unreachable |
| Časový limit vášho klienta | Odporúčané: 65 sekúnd | Je to viac ako 60 sekúnd webového servera, takže dostanete jeho 504, a nie vypršanie vlastného časového limitu. V oboch prípadoch zopakujte s tým istým kľúčom. |
| Limit | Hodnota | Po prekročení |
|---|---|---|
| Telo požiadavky | 1 MiB cez doménu obchodu | 413 (HTML) z webového servera |
line_items | 100 na požiadavku | 400 |
| Množstvo v položke | 999 | Zníži sa na 999 bez správy |
| Zľavové kódy | 10, každý najviac 50 znakov | 400 (dlhšie kódy sa skrátia) |
ids v požiadavke lookup | 20, každé najviac 255 znakov | 400 request_too_large (dlhšie id sa skrátia) |
Veľkosť strany vyhľadávania (pagination.limit) | 20, predvolene 10 | Zníži sa na 20 |
query vyhľadávania | 200 znakov | Skráti sa na 200 |
Hlavička UCP-Agent | 4096 znakov; adresa profilu v nej 1000 | 400 invalid_profile_url |
Idempotency-Key | 255 znakov | 400 invalid_request |
| Profil vašej platformy | 256 KiB | 424 profile_unreachable |
Cache
| Odpoveď | Cache-Control |
|---|---|
Profil obchodu, GET /.well-known/ucp | public, max-age=300 — môžete si ho uložiť najviac na 5 minút |
| Všetko ostatné: katalóg, košíky, checkouty, každá chyba, stránka odovzdania | no-store |
- Odpovede katalógu ani relácie si neukladajte ani na svojej strane: ceny, sklad aj kódy sa kontrolujú naživo a relácia sa môže medzitým zmeniť (objednávka kupujúceho ju dokončí, uplynutie platnosti zruší).
- Odpovede nesú
ETag: silný, ako ho pošle API, a slabý (W/"…"), keď odpoveď skomprimuje webový server, čo urobí, keď pošleteAccept-Encoding: gzip.GET, ktorý pošleIf-None-Matchs aktuálnymETagv ktoromkoľvek z týchto tvarov, dostane304 Not Modifiedbez tela, hoci odpoveď jeno-store; ak vaša HTTP knižnica pridávaIf-None-Matchsama, odpoveď304ošetrite. Na zápisy obchod nikdy neodpovedá304. - Žiadna proxy medzi vami a API odpovede UCP neukladá.
- Zmena obchodníka — zapnutie alebo vypnutie UCP, vypnutie obchodu alebo jeho kľúča, odobratá doména — platí hneď. Novo pridanú doménu API začne poskytovať, keď má overené DNS a aktívny certifikát, a potom do 30 sekúnd. Vaša kópia profilu môže byť až 5 minút stará.
- Obchod si váš profil platformy uloží na čas podľa jeho
max-age, obmedzený na 60 sekúnd až 1 hodinu (bez neho na 5 minút). Zmena vášho profilu — nová schopnosť, opravená verzia — sa prejaví po tom, ako táto kópia vyprší: prvá požiadavka potom ešte dostane odpoveď podľa starej kópie, kým si obchod na pozadí sťahuje novú. Ak sa stiahnutie nepodarí, stará kópia sa používa ešte do hodiny po jej vypršaní a stiahnutie sa skúša najviac raz za minútu.
Platnosť relácií
| Relácia | Platí | Predĺži ju | Potom |
|---|---|---|---|
| Košík | 7 dní | vytvorenie, PUT | Na každé volanie obchod odpovie 200 not_found. |
| Checkout | 24 hodín | vytvorenie (aj z košíka), PUT | Najbližšia požiadavka, ktorá sa ho dotkne, ho zruší: status: "canceled". |
| Relácia, ktorú si kupujúci odniesol do obchodu | do jej posledného expires_at | nič: zápisy sú zmrazené | Ako vyššie. |
| Údaje akejkoľvek relácie | do 30 dní po expires_at | — | Vymažú sa; na volania obchod odpovedá 200 not_found. |
GET, dokončenie a zrušenie reláciu nikdy nepredĺžia. expires_at v každej
odpovedi je termín v UTC. Platnosť sa neukončuje sama: v samotnom termíne sa
nič nestane a checkout zruší až najbližšia požiadavka, ktorá ho číta, do neho
zapisuje alebo znova prevádza jeho košík. Podrobnosti sú v kapitole
Princípy a životný cyklus.
Ak chcete otvorený checkout udržať, kým sa kupujúci rozhoduje, pošlite naň
pred expires_at PUT — PUT s {} ho nanovo nacení a predĺži o 24 hodín.
Keď si ho kupujúci odniesol do obchodu, už ho nepredĺži nič.
Čakanie na objednávku
API neposiela žiadne webhooky. To, že kupujúci objednal, zistíte čítaním
checkoutu: GET /ucp/v1/checkout-sessions/{id} vráti status: "completed" a
order, keď je objednávka v obchode vytvorená.
- Odovzdávajte checkout, nie košík: dokončeným (
completed) sa môže stať iba checkout. - Interval. Server žiadny neodporúča. Rozumné je zisťovať stav každých
5–10 sekúnd počas prvej štvrťhodiny po tom, ako kupujúci dostal
continue_url, a potom raz za minútu. - Na jeden checkout jedna rozpracovaná požiadavka. Každé čítanie
checkoutu čaká na rad relácie; pri
503počkajteRetry-Aftera pokračujte. - Skončite pri
completed(prečítajteorder) alebocanceled. Skončite aj poexpires_at: ešte jedno čítanie po termíne dá konečnú odpoveď —completed, ak objednávka prišla skôr, inakcanceled. - Počítajte s tým, že sa nedokončí. Checkout zostane otvorený, kým mu neuplynie platnosť, keď kupujúci neobjedná, keď sa po odovzdaní ako hosť prihlási až v pokladni obchodu, alebo keď tovar z košíka odstráni; zriedka aj vtedy, keď dokončenie zlyhá na strane Monokaida (nie je zaručené a chyba sa iba zapíše do logu). Kupujúci vidí vlastné potvrdenie obchodu.
- Stav zisťujte pri každom otvorenom stave. Aj checkout odovzdaný v stave
incompletesa objednávkou dokončí.
import { setTimeout as sleep } from 'node:timers/promises';
// Poll a handed-over checkout until it is completed, canceled or past its deadline.
async function waitForOrder(api, checkoutId, profileUrl) {
const started = Date.now();
for (;;) {
const res = await ucpCall('GET', `${api}/checkout-sessions/${checkoutId}`, { profileUrl });
if (res.status === 404) return { outcome: 'not_served' }; // the shop stopped serving UCP: re-discover it later
if (!res.ok) throw new Error(`HTTP ${res.status}`); // retries spent: try again later
const checkout = await res.json();
if (checkout.ucp?.status === 'error') return { outcome: 'gone' }; // not_found
if (checkout.status === 'completed') return { outcome: 'ordered', order: checkout.order };
if (checkout.status === 'canceled') return { outcome: 'canceled' };
const pastDeadline = Date.now() > Date.parse(checkout.expires_at);
const interval = Date.now() - started < 15 * 60_000 ? 5_000 : 60_000;
await sleep(pastDeadline ? 1_000 : interval); // one last read decides
}
}
import time
from datetime import datetime
def wait_for_order(api, checkout_id, profile_url):
"""Poll a handed-over checkout until it is completed, canceled or past its deadline."""
started = time.monotonic()
while True:
r = ucp_call("GET", f"{api}/checkout-sessions/{checkout_id}", profile_url)
if r.status_code == 404:
return {"outcome": "not_served"} # the shop stopped serving UCP: re-discover it later
r.raise_for_status() # retries spent: try again later
checkout = r.json()
if checkout.get("ucp", {}).get("status") == "error":
return {"outcome": "gone"} # not_found
if checkout["status"] == "completed":
return {"outcome": "ordered", "order": checkout["order"]}
if checkout["status"] == "canceled":
return {"outcome": "canceled"}
deadline = datetime.fromisoformat(checkout["expires_at"].replace("Z", "+00:00"))
past_deadline = datetime.now(deadline.tzinfo) > deadline
interval = 5 if time.monotonic() - started < 15 * 60 else 60
time.sleep(1 if past_deadline else interval) # one last read decides
Termín checkoutu môže byť až o deň. Zisťovanie stavu spúšťajte v úlohe na pozadí, ktorá prežije reštart — id checkoutu a čas ďalšej kontroly majte vo svojom úložisku — a nie v požiadavke, ktorá obslúžila kupujúceho.
Kontrolný zoznam pre produkciu
- Každý zápis nesie
Idempotency-Key, vygenerovaný raz na logický zápis, uložený spolu s ním a znova použitý pri každom opakovaní s tým istým telom. - Opakovanie sa riadi tabuľkou vyššie: najviac 5 pokusov,
Retry-After, keď príde, inak exponenciálny odstup s rozptylom, žiadne opakovanie ostatných4xxa obchodných chýb. - Na jednu reláciu jedna rozpracovaná požiadavka, nikdy viac paralelne: obchod za bežiacou zaradí najviac štyri.
- Časový limit klienta je 65 sekúnd a presmerovania sa nesledujú.
- Odpovede, ktoré nie sú JSON, sa ošetria podľa stavu HTTP:
413,429,502,504z webového servera. - Limity sa sledujú pre každú výstupnú IP adresu a operáciu, naprieč
všetkými obchodmi, ktoré obsluhujete:
X-RateLimit-Remaining, počty429. Bežná prevádzka zostáva najviac na polovici každého limitu. - Profil vašej platformy má jednu stálu adresu,
Cache-Control: public, max-age=3600, a podáva sa rýchlo so stavom200z verejnej adresy. - Do cache sa neukladá nič okrem profilu obchodu, a ten najviac na 5 minút.
- Zisťovanie stavu beží na pozadí, skončí pri
completed,canceledalebo po jednom čítaní poexpires_ata „nikdy nedokončené“ berie ako bežný výsledok. - Každé volanie sa zapíše do logu s časom v UTC, hostiteľom, metódou,
cestou, stavom,
codea vaším korelačným id — API žiadne id požiadavky nevracia.