ACP Checkout Endpoints Reference
The Route Table
POST /checkout_sessions create a session
GET /checkout_sessions/{id} retrieve a session
POST /checkout_sessions/{id} update a session
POST /checkout_sessions/{id}/complete pay with a delegated token
POST /checkout_sessions/{id}/cancel cancel a session
These are the routes an agent platform calls on your store. They are served by the standalone server and the Lambda handler out of the box, and embedders mount the same router in their own HTTP stack with one function call, as shown in the embedding guide. An unknown path or method under the prefix returns a 404 with code not_found rather than falling through to other routes.
Required Headers and Authentication
Every request must carry Authorization: Bearer <token> matching the configured ACP_BEARER_TOKEN. The endpoints are closed by default: with no token configured, every request is rejected with 401, opening the API is an explicit configuration act, never an accident. Token comparison is constant-time, so the check leaks nothing through timing.
Every request must also carry API-Version. The module advertises and echoes 2026-04-17, the published spec snapshot it implements, and requires the header to be present without rejecting nearby values, so a platform pinned to an adjacent snapshot still interoperates while the protocol is in beta. A missing header is a 400 with code missing_api_version.
Optionally, requests can be HMAC-signed: a Signature header carrying a base64 SHA-256 HMAC of the exact raw request body, verified against ACP_SIGNING_SECRET, with an optional Timestamp header checked against a five minute clock skew window. The enforcement logic fails closed: when a secret is configured, a missing or wrong signature is a 401, a caller cannot bypass verification by omitting the header. The acp.requireSignature config flag additionally fails requests when the secret itself is missing, protecting against a deployment that forgot the secret. POST bodies must be application/json, enforced with a 415 otherwise.
Idempotency on Every POST
Payments APIs live and die on retry safety, so the module enforces the ACP idempotency rules strictly. Every POST must include an Idempotency-Key (up to 255 characters), missing keys are a 400. The key is scoped to the caller identity and the specific endpoint, so two platforms or two routes can never collide on the same key string. Four outcomes exist. A new key processes normally and the response is stored. The same key with the same body replays the stored response, marked with an Idempotent-Replayed: true header, this is what makes network-level retries free. The same key with a different body is a 422 idempotency_conflict, catching buggy clients before they double-purchase. And a key whose first request is still processing returns a 409 idempotency_in_flight with a Retry-After hint, serializing concurrent retries instead of racing them. Server-side 5xx responses are not stored, so a transient failure does not poison the key and the platform's retry gets a fresh attempt.
The Session Object and Its States
All five routes return the same resource: an ACP checkout session. Its core fields are id, status, currency, buyer, line_items (each with the sellable item id and quantity, name, and integer-cent base_amount, discount, subtotal, tax, and total), fulfillment_options and the selected option (digital delivery by default), totals (typed entries such as items_base_amount, subtotal, tax, and total, each with display text), messages (buyer-visible info and warnings), links (your terms, privacy, and return policy URLs from config), payment.handlers (the Stripe handler advertisement), and once paid, an order with id and permalink.
Status moves through five values. not_ready_for_payment means the session cannot be paid yet, for example it has no items. ready_for_payment means totals are final pending payment. in_progress means a hosted payment is live for the session, it is frozen and cannot be updated or re-priced underneath the shopper. completed and canceled are terminal. The transition rules are conservative on purpose: updates to frozen sessions fail with invalid, completing an already-completed session returns it unchanged (idempotent success), canceling a completed session is a 405 not_cancelable, and a late payment webhook can never resurrect a canceled session.
Create, Update, and the Re-Pricing Rule
Create takes items (sellable id plus quantity), optional buyer, and optional fulfillment_details. A sellable id is a variant id when the product has variants, otherwise the product id, one id namespace across the whole catalog, as defined in the products file reference. The module resolves every item against the live catalog, prices it server-side, and returns the session, typically already ready_for_payment since digital fulfillment needs no address.
Update replaces items when given, merges buyer fields, validates a fulfillment option choice, and merges fulfillment details. After any change, and on every retrieve of a non-frozen session, the module re-derives every line's price and title from the catalog, recomputes tax if an address is now known, rebuilds totals, and recalculates status. Items that vanished or went unavailable since the session was created are dropped with a warning message attached to the session (out_of_stock, invalid, or missing coded), so the platform can tell the shopper exactly what changed instead of discovering a different total silently. Client-supplied amounts are never trusted anywhere in this flow, the catalog is the only price authority.
Complete and Cancel
Complete is the delegated-payment endpoint: the platform sends payment_data containing a payment token (a Stripe Shared Payment Token) and optionally a billing address, plus optional final buyer details. The module applies the billing address first and recomputes so tax reflects the buyer's location, validates the session has items, then charges the token through the payment provider. Success marks the session completed, attaches the order, and fires your order webhook. A declined charge is a 400 with code payment_declined and the decline reason in the message, a card requiring authentication is code requires_3ds, and a missing token is invalid. The full payment mechanics, including how this path differs from the hosted Stripe Checkout flow, are in payment modes.
Cancel marks a session canceled and is allowed from any state except completed. Canceled is final: subsequent updates fail, completes fail, and webhook confirmations are ignored for it.
Error Shape
Every error is a flat JSON object per the ACP spec: a type of invalid_request, processing_error, service_unavailable, or rate_limit_exceeded, a machine code, and a human message. The module maps its internal errors directly: missing is a 404, not_cancelable a 405, invalid, out_of_stock, payment_declined, and requires_3ds are 400s, and anything unexpected is a 500 internal_error whose details are logged server-side but never leaked to the caller, no stack traces, no database errors, no internal identifiers on the wire.
A Worked Wire Example
The smallest complete platform interaction, three calls. Create: POST /checkout_sessions with {"items":[{"id":"pro-single","quantity":1}]} and an Idempotency-Key of k1 returns 201 with a session in ready_for_payment, a total entry of 4999, and your policy links attached. Update: POST /checkout_sessions/{id} with a buyer email and key k2 returns 200 with the buyer merged and identical totals, re-priced against the catalog on the way through. Complete: POST /checkout_sessions/{id}/complete with payment_data carrying the platform's token and a billing address, key k3, returns 200 with status completed and an order object whose permalink your shopper can visit. Resending that exact complete request with key k3 returns the same body with Idempotent-Replayed: true and charges nothing, which is the single most important property to demonstrate to a platform reviewer, and it works out of the box.
