Protocol surfaces
ACP checkout
Experimental ACP 2026-04-17: discovery and the five checkout operations under /acp.
2026-04-17, REST binding, checkout service only. Off unless protocols.acp.enabled is true.Discovery
/.well-known/acp.jsonpublic{
"protocol": {
"name": "acp",
"version": "2026-04-17",
"supported_versions": ["2026-04-17"]
},
"api_base_url": "http://localhost:8080/acp",
"transports": ["rest"],
"capabilities": { "services": ["checkout"] }
}Public, Cache-Control: public, max-age=3600. Optional metadata (documentation_url, supported_currencies, supported_locales, intervention_types) appears only when configured.
Required headers
| Header | Where | Rule |
|---|---|---|
Authorization | every checkout route | Bearer <token>, constant-time; a refusal is 401 with WWW-Authenticate: Bearer |
API-Version | every checkout route | exactly 2026-04-17; missing and unsupported are distinct errors |
Content-Type | every POST | application/json or any +json type; only an empty body may omit it, and a non-JSON type gets 415 |
Idempotency-Key | every POST | 1–255 printable ASCII characters |
Request-Id | optional | bounded and filtered; echoed on every checkout response, refusals included |
The gateway checks routing, authentication, headers, body size, JSON, schema and content type before claiming an idempotency key. It does not store failures from those checks. A POST refusal echoes a well-formed Idempotency-Key, and a routing 405 includes Allow.
Headers sent to the merchant
The gateway forwards Accept-Language, User-Agent, Request-Id, API-Version, Signature and Timestamp when present, printable ASCII and at most 2048 characters long. It never forwards the agent's Authorization, the caller's Idempotency-Key (the derived key replaces it) or any other caller header. A backend.headers entry of the same name wins, so configure a fixed User-Agent there if your backend needs one.
Signature and Timestamp pass through unverified. The gateway re-serializes the JSON body, so a merchant that verifies the raw request bytes fails; only one that verifies a canonical form of the parsed JSON can succeed.
Operations
| Operation | Route | Success |
|---|---|---|
createCheckoutSession | POST /acp/checkout_sessions | 201 |
updateCheckoutSession | POST /acp/checkout_sessions/{id} | 200 |
getCheckoutSession | GET /acp/checkout_sessions/{id} | 200 |
completeCheckoutSession | POST /acp/checkout_sessions/{id}/complete | 200 |
cancelCheckoutSession | POST /acp/checkout_sessions/{id}/cancel | 200 |
curl -s http://localhost:8080/acp/checkout_sessions \
-H "Authorization: Bearer $ACP_BEARER_TOKEN" \
-H "API-Version: 2026-04-17" \
-H "Content-Type: application/json" \
-H "Idempotency-Key: $(uuidgen)" \
-d '{"line_items":[{"id":"item_123"}],"currency":"usd","capabilities":{}}'Requests are validated against the vendored schema before the pipeline runs, and your backend's answer is validated before anything is returned. payment_data reaches your backend unchanged; it is never converted into an x402 payment.
Validation accepts unknown values in the supported and required lists of capabilities.interventions, and in enums the schema calls extensible. Other enum values and fields disallowed by the schema are refused. A session id such as gid://shop/Checkout/1 works when encoded as one URL segment. Empty ids and ASCII control characters are refused.
Idempotency
| Situation | Answer |
|---|---|
| first request | claimed atomically before the merchant is called |
| same key, same body, still running | 409 idempotency_in_flight with Retry-After |
| same key, same body, finished | the stored answer with Idempotent-Replayed: true |
| same key, different body | 422 idempotency_conflict |
| an ACP answer below 500 after the claim | cached |
| a 500 or higher after the merchant may have run | 409 idempotency_unresolved on retry by default; with merchantIdempotent, the claim is released for a retry |
| a 500 or higher before the merchant was called, or a merchant 429 | claim released; a clean retry runs |
A merchant 429 releases the claim and becomes ACP 503. Have your backend send 429 only before any side effect. A merchant 503 also becomes ACP 503, but its claim stays unresolved unless merchantIdempotent is set because the merchant may have done some work. Merchant 401 and 403 become ACP 502 and also leave the claim unresolved.
ACP expects retries after a 5xx. By default, the gateway holds a claim when the merchant may have acted, avoiding a duplicate order. If your backend deduplicates by the derived key, set protocols.acp.idempotency.merchantIdempotent: true. The gateway then releases the claim after a 5xx and frees in-flight claims at its next start.
The scope is the deployment, the endpoint and your key — never the bearer token, so rotating it keeps outstanding claims. Only completed records expire, after at least 24 hours; unresolved ones wait for an operator. The gateway forwards a derived Idempotency-Key to the merchant, stable across retries, restarts and token rotation.
Crashes can leave claims in flight. Startup marks them unresolved, or frees them with merchantIdempotent, and logs the count. Since 2.6.0, claims use the receipt database; run one gateway per database. Expired completed claims are purged on later key claims, without a background sweep.
Errors
| Cause | ACP answer |
|---|---|
| pipeline INPUT_INVALID | 400 invalid_request_body |
| merchant 404 | 404 checkout_session_not_found |
| merchant 405 on cancel | 405 checkout_session_not_cancelable |
| merchant 405 on another operation | 405 method_not_allowed |
| merchant 400 / 422 | the same status, invalid_request_body |
| a valid merchant ACP error on a relayed status | that status, with its type, safe code and optional param |
| merchant 409 | 409 checkout_session_conflict |
| merchant 429 / 503 | 503 service_unavailable with Retry-After |
| merchant 401 / 403 / another 5xx / other | 502 processing_error |
| backend timeout | 504 service_unavailable |
| more than 20 requests at once on the mount | 503 with the gateway's GATEWAY_BUSY envelope, not an ACP error, and Retry-After: 1 |
| mapping or an unexpected payment challenge | 500 processing_error |
| a storage or other gateway failure | 500 internal_error, type processing_error |
Errors are ACP Error documents — type, code, message, param where safe, and supported_versions for API-version errors. From a valid merchant ACP error the gateway keeps only the type, a snake_case code of at most 64 characters (never one starting with idempotency_, which only the gateway issues) and a bounded, printable param; it writes its own message. No other merchant body field is forwarded, and a merchant 401 or 403 is never presented as a failure of your bearer token.
For a merchant 429 or 503, the gateway parses a whole-second Retry-After value and clamps the ACP 503 delay between 1 and 300 seconds. For other forms, it uses 5 seconds. It refuses merchant markdown containing an HTML tag or comment as processing_error before the agent sees it.