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
ACP is gated behind brand.features.acp (default false on most templates, true on agent-marketplace). When off, every /api/acp/* route returns 404 — your storefront surface stays clean.
// brand.config.ts
features: {
acp: true, // turn the ACP surface on
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):
{"id":"prod_001","title":"Sunglass Frame Mahogany","price":{"amount":"599.00","currency":"DKK"},"availability":"in_stock","brand":"Atelier","image_link":"https://example-shop.app/img/prod_001.jpg","link":"https://example-shop.app/products/sunglass-frame-mahogany"}
{"id":"prod_002","title":"Lens Pair Polarised","price":{"amount":"299.00","currency":"DKK"},"availability":"in_stock","brand":"Atelier","image_link":"https://example-shop.app/img/prod_002.jpg","link":"https://example-shop.app/products/lens-pair-polarised"}The feed is regenerated on demand — no cron, no stale snapshots. Field shape follows the ACP product-feed spec; Cartwright maps Product, Category, and ProductVariant rows into the canonical fields.
Buyer agents typically pull the feed once per session, then refresh on cache-miss. Use Cache-Control: max-age=300 if your inventory is volatile — the route honours it.
POST /api/acp/v1/checkout_sessions
Create a checkout session. The buyer agent posts line items + shipping 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 "Authorization: Bearer <api-key>" \
-H "Idempotency-Key: 7f3e9a1b-4d8c-4e5a-9b2f-1a3c5d7e9f0a" \
-H "Content-Type: application/json" \
-d '{
"line_items": [
{ "product_id": "prod_001", "quantity": 2 }
],
"shipping_address": {
"name": "Buyer Agent",
"line1": "Rådhuspladsen 1",
"city": "Copenhagen",
"postal_code": "1550",
"country": "DK"
}
}'Response:
{
"id": "cs_2NjAaP1eZvKYlo2C0",
"object": "checkout_session",
"status": "open",
"currency": "DKK",
"line_items": [
{ "product_id": "prod_001", "quantity": 2, "amount_subtotal_minor": 119800 }
],
"totals": {
"subtotal_minor": 119800,
"tax_minor": 29950,
"shipping_minor": 4900,
"total_minor": 154650
},
"expires_at": "2026-05-25T09:00:00Z"
}Update, cancel, complete
Sessions are stateful (DB-backed) but the protocol is request-idempotent. Send the same Idempotency-Key and the same response replays.
GET /api/acp/v1/checkout_sessions/{id}— read current statePOST /api/acp/v1/checkout_sessions/{id}/update— change line items or shippingPOST /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/cs_2NjAaP1eZvKYlo2C0/complete \
-H "Authorization: Bearer <api-key>" \
-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 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 acp plus merchantFeed. The gating is deliberate: the shop never advertises a capability it can't honour.
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
All POST endpoints require a Bearer token. API keys are managed at /admin/api-keys and can be scoped to acp:checkout only — restricting what the key can do is enforced before the handler runs.
The feed endpoint is public by default (so crawlers can index it). If your catalogue contains private inventory or wholesale pricing, override the auth check in app/api/acp/feed/route.ts — there's a requireFeedAuth() helper commented out for that case.
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 | Verify-then-Pay release | Cartwright A2A v1 |
A shop with only features.acp = true ships a "just-buy-it" surface (fixed prices, no haggling). A shop with both acp and a2a lets agents discover, negotiate, then settle via either ACP checkout or A2A escrow.
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.