Skip to main content

Agentic Commerce Protocol: a checkout, approved once

Agent Runtimes speaks the Agentic Commerce Protocol (OpenAI, Stripe) 2026-04-17 as a buyer, in Python and TypeScript, over REST. In code it is agentic_commerce, never acp: acp is the Agent Client Protocol, an editor protocol Agent Runtimes also speaks.

PythonTypeScript
Packageagent_runtimes.protocols.agentic_commerce@datalayer/agent-runtimes/lib/protocols/agentic-commerce
Modelsgenerated from spec/2026-04-17/json-schema/schema.agentic_checkout.json by datamodel-code-generator 0.34.0generated by json-schema-to-typescript 15.0.4
Pluginagent-runtimes.protocols.agentic-commerceAgenticCommercePlugin

No maintained client exists from the specification's authors (the agentic-commerce-protocol package of PyPI and npm is a third party's), so the models are generated here, pinned to the dated version: scripts/codegen/generate_agentic_commerce.sh [VERSION] regenerates both.

A purchase is a proposal​

As with UCP, nothing is bought in one call: a ready_for_payment session becomes an exact, revisioned proposal the person approves, completed once, the proposal's id its Idempotency-Key:

client = AgenticCommerceClient(
seller="https://outfitters.example",
credentials=credentials_of(platform),
extensions=declared_extensions(platform),
)
session = await client.create_checkout([{"id": "socks", "quantity": 2}], "usd")
session = await client.update_checkout(session["id"], {"buyer": {"email": "ana@example.com"}})
proposal = await client.propose_completion(session["id"])
order = await client.complete(
proposal,
approver=approver_of(platform),
payment=payment_data_of(platform),
store=proposal_store_of(platform),
)

Every request carries Authorization: Bearer (the agent's credential with that seller, from the host), API-Version: 2026-04-17, and on every POST an Idempotency-Key.

One item per unit. The 2026-04-17 schema's Item has no quantity and forbids other fields, while the specification's examples send one; the schema is followed: a quantity of two is the item twice, and the seller counts them.

The plugin and its points​

PointDefaultWhat a host contributes
agentic_commerce.approvernone: nothing is boughtthe same approver it gives ucp.approver
agentic_commerce.paymentDatanonea delegated payment token (spt) from custody
agentic_commerce.credentialsnonethe agent's bearer with each seller
agentic_commerce.proposalStorememorya store shared by its processes
agentic_commerce.extension.discountnonethe discount extension
agentic_commerce.extension.othernoneany other extension, by name

An agent declares in capabilities.extensions only the extensions contributed to these points (declared_extensions); the seller answers with those it activates.

In an application​

An application lists acp in protocols and names each merchant in its commerce block (commerce.merchants[]: protocol: acp, the merchant's domain, a level and, to buy, the most one purchase may cost, limit). For now Datalayer shops at test merchants only, with no real payment (decided 2026-10-13). On Datalayer the runtime holds nothing of it: each merchant is one server, acp_<domain> (acp_outfitters_example), which is the MCP gateway's acp toolset (/mcp?only=acp&acp-merchant=<domain>) reached in the person's name; its tools are named with it (acp_outfitters_example_acp_complete).

The application's Rules decide those tools by what agentspecs says each does (protocol-tools/agentic-commerce.yaml): reading reads, filling a checkout writes, acp_complete buys. A purchase is always asked of the person, by the gateway, with the exact checkout — whatever the rule on buying says, the runtime lets the call reach the gateway rather than ask twice — and made at most once; Leave it to me buys nothing. A connection that reads gives only what reads.

Reading a seller's discovery​

AgenticCommerceClient.discover() reads /.well-known/acp.json through agent_runtimes.protocols.egress.metadata_fetch, the PAP SDK's resolving client: the name is resolved first and refused unless https with every address public, only that address is dialled, no redirect is followed, no proxy is used, and the document is bounded (256 KiB). A local test seller is read over the client you trust by saying so — metadata=client_fetch(http) (test_protocols_metadata_egress.py).

The test seller and the example​

agent_runtimes.protocols.agentic_commerce.merchant.ExampleOutfitters is a local seller (discovery, checkout sessions, the discount extension), holding an agent to Authorization, API-Version and Idempotency-Key (409 on a reused key with another body); every answer is validated by the generated models.

Example​

Runs locally, against the test seller above, in one process with no network and no model (example EX-05):

python examples/protocols/agentic_commerce/checkout.py --yes

It prints the seller's discovery (outfitters.example speaks the Agentic Commerce Protocol 2026-04-17, the discount extension), a session not_ready_for_payment until the buyer's email is given, the WELCOME10 discount applied (ready_for_payment), the exact purchase to approve (Buy 2 × Wool socks at outfitters.example for USD 29.16), the order once bought, and the same proposal refused a second time. Without --yes it asks you to approve. The TypeScript client runs against a seller answering with the shared fixture (agenticCommerce.unit.test.ts) and against the Python seller itself over HTTP: seller_server.py serves Example Outfitters on localhost and acpSeller.integration.test.ts buys there (a session, the buyer's email, the purchase approved and completed once with a test token no summary shows, the same proposal refused); test_protocols_agentic_commerce_two_languages.py runs both.

python examples/protocols/agentic_commerce/seller_server.py --port 8813
ACP_SELLER=http://127.0.0.1:8813 npx vitest run --project=integration \
src/protocols/agentic-commerce/acpSeller.integration.test.ts

Conformance​

Agentic Commerce 2026-04-17. agentic_commerce/fixtures/session.json (a session, the discovery document, the revision digest) is read by test_protocols_agentic_commerce.py and agenticCommerce.unit.test.ts; both validate with models generated from the same JSON Schema. All protocols side by side: Conformance.

What never reaches a model​

The payment token and the bearer go to the seller and nowhere else: session_summary and proposal.summary() are what an agent sees, and the tests check that neither secret is in them.