Safeguard
Supply Chain Security•

Agentic Commerce Protocol Explained: Checkout, Delegated Payments, Risks

ACP is the open Stripe and OpenAI standard for agent checkout: five checkout endpoints, single use payment tokens, signed requests, and the risks.

Safeguard Security Team
40 min read

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_id and expires_at.
  • Integrity controls vary in strength. Bearer auth, Idempotency-Key and API-Version are required; request Signature and Timestamp are 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 changedSource
2025-09-29Initial release: Agentic Checkout specification and OpenAPI schema, Delegate Payment specification and OpenAPI schema, examples and RFCschangelog/2025-09-29.md
2025-12-12Breaking changes from early adapter implementations: flat fulfillment_address became nested fulfillment_details; fulfillment_option_id became a selected_fulfillment_options[] arraychangelog/2025-12-12.md
2026-01-16Open authentication provider model for 3D Secure: AuthenticationProvider schema and a required merchant_id on PaymentProviderchangelog/2026-01-16.md
2026-01-30Capability negotiation, the payment handlers framework (breaking), an extensions framework and a discount extensionchangelog/2026-01-30.md
2026-04-17Carts, 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 deprecatedchangelog/2026-04-17.md
24 October 2026The 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.

RoleAlso calledWhat it does in ACPWhat it never has to do
BuyerCustomer, userAsks the agent for something, reviews the cart, approves the purchase in the agent's interfaceVisit the merchant's site
AI agent platformAgent, client, "ChatGPT" in OpenAI's docsCalls the merchant's checkout endpoints, renders the cart, collects buyer, fulfilment and payment details, requests a delegated payment tokenBe merchant of record or process the payment
MerchantSeller, businessHosts the checkout endpoints, validates items and prices, computes tax and shipping, runs risk checks, charges the token, creates the order, sends order webhooksHand over control of its catalogue, pricing or fulfilment
Payment service provider (PSP)Processor, vaultImplements the Delegate Payment API, stores the credential, issues a token bound to an allowance, processes the charge for the merchantTrust 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

CallMethod and pathSuccess statusWhat it carries
CreatePOST /checkout_sessions201 CreatedItems (id, quantity), optional buyer and fulfillment_details, the agent's capabilities
UpdatePOST /checkout_sessions/{checkout_session_id}200 OKChanged items, fulfilment details or selected_fulfillment_options
RetrieveGET /checkout_sessions/{checkout_session_id}200 OK, or 404Nothing; returns the latest authoritative state
CompletePOST /checkout_sessions/{checkout_session_id}/complete200 OKpayment_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
CancelPOST /checkout_sessions/{checkout_session_id}/cancel200 OK, or 405 if already completed or canceledOptional 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:

StatusMeaningTypical next step
not_ready_for_paymentSomething is missing, such as an address needed for tax and shippingAgent updates the session
ready_for_paymentTotals are final enough to payAgent completes the session
authentication_requiredThe issuer wants 3D Secure; the response must include authentication_metadataAgent runs authentication and calls complete with authentication_result
in_progressMerchant is processingAgent retrieves the session
completedOrder created; response includes the order objectMerchant sends order webhooks
canceledSession ended without an orderNone; 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:

FieldRequiredContents
payment_methodYesToday 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
allowanceYesreason (must be one_time), max_amount (integer minor units), currency (lowercase ISO 4217), checkout_session_id, merchant_id, expires_at (RFC 3339)
billing_addressNoAddress for AVS style checks
risk_signalsYesSignals such as type: card_testing with a score and an action of blocked, manual_review or authorized
metadataYesFree 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:

HeaderStatus in the RFCPurpose
Authorization: Bearer <token>RequiredIdentifies the calling platform
Idempotency-KeyRequired on every POSTSafe retries; max 255 characters, UUID v4 recommended
API-VersionRequiredSelects the dated spec snapshot
SignatureRecommendedBase64url detached signature over canonical JSON of the request
TimestampRecommendedRFC 3339 time for freshness checks
Request-IdRecommendedTracing; echoed in responses
Accept-Language, User-AgentRecommendedLocale 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 with code: 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.0 versus 1 do not matter; array order does, and null is different from an absent key, because null means "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_flight and a Retry-After header.
  • 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:

TopicRepository (RFC or 2026-04-17 OpenAPI)OpenAI developer docs
Same idempotency key, different body422 idempotency_conflict (409 is for in-flight)Production checklist says idempotency_conflict with HTTP 409
Complete checkout success status200 OKProduction checklist asks you to confirm HTTP 201 Created for order completion
TLSRFCs say TLS 1.3 must be usedTLS 1.2 or later on port 443 with a valid public certificate
Example API-Version2026-04-17 is the latest stable snapshotCheckout spec page shows 2025-09-12
MCPMCP binding added in 2026-04-17Checkout 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 isREST (and MCP) checkout API plus a Delegate Payment APIA 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 modelMerchant trusts an authenticated agent platform it chose to accept; PSP trusts the platform to request tokens; the platform's interface is where the buyer confirmsEach 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 spendingA delegated vault token bound to an allowance: one time, maximum amount, currency, checkout session, merchant and expiryCheckout and Payment Mandates, in open (constraints) and closed (bound to one transaction) forms, secured as SD-JWTs
Proof of user intentImplicit: the platform says the buyer approved; the merchant sees a token and an authenticated callExplicit: a signed mandate the merchant and payment parties can check, and can use as dispute evidence
Payment railsCards today through PSP vault tokens and network tokens; seller-backed handlers for gift cards, points and store creditInstrument agnostic; new payment instruments are added by defining a type
TransportHTTPS REST; MCP binding since 2026-04-17Carried inside a commerce protocol, whose APIs AP2 leaves out of scope
Who publishes it, statusOpenAI and Stripe as Founding Maintainers, Meta on the TSC; Apache 2.0; betaDocumentation 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.

RiskWhat it looks likeControls in the specControls you add
Prompt injection leads to an unwanted purchaseText in a product page, review or feed steers the agent to buy or change the addressBuyer confirms in the platform UI; requires_buyer_review; allowance capped to the shown totalDeterministic confirmation outside the model; merchant velocity limits and review flags
Merchant spoofingAgent pointed at a look-alike domain or forged discovery documentHTTPS, platform onboarding, discovery on the merchant's own originAllowlist approved merchant origins; tokens bound to merchant_id
Replays and duplicate chargesA captured or retried complete call or webhook is sent againIdempotency replay without side effects; 409 in flight; webhook 300 second windowRequire Signature and Timestamp; one transaction for key and order; single use tokens at the PSP
Over-broad or stored tokensAllowance above the total, long expiry, token kept for laterone_time, max_amount, session, merchant and expiry bindingExact total, expiry in minutes, revoke on cancel, never persist tokens
Manipulated pricesAgent, or something injecting into it, supplies a priceRequests carry item ids and quantities; merchant returns the authoritative cartNever read an amount from the request
Malicious content in the agent UIProduct text or messages carry HTML or hidden instructionsCommonMark without raw HTML, rejected by servers, sanitised by agentsTreat feed text as untrusted input
Too much personal dataBuyer details reach merchant logs before the buyer commitsNo full card number or CVC in logs; redact addressesCollect fields only when needed; mask PII
Webhook forgeryFake order updates show false shipping or refundsRequired HMAC Merchant-Signature, 401 on failureRotate 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:

TestExpected result
POST without Idempotency-Key400 idempotency_key_required
Same key and same body twiceSame status and body, Idempotent-Replayed: true, one session only
Same key, different quantity422 idempotency_conflict
Same key sent while the first call sleeps409 idempotency_in_flight with Retry-After
Body changed after signing401
Create with an added "price": 1 on an itemPrice ignored; totals from catalogue
Complete from not_ready_for_payment4XX, no PSP call
Complete with a token whose limit is below the totalPSP declines; session stays unpaid; payment_declined message
Retrieve another platform's session404

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 Signature as optional because the spec does. Make it required in your platform agreements and reject unsigned or stale calls.
  • Ignoring null versus absent. null means "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 situationWhat to use
A fixed version existsAutonomous remediation opens the upgrade pull request; Gold Open Source confirms the safe version
You need to know today whether you are affectedGold Open Source lookup and "Am I affected?" checker, free and without login
You want vulnerable versions to stop shippingThe Gold scan GitHub Action in CI, plus Safeguard policies and gates
Upstream has no fix yet, or will not ship oneA custom Gold request; fork and patch (early access)
You want to start clean on new projectsPull 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

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.

Never miss an update

Weekly insights on software supply chain security, delivered to your inbox.

Self-healing security runs on Safeguard.

Your first fix PR is minutes away.

No sales call required, even your agent can complete the purchase over MCP.