The Agentic Commerce Protocol (ACP) is an open, Apache 2.0 licensed standard, developed by Stripe and OpenAI and first released as specification version 2025-09-29, that lets an AI agent run a purchase against a merchant's own checkout. The merchant exposes five checkout session endpoints, the agent platform turns the buyer's payment method into a single use token capped by amount and expiry, and the merchant charges that token through its existing payment processor.
This post goes one level deeper than our opinion piece on why SaaS must let AI agents buy and works as a sibling to the Agent Payments Protocol (AP2) explainer. For transport background, see our post on A2A versus MCP on the wire. Here we walk through the endpoints, headers, token limits and state machine in the specification, then the risks in the gaps between them.
Key takeaways
- What it is. A REST (and, since version 2026-04-17, MCP) checkout contract between an agent platform and a merchant, plus a Delegate Payment API that issues constrained payment tokens. It is not a payment rail.
- Who runs it. OpenAI and Stripe are Founding Maintainers; the governance document lists OpenAI, Stripe and Meta on a Technical Steering Committee of up to seven seats. Apache 2.0, status beta.
- The merchant stays merchant of record. It prices the cart, calculates tax, runs its own risk checks, charges through its own processor, and handles refunds and chargebacks.
- Five checkout calls: create, update, retrieve, complete and cancel, each returning the full authoritative cart.
- Tokens are single use and bounded by
max_amount,currency,checkout_session_id,merchant_idandexpires_at. - Integrity controls vary in strength. Bearer auth,
Idempotency-KeyandAPI-Versionare required; requestSignatureandTimestampare only recommended; webhook HMAC signatures are required. - The main risks are prompt injection leading to unwanted purchases, merchant spoofing, replays, over-broad tokens and oversharing of personal data.
- Sources disagree in places. OpenAI's guides, the RFCs and the latest OpenAPI files differ on some status codes, the TLS floor and version strings.
What ACP is, who published it and under what license
ACP describes itself on agenticcommerce.dev as "an open standard for programmatic commerce flows between buyers, AI agents, and businesses." The site says Stripe and OpenAI developed it as a shared vocabulary for how agents and businesses transact, covering checkout coordination and the sharing of payment credentials. Stripe's own documentation now describes ACP as "created by Stripe, OpenAI, and Meta."
The specification lives in the GitHub repository agentic-commerce-protocol/agentic-commerce-protocol. Its README marks the status as beta, names OpenAI and Stripe as maintainers and points to their two first implementations; the MAINTAINERS.md file lists OpenAI, Stripe and Meta as lead maintainers.
Versioning: dated snapshots, not semantic versions
ACP uses date based versions (YYYY-MM-DD). Each release is a complete snapshot under spec/<version>/ with examples and a changelog; new work collects in spec/unreleased/, and older versions are marked deprecated. Clients send their version in an API-Version header on every request.
Dated timeline
| Date (version) | What changed | Source |
|---|---|---|
| 2025-09-29 | Initial release: Agentic Checkout specification and OpenAPI schema, Delegate Payment specification and OpenAPI schema, examples and RFCs | changelog/2025-09-29.md |
| 2025-12-12 | Breaking changes from early adapter implementations: flat fulfillment_address became nested fulfillment_details; fulfillment_option_id became a selected_fulfillment_options[] array | changelog/2025-12-12.md |
| 2026-01-16 | Open authentication provider model for 3D Secure: AuthenticationProvider schema and a required merchant_id on PaymentProvider | changelog/2026-01-16.md |
| 2026-01-30 | Capability negotiation, the payment handlers framework (breaking), an extensions framework and a discount extension | changelog/2026-01-30.md |
| 2026-04-17 | Carts, a Feed API, native orders, Delegate Authentication (3DS2), the /.well-known/acp.json discovery document, an MCP transport binding, mandatory idempotency rules, a defined webhook signature format and seller-backed payment handlers. Version 2026-01-30 deprecated | changelog/2026-04-17.md |
| 24 October 2026 | The README lists 2026-04-17 as the latest stable version, with further work in unreleased/ | Repository README |
The README labels 2026-01-16 "Capability negotiation," but the changelog places that feature in 2026-01-30; the table follows the changelogs.
License and governance
The code and specification are under the Apache 2.0 license, and every contributor must sign a contributor license agreement (individual or corporate) before a pull request is accepted.
The governance document sets up a Technical Steering Committee (TSC) with up to seven seats, one per organisation. At the time of writing three seats are filled: OpenAI, Stripe and Meta. Significant changes go through Specification Enhancement Proposals (SEPs). A SEP needs a TSC sponsor, a public review window of seven business days that spans at least one weekly TSC meeting, and a simple majority vote. OpenAI and Stripe, as Founding Maintainers, appoint and remove TSC members and keep a reserve veto that the document says is for "extremely rare situations" and must come with a written explanation. The stated long term goal is a neutral foundation, once adoption and community participation justify it.
For security teams the point is plain: the protocol changes every few months, sometimes with breaking changes, so pin the version you implement and watch the changelog.
The roles, and what the merchant keeps control of
ACP has four parties. The specification and the OpenAI and Stripe docs use slightly different names for them, so here is a mapping.
| Role | Also called | What it does in ACP | What it never has to do |
|---|---|---|---|
| Buyer | Customer, user | Asks the agent for something, reviews the cart, approves the purchase in the agent's interface | Visit the merchant's site |
| AI agent platform | Agent, client, "ChatGPT" in OpenAI's docs | Calls the merchant's checkout endpoints, renders the cart, collects buyer, fulfilment and payment details, requests a delegated payment token | Be merchant of record or process the payment |
| Merchant | Seller, business | Hosts the checkout endpoints, validates items and prices, computes tax and shipping, runs risk checks, charges the token, creates the order, sends order webhooks | Hand over control of its catalogue, pricing or fulfilment |
| Payment service provider (PSP) | Processor, vault | Implements the Delegate Payment API, stores the credential, issues a token bound to an allowance, processes the charge for the merchant | Trust the agent beyond the allowance |
The spec site says the business stays merchant of record and controls which products are sold, how they are presented and how orders are fulfilled. OpenAI's docs add that the merchant runs validation, tax and risk checks on its own systems, charges through its own processor, handles refunds and chargebacks, and appears on the buyer's card statement. The spec site's FAQ also lets merchants accept or decline per agent, per transaction or with custom logic. That is the merchant's most important control: an ACP endpoint is not an obligation to sell to any caller.
What the merchant gives up is the page: checkout renders in the agent's interface, so browser based fraud signals and the merchant's own confirmation step disappear.
How the checkout endpoints work
The Agentic Checkout RFC defines a REST contract that "merchants SHOULD implement." Every endpoint uses HTTPS and JSON, and every money field is an integer in minor units (cents for US dollars).
The five endpoints
| Call | Method and path | Success status | What it carries |
|---|---|---|---|
| Create | POST /checkout_sessions | 201 Created | Items (id, quantity), optional buyer and fulfillment_details, the agent's capabilities |
| Update | POST /checkout_sessions/{checkout_session_id} | 200 OK | Changed items, fulfilment details or selected_fulfillment_options |
| Retrieve | GET /checkout_sessions/{checkout_session_id} | 200 OK, or 404 | Nothing; returns the latest authoritative state |
| Complete | POST /checkout_sessions/{checkout_session_id}/complete | 200 OK | payment_data (handler id plus instrument and credential, such as a delegated token), optional buyer and billing address, and authentication_result when 3D Secure is required |
| Cancel | POST /checkout_sessions/{checkout_session_id}/cancel | 200 OK, or 405 if already completed or canceled | Optional cancellation context |
OpenAI's checkout guide lists the same five endpoints, and the 2026-04-17 MCP binding maps them to five tools such as create_checkout_session and complete_checkout_session.
The authoritative cart
Every response returns the whole session, not a delta: id, status, currency, line_items[] with integer amounts and tax, fulfillment_options[], the selected options, a totals[] array, messages[], policy links[], payment handlers and, after completion, an order with id, checkout_session_id and permalink_url.
In the RFC's example, one item at 300 cents plus 30 cents tax and 100 cents standard shipping totals 430; switching to 500 cent express shipping makes the merchant return 830. The agent never adds anything up.
Session states
The status field moves through a small set of values:
| Status | Meaning | Typical next step |
|---|---|---|
not_ready_for_payment | Something is missing, such as an address needed for tax and shipping | Agent updates the session |
ready_for_payment | Totals are final enough to pay | Agent completes the session |
authentication_required | The issuer wants 3D Secure; the response must include authentication_metadata | Agent runs authentication and calls complete with authentication_result |
in_progress | Merchant is processing | Agent retrieves the session |
completed | Order created; response includes the order object | Merchant sends order webhooks |
canceled | Session ended without an order | None; cancel again returns 405 |
If an agent calls complete while the session is in authentication_required without an authentication_result, the server must return a 4XX error with type: invalid_request, code: requires_3ds and param: $.authentication_result.
Messages, markdown and capability negotiation
Errors and notices travel in messages[], with codes such as missing, invalid, out_of_stock, payment_declined, requires_3ds and intervention_required. Each message can carry a resolution: recoverable (the agent can fix it), requires_buyer_input or requires_buyer_review (the buyer must authorise before the order is placed). The last one is a useful lever: a merchant can demand a human decision in machine readable form instead of hoping the agent asks.
Markdown content must follow CommonMark 0.31.2 with no raw HTML. Sellers must not author it, servers must reject it and agents must render with raw HTML disabled or sanitised, which blocks a merchant, or a compromised merchant, from injecting script or hidden content into the agent's interface.
Since 2026-01-30 the agent sends a capabilities object on create and the merchant returns the intersection, plus anything it requires. If a required intervention such as 3D Secure is unsupported and enforcement is always, the merchant returns intervention_required before any payment is attempted. Payment methods are described as handlers (for example dev.acp.tokenized.card), each declaring its PSP, schemas, requires_delegate_payment and requires_pci_compliance; the Payment Handlers RFC recommends requires_delegate_payment: true for all of them. The 2026-04-17 release added seller-backed handlers for saved cards, gift cards, points and store credit that resolve on the merchant's backend without moving a credential.
The other surfaces: feeds, discovery, carts and webhooks
Product feeds. OpenAI's getting started guide begins with the feed: structured product data (titles, descriptions, images, price, availability) so ChatGPT can index products, ideally a daily full file plus API updates. Feed onboarding is limited to approved partners, and OpenAI lists prohibited categories such as age restricted goods, weapons and prescription only medicines. In the spec, the Feed API is a push model hosted by the agent (POST /feeds, GET /feeds/{id}, GET and PATCH /feeds/{id}/products).
Discovery. An agent can fetch https://<merchant domain>/.well-known/acp.json, served at the origin root per RFC 8615, public without a token, cacheable for an hour. It lists supported versions, the api_base_url, transports (rest, mcp) and seller capabilities, and must not expose merchant identifiers when a platform hosts it for many sellers.
Carts. 2026-04-17 added POST /carts, GET /carts/{id}, PUT /carts/{id} and POST /carts/{id}/cancel, and changed quantity from an integer to a decimal greater than zero for business to business sales by weight.
Order webhooks. After completion the merchant pushes full order objects to the platform. The webhook spec requires a Merchant-Signature header of the form t=<unix_seconds>,v1=<64 hex characters>: HMAC-SHA256 over the timestamp, a dot and the raw body with a shared secret. Receivers return 401 if it is missing, malformed, wrong or outside the timestamp window (300 seconds recommended). OpenAI's guide says the signing key is provided by OpenAI and that order updates should also report refunds and chargebacks.
How delegated payment works
Delegated payment lets an agent pay without the merchant handling the buyer's card number. It has its own RFC and openapi.delegate_payment.yaml.
The one endpoint
POST /agentic_commerce/delegate_payment
The RFC calls it a single, MUST implement endpoint that issues a delegated vault token. OpenAI's payment spec gives the call direction as OpenAI to PSP: the platform sends the credential to the merchant's PSP (or a merchant's own vault) and gets a token back. OpenAI limits direct integration to PSPs and PCI DSS level 1 merchants with their own vaults.
The request body has five parts:
| Field | Required | Contents |
|---|---|---|
payment_method | Yes | Today exactly one type, card: card_number_type (fpan or network_token), virtual, number, expiry, cvc, display fields such as brand and last four digits, and metadata |
allowance | Yes | reason (must be one_time), max_amount (integer minor units), currency (lowercase ISO 4217), checkout_session_id, merchant_id, expires_at (RFC 3339) |
billing_address | No | Address for AVS style checks |
risk_signals | Yes | Signals such as type: card_testing with a score and an action of blocked, manual_review or authorized |
metadata | Yes | Free form string pairs for correlation |
On success the PSP returns 201 with a token id, a created timestamp and echoed metadata. The normative rule is short: the token "MUST ONLY be usable within the provided Allowance," and it "MUST become invalid at or after allowance.expires_at."
A worked example of an allowance
Take the RFC's example session: a 300 cent item, 30 cents tax, express shipping at 500 cents, total 830 cents. A tight allowance for that purchase looks like this:
{
"allowance": {
"reason": "one_time",
"max_amount": 830,
"currency": "usd",
"checkout_session_id": "checkout_session_123",
"merchant_id": "acme",
"expires_at": "2026-10-24T10:15:00Z"
}
}
The PSP should then refuse 831 cents, another currency, another merchant, another session, a second charge and anything after 10:15 UTC. OpenAI says the platform sets "a maximum chargeable amount and expiry" from what the user chose; Stripe says the agent sets the maximum to the transaction total. If the merchant's total rises after issue, the charge should fail and the flow return to the buyer.
Why the merchant never sees the raw card, and where it does go
The merchant receives only a token in payment_data.instrument.credential on the complete call and charges it through its PSP.
Be precise about the agent side, though. In a direct Delegated Payment Spec integration the platform's payment systems do send card data to the PSP, which is why OpenAI says direct integration "will likely be in PCI scope" and may require an attestation of compliance. The card travels from those systems to a PCI scoped vault, never to the merchant and never into the model's context. OpenAI adds that eligible cards are upgraded with network tokens, that the feed and checkout specs carry no cardholder data, and that using your PSP's own delegated payment implementation may leave your PCI scope unchanged.
Stripe's Shared Payment Token
Stripe is the first compatible PSP, through its Shared Payment Token (SPT), which its public preview docs call "a scoped grant to use the customer's payment method." The agent grants an SPT to the seller's Stripe profile with usage_limits (currency, max_amount, expires_at). The seller charges an SPT through a PaymentIntent, or resolves it to network credentials for a third party processor. A revoked SPT cannot be charged, and a shared_payment.granted_token.deactivated event fires when a token is consumed, resolved, expired or revoked. Stripe lists availability in the US, Canada and select European countries.
Delegate Authentication for 3D Secure
The 2026-04-17 release added a Delegate Authentication API so the agent can run 3DS2 directly with an authentication provider the merchant names (browser channel, native fingerprint and challenge flows). The result returns to the merchant as authentication_result on the complete call. Stripe's ACP overview summarises this building block as delegated authorisation with OAuth 2.0; the RFC itself describes 3DS2, so treat the RFC as the reference.
How signing, idempotency and versioning protect the flow
Request headers
The checkout RFC lists these headers on calls from the agent platform:
| Header | Status in the RFC | Purpose |
|---|---|---|
Authorization: Bearer <token> | Required | Identifies the calling platform |
Idempotency-Key | Required on every POST | Safe retries; max 255 characters, UUID v4 recommended |
API-Version | Required | Selects the dated spec snapshot |
Signature | Recommended | Base64url detached signature over canonical JSON of the request |
Timestamp | Recommended | RFC 3339 time for freshness checks |
Request-Id | Recommended | Tracing; echoed in responses |
Accept-Language, User-Agent | Recommended | Locale and client identification |
Request signing
Servers should publish acceptable signature algorithms out of band, and clients should sign the canonical JSON body and send a Timestamp. The Delegate Payment RFC's preparation steps use MUST for canonical serialisation and a detached signature (Ed25519 and ES256 are its examples), yet its header table and security section still mark Signature and Timestamp as recommended.
So signing is a bilateral agreement. Merchants should require it from every platform they accept, agree algorithm, keys, canonical form and clock skew in writing, and reject unsigned or stale requests. A bearer token proves possession of a secret, not that the body is unaltered or fresh. The spec does not name the canonical JSON scheme for signatures; RFC 8785 (JCS), which it mentions for idempotency fingerprints, is a sensible default.
Idempotency
Idempotency got its own normative section in the 2026-04-17 cycle (SEP #120). The rules for checkout and delegate payment are the same:
- Every POST must carry
Idempotency-Key. Missing it returns 400 withcode: idempotency_key_required. - Keys are scoped to the authenticated identity plus endpoint path.
- Equivalence is semantic JSON equality of the body only. Key order and
1.0versus1do not matter; array order does, andnullis different from an absent key, becausenullmeans "clear this field." - Same key, same body: return the original response with the same status, ideally with
Idempotent-Replayed: true, and never re-run side effects such as payment capture or token creation. - Same key, different body: 422 with
idempotency_conflict, not retryable. - Same key while the first request is still running: 409 with
idempotency_in_flightand aRetry-Afterheader. - Never cache a 5xx against a key; a retry after a server error is a fresh request.
- Keep key to response mappings for at least 24 hours.
The RFC also recommends committing the key record and the business operation in one database transaction, with recovery points around external calls such as a PSP authorisation, so a retry resumes rather than charges twice.
Versioning
The client must send API-Version and the server must check it, returning 400 with a supported_versions array when it is missing or unsupported. That stops a merchant silently misreading a body whose field names changed, as fulfillment_address did in 2025-12-12.
Where the sources disagree
OpenAI's guides and the repository do not line up everywhere. These are the differences we found on 24 October 2026:
| Topic | Repository (RFC or 2026-04-17 OpenAPI) | OpenAI developer docs |
|---|---|---|
| Same idempotency key, different body | 422 idempotency_conflict (409 is for in-flight) | Production checklist says idempotency_conflict with HTTP 409 |
| Complete checkout success status | 200 OK | Production checklist asks you to confirm HTTP 201 Created for order completion |
| TLS | RFCs say TLS 1.3 must be used | TLS 1.2 or later on port 443 with a valid public certificate |
Example API-Version | 2026-04-17 is the latest stable snapshot | Checkout spec page shows 2025-09-12 |
| MCP | MCP binding added in 2026-04-17 | Checkout page says MCP server support will come in the future |
None of these change the security model, but they will break a conformance test. Ask your platform which version its certification uses. The repository also disagrees with itself in one place: the 2026-04-17 changelog removed the minimum of one item from risk_signals, while the Delegate Payment RFC text still says at least one risk signal is required. |
ACP compared with AP2
ACP and the Agent Payments Protocol (AP2) are often mentioned together, but they solve different problems. ACP is a checkout contract; AP2 is a layer of signed evidence about what the user approved. The AP2 column below comes only from the AP2 documentation at ap2-protocol.org.
| Agentic Commerce Protocol (ACP) | Agent Payments Protocol (AP2) | |
|---|---|---|
| What it is | REST (and MCP) checkout API plus a Delegate Payment API | A protocol for securing payments that agents perform; it "operates as a security feature within a Commerce Protocol" and leaves catalog and checkout APIs out of scope |
| Trust model | Merchant trusts an authenticated agent platform it chose to accept; PSP trusts the platform to request tokens; the platform's interface is where the buyer confirms | Each party verifies signed mandates; a Trusted Surface role is the user interface trusted to collect informed consent before a user signed mandate is created |
| What limits spending | A delegated vault token bound to an allowance: one time, maximum amount, currency, checkout session, merchant and expiry | Checkout and Payment Mandates, in open (constraints) and closed (bound to one transaction) forms, secured as SD-JWTs |
| Proof of user intent | Implicit: the platform says the buyer approved; the merchant sees a token and an authenticated call | Explicit: a signed mandate the merchant and payment parties can check, and can use as dispute evidence |
| Payment rails | Cards today through PSP vault tokens and network tokens; seller-backed handlers for gift cards, points and store credit | Instrument agnostic; new payment instruments are added by defining a type |
| Transport | HTTPS REST; MCP binding since 2026-04-17 | Carried inside a commerce protocol, whose APIs AP2 leaves out of scope |
| Who publishes it, status | OpenAI and Stripe as Founding Maintainers, Meta on the TSC; Apache 2.0; beta | Documentation at ap2-protocol.org, currently v0.2; see our AP2 explainer for its history |
The practical takeaway: ACP tells a merchant how to sell to an agent; AP2 tells a payment chain how to check that a person meant it. Nothing in ACP stops a platform from also attaching stronger proof of intent, and that is where the two could meet.
Risks and the controls that apply
The ACP documents have no threat model section, so this one is ours. The OWASP GenAI Security Project's prompt injection and excessive agency entries are the right background for the agent side.
| Risk | What it looks like | Controls in the spec | Controls you add |
|---|---|---|---|
| Prompt injection leads to an unwanted purchase | Text in a product page, review or feed steers the agent to buy or change the address | Buyer confirms in the platform UI; requires_buyer_review; allowance capped to the shown total | Deterministic confirmation outside the model; merchant velocity limits and review flags |
| Merchant spoofing | Agent pointed at a look-alike domain or forged discovery document | HTTPS, platform onboarding, discovery on the merchant's own origin | Allowlist approved merchant origins; tokens bound to merchant_id |
| Replays and duplicate charges | A captured or retried complete call or webhook is sent again | Idempotency replay without side effects; 409 in flight; webhook 300 second window | Require Signature and Timestamp; one transaction for key and order; single use tokens at the PSP |
| Over-broad or stored tokens | Allowance above the total, long expiry, token kept for later | one_time, max_amount, session, merchant and expiry binding | Exact total, expiry in minutes, revoke on cancel, never persist tokens |
| Manipulated prices | Agent, or something injecting into it, supplies a price | Requests carry item ids and quantities; merchant returns the authoritative cart | Never read an amount from the request |
| Malicious content in the agent UI | Product text or messages carry HTML or hidden instructions | CommonMark without raw HTML, rejected by servers, sanitised by agents | Treat feed text as untrusted input |
| Too much personal data | Buyer details reach merchant logs before the buyer commits | No full card number or CVC in logs; redact addresses | Collect fields only when needed; mask PII |
| Webhook forgery | Fake order updates show false shipping or refunds | Required HMAC Merchant-Signature, 401 on failure | Rotate secrets; constant time compare; alert on failures |
Prompt injection that leads to an unwanted purchase
An agent reads untrusted text: product descriptions, reviews, merchant messages, web pages. Some of it may be written to steer the model toward buying. ACP's structure limits the damage but does not remove the risk. Injected text cannot change the price, because the merchant computes the cart; the allowance caps the token to the total the buyer saw; and in OpenAI's flow the session completes only after the buyer clicks Buy. But the spec does not require a human confirmation step for every agent platform, and another agent could be built to complete on its own.
Keep the buy decision in deterministic platform code that shows the exact cart and needs an explicit action, never a model output. Merchants should use their right to decline per agent and per transaction, return requires_buyer_review for anything unusual, and watch for patterns such as address changes just before completion. For merchant spoofing, replays and over-broad tokens, the table above lists the controls; the common thread is that a token bound to one merchant, one session, one amount and a short expiry is worth little to anyone who intercepts or misdirects it.
Data shared with the agent and the merchant
ACP moves personal data both ways: name, email, phone and address to the merchant during create and update, order details back through webhooks. OpenAI says customers should need at most their email address to view order details at the permalink, so that URL must not leak data to anyone who guesses it. Collect fields only when the state needs them, redact logs, set retention for sessions that never became orders, and do not treat checkout as marketing consent; the 2026-04-17 marketing_consent_options field exists for that.
Step by step: read the spec, stand up a sandbox merchant, verify signatures and idempotency, log every state change
This walkthrough builds a local, test only merchant endpoint so you can see the contract and the controls. It does not connect to any agent platform or move real money. Use a disposable machine or container.
Step 1: get the specification at a pinned version
git clone https://github.com/agentic-commerce-protocol/agentic-commerce-protocol.git
cd agentic-commerce-protocol
git log -1 --format='%H %cd' # record the commit you read
ls spec/ # 2025-09-29 ... 2026-04-17 unreleased
ls spec/2026-04-17/openapi/
# openapi.agentic_checkout.yaml openapi.agentic_checkout_webhook.yaml
# openapi.cart.yaml openapi.delegate_authentication.yaml
# openapi.delegate_payment.yaml openapi.feed.yaml
cat changelog/2026-04-17.md | less
Always read the changelog for the version you target before the RFCs. The RFC files carry their own version labels (the checkout RFC header says 2026-01-16), so when an RFC and the dated OpenAPI file differ, the OpenAPI snapshot is what conformance tests will check.
Step 2: validate the OpenAPI files and list the operations
python3 -m venv .venv && . .venv/bin/activate
pip install openapi-spec-validator pyyaml
openapi-spec-validator spec/2026-04-17/openapi/openapi.agentic_checkout.yaml
openapi-spec-validator spec/2026-04-17/openapi/openapi.delegate_payment.yaml
Then list each path and its response codes from the YAML (for example with a few lines of Python and yaml.safe_load). You should see the five checkout operations with 201, 400, 409 and 422 on create and 200 and 405 on cancel. Keep that list as your contract checklist.
Step 3: write a sandbox merchant with the controls built in
The sketch below uses FastAPI and shows only the guard that sits in front of every handler, as the RFC recommends: bearer token, API-Version, an Ed25519 request signature with a timestamp window, and idempotency scoped to identity and path. Business logic (catalogue pricing, the state table, the PSP call) goes behind it.
# sandbox_merchant.py (test only; not production code)
import base64, hashlib, json, time
from datetime import datetime
from fastapi import Request, HTTPException
from fastapi.responses import JSONResponse
from cryptography.hazmat.primitives.asymmetric.ed25519 import Ed25519PublicKey
SUPPORTED = ["2026-04-17"]
TOKENS = {"test_platform_token": "platform_a"} # bearer -> identity
KEYS = {"platform_a": Ed25519PublicKey.from_public_bytes(
base64.b64decode(open("platform_a_ed25519.pub.b64").read()))}
SKEW = 300
idem = {} # use a database row committed with the order in real code
def canonical(body): # agree the scheme with each platform; RFC 8785 is a sound default
return json.dumps(body, sort_keys=True, separators=(",", ":"),
ensure_ascii=False).encode()
def err(status, code, msg, **extra):
return JSONResponse({"type": "invalid_request", "code": code,
"message": msg, **extra}, status_code=status)
async def guard(req: Request):
identity = TOKENS.get(req.headers.get("authorization", "").removeprefix("Bearer "))
if not identity:
raise HTTPException(401)
if req.headers.get("api-version") not in SUPPORTED:
return identity, None, err(400, "unsupported_api_version",
"Unsupported API-Version", supported_versions=SUPPORTED), None
body = json.loads(await req.body() or b"{}")
try: # the spec says SHOULD sign; this sandbox requires it
ts = datetime.fromisoformat(req.headers["timestamp"].replace("Z", "+00:00"))
if abs(time.time() - ts.timestamp()) > SKEW:
raise ValueError("stale")
KEYS[identity].verify(base64.urlsafe_b64decode(req.headers["signature"] + "=="),
canonical(body))
except Exception:
raise HTTPException(401, "bad signature or timestamp")
if req.method != "POST":
return identity, body, None, None
key = req.headers.get("idempotency-key")
if not key or len(key) > 255:
return identity, body, err(400, "idempotency_key_required",
"Idempotency-Key header is required on all POST requests"), None
scope, fp = (identity, req.url.path, key), hashlib.sha256(canonical(body)).hexdigest()
rec = idem.get(scope)
if rec and rec["state"] == "in_flight":
r = err(409, "idempotency_in_flight", "Request with this key is still processing")
r.headers["Retry-After"] = "1"
return identity, body, r, None
if rec and rec["fp"] != fp:
return identity, body, err(422, "idempotency_conflict",
"Idempotency-Key reused with a different body"), None
if rec:
return identity, body, JSONResponse(rec["body"], status_code=rec["status"],
headers={"Idempotent-Replayed": "true"}), None
idem[scope] = {"state": "in_flight", "fp": fp}
return identity, body, None, scope
def finish(scope, status, body):
if scope and status < 500: # never cache 5xx; keep records 24 hours or more
idem[scope] = {"state": "done", "fp": idem[scope]["fp"],
"status": status, "body": body, "at": time.time()}
elif scope:
idem.pop(scope, None)
return JSONResponse(body, status_code=status)
Each handler calls guard(), returns early if it gets a response back, and otherwise does its work and passes the result through finish(). Inside the handlers, keep three rules. Copy only id and quantity from each item, so any price or total a caller adds is dropped, and compute every amount from your catalogue. Store the calling identity on the session and return 404 to any other platform. Enforce the state table from earlier: complete only from ready_for_payment, 405 on cancel from completed or canceled, and in complete take only the token from payment_data.instrument.credential and charge the server computed total.
Step 4: get a sandbox payment token
If your PSP is Stripe, its documentation shows a test helper that grants your account an SPT with simulated agent limits, without an agent:
curl https://api.stripe.com/v1/test_helpers/shared_payment/granted_tokens \
-u "sk_test_...:" \
-H "Stripe-Version: 2026-09-30.preview" \
-d payment_method=pm_card_visa \
-d "usage_limits[currency]=usd" \
-d "usage_limits[max_amount]=830" \
-d "usage_limits[expires_at]=1798761600"
Use test keys only. Then call your sandbox complete endpoint with that token as the credential, and confirm that your PSP call charges exactly the server computed total. For other PSPs, use their documented sandbox for the Delegate Payment API.
Step 5: test signatures, idempotency and limits with negative cases
Write tests that must fail before you write tests that must pass. A minimal list, mapped to the spec:
| Test | Expected result |
|---|---|
POST without Idempotency-Key | 400 idempotency_key_required |
| Same key and same body twice | Same status and body, Idempotent-Replayed: true, one session only |
| Same key, different quantity | 422 idempotency_conflict |
| Same key sent while the first call sleeps | 409 idempotency_in_flight with Retry-After |
| Body changed after signing | 401 |
Create with an added "price": 1 on an item | Price ignored; totals from catalogue |
Complete from not_ready_for_payment | 4XX, no PSP call |
| Complete with a token whose limit is below the total | PSP declines; session stays unpaid; payment_declined message |
| Retrieve another platform's session | 404 |
Step 6: verify webhook signatures you send and receive
If you build the agent side, or you want to check what you send, verify Merchant-Signature exactly as the webhook spec describes:
import hmac, hashlib, time
def verify_merchant_signature(header: str, raw_body: bytes, secret: bytes,
tolerance: int = 300) -> bool:
try:
parts = dict(p.split("=", 1) for p in header.split(","))
t, v1 = int(parts["t"]), parts["v1"]
except (ValueError, KeyError):
return False
if abs(time.time() - t) > tolerance:
return False # outside replay window
expected = hmac.new(secret, f"{t}.".encode() + raw_body,
hashlib.sha256).hexdigest()
return hmac.compare_digest(expected, v1) # constant time compare
Sign over the raw bytes you send, not a re-serialised copy, or the receiver will compute a different HMAC.
Step 7: log every state change
For every transition, write one structured line to append only storage: session id, old and new status, server computed total, calling platform, whether the request was signed and, at completion, the token id (never the token value), PSP charge id and order id. When a buyer says "I did not order this," you answer from records. Never log a full card number or CVC, and redact addresses as your policy requires.
Common mistakes
- Trusting agent supplied prices. The spec sends item ids and quantities; prices, tax, shipping and totals come from the merchant. Any code path that reads an amount from the request is a bug.
- Skipping idempotency, or keeping it in memory. A key store that is lost on restart, or not committed with the order, double charges on the first network hiccup.
- Caching 5xx responses against the key. A retry after a server error must be processed fresh.
- Storing tokens beyond their use. Delegated tokens are single use and bound to one session. Do not keep them for reorders, later retries or analytics, and log only token ids.
- Treating
Signatureas optional because the spec does. Make it required in your platform agreements and reject unsigned or stale calls. - Ignoring
nullversus absent.nullmeans "clear this field." A serializer that drops or adds nulls changes meaning and breaks idempotency comparison. - Rendering merchant markdown as HTML, or letting guessable order permalinks show full order details.
- Coding to one document. The RFCs, OpenAPI files and OpenAI's guides differ on some codes and versions. Build to the dated OpenAPI snapshot, then test against your platform's certification checklist.
FAQ
What is the Agentic Commerce Protocol in simple terms?
It is an open specification that lets an AI agent buy from a merchant on a buyer's behalf. The merchant exposes checkout endpoints, the agent platform gets a single use payment token for the exact purchase, and the merchant charges it with its existing processor.
Who created ACP and who controls it?
The spec site says Stripe and OpenAI developed it; Stripe's docs say Stripe, OpenAI and Meta created it. The repository names OpenAI and Stripe as Founding Maintainers, and the governance document lists OpenAI, Stripe and Meta on the Technical Steering Committee. It is licensed under Apache 2.0.
Does the AI agent see my credit card number?
The merchant never sees it; it receives only a token. The language model should never see it either. The agent platform's payment systems, or the PSP's wallet, hold the card and send it to a PCI scoped vault, which is why OpenAI says direct delegated payment integration is for PSPs and PCI DSS level 1 merchants with their own vaults.
What stops a payment token being used for more than one purchase?
The allowance. It must have reason: one_time, a max_amount, a currency, the checkout session id, the merchant id and an expiry, and the spec says the token must only be usable within it and becomes invalid at expiry.
Do I need to change payment processors to support ACP?
No. The spec site says businesses can use ACP with any compatible PSP. Stripe was the first, through its Shared Payment Token. Merchants with their own PCI DSS level 1 vault can integrate directly.
Does ACP work with MCP?
Yes, since the 2026-04-17 release, which added an MCP transport binding with five tools that map to the checkout endpoints. The discovery document advertises rest, mcp or both. OpenAI's checkout page still describes MCP support as future, so check what your platform supports.
How is ACP different from AP2?
ACP defines the checkout API and the delegated payment token. AP2 defines signed mandates that prove what a user approved and describes itself as a security feature inside a commerce protocol. They solve different layers of the same problem.
Is request signing required in ACP?
Not strictly. Bearer authentication, Idempotency-Key on POST and API-Version are required; Signature and Timestamp are recommended in the header tables. Webhooks are different: the Merchant-Signature HMAC header is required. Treat request signing as required in your own agreements.
Where Safeguard fits
Safeguard does not process payments, issue or verify payment tokens, detect prompt injection or judge whether an agent purchase was intended, and it is not a fraud tool. The overlap is the software that implements an ACP integration: your web framework, JSON and YAML parsers, the cryptography library that checks Ed25519 signatures, HTTP clients, your PSP's SDK, MCP server libraries and the container images you deploy them in. A flaw in the library that verifies a signature or parses a request body is a flaw in your checkout. Safeguard can scan those repositories and images (safeguard scan --dir ., safeguard scan --image acp-merchant), match components against vulnerability databases, use reachability analysis to rank the code paths your service actually calls, show which projects carry a given library version, and gate builds with policies. The Safeguard CLI's assistant can also edit code and run commands with your approval, which suits upgrading a pinned dependency and rerunning the negative tests above.
The fit is indirect. Safeguard covers the code you build and ship around ACP, not the protocol's design, your agent platform's confirmation flow or anyone's payment decisions.
An ACP merchant service is a small payment-adjacent API built almost entirely from open source: a web framework, parsers, crypto and HTTP libraries and a base image, all pulled again on every rebuild. When one of them has a known vulnerability, the window between a fix being available and that fix running in your checkout service is risk you can close without changing the protocol.
Fix it without waiting for upstream
Whatever the vulnerability, most of the risk sits in two gaps. The first is the time between a fix being published and that fix actually running in your production builds. The second, and worse, is the time before any fix exists at all, while a maintainer works out a patch and a release schedule. Waiting through both gaps is a habit, not a requirement. Safeguard gives teams three ways to close them.
1. Autonomous remediation: pull requests, not tickets
When a fixed version exists, Safeguard's Griffin models open a tested, policy gated pull request that moves the affected dependency to the safe version. You can run it on one repository or in bulk across every project that carries the vulnerable version. Remediation covers eight categories, including dependencies, licenses, security posture and findings. Reachability analysis ranks the vulnerable code paths your application actually calls first, so the first pull requests you merge are the ones that matter most. Autonomous remediation ships switched off, with every category set to Manual, so your team decides how much runs on its own and what still needs a human approval.
2. Gold Open Source: the safe version in seconds, free
gold.safeguard.sh is free and needs no login. Search any CVE or package to see affected and fixed versions, CVSS, EPSS, CISA KEV status and a recommended safe version, or use the "Am I affected?" checker. The same data is available through a free read API, README status badges and RSS threat feeds. The safeguard-sh/gold-scan-action GitHub Action can fail a build on a known exploited vulnerability, on any known vulnerability, or never, so a version you have just removed cannot quietly come back in next week's merge.
3. Gold Registry: start from components that are already fixed
registry.safeguard.sh provides hardened, drop in replacements for upstream packages and container images. Every Gold artifact is held continuously to zero exploitable critical or high CVEs, zero malware and a completed license check, with SLSA L3 or higher provenance, a Sigstore signature and a public evidence page you can hand to an auditor. An artifact loses its Gold badge if it regresses. Teams can point their own package directory at the registry so hardened equivalents resolve without changing how developers install anything.
When upstream has no fix yet, or will not ship one, Safeguard first works to get the fix accepted upstream. If the issue is severe or upstream will not move, Safeguard builds a fork, fixes the vulnerability there and publishes a hardened Gold build, so you are not stuck on a maintainer's release schedule. You can also request a hardened, compatibility tested build of a specific package. This fork and patch flow is in early access today, so ask which packages it covers before you plan around it.
| Your situation | What to use |
|---|---|
| A fixed version exists | Autonomous remediation opens the upgrade pull request; Gold Open Source confirms the safe version |
| You need to know today whether you are affected | Gold Open Source lookup and "Am I affected?" checker, free and without login |
| You want vulnerable versions to stop shipping | The Gold scan GitHub Action in CI, plus Safeguard policies and gates |
| Upstream has no fix yet, or will not ship one | A custom Gold request; fork and patch (early access) |
| You want to start clean on new projects | Pull hardened components and images from Gold Registry |
One honest limit: when the vulnerable code lives in a vendor appliance, a hosted service or an operating system runtime you do not build yourself, the fix still has to come from that vendor or distribution. The steps above apply to the open source dependencies and container images you build and ship, which is where most teams carry the largest and least tracked share of their exposure.
See how Safeguard would help
None of these demos shows an ACP checkout; they show Safeguard's agent surfaces handling tool access, approvals and scanning. You can download Safeguard and follow the CLI docs to try them.
This is a real CLI session: asked to fix a lodash CVE, the CLI reads the finding and advisory, asks before each command, edits package.json to lodash 4.17.21, runs npm install and npm test with approval, and pushes nothing. The same flow applies when the signature or JSON library under your sandbox merchant needs an upgrade: change the pin, rerun the negative idempotency and signature tests, and approve each step.
Click through it at your own pace, or open it full screen.
This demo connects Claude to the Safeguard MCP server with OAuth and lets you choose which tools Claude can use. It mirrors the principle behind a tight allowance: give an agent a short, named list of what it may do, decided by a person, rather than broad access.
Click through it at your own pace, or open it full screen.
This demo sets up the runner on your own machine, approves it with its device code, and scans from the terminal. It suits the disposable machine you use for the sandbox merchant: scan the cloned specification tools and your service's virtual environment locally before you run anything.
Click through it at your own pace, or open it full screen.
Quick check
A buyer approves a cart of 830 cents. The agent platform requests a delegated token with max_amount 830, then the merchant changes the shipping option and the total becomes 1230. What should happen at complete?
The PSP should decline, because the charge exceeds the allowance's max_amount of 830. The merchant returns a payment_declined message, the session stays unpaid, and the platform has to show the new total to the buyer and request a new token. That is the allowance working as designed.
An agent platform retries POST /checkout_sessions/cs_1/complete with the same Idempotency-Key after a timeout, but this time the body includes a different billing address. What must the merchant return, and must it charge again?
It must return 422 with code: idempotency_conflict, which is not retryable, and it must not charge again. If the first request were still running it would return 409 idempotency_in_flight with Retry-After; if the body were identical it would replay the original response without re-running the capture.
Sources
- Agentic Commerce Protocol site (agenticcommerce.dev)
- Agentic Commerce Protocol llms.txt summary
- ACP GitHub repository and README
- ACP RFC: Agentic Checkout, Merchant REST API
- ACP RFC: Delegate Payment API
- ACP RFC: Payment Handlers
- ACP RFC: Capability Negotiation
- ACP RFC: Discovery
- ACP RFC: Delegate Authentication
- ACP RFC: Product Feeds
- ACP OpenAPI 2026-04-17: Agentic Checkout
- ACP OpenAPI 2026-04-17: Delegate Payment
- ACP OpenAPI 2026-04-17: Agentic Checkout Webhooks
- ACP changelog 2025-09-29
- ACP changelog 2025-12-12
- ACP changelog 2026-01-16
- ACP changelog 2026-01-30
- ACP changelog 2026-04-17
- ACP governance
- ACP MAINTAINERS
- OpenAI developers: Agentic Commerce Protocol
- OpenAI developers: Key concepts
- OpenAI developers: Get started
- OpenAI developers: Agentic Checkout Spec
- OpenAI developers: Delegated Payment Spec
- OpenAI developers: Agentic commerce in production
- Stripe docs: Agentic commerce
- Stripe docs: Agentic Commerce Protocol
- Stripe docs: Shared payment tokens
- AP2 specification: Agentic Payment Protocol v0.2
- AP2 overview
- Model Context Protocol specification
- OWASP GenAI Security Project: LLM01:2025 Prompt Injection
- OWASP GenAI Security Project: LLM06:2025 Excessive Agency
- RFC 8785: JSON Canonicalization Scheme (JCS)
- RFC 9535: JSONPath, Query Expressions for JSON
- RFC 8615: Well-Known Uniform Resource Identifiers
- RFC 2104: HMAC, Keyed-Hashing for Message Authentication
Last reviewed: 24 October 2026. ACP is in beta and ships dated versions every few months; check the repository changelog and your agent platform's documentation for the current version before you build.