Experimental. Enable with protocols.acp.enabled: true.
| Property | Value |
|---|---|
| Binding | REST over HTTP(S) |
Snapshot and accepted API-Version |
2026-04-17 only |
| Vendored schema | src/protocols/acp/spec/2026-04-17/ |
| Discovery | GET /.well-known/acp.json |
| Default mount | /acp |
| Service | checkout only |
| Authentication | bearer token on every checkout route |
| Idempotency | durable key on every POST, completed rows retained for at least 24 hours |
The schema is vendored and never fetched. Runtime code imports only the dated
snapshot; it reads nothing from spec/unreleased. The adapter validates its
discovery document at startup and fails with CONFIG_INVALID if it does not
match. A version upgrade adds a new snapshot rather than editing this one.
| Header | Scope | Rule |
|---|---|---|
Authorization |
every checkout route | Bearer <token>, constant-time comparison; 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 | if present, application/json or any media type ending in +json; an empty body may omit it, but a supplied non-JSON type still gets 415 |
Idempotency-Key |
every POST | 1-255 printable ASCII characters |
Request-Id |
optional | bounded and filtered; echoed on every checkout response, refusals included; separate from the gateway request id |
An absent, older, newer or malformed API version is not mapped to the supported snapshot.
The gateway forwards Accept-Language, User-Agent, Request-Id,
API-Version, Signature and Timestamp to the merchant when present,
printable ASCII and at most 2048 characters long. It never forwards the
agent's Authorization, the caller's Idempotency-Key, which the derived key
replaces, or any other caller header. A backend.headers entry of the same name
wins, so an operator who wants a fixed User-Agent at the merchant must
configure 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. No signing agent
has been tested against this path.
| Operation | Route | Success | Canonical input |
|---|---|---|---|
createCheckoutSession |
POST {mount}/checkout_sessions |
201 |
{ body } |
updateCheckoutSession |
POST {mount}/checkout_sessions/{id} |
200 |
{ path, body } |
getCheckoutSession |
GET {mount}/checkout_sessions/{id} |
200 |
{ path } |
completeCheckoutSession |
POST {mount}/checkout_sessions/{id}/complete |
200 |
{ path, body } |
cancelCheckoutSession |
POST {mount}/checkout_sessions/{id}/cancel |
200 |
{ path }, plus body if supplied |
Each operation maps to a resource in protocols.acp.checkout.operations.
All five mappings are required, and each operation must use a different resource.
Each resource must include expose: [acp], be free at the Agent Commerce invocation
layer and have an empty payments list.
Request bodies are validated against the pinned schema before pipeline
execution; an invalid request returns 400 invalid_request_body. Successful
merchant responses are validated before return. An invalid merchant document or
success status outside the ACP contract becomes a safe processing_error; the
merchant body is not relayed or cached.
Validation accepts unknown values in the supported and required lists of
capabilities.interventions, in both directions, and passes them through,
because the capability negotiation RFC (4.6.2) says to ignore unknown
capability values. It also accepts an unknown value of an enum whose schema
description calls it extensible. Unknown fields, and unknown values of every
other enum, are still refused.
Completion accepts the schema's two outcomes: a completed session with an order, or an ordinary non-completed session without one.
ACP payment_data belongs to the merchant checkout flow. It remains ordinary
backend input and is not converted into an Agent Commerce _payment, x402 or
MPP proof.
Config rejects paid checkout resources. If the pipeline nevertheless returns a payment challenge, the adapter logs the broken deployment and returns a safe 500 because ACP has no Agent Commerce challenge representation.
The ACP adapter contains no payment logic and never calls a merchant backend directly; it sends one canonical request through the pipeline. Its bearer token is used only for constant-time request authentication and is not written to the receipt database, logs or responses.
The configured gateway stores claims in the receipt database. To import a
2.5 file, keep idempotency.path set when running 2.6.0 start; see
Deprecated store files.
The adapter validates Idempotency-Key before reading a POST body. After body
parsing, it claims the key before calling the merchant. The scope is
(deployment public base URL, concrete endpoint path, caller key); bearer
credentials are not part of it. Two clients using the same caller key on the
same endpoint therefore share one claim.
The body fingerprint uses parsed JSON. Object key order and numeric spelling
such as 1 versus 1.0 normalize; array order, type, and null versus absence
remain distinct.
| Situation | Response |
|---|---|
| first request | atomically claimed before merchant execution |
| same key and body, still running | 409 idempotency_in_flight with Retry-After |
| same key and body, completed | stored response with Idempotent-Replayed: true; no merchant call |
| same key, different body | 422 idempotency_conflict; no merchant call |
ACP response below 500 after the claim, including pipeline INPUT_INVALID |
cached |
| ACP response 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 |
| ACP response 500 or higher when the merchant was not called or answered 429 | claim released; a clean retry may run |
The ACP response status controls claim finalization. The gateway treats a
merchant 429 as unprocessed, releases the claim and returns ACP 503. Merchants
must send 429 before side effects. A merchant 503 also maps to ACP 503 but may
follow partial work, so its claim stays unresolved unless
merchantIdempotent is set. Merchant 401 and 403 map to ACP 502 and stay
unresolved. A post-claim pipeline INPUT_INVALID maps to ACP 400 and is cached.
Request guards run before the claim, so their failures are never stored. These
include routing, authentication, header, body, JSON, schema and content-type
errors. The response echoes a filtered Request-Id; a POST also echoes a
well-formed Idempotency-Key, even when a guard fails before checking it. A
malformed key is never echoed, and routing 405 includes Allow.
ACP requires a fresh attempt after a 5xx. By default, the gateway keeps the
claim when the merchant may have acted, to avoid a duplicate order. Set
protocols.acp.idempotency.merchantIdempotent: true only if the merchant
deduplicates by the derived key. The gateway then releases the claim after a
5xx and frees claims left in flight when it next starts.
An in-flight row becomes completed, released or unresolved when the request
finishes. A crash or a SQLite error while finalizing can leave it in-flight;
the next gateway start marks it unresolved by default or frees it when
merchantIdempotent is enabled. Startup logs the count. This assumes one
gateway process per receipt database.
Only completed records expire, after at least 24 hours. Unresolved records remain for operator reconciliation. This avoids rerunning an unknown remote side effect but is not an exactly-once transaction across HTTP and SQLite. Expired completed rows are purged lazily when a later request claims a key; there is no background sweep.
Schema-v1 idempotency files used bearer-token scopes that cannot be translated to deployment scopes. Their rows are skipped. If any claims were not completed, startup logs a warning to reconcile those operations with the merchant before trusting a retry.
For side-effecting ACP operations, the gateway sends the merchant a derived
Idempotency-Key: SHA-256 over deployment, endpoint and caller key. It is
stable across retries, restarts and bearer-token rotation; differs across
endpoints and deployments; and reveals no caller key. A static
Idempotency-Key in backend headers is replaced by this derived value.
The merchant should store and replay results under the derived key. In the
default mode, the gateway holds uncertain claims to block duplicate checkout
calls. With merchantIdempotent enabled, the merchant must prevent duplicates
on retry. A proxy retry outside the gateway can also repeat a side effect if
the merchant does not deduplicate.
| Cause | Response |
|---|---|
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 or 422 | the same status, invalid_request_body |
| 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 or 503 | 503 service_unavailable with Retry-After |
| merchant 401, 403, another 5xx or another unmapped status | 502 processing_error |
| backend timeout | 504 service_unavailable |
| load shedding | 503 service_unavailable |
| mapping, storage or unexpected payment challenge | 500 processing_error |
ACP errors contain type, code, message, a safe param when available,
and supported_versions for API-version errors. For a valid merchant ACP error
on a relayed status, the gateway uses its type and a snake_case code of at
most 64 characters. A code starting with idempotency_ is not relayed,
because only the gateway issues those codes. It also includes a bounded,
printable param when present. The gateway supplies its own message; no
other merchant body fields are sent to the agent. Merchant 401/403 is not
presented as a failure of the agent's ACP bearer token.
Retry-After on a 503 for a merchant 429 or 503 is the merchant's value when
it is whole seconds, clamped to 1-300, and 5 otherwise.
Session ids such as gid://shop/Checkout/1 are accepted when encoded as one
URL segment. Empty ids and ASCII control characters are refused. Merchant
markdown containing an HTML tag or comment is refused as processing_error
before it reaches the agent.
{
"protocol": {
"name": "acp",
"version": "2026-04-17",
"supported_versions": ["2026-04-17"]
},
"api_base_url": "https://merchant.example.com/acp",
"transports": ["rest"],
"capabilities": { "services": ["checkout"] }
}Discovery is public while ACP is enabled and uses
Cache-Control: public, max-age=3600. Optional documentation, currency,
locale and intervention metadata appears only when configured. Tokens, backend
URLs, resource ids and database paths are excluded.
The adapter excludes carts, feed and standalone orders; delegated payment and
authentication; the ACP MCP binding; webhooks and outbound delivery; the client
role; Signature and Timestamp verification; discounts and the general
extension framework; seller-backed payment handlers; and other ACP versions.
A Signature header does not replace bearer authentication.
No ACP SDK ships. Conformance tests use the vendored official schema and examples.