Agent integration
Discovery and checkout, end to end
Which endpoint to call at each step, which credential it needs, and what to do with each error. This page renders the same document agents read at /.well-known/agent-checkout-guide.json.
Machine-readable version: /.well-known/agent-checkout-guide.json
Which path are you on?
Are you exercising the protocol, or buying something for a user with real money?
Path A: testing the protocol (demo-store, simulated payment)
- Goal
- Exercise the full protocol end to end, with fabricated goods and simulated payment.
- Credential
- self-service sandbox key (see `paths.testing`)
- Money
- NONE. Payments are simulated; `payment_method: "MOCK"` confirms an order in our database and charges nobody.
- Mandate
- preview_checkout, complete_checkout, process_agent_payment, complete_visa_payment, ucp_complete_checkout, ucp_preview_checkout — JSON Schema
- Confirmation
- Required for a non-zero demo cart before complete_checkout; the endpoint does not verify human presence.
Do NOT present this path to a user as a purchase, and do not carry its credential to a real merchant: the sandbox key is rejected everywhere except demo-store, by a constant in the code rather than by configuration.
Path B: buying for a user on a real merchant
- Goal
- Complete a real purchase on a real merchant on behalf of a user.
- Credential
- Dynamic client registration (RFC 7591) is open: POST to the `registration_endpoint` published in the authorization server metadata and you get a client back, no credential needed. Registered clients are third-party applications — a user consent screen is always shown and cannot be skipped. Client credentials for server-to-server use are still issued by agreement: write to developers@trusteed.xyz. The registration lives on the authorization server (auth.trusteed.xyz), not on the site domain: `https://trusteed.xyz/register` is a human sign-up page (GET), not the RFC 7591 endpoint. Exception: a store that is in the platform's guest-checkout list issues a 24 h guest credential with `create_agent_credential` (guest access to that store only). Whether money actually moves depends on the store having a settlement rail (accepts_real_payments in merchant-index.json); read this guide before assuming a charge is real. The store's MCP `instructions` say whether it issues the credential.
- Money
- Autonomous settlement by the agent: no rail is LIVE (x402: SANDBOX, acp: SANDBOX, ap2: PLANNED, mcap: DORMANT, paypal: DORMANT); a rail in CODE_COMPLETE is implemented and may settle real value where an operator enabled it, so check the registry and the store before promising a user anything. Money can still move outside those rails: a real merchant may hand the person off to its own checkout or to a browser payment page. `complete_checkout` then answers `PENDING_EXTERNAL_CONFIRMATION` with `payment_url` (browser payment, charged with the merchant's own processor key) or `next_action` (the merchant's native Shopify checkout, or a PayPal approval); the person pays there with real money and the agent never settles it. `approval_url` is only the approval of the demonstration store: there, `MOCK` takes no payment, and a `payment_option` (Stripe test mode or Base Sepolia) charges test funds only, never real money. Read `paths.buying.today` before promising a user anything.
- Mandate
- preview_checkout, complete_checkout, process_agent_payment, complete_visa_payment, ucp_complete_checkout — JSON Schema
- Confirmation
- Depends on merchant and payment policy; do not infer approval from the mandate.
Path A: testing the protocol (demo-store, simulated payment)
demo-store ONLY. Simulated payment. Not a purchase.: Complete, working protocol run on demo-store without merchant credentials. Every step below was measured end to end at the 2026-09-09 probe, to `status: "COMPLETED"` with an idempotent replay returning the same `order_id`. That is evidence the flow works, not a claim about this minute: reproduce it yourself with POST /api/v1/sandbox/key and the steps below. Mind the quota before you start — 25 sandbox keys per IP per 24h, so keep the one you get.
1. Get a sandbox key
POST https://api.trusteed.xyz/api/v1/sandbox/key with an empty JSON body
Returns: `agnt_sandbox_<32 hex>`, valid 24h
Rate limited to 25 keys per IP per 24h. Keep the key — do not request a new one per attempt, or you will lock yourself out for a day.
2. Send it on every checkout call
`Authorization: Bearer agnt_sandbox_…` (or `X-Agent-Api-Key`)
This credential works ONLY against demo-store, and only on the checkout bucket. It is not an OAuth token and does not unlock any real merchant.
3. Declare your spending cap (the payment mandate)
`X-Payment-Mandate` header carrying JSON (or a JWS whose payload is that JSON) with ALL of: `mandate_id`, `max_amount_cents` (integer, > 0), `currency` (exactly 3 characters), `exp` (unix seconds), `sub` (the identity you authenticate as), `aud` (the store slug). It is a spending cap you declare to limit yourself: its signature is not verified, it authorises nothing and it does not prove the buyer's consent.
A missing or malformed mandate is an authorization denial, not an authentication one — re-authenticating will not help. A store that requires a platform-issued mandate answers `payment_mandate_not_platform_issued` instead of accepting one you wrote: see `error_decision_table`. On demo-store it arrives in-band (HTTP 200, JSON-RPC error whose `error.data.code` says whether the mandate was missing or malformed); read `error.data.code`, not the HTTP status. A WELL-FORMED mandate that does not fit is not a 403: it comes back as a tool error at `preview_checkout`, and the code tells you which boundary you crossed. This is the demo-store rail, which carries its own mandate boundary and denies here. Before assuming the same on a real merchant, read `the_cap_is_not_your_spending_control` in `paths.buying`.
Enforced boundaries
- `MANDATE_LIMIT_EXCEEDED` — cart total above `max_amount_cents`. Remediation `reduce_cart_total`. Spending exactly the cap is allowed.
- `MANDATE_EXPIRED` — `exp` is in the past. Remediation `renew_mandate`. Reducing the cart will NOT help.
- `MANDATE_AUDIENCE_MISMATCH` — `aud` names a different store. Remediation `present_mandate_for_this_store`.
- `MANDATE_CURRENCY_MISMATCH` — `currency` differs from the cart's. Remediation `present_mandate_in_cart_currency`.
- `MANDATE_SUBJECT_MISMATCH` — `sub` is not the identity this call authenticated as; `details.expected_sub` carries the value to use. Remediation `present_mandate_for_this_agent`. The comparison denies wherever the mandate boundary decides (demo-store, guest credentials and the OAuth lane in enforce); in the OAuth lane in observe it is only measured, never denied. Write the right `sub` in every case.
A denial and the receipt
A mandate denial DOES leave a Trust Receipt, in either pass, because a receipt attests a CALL to the tool and what it returned, not a payment. Denied at `preview_checkout`: the receipt records that call and carries no `outcome` (that tool does not settle). Denied at `complete_checkout`: the receipt carries the signed `outcome: "FAILURE"`, and its `outputHash` commits to the denial response (the `MANDATE_*` code). Either way nothing was paid and `order_placed` is false: read the "how to read a receipt" entry in step 6 before telling anyone a purchase happened. After a denial at `complete_checkout`, fetching by session returns that receipt once the queue issues it (seconds; until then 404 `receipt_not_issued_checkout_not_completed`). A session that never called `complete_checkout` keeps answering that 404, on purpose, not 202.
Reading the denial
Branch on `code`, or on `details.remediation` — never on the prose. All four also carry `details.mandate_error` with the same distinction. The numbers in `details` (`limit_cents`, `cart_total_cents`) are YOUR mandate's, never the merchant's policy: this axis never leaks a merchant threshold.
4. Record demo confirmation out of band
POST https://api.trusteed.xyz/api/v1/demo/checkout-approval with `{ store_slug, checkout_session_id }` and your sandbox key in `X-Agent-Api-Key`
Returns: `approval_ref` + `channel: demo_playground_confirmation`
Do this AFTER preview_checkout and BEFORE complete_checkout. It is required for a non-zero demo cart. Do not confuse it with the PLATFORM approval that `preview_checkout` may open (spec-067, the `approval_url` it returns): that one issues a platform payment mandate, is NOT enforced while the store runs it in observe mode, and is a different URL. In every mode other than off `complete_checkout` still needs a payment mandate — the platform one once approved, or in observe one of your own — and without any it answers `payment_mandate_required`. This demo confirmation is the mandatory one on demo-store. When the platform approval is enforced, present the mandate it issues in the `payment_mandate` argument or the `x-payment-mandate` header; where the platform holds it for the session, calling `complete_checkout` again with the same arguments is enough, provided you are the authenticated identity that opened the approval and the same session. If you pass a `payment_option`, the approval needs the single-use `approval_token` from `approval_url?token=…` plus `quote_version` (from `/api/v1/demo/checkout-approval/summary`) and `buyer`; the agent key alone gets 401 `approval_token_required` (see `minimal_demo_sequence` step 7). The endpoint records that a caller submitted confirmation; it reports human_presence_verified: false and identity_verified: false. The same sandbox key that created the cart is required by default, with an explicit deployment rollback flag. The record does not prove buyer consent. MCP elicitation cannot supply it (see known_limitations).
5. Settle
`complete_checkout` with `checkout_session_id`, `idempotency_key`, `buyer`, and `payment_method: "MOCK"` (enum: ACP | KYAPAY | PAYPAL | MOCK | X402)
Returns: `status: "COMPLETED"` and an `order_id`
Use a DIFFERENT `Idempotency-Key` HTTP header per distinct call, and the SAME one to retry. Reusing one key across different calls is 422 `idempotency_key_mismatch`. On a retry you get the FIRST execution's JSON-RPC payload and status code back unchanged — including its `idempotent: false`, which described that first execution. The FRAMING is not identical and is not meant to be: the first execution is streamed as `text/event-stream`, the replay is sent as `application/json`. Compare the parsed JSON, never the bytes. The signal that you are seeing a replay is `result._meta["io.trusteed/idempotency"].replayed === true` in the JSON-RPC body (a replay of a result carrying `result`; an error response without `result` carries no mark), plus the `X-Idempotency-Replay: true` response header for HTTP clients that can read headers. The first execution never carries the mark; every other field is the stored one, so a replayed `status` is the state at the first execution, not the current state: read the current state with a read tool before acting on it.
If not mock
If you pass `payment_method: "PAYPAL"` instead of `"MOCK"`, `complete_checkout` can come back `status: "PENDING_EXTERNAL_CONFIRMATION"` with `order_placed: false`, an `approval_url` and a `next_action` — no order has completed yet. There are two equivalent ways to resolve it: a person opens `approval_url`, or the integrator calls the REST settlement endpoint. Neither is something the model does on the person's behalf. After the person approves, call `complete_checkout` again with the SAME `checkout_session_id` and `idempotency_key`: the server reads the PayPal approval itself and captures, so there is no token or confirmation to send back. Retrying is safe (it never charges twice) and, until the payment is approved, returns the same pending step. `MOCK` never takes this branch: it always resolves straight to `COMPLETED`.
6. Collect the signed receipt
GET https://api.trusteed.xyz/api/v1/trust/receipts/by-session/{checkout_session_id} with the same credential
Returns: `receipt_id`, `call_id`, `jws` and where to verify it
202 means 'not yet', never 'never' — and it is only ever sent for a session that reached `COMPLETED` or is still running. A session without a purchase answers 404 with a reason (for the few seconds after a `complete_checkout` call, see the 404 line above). The receipt is released only to the agent whose credential matches the one recorded on the session; anything else is a 404, on purpose.
Outcomes
- 200 — the receipt, signed (`signature_status: "signed"`) or explicitly `unsigned`. An unsigned receipt is published as such rather than passed off as verifiable.
- 202 `receipt_pending` / `checkout_in_progress` with `retry_after_seconds` — `receipt_pending`: the session reached `COMPLETED` and its receipt is still queued; `checkout_in_progress`: the checkout is running or awaiting external confirmation. Retry. Once the receipt arrives, still read `order_placed` and the signed `outcome` before telling anyone a purchase happened.
- 404 `receipt_not_issued_checkout_not_completed` / `..._canceled` / `..._expired` — the session has not completed a purchase and no receipt has been issued for it. A receipt is issued for each `complete_checkout` call, whatever its result (pending approval, failed or charged); it attests that call, not a payment. If you called `complete_checkout` seconds ago (a denial, a failure or a pending approval leaves the session open), its receipt may still be queued: fetch once more after a few seconds. Otherwise there is nothing to fetch: do NOT poll.
- 404 `receipt_not_found` — the session is not yours, or does not exist. The two are deliberately indistinguishable.
- 500 `receipt_issuance_overdue` — the session reached `COMPLETED` but issuance is past its budget. That is our failure, not yours; it is surfaced as an error so it counts as one.
How to read a receipt
A Trust Receipt attests a CALL to the settlement tool (`complete_checkout`, `ucp_complete_checkout`…) and its result; it is not proof of payment. It is issued whenever that tool is called, whatever the outcome: pending approval, failed or charged. To state that a purchase happened you need BOTH `order_placed: true` in the lookup response AND `outcome: "SUCCESS"` in the signed payload. The signed `outcome` only ever appears as `SUCCESS` or `FAILURE`; if it is absent the outcome of the call is not attested, so say so instead of assuming success. The signed `outputHash` commits to the response the tool returned (its `status`, `order_placed`, `PENDING_EXTERNAL_CONFIRMATION`…), so a verifier holding that response can check it was not altered. The signed `inputHash` commits to the call arguments WITHOUT the protocol credentials (`agent_key`, `payment_mandate`, `widget_cart_token`), so a verifier must strip those three before recomputing it (since 2026-10-15). Payment settlement is a separate dimension, reported only when the rail supplies evidence. The read-only tools of the checkout bucket also carry a receipt per call; it attests that read and does not prove a payment, and the lookup by session never returns one as the purchase receipt. `get_trust_receipt` and `verify_trust_receipt` do not issue one.
None of this transfers to a real merchant. The sandbox credential, the MOCK payment method and the demo approval endpoint are all scoped to demo-store by code constants. If you are buying for a user, read `paths.buying` instead.
Path B: buying for a user on a real merchant
No autonomous rail is LIVE (x402: SANDBOX, acp: SANDBOX, ap2: PLANNED, mcap: DORMANT, paypal: DORMANT), so do not promise a user an autonomous settlement. A rail in CODE_COMPLETE is implemented and may settle real value where an operator has enabled it: check the signed registry and that store's payment methods before you act. Where the store offers a handoff, your user can pay through it with real money. Money can still move outside those rails: a real merchant may hand the person off to its own checkout or to a browser payment page. `complete_checkout` then answers `PENDING_EXTERNAL_CONFIRMATION` with `payment_url` (browser payment, charged with the merchant's own processor key) or `next_action` (the merchant's native Shopify checkout, or a PayPal approval); the person pays there with real money and the agent never settles it. `approval_url` is only the approval of the demonstration store: there, `MOCK` takes no payment, and a `payment_option` (Stripe test mode or Base Sepolia) charges test funds only, never real money. Unless a rail applies to that store, treat the agent's part as cart construction plus policy and trust evaluation, and tell your user exactly that.
What you need
- An OAuth 2.1 client-credentials token with a client id. `authenticateAgenticTrust` rejects anything that is not `oauth2` with a `clientId` — a sandbox or REST API key never satisfies it. If the store is in the guest-checkout list, call `create_agent_credential` instead: it issues a 24 h guest credential for that store only. A real merchant not in the guest-checkout list still needs this OAuth token.
- The scope `mcp:checkout` on that token (or `mcp:admin`). Without it the checkout bucket answers 403 `insufficient_scope`.
- An `X-Payment-Mandate` header with the same six claims named at mandate_schema_url and in the on_error hint of the `preview_checkout` step of minimal_demo_sequence. Missing or malformed is 403 `payment_mandate_required` / `payment_mandate_malformed`.
- An agent trust signal above the floor: identities reporting NONE/UNVERIFIED/empty are rejected with 403 `agent_trust_required`.
- The merchant to have a settlement rail configured. Most do not.
How to authenticate
- Call any checkout tool without a token on a REAL merchant. You get HTTP 401 with `WWW-Authenticate: Bearer realm="checkout-mcp", resource=…, resource_metadata=…`. On `demo-store` you get none of that: the denial is HTTP 200 with the JSON-RPC error (`error.code -32000`, `error.data.code: "no_credentials"`, remediation) and no challenge — there is no OAuth to follow there; take the sandbox key in `paths.testing`. Branch on `error.data.code`, not on the HTTP status.
- Fetch the `resource_metadata` URL — the RFC 9728 Protected Resource Metadata of this API.
- Read `authorization_servers[0]` from it. Today that is `https://auth.trusteed.xyz/`.
- Fetch that server's `/.well-known/oauth-authorization-server` and use its `token_endpoint` with the `client_credentials` grant, requesting `scope=mcp:checkout`. The checkout bucket denies a token without that scope (or `mcp:admin`) with 403 `insufficient_scope`.
- Send the resulting token as `Authorization: Bearer …`, plus the `X-Payment-Mandate` header.
Do NOT look for `/.well-known/oauth-authorization-server` on this API: it is a protected resource, not an authorization server, and serving that document there would be wrong. The API publishes `/.well-known/oauth-protected-resource`, which is what the challenge points at.
Dynamic client registration (RFC 7591) is open: POST to the `registration_endpoint` published in the authorization server metadata and you get a client back, no credential needed. Registered clients are third-party applications — a user consent screen is always shown and cannot be skipped. Client credentials for server-to-server use are still issued by agreement: write to developers@trusteed.xyz.
Known rough edges
- Until September 2026 the API's Protected Resource Metadata declared a `resource` on a different host than the one serving it; that mismatch is fixed. The bare `/.well-known/oauth-protected-resource` on the site host is intentionally not served — follow the `resource_metadata` URL from the challenge, which names the per-resource document.
- Until 2026-09-09 the bucket challenges carried only `realm`, with no `resource_metadata`: the chain above was unfollowable from its first step even though every later step was live.
Read this first
- Use the bucket endpoints, not the legacy alias. `/{store}/mcp` still answers, but it routes some tools through a different dispatcher than others; a flow that works there can fail halfway with no useful error.
- `api.trusteed.xyz` and `trusteed.xyz` are the only hosts that serve this platform. Any `*.up.railway.app` endpoint is residual infrastructure and serves no store: it answers `308` towards the canonical host, preserving method and body, so a client that follows redirects survives one more hop. Do not rely on it — the redirect is a courtesy for connectors whose URL you cannot edit, not a supported endpoint. Point your configuration at `api.trusteed.xyz`.
- No autonomous settlement rail (one where the agent itself settles funds) is LIVE in the signed capability registry (x402: SANDBOX, acp: SANDBOX, ap2: PLANNED, mcap: DORMANT, paypal: DORMANT); none in CODE_COMPLETE can settle real value wherever an operator has enabled it, and the registry's evidence says whether a charge has been observed. Money can still move outside those rails: a real merchant may hand the person off to its own checkout or to a browser payment page. `complete_checkout` then answers `PENDING_EXTERNAL_CONFIRMATION` with `payment_url` (browser payment, charged with the merchant's own processor key) or `next_action` (the merchant's native Shopify checkout, or a PayPal approval); the person pays there with real money and the agent never settles it. `approval_url` is only the approval of the demonstration store: there, `MOCK` takes no payment, and a `payment_option` (Stripe test mode or Base Sepolia) charges test funds only, never real money. The authoritative per-rail state is the signed registry at /.well-known/trusteed-capabilities.json.
- `demo-store` is a synthetic catalogue for exercising the protocol. Products, prices, stock and payments are fabricated.
- You CAN complete a purchase on demo-store today, with no merchant credentials — see `paths.testing`. It was measured end to end at the 2026-09-09 probe, to `status: "COMPLETED"`: that is evidence the flow works, NOT a claim about this minute. Reproduce it yourself — POST /api/v1/sandbox/key and follow the steps in `paths.testing`. What you cannot do is settle anonymously: `preview_checkout` and `complete_checkout` need a sandbox key plus a mandate, because they were deliberately removed from the unauthenticated path (it routed through a dispatcher that issues no Trust Receipt and does not enforce the agent mandate). That is an authentication wall, not an identity one: a valid HTTP signature does not lift it, and no signature is required to pass it.
Endpoints
discovery
https://api.trusteed.xyz/{store_slug}/mcp/discovery
Auth: none
Tools: browse_catalog_feed, browse_categories, compare_products, explore_store, get_merchant_profile, get_page_content, get_payment_methods, get_product_details, get_site_map, get_tool_details, onx_get_inventory, run_scenario, search_docs, search_products, search_products_enriched, search_tools
Public and unauthenticated. Agent identity enforcement never blocks this bucket. `tools` is a ceiling — see `tools_listed_here`.
checkout
https://api.trusteed.xyz/{store_slug}/mcp/checkout
Auth: OAuth 2.1 Bearer token (Authorization header) for real merchants (a store in the guest-checkout list instead issues a 24 h guest credential with `create_agent_credential`, guest access to that store only); on `demo-store`, a sandbox key (see `paths.testing`, also accepted as `X-Agent-Api-Key`). Both also require an `X-Payment-Mandate` header, but only on the tools that consume it — the ones that evaluate the cap or settle: `preview_checkout`, `complete_checkout`, `process_agent_payment`, `complete_visa_payment`, `ucp_complete_checkout`, `ucp_preview_checkout`. The payment mandate is distinct from the idempotency key: `select_shipping_option` requires `idempotency_key` even though it does not require a payment mandate.
Tools: apply_discount, attach_agent_identity, complete_checkout, create_cart, get_checkout_state, get_shipping_rates, preview_checkout, quote_checkout, select_shipping_option, ucp_add_items_to_checkout, ucp_cancel_checkout, ucp_complete_checkout, ucp_create_checkout, ucp_get_checkout, ucp_update_checkout
Idempotency-Key is required for the idempotent tools. A request without credentials is denied with `error.data.code: "no_credentials"` — an auth failure, not a signature failure. How it travels depends on the store: on a real merchant it is HTTP 401 with a `WWW-Authenticate` challenge; on `demo-store` it is HTTP 200 carrying the JSON-RPC error (`error.code -32000`, `error.data.code`, remediation) and NO challenge, because the demo credential is self-served and there is no login to go to. Branch on `error.data.code`, not on the HTTP status. `tools` is a ceiling — see `tools_listed_here`.
customer
https://api.trusteed.xyz/{store_slug}/mcp/customer
Auth: OAuth 2.1 Bearer token bound to a customer
Tools: check_agent_consent, get_acp_orders, onx_cancel_order, onx_create_return, onx_get_fulfillments, onx_get_orders, onx_get_returns
Order lookups, cancellations and returns for a known customer. `tools` is a ceiling — see `tools_listed_here`.
legacy_alias
https://api.trusteed.xyz/{store_slug}/mcp
Kept for existing integrations. Prefer the three buckets above: they have one dispatcher, one auth model and one error contract.
rest_openapi
https://api.trusteed.xyz/api/v1/openapi.json
Auth: X-Agent-Api-Key header
A DIFFERENT surface with a DIFFERENT credential model. On `demo-store`, the sandbox key is accepted on MCP as `X-Agent-Api-Key` or `Authorization: Bearer` — the same header this REST surface takes (see `paths.testing` step 2). On a real merchant, the MCP endpoints require OAuth 2.1 and do not accept an API key in either header (the only exception is the 24 h guest credential from `create_agent_credential` on a store in the guest-checkout list): `authenticateAgenticTrust` requires `type: "oauth2"` with a `clientId`, which an API key never carries (see `paths.buying.what_you_would_need`). Nor does this REST surface accept the OAuth Bearer token issued for a real merchant on MCP.
The flow
1. Find merchants
GET https://trusteed.xyz/.well-known/merchant-index.json
Produces: store slugs plus, per row, the MCP endpoint that actually serves that merchant (they are not all on the same host)
Use the endpoint published for each row. Do not assume every merchant answers on the same host as the index.
2. Find products
tools/call `search_products` on the discovery endpoint
Produces: product ids to pass to `create_cart`; the field is `id` and it is NOT the SKU
`get_product_details` takes the same id. Never invent ids.
3. Judge the merchant
tools/call `get_merchant_profile` on the discovery endpoint
Produces: trust score (0-100 scale, version 4.1), verification level and a verifyUrl that re-serves the same number signed
Every discovery response also carries a `merchant_trust` block with the same figure. If two surfaces disagree, the signed one wins and it is a bug — report it.
4. Build the cart
tools/call `create_cart` on the CHECKOUT endpoint
Requires: A credential: OAuth 2.1 Bearer for a real merchant (on a store in the guest-checkout list, the 24 h guest credential from `create_agent_credential` instead), or a sandbox key on demo-store. `X-Payment-Mandate` is required too, but only on the tools that consume it (`preview_checkout`, `complete_checkout`, `process_agent_payment`, `complete_visa_payment`, `ucp_complete_checkout`) — `create_cart` itself does not ask for one.
Produces: cart_id
This is where the credential starts to matter. Send it from step 4 onwards to avoid a mid-flow auth denial (401 on a real merchant, an in-band `error.data.code` on demo-store) — and send it to the CHECKOUT endpoint: the legacy alias lets some of these tools through unauthenticated and rejects the others, so a flow that mixes endpoints breaks halfway with no useful error.
5. Shipping
`get_shipping_rates`, then `select_shipping_option` with `{ cart_id, shipping_handle, idempotency_key }`
Produces: totals including shipping
Exact argument names: `get_shipping_rates` takes `shipping_address` (with `address1`, `city`, `province_code`, `zip`, `country_code`). `select_shipping_option` takes `cart_id`, `shipping_handle` (the `handle` field of a rate), and a fresh `idempotency_key` argument. Reuse that key only to retry the same cart and option.
6. Review before paying
`preview_checkout`
Produces: final total, the merchant policy verdict, and a state commitment binding what you were shown
If the merchant's catalogue changed since you looked, this is where you are told, and you must re-confirm rather than proceed. If you only need the price, call `quote_checkout` with the same `cart_id` instead: it returns the same totals with `status: "QUOTE_ONLY"`, needs your credential but no mandate, and does not advance the session, open an approval or enable payment — `complete_checkout` keeps refusing until `preview_checkout` runs. It does not check your payment mandate's spending cap: that happens here. It writes nothing on Trusteed's side; where the store prices orders on its own platform (WooCommerce), the store builds a temporary cart to compute the quote and discards it.
7. Settle
`complete_checkout`
Requires: Idempotency-Key
Produces: order status and, where the merchant has receipts enabled, a signed Trust Receipt
Retry with the SAME Idempotency-Key; a new key is a second attempt, not a retry. A non-zero demo cart needs a confirmation recorded beforehand at the endpoint in `paths.testing` step 4. That endpoint does not verify human presence. Real payment methods may carry their own human confirmation; MCP elicitation cannot provide it here.
Signing your requests
Signing your requests is OPTIONAL for discovery and recommended for checkout. What is never required is that you sign — what matters is that if you do sign, the signature verifies.
- web-bot-auth: The profile ChatGPT and other crawler/agent fleets sign with. Publish your key at /.well-known/http-message-signatures-directory on the origin you name in `Signature-Agent`.
- RFC 9421 without a tag: Plain HTTP Message Signatures. Verified the same way.
- agent-browser-auth / agent-payer-auth: Visa Trusted Agent Protocol.
- Unsigned
- Allowed. An unsigned request is `unverified`, never `spoofed`, and the production mode (`block_spoofed`) lets it through outside of merchant-specific policy.
- Unknown profile
- A signature carrying a `tag` we do not know is `unverified` too — not rejected. It is simply not promoted to `verified`.
- Rejected
- Only a signature we can prove wrong is rejected: key not found in the directory you pointed at, signature that does not validate, replayed nonce, or expired beyond 300s. The 403 names which one in `error.data.reason`.
Until 2026-09-09 a valid `web-bot-auth` signature was rejected as spoofed, because that profile tag was missing from our allowlist. If you integrated before that date and worked around it by stripping your signature, you can stop.
Errors and remedies
| Code | Meaning | Remedy |
|---|---|---|
| agent_signature_spoofed (403) | Your signature was present and could be proven wrong. `error.data.reason` says how. | Check `reason`. `agent_signature_key_not_found` → the keyid is not in the directory served from your `Signature-Agent` origin. `agent_signature_invalid` → the signature base does not match (most often a signed component we reconstruct differently, e.g. `@authority` behind your proxy). `signature_expired` → your `created` is older than 300s. |
| no_credentials ([object Object]) | No credential on a bucket that requires one. This is NOT about signatures. On a real merchant it is HTTP 401 with a `WWW-Authenticate` challenge. On `demo-store` it is HTTP 200 with the JSON-RPC error (`error.code -32000`, `error.data.code: "no_credentials"`, remediation) and no challenge. The code in `error.data.code` is the same in both; the HTTP status is not. | Branch on `error.data.code`, not on the HTTP status. Real merchant: on a store in the guest-checkout list call `create_agent_credential` (24 h, guest access to that store only); on any other real merchant obtain an OAuth 2.1 token by following the challenge's `resource_metadata`. demo-store: take a sandbox key with `create_sandbox_key` (see `paths.testing`) — reconnecting or re-authenticating via OAuth will not produce a credential the demo accepts. |
| tool_not_found_in_bucket (404) | The tool exists but not on the endpoint you called. | Use the endpoint listed for that tool in `endpoints` above. |
| PRODUCT_NOT_FOUND | The product id does not exist in that store. | Use the `id` returned by `search_products`. A SKU is not an id. |
| SHIPPING_COUNTRY_UNSUPPORTED | The merchant does not ship to that country. `details` carries `requested_country`, `allowed_countries` and `derived_from` — the rules the allowlist came from, so you can see it is the merchant's configuration and not a platform default. | Use an address in `allowed_countries`, or tell the user this merchant does not serve their country. The cart is untouched, so you can retry with a different address on the same session. A merchant that has configured NO country rules does not produce this error and never blocks on destination — absence of the field in `get_merchant_profile` is absence of a declared rule, not permission to ship anywhere. |
| ENFORCEMENT_UNAVAILABLE | We could not read the merchant's rules. This is our failure, not a verdict about your request. | Retry with backoff. It is deliberately NOT reported as a policy denial: we do not know that anything was denied, and telling you to 'try another country' could be false. Do not treat it as permission either. |
| QUANTITY_LIMIT_EXCEEDED | A single cart line asked for more units than the protocol accepts. `details` carries `requested_quantity` and `maximum_quantity`, so you do not have to find the ceiling by bisection. | Split the order or lower the quantity. Do NOT read `maximum_quantity` as the limit that will be enforced: it is a sanity bound of the cart API, deliberately loose because a merchant may raise its own per-line ceiling above the platform default. The limit that actually decides is the platform per-line ceiling — lower than this bound — evaluated at `complete_checkout` and raisable by the merchant. A quantity that clears `create_cart` can still be refused there with `CHECKOUT_BLOCKED_BY_POLICY`. |
| INSUFFICIENT_STOCK | The catalogue replica says there are fewer units than you asked for. `details` carries `available_quantity` and `source` — it is a snapshot of the catalogue, which can lag the merchant's platform. | Lower the quantity to `available_quantity` or pick another variant. A stock of zero in the replica is NOT treated as sold out — absence of data is not evidence — so this error only ever fires against a positive figure. |
| idempotency_key_mismatch (422) | Same Idempotency-Key, different arguments. | A retry must repeat the same tool with the same arguments. They are compared as an RFC 8785 (JCS) canonical hash of `{tool, arguments}`, so key order and whitespace are irrelevant — any changed VALUE is a mismatch. Changing the cart means a new key. |
Known limitations
- UCP tools on `demo-store`: `ucp_create_checkout`, `ucp_get_checkout`, `ucp_update_checkout` and `ucp_cancel_checkout` are served on /demo-store/mcp/checkout, with the same sandbox key and headers as `create_cart` (see `paths.testing`). On `demo-store`, `ucp_complete_checkout` is served only while the platform has the test settlement enabled: call `tools/list` on /demo-store/mcp/checkout and use it if it is listed; if it is not, use `complete_checkout`. When listed it still needs a sandbox key (sent as `Authorization: Bearer` or `X-Agent-Api-Key`) and a `payment_mandate` whose `sub` is `demo-sandbox:<id>` (the id `create_sandbox_key` returns); listing is not authorization, and a call without them answers `no_credentials`. Path: `ucp_create_checkout`, then `ucp_update_checkout` (`fulfillment.destination`, then `fulfillment.selected_option_id`), then `preview_checkout` with `cart_id` set to the UCP `checkout_id`, then `ucp_complete_checkout` with `buyer` (name and email) and `payment_option` chosen by the person (or `x402_payload` signed for the exact offer the server returns). The first call returns `requires_escalation` with an `approval_url` that someone must open before the purchase can be placed; the approval is a link and does not prove a person was present; repeat the call after it. It settles with test funds only (Base Sepolia or Stripe test mode), no real money moves, and each purchase is capped: the `total_note` of `preview_checkout` states the cap and whether your total fits, and a total over it is refused with `over_demo_cap` and nothing is charged. Avoid the deprecated alias /{store}/mcp for these names: it dispatches them only when the request carries NO `Authorization` header, and a Bearer token routes you to a dispatcher that does not know them, so you get `Tool not found`. On other stores the `ucp_*` tools are served on /{store}/mcp/checkout when the merchant enabled them — call `tools/list` there. Note that `ucp_complete_checkout` does not get the price-and-stock revalidation before charging (State Witness) that `complete_checkout` gets. If you need UCP semantics rather than these tool names, use the UCP surface at /.well-known/ucp.
- MCP elicitation (`elicitation/create`) is not reachable on any of our transports: they run without a session id, so the SDK never negotiates the capability. Tools that would elicit fall back to an out-of-band confirmation channel instead. That demo channel does not verify human presence. Do not wait for an elicitation prompt.
- The `demo-store` catalogue is synthetic and its payments are simulated.
Related documents
The technical body is served in English in both languages: it is the same text agents read from the JSON, and a translated copy would drift from it.