Agentic Commerce Protocol (ACP)
Stateless, idempotent checkout endpoints that let buyer agents purchase from your shop with a single POST — ChatGPT Instant Checkout-compatible.
The Agentic Commerce Protocol is the spec ChatGPT Instant Checkout and other AI shopping engines speak when they want to actually buy from a shop. Cartwright ships it natively: a JSONL product feed plus stateless checkout sessions that wrap your existing pricing, tax, and inventory rules.
ACP is distinct from A2A. A2A is the negotiation surface (agent ↔ shop bargaining via Anchor-and-Resume). ACP is the purchase surface (agent ↔ shop transacts). A shop can opt into one, both, or neither.
Feature gate
Two switches in brand.config.ts carry the name acp, and they do different jobs:
acp.enabledis the one the checkout routes read (acpDisabledResponseinlib/acp/http.ts). It isfalseon every template. While it is off, every route under/api/acp/v1/returns 404, whateverfeatures.acpsays, so your storefront surface stays clean.features.acpgates no route. It decides whether the shop advertises ACP: the ACP section ofllms.txt, the checkout capability in/.well-known/ucp, thenative_commerceattribute in the Google Merchant feed and the purchase line of the public agent skill.--template agent-marketplacesets it totrueand leavesacp.enabledfalse, so that shop's/.well-known/ucpadvertises a checkout whose routes answer 404 until you setacp.enabledtoo.
Set both to sell through ACP. The feed has a gate of its own: see Authentication.
// brand.config.ts
acp: {
enabled: true, // the switch the checkout routes read
},
features: {
acp: true, // advertise ACP in llms.txt, /.well-known/ucp and the Merchant feed
a2a: false, // independent of A2A
webshop: true, // a shop can serve humans AND ACP buyers
}GET /api/acp/feed
A newline-delimited JSON feed of your live catalogue. ChatGPT Instant Checkout reads this directly — no merchant centre upload, no XML schema, no GTIN guesswork.
curl https://example-shop.app/api/acp/feedResponse (one JSON object per line, shortened):
{"item_id":"sunglass-frame-mahogany","title":"Sunglass Frame Mahogany","url":"https://example-shop.app/en/product/sunglass-frame-mahogany","price":"599.00 DKK","availability":"in_stock","image_url":"https://example-shop.app/img/mahogany.jpg","brand":"Atelier"}
{"item_id":"lens-pair-polarised","title":"Lens Pair Polarised","url":"https://example-shop.app/en/product/lens-pair-polarised","price":"299.00 DKK","availability":"in_stock","image_url":"https://example-shop.app/img/polarised.jpg","brand":"Atelier"}The route builds the feed on each request that reaches it (no cron) and answers with Cache-Control: public, max-age=900, s-maxage=900, so an agent or a CDN may serve a copy up to 15 minutes old. Field shape follows the ACP product-feed spec; Cartwright maps Product, Category, and ProductVariant rows into the canonical fields. There is one line per purchasable item: a product, or each of its variants. Every line also carries a description, the seller_* fields and the is_eligible_search / is_eligible_checkout flags, left out above. A line's item_id is the id a checkout line item sends back.
The route reads no request header, Cache-Control included, so the shop sets the 15 minutes, not the agent. A shop with volatile stock shortens the cache-control line in app/api/acp/feed/route.ts.
POST /api/acp/v1/checkout_sessions
Create a checkout session. The buyer agent posts line items + a fulfillment address; Cartwright responds with totals computed by lib/pricing.ts and a session id the agent uses to complete or update.
curl -X POST https://example-shop.app/api/acp/v1/checkout_sessions \
-H "Content-Type: application/json" \
-d '{
"line_items": [
{ "item_id": "sunglass-frame-mahogany", "quantity": 2 }
],
"buyer": { "email": "orders@buyer.example" },
"fulfillment_address": {
"name": "Buyer Agent",
"address_line1": "Rådhuspladsen 1",
"postal_code": "1550",
"city": "Copenhagen",
"country": "DK"
}
}'A line item names what it buys with item_id: the id the catalog feed lists for that item. id is accepted as an alias; slug (a product slug) and sku (a variant sku) name the item directly. quantity is 1 to 10 per line, with at most 20 lines. Other keys inside a line item, product_id among them, are dropped, so a line item that names its item any other way is refused with 422 acp_line_item_identifier_required. The body itself is strict: a top-level key other than line_items, buyer, fulfillment_address, fulfillment_option and discount_code is a 422 validation_error.
Response (201):
{
"id": "acs_3f0c9b1e7a2d4c5b8e6f1a2b3c4d5e6f",
"status": "ready_for_payment",
"currency": "dkk",
"line_items": [
{
"id": "sunglass-frame-mahogany",
"product_id": "cm9x2k7q40001abcd",
"variant_id": null,
"slug": "sunglass-frame-mahogany",
"sku": null,
"name": "Sunglass Frame Mahogany",
"quantity": 2,
"unit_amount": 59900,
"amount_total": 119800
}
],
"fulfillment_address": {
"name": "Buyer Agent",
"address_line1": "Rådhuspladsen 1",
"postal_code": "1550",
"city": "Copenhagen",
"country": "DK"
},
"fulfillment_option": null,
"buyer": { "email": "orders@buyer.example", "name": null, "phone": null },
"totals": {
"subtotal": 119800,
"total_shipping": 0,
"total_discount": 0,
"amount_total": 119800
},
"messages": []
}Amounts are integers in minor units (øre for DKK), and currency is lowercase ISO-4217. Shipping is the shop's flat rate, waived from its free-shipping threshold (DKK 499 by default, so 0 here); there is no separate tax line. Each line's id repeats the feed id it was ordered by. product_id is the shop's internal database id and appears in responses only. status is ready_for_payment once the fulfillment address is complete, and not_ready_for_payment, with an entry in messages, until then. A session expires 30 minutes after it is created, and complete also needs a buyer email.
Update, cancel, complete
Sessions are stored in the database. Only complete replays: send the same idempotency_key in its body and the response of the call that completed comes back. create reads no idempotency key, so a retried create opens a second session.
GET /api/acp/v1/checkout_sessions/{id}— read current statePOST /api/acp/v1/checkout_sessions/{id}— changebuyer,fulfillment_address(a full replacement),fulfillment_optionordiscount_code. Line items are fixed when the session is created; to change them, create a new sessionPOST /api/acp/v1/checkout_sessions/{id}/cancel— abandonPOST /api/acp/v1/checkout_sessions/{id}/complete— finalise the session. Gated behindACP_PAYMENT_COMPLETION=1(default off). The full flow is now wired: it charges via a Stripe Shared Payment Token (off-session PaymentIntent), builds the order from the session line items (not the cart), replays on the idempotency key, and refunds if order creation fails. It stays inert (501) until external Stripe SPT access + ChatGPT merchant onboarding are connected — so it can never accidentally move money before you go live
curl -X POST https://example-shop.app/api/acp/v1/checkout_sessions/acs_3f0c9b1e7a2d4c5b8e6f1a2b3c4d5e6f/complete \
-H "Content-Type: application/json" \
-d '{ "shared_payment_token": "spt_…", "idempotency_key": "7f3e9a1b-…" }'When live, complete returns the completed session plus an order reference; the off-session charge resolves to succeeded or fails cleanly (a card requiring 3DS/SCA can't be resolved off-session, so it's treated as a failed charge with an automatic refund).
Completion is wired but inert by default. /complete ships behind ACP_PAYMENT_COMPLETION=1 (default off). The full path is now code-complete and unit-tested — Stripe Shared Payment Token charge (off-session PaymentIntent), order creation from the session line items, idempotency replay, and refund-on-failure. It only goes live once you connect Stripe SPT access + ChatGPT merchant onboarding; until then the endpoint responds 501 (it can never charge a card while the external integration is pending). create / update / retrieve / cancel are fully live.
UCP: native_commerce capability
Google's Unified Commerce Protocol (UCP) lets agents discover which catalogue products are native-buyable. When features.acp is on, the Google Merchant feed (/feed/google.xml) emits <g:native_commerce>enabled</g:native_commerce> per item, and /.well-known/ucp advertises a native_commerce capability — gated on features.acp plus merchantFeed. Neither reads acp.enabled, so set both switches (see Feature gate), or the shop advertises a checkout its routes refuse.
The exact native_commerce attribute is an emerging March-2026 Google spec — verify the attribute string against Google's current Merchant Center docs before go-live. The structure and gating are correct.
UCP: identity-linking (OAuth 2.0)
UCP also defines dev.ucp.common.identity_linking — how an agentic platform gets delegated, scoped permission to act on a shopper's behalf across merchants. Cartwright ships it as a full OAuth 2.0 Authorization Code + PKCE server behind the ucpIdentityLinking flag (default-off): RFC 8414/9728 discovery metadata, RFC 7591 dynamic registration, /oauth/{authorize,token,revoke} with a consent screen, and a sample protected resource at /api/ucp/orders. See UCP identity-linking for the endpoints, scopes, and setup.
Authentication
The ACP routes authenticate no caller. No route under /api/acp/ reads an Authorization header or an API key, which is why the examples above send none, and lib/scopes.ts has no ACP scope to limit a key to. The Guardian's agentic policy names products.read and checkout.create, but no ACP route calls the Guardian. What a session route checks is acp.enabled, the request schemas above and, for complete, ACP_PAYMENT_COMPLETION plus a Stripe Shared Payment Token. Anyone who can reach the shop can open a session.
The feed is public on every shop with ecommerceEnabled, whatever acp.enabled and features.acp say. app/api/acp/feed/route.ts has no auth check and no helper to switch one on. If your catalogue contains private inventory or wholesale pricing, add a check to that route yourself.
Relationship to A2A
ACP is the purchase surface. A2A is the negotiation surface. The two sit side by side:
| Endpoint | Purpose | Spec |
|---|---|---|
GET /api/acp/feed | Product discovery (JSONL) | ACP product feed |
POST /api/acp/v1/checkout_sessions | Stateless purchase | ACP v1 |
GET /api/agent-card | Shop discovery (signed JSON) | Cartwright A2A v1 |
POST /api/negotiate | Bargaining | Cartwright A2A v1 |
POST /api/escrow/verify | Proof submission (Verify-then-Pay) | Cartwright A2A v1 |
A shop with ACP on (acp.enabled and features.acp both true, see Feature gate) and features.a2a off ships a "just-buy-it" surface (fixed prices, no haggling). A shop that also turns features.a2a on lets agents discover and negotiate, then buy through ACP checkout at the catalog price. The negotiated price does not carry into a session, and no engine route opens or funds an escrow row, so there is no A2A escrow to settle through yet; see the AI sale walkthrough.
Industry templates
Five templates that scaffold the right defaults for your shop archetype — from corporate site to pure A2A marketplace.
UCP identity-linking (OAuth 2.0)
A built-in OAuth 2.0 Authorization Code + PKCE server implementing UCP dev.ucp.common.identity_linking — so an agentic platform can act on a shopper's behalf, with consent, scoped tokens, and revocation.