Protocol surfaces

ACP checkout

Experimental ACP 2026-04-17: discovery and the five checkout operations under /acp.

ExperimentalACP stable snapshot 2026-04-17, REST binding, checkout service only. Off unless protocols.acp.enabled is true.

Discovery

GET/.well-known/acp.jsonpublic
json
{
  "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

HeaderWhereRule
Authorizationevery checkout routeBearer <token>, constant-time; a refusal is 401 with WWW-Authenticate: Bearer
API-Versionevery checkout routeexactly 2026-04-17; missing and unsupported are distinct errors
Content-Typeevery POSTapplication/json or any +json type; only an empty body may omit it, and a non-JSON type gets 415
Idempotency-Keyevery POST1–255 printable ASCII characters
Request-Idoptionalbounded 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

OperationRouteSuccess
createCheckoutSessionPOST /acp/checkout_sessions201
updateCheckoutSessionPOST /acp/checkout_sessions/{id}200
getCheckoutSessionGET /acp/checkout_sessions/{id}200
completeCheckoutSessionPOST /acp/checkout_sessions/{id}/complete200
cancelCheckoutSessionPOST /acp/checkout_sessions/{id}/cancel200
Create a checkout sessionbash
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

SituationAnswer
first requestclaimed atomically before the merchant is called
same key, same body, still running409 idempotency_in_flight with Retry-After
same key, same body, finishedthe stored answer with Idempotent-Replayed: true
same key, different body422 idempotency_conflict
an ACP answer below 500 after the claimcached
a 500 or higher after the merchant may have run409 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 429claim 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

CauseACP answer
pipeline INPUT_INVALID400 invalid_request_body
merchant 404404 checkout_session_not_found
merchant 405 on cancel405 checkout_session_not_cancelable
merchant 405 on another operation405 method_not_allowed
merchant 400 / 422the same status, invalid_request_body
a valid merchant ACP error on a relayed statusthat status, with its type, safe code and optional param
merchant 409409 checkout_session_conflict
merchant 429 / 503503 service_unavailable with Retry-After
merchant 401 / 403 / another 5xx / other502 processing_error
backend timeout504 service_unavailable
more than 20 requests at once on the mount503 with the gateway's GATEWAY_BUSY envelope, not an ACP error, and Retry-After: 1
mapping or an unexpected payment challenge500 processing_error
a storage or other gateway failure500 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.