Skip to content

Security and data protection

What UCP accepts and stores, how buyer data is protected, why it never handles payments, and how each shop is kept apart.

On this page

This chapter is written for the security review of an integration. It states what a Monokaido shop's UCP service trusts, what it accepts, what it stores and for how long, and what your platform has to protect on its side. In short: the API is open and identifies the shop by its domain, a session id is the key to that session, no payment ever passes through it, and buyer data stays with the shop for a bounded time.

The examples use the shop https://shop.example.com and a platform whose profile is https://platform.example.com/.well-known/ucp.

The security model in five sentences

  1. The domain is the shop. A request is answered by the shop whose verified custom domain it was sent to; nothing else selects a shop.
  2. Calls are not authenticated. There is no API key, no OAuth and no request signature; UCP-Agent names your platform but does not prove it.
  3. A session id is a bearer capability. Whoever knows a cart or checkout id can read and change that session on that shop.
  4. No payment data, ever. The buyer pays on the shop's own website; the API reads no payment field and takes no payment.
  5. Buyer data is kept for a bounded time. Session data is deleted 30 days after the session expires, stored answers after 25 hours.

Transport

AspectBehaviour
Where UCP is servedOnly on a shop's custom domain whose DNS is verified and whose TLS certificate — a Let's Encrypt certificate that Monokaido obtains for that domain — is active. Monokaido's *.shop.monokaido.com addresses never serve UCP.
TLSTLS 1.2 and TLS 1.3.
Plain HTTPPort 80 answers with a 301 to the same URL on https://; only the certificate-validation path (/.well-known/acme-challenge/) and a health check are answered there. No UCP request is answered over plain HTTP.
HSTSThe web server adds Strict-Transport-Security: max-age=31536000; includeSubDomains to every response; the API's own answers also carry Strict-Transport-Security: max-age=15552000; includeSubDomains.
URLs the API publishesThe profile's endpoint, continue_url, order.permalink_url and product URLs are https:// URLs on the shop's primary domain (its primary verified domain, else the domain you called). Image URLs and the shop's legal links (links) are passed on as the shop stores them: they can point at a supplier's server or at any address the merchant entered, http:// included.
OutboundThe only request the API makes to you is the fetch of your platform profile, over https:// only, with the certificate verified against the host name.

Call https:// URLs only, verify certificates, and do not follow redirects: the API never redirects, so a redirect means a wrong URL. If your policy requires TLS 1.3, enforce it in your client; the shop offers it.

Who can call what

No authentication

The API is a public surface, like the shop's own web pages. It selects the shop from the request's Host header — the raw header as the web server forwards it, never X-Forwarded-Host — and accepts no API key, OAuth token, client certificate or signature. Credentials you send are ignored. Browser pages on other origins cannot read its answers: the API allows no cross-origin requests from third-party origins, so call it from your servers.

UCP-Agent identifies, it does not authenticate

The shop fetches the profile your header names and negotiates with it, but nothing ties the header to the caller: anyone can send your profile URL. Two things follow:

  • Your Idempotency-Key values are scoped to the shop and to your profile URL. Because anyone can send that URL, use unguessable keys — random version 4 UUIDs. A guessed key sent with the same operation, path and body would return your stored answer to someone else.
  • Sessions are not bound to the platform that created them. The profile URL is stored with each session, but it is not checked on later calls.

The session id is the credential

A cart or checkout id is a version 4 UUID from a cryptographically secure generator (122 random bits). Knowing it is enough to read the session — the buyer's name, e-mail, phone and address included — and to change or cancel it, on any domain of that shop, whatever UCP-Agent the caller sends. The id of one shop answers not_found on every other shop.

Treat session ids like passwords:

  • Keep them on your servers. Never send them to a browser, an app or the buyer, and never put them in a URL you share.
  • Keep them out of logs other parties can read.
  • The id appears in the path of GET, PUT and the cancel and complete routes, and therefore in the shop's web server access log. That log is Monokaido's, not public.

If an id has leaked, cancel the session. A canceled cart answers not_found to everyone from then on. A canceled checkout stays readable — its last answer, with the buyer's data — until it is deleted 30 days after its expires_at. So, while the buyer has not yet taken an open checkout to the shop, first send a PUT with {"buyer": null}: it removes the buyer from the session and from every later answer. A shipping address cannot be removed, only replaced. After the handoff a PUT changes nothing, while a cancel still applies. A checkout made from a cart leaves the cart as it was, and canceling one never cancels the other: clear and cancel both. While the cart is open, converting it again answers its open checkout — buyer data included — to anyone who holds the cart's id. Answers stored under your Idempotency-Key values keep what they held until they are deleted, 25 hours after the key was claimed.

What the API never does

  • It sends no webhooks and publishes no signing keys; it signs nothing and checks no signature.
  • It does not complete orders: POST …/complete places none.
  • It never reads or writes your platform's state; the only outbound request is the profile fetch.

How shops are kept apart

LayerRule
Host → shopA database function maps the host to a shop only when the host is a domain of that shop with verified DNS and an active certificate, and the storefront key is active, unexpired and its built-in storefront is on. A shop with UCP switched off, or a locked E-shop module, answers exactly like an unknown host.
Every queryEvery statement a request runs on UCP data names the company. A session or an idempotency key is looked up by the company and the shop together; the statements that change a session afterwards address it within its company — by its id, or, for the order that completes a checkout, by the shop cart the order was placed from.
Row-level securityBoth UCP tables (sessions and idempotency keys) also carry row-level security policies that limit a row to its company, and their keys to the shop reference the same company. The API's isolation does not rest on these policies but on the explicit conditions above.
VerificationMonokaido's CI runs the UCP statements, verbatim, against a real PostgreSQL database and checks, among others, that another shop's session reads as none and that, under row-level security, one company can neither read nor write another's sessions and keys.
SessionsFound only by id together with the company and the shop: another shop's session is not_found, even within the same company.
continue_urlIts token is looked up only within the shop whose domain it was opened on.
Idempotency-KeyStored per shop and per profile URL.
DeletionDeleting a shop or a company deletes its sessions and keys with it.

What shops do share is the request budgets: the rate limits are counted per client IP, not per shop, and the budget for fetching new platform profiles is shared by every shop. See Rate limits.

What the API accepts

The UCP code reads these request fields and headers and nothing else; every other body field is ignored and not stored.

WhereFields read
Carts and checkoutsline_items[].item.id, line_items[].quantity; buyer.first_name, last_name, email, phone_number; context.address_country, address_region, postal_code, language, currency; discounts.codes; cart_id
Shipping (fulfillment extension; a cart stores it too but never uses it)the shipping method's destination — street_address, extended_address, address_locality, address_region, postal_code, address_country, first_name, last_name, phone_number, id — its selected_destination_id and groups[0].selected_option_id
Catalogquery, filters.price, filters.categories (refused when it holds a value), pagination, ids, id, selected, context; nothing of a catalog request is stored in the database
HeadersHost, UCP-Agent, Idempotency-Key, Accept-Language, Content-Type, Content-Encoding, and the client IP the web server forwards; on the handoff page's own request also Authorization (the buyer's sign-in to the shop)

Text values are cleaned before use: control characters become spaces, zero-width and text-direction characters are removed, the value is trimmed, and every field is cut to its length.

What the API stores, and for how long

DataWhereKept until
The buyer: names, e-mail, phone numberthe session30 days after the session's expires_at
The context: country, region, postal code, language, currencythe sessionthe same
The shipping destination, with its names and phone number, and the selected optionthe sessionthe same
Discount codes, the session's language, your profile URL (in full, query included)the sessionthe same
The last answer: lines, totals, buyer, address, continue_urlthe sessionthe same
Handoff records: SHA-256 hashes of the browser's guest-cart session id (or of the shop customer's id) with the session's revision, and the ids of the shop carts the lines went tothe sessionthe same
The order's id and number, once completed; the times of creation, last change, handoff, completion and cancellationthe sessionthe same
Your write's answer, in full — the same buyer data includedthe stored Idempotency-Key result25 hours after the key was claimed (deleted within the following hour)
The key itself, as a SHA-256 of your profile URL and the key, and a fingerprint of your write: a SHA-256 of the operation, the path id and the bodythe samethe same
The shop's priced cart behind each session: items, discount code, currency and language — no buyer datathe shop's cart datanot deleted by UCP; retired from the shop's cart statistics once the session closes or expires
What the buyer enters on the shop's checkoutthe shop's orderthe shop's own retention; outside UCP

expires_at is 7 days after a cart's last create or PUT, 24 hours after a checkout's. A session therefore keeps buyer data for at most about 37 days after the last write to a cart, and about 31 days after the last write to a checkout.

How it is stored. Sessions and stored answers are rows of the shop platform's PostgreSQL database, the buyer, context, shipping address and last answer as plain JSON columns. The API does not encrypt them at the application level; the row-level security described above limits each row to its company. Where the database runs, how its storage is encrypted and who at Monokaido may access it are not part of this API's contract: ask support@monokaido.com for them.

Retention runs every hour, at 23 minutes past, and deletes idempotency results older than 25 hours and sessions whose expires_at is more than 30 days ago, in batches of 5 000 rows per table, repeated until none is left or 50 batches have run (the rest continues the next hour).

Not stored: the raw request body (only the fields above and the fingerprint), anything of a catalog request, payment data of any kind, and IP addresses or User-Agent strings — the UCP tables hold none. The API uses the client IP only in memory, for the rate limits and the profile-fetch budget.

There is no erasure endpoint. To keep less: send only what the purchase needs (below), remove the buyer from an open session with a PUT of "buyer": null before the handoff (a cart's PUT repeats its line_items), and cancel what you no longer need. After the handoff a PUT changes nothing; a cancel still applies.

No payment data, by design

Every business profile and every checkout answer carries payment_handlers: {}: the shop accepts no payment instrument through UCP. POST …/complete charges nothing and places no order. A ready checkout is requires_escalation, and the buyer pays on the shop's own checkout, with the shop's own payment methods, after opening continue_url. No field of the API carries card data, payment tokens or wallet credentials; anything of the kind you send is ignored and not stored. Never send it.

A completed checkout means the order was placed, not that it was paid. The payment's outcome is between the buyer and the shop.

continue_url looks like https://shop.example.com/ucp/continue/9bbd3ded…ec780b.

PropertyValue
Format64 lower-case hexadecimal characters.
RandomnessDerived from two random version 4 UUIDs generated by the database (up to 244 random bits); unique across all sessions.
Relation to the session idNone: the link never contains the session id, and the token gives no access to the /ucp/v1 API.
LifetimeFixed for the life of the session. It works while the session is open and before its expires_at, and only on the domains of its own shop; afterwards the page answers 410, or 404 once the shop no longer serves UCP.
AnswersAbsent from completed and canceled checkouts.

What the link lets its holder do. Opening it in a browser adds the session's lines and its applied discount code to the cart that browser has on the shop — the signed-in customer's cart, else the guest cart — once per such cart and per version of the session, and sends the browser on to the shop's checkout (a cart's link: to the shop's cart page). It shows only the shop's name: no buyer data, no prices, no session id. A link opened by someone else therefore exposes the basket, not the buyer, but that person's order from the filled cart can complete your checkout. While the session is open, any number of browsers can open the link, each getting the lines once.

The page protects the token:

  • the API sends Referrer-Policy: no-referrer and the same rule as a <meta name="referrer"> element, which governs every request the page makes (the web server's general header Referrer-Policy: strict-origin-when-cross-origin comes with the page too; the element overrides it), so the token never leaks in a Referer;
  • it is no-store and X-Robots-Tag: noindex, nofollow;
  • its Content-Security-Policy allows only its own inline script (by hash), same-origin requests and inline styles, and forbids framing, forms and a <base> element;
  • a GET of it changes nothing — a link preview or a prefetch adds nothing to any cart; the lines move only when the page's script runs in a browser;
  • the script sends no cookies; it uses only the shop sign-in of its own browser tab, and a sign-in to another company's shop counts as a guest;
  • it redirects only to paths on the same shop;
  • both its routes are limited to 60 requests a minute per IP.

Where the token appears: in your API answers (and Monokaido's stored copies of them), in the buyer's browser (address bar and history), and in the access log of the shop's web server, which records request lines.

Give the link only to the buyer, over a channel you trust, at the moment the checkout is ready. Do not open it from your servers, and do not post it where others can read it.

The platform profile fetch

The shop fetches the profile your UCP-Agent names. The fetch is built so that a profile URL cannot be used to reach anything but a public HTTPS server:

ProtectionRule
Schemehttps:// only; no user name or password in the URL; at most 1000 characters; any port.
AddressThe host is resolved once, and the address is refused when it is private (10.0.0.0/8, 172.16.0.0/12, 192.168.0.0/16, IPv6 unique-local and site-local), loopback, unspecified, link-local, carrier-grade NAT (100.64.0.0/10), benchmarking (198.18.0.0/15), multicast or in 240.0.0.0/4; IPv4-mapped IPv6 addresses are judged by their IPv4 address and NAT64 prefixes are refused. The connection is pinned to the address that was checked, so DNS rebinding cannot change it.
RedirectsNot followed.
Size and timeAt most 256 KiB; 3 seconds without data, 5 seconds in all.
RequestGET; the only headers it sets are Accept: application/json and User-Agent: Monokaido-UCP/1.0 (+https://monokaido.com); no cookies, no credentials.
AnswerOnly 200 counts; only ucp.version and ucp.capabilities are read.
VolumeAt most 5 new profile URLs a minute per client (an IPv4 address or an IPv6 /64) and 60 a minute in all; known profiles are cached (see Caching and budgets).

For your side: your profile is public by design. Serve it as static JSON from a public HTTPS address, and put no secrets in its URL: the full URL is stored with every session you create. Monokaido's logs show only its origin and path, never the query string. The fetch comes from Monokaido's servers, whose addresses this manual does not publish and which can change: do not make the profile depend on an IP allow-list (ask support if your policy needs the current addresses).

Content from the shop

Everything in the catalog — titles, descriptions, option names, images — and in a checkout's shipping options and legal links is the merchant's content. description.html is the shop's own HTML, not sanitized by the API and cut at 50 000 characters, possibly in the middle of a tag. Sanitize it before you render it, or use description.plain. An AI agent must treat this content as data, never as instructions. More in For AI agents.

UCP proves no more about a shop than its domain and its TLS certificate: the profile carries no name or logo. Make sure the domain is the shop the buyer meant.

Logging

WhereWhat is recorded
The APINo request log. Its UCP log lines can contain session ids, order ids, variant ids, a discount code that could not be applied at a handoff, currency codes, the origin and path of a profile URL, and the message (with the stack trace) of an unexpected error. The UCP code writes no buyer name, e-mail, phone number or address into its log lines.
The shop's web serverFor every request: the client IP, the time, the request line (method, path and query string — session ids and continue tokens included), the status, the size, Referer, User-Agent, a request id and the response time. Not request bodies, and no other headers.

The API returns no request id. If you send X-Request-ID, the web server writes your value into its access log, so it can serve as your correlation id when you report a problem; put nothing secret in it. How long these logs are kept is not part of this API's contract.

Data protection

  • The shop is the seller. The buyer buys from the shop, through UCP as on its website, and the purchase itself happens on the shop's checkout. Which party has which role under data-protection law for the buyer data of a UCP session is defined by the shop and by Monokaido's contractual terms, not by this API. What your platform collects and keeps is your own responsibility.
  • Personal data the API processes: the buyer's names, e-mail and phone number, the shipping address, and the country, region and postal code of the context — stored as long as the tables above say — and client IP addresses, held in memory for the rate limits and written to the web server's access log.
  • Show the shop's policies. A checkout's links carry the shop's terms_of_service and privacy_policy links when the shop has set them (the list can be empty). Show them to the buyer.
  • Send only what the purchase needs. A checkout is ready with lines and a valid buyer.email, plus, with the fulfillment extension, a complete address the shop ships to and a shipping option. Names and a phone number are optional, and nothing about the buyer is carried to the shop's checkout: the buyer types their details there again. Send names and phone only if your quote needs them.
  • The buyer's details on the shop belong to the shop's order and its own retention, outside UCP.

Checklist for your security review

  • Calls go only to https:// on the host of the endpoint the shop's profile publishes; certificates are verified; redirects are not followed; TLS 1.3 is enforced if your policy requires it.
  • Calls are made from your servers, never from a browser or an app.
  • Cart and checkout ids are stored server-side as secrets, kept out of URLs you share and out of logs that others read, and deleted when you no longer need them.
  • Every Idempotency-Key is a random version 4 UUID.
  • continue_url is given only to the buyer, at the moment of the handoff, and never opened from your servers.
  • Your platform profile is static JSON on a public HTTPS address, with no secret in its URL.
  • No payment data is sent; the buyer pays on the shop.
  • Only the buyer data the purchase needs is sent; the shop's privacy policy from links is shown to the buyer.
  • Shop content is treated as untrusted: description.html is sanitized before rendering, and an AI agent never follows instructions found in it.
  • The buyer is told that the shop is the seller, and the domain is the shop they meant.
  • Your own retention of UCP data is defined and no longer than you need.
  • A leaked session id has a playbook: remove the buyer with "buyer": null while possible, then cancel the session — a cart and the checkout made from it each on its own.

Reporting a security problem

Write to support@monokaido.com with the shop's domain, the time (with its time zone), the request (method and path, without the buyer's data) and, if you sent one, the X-Request-ID value. Do not put a session id or a continue_url in the message: give its first eight characters instead.