ACP Checkout & Delegated Payment
Protocol: Agentic Commerce Protocol (ACP)
Base URL: https://ainative-browser-builder.up.railway.app (bare apex paths, no /api/v1 prefix — per the ACP spec)
Auth: None required to create a checkout session or delegate a payment method — the same no-auth checkout pattern the platform's existing Stripe checkout flow uses
Source: acp_checkout.py, acp_delegate_payment.py (Refs core#9247)
Both endpoints are real and live in production today:
curl -i -X OPTIONS https://ainative-browser-builder.up.railway.app/checkout_sessions
# HTTP/1.1 204 No Content
# Allow: POST, OPTIONS
What it's for
ACP (Agentic Commerce Protocol) is an emerging standard that lets an AI agent — not a human in a browser — initiate and complete a real purchase: create a checkout session for a line item, then vault a payment method under a spend-limited allowance so the agent can be delegated to pay without ever seeing a raw card number.
AINative implements the two core ACP write operations against real AINative subscription plans and a real Stripe account — not a mock or sandboxed stand-in:
POST /checkout_sessionscreates an actualstripe.checkout.Session(mode="subscription") for one of AINative's real plans, the same plan catalog (plan_catalog_service.py'sSTRIPE_PRICE_IDS) the platform's own pricing page checks out against.POST /agentic_commerce/delegate_paymentvaults an already-tokenized StripePaymentMethodid into a real, single-usestripe.SetupIntent, constrained by the allowance'smax_amount/currency/expires_atrecorded as SetupIntent metadata.
This implements the functional core of ACP — real Stripe-backed checkout sessions and real vaulted payment delegation with an ACP-shaped request/response. It does not yet implement the full ACP request-signing/idempotency-header contract (HMAC Signature header, Idempotency-Key echo, webhook delivery, etc.) — that is a protocol-compliance hardening pass, tracked separately from the baseline functionality below.
POST /checkout_sessions
Creates a real Stripe checkout session for one line item.
Request
POST /checkout_sessions
Content-Type: application/json
| Field | Type | Required | Description |
|---|---|---|---|
items | array | Yes | Non-empty list of items to purchase. Only items[0] is used — ACP checkout here is single-line-item. |
items[].id | string | Yes | A real AINative plan name: Hobbyist, Starter, Pro, Business, Enterprise, or Cody — Your Virtual CTO. Anything else is rejected — there is no silent fallback to a different plan. |
items[].quantity | integer | No (default 1) | Must be >= 1. |
buyer | object | No | Optional buyer info. buyer.email, if present, is passed to Stripe as customer_email. |
fulfillment_address | object | No | Accepted but not currently used (AINative plans are digital subscriptions, no shipping). |
{
"items": [
{ "id": "Pro", "quantity": 1 }
],
"buyer": {
"email": "agent-buyer@example.com"
}
}
Response — 201 Created
{
"id": "cs_live_a1B2c3D4e5F6g7H8i9J0...",
"payment_provider": {
"provider": "stripe",
"supported_payment_methods": ["card"]
},
"status": "ready_for_payment",
"currency": "usd",
"line_items": [
{
"id": "line_item_Pro",
"item": { "id": "Pro", "quantity": 1 }
}
],
"checkout_url": "https://checkout.stripe.com/c/pay/cs_live_...",
"messages": []
}
| Field | Type | Description |
|---|---|---|
id | string | The real Stripe Checkout Session id. |
payment_provider.provider | string | Always "stripe". |
payment_provider.supported_payment_methods | array | Always ["card"] currently. |
status | string | "ready_for_payment" if the Stripe session is open; otherwise "not_ready_for_payment". |
currency | string | Always "usd". |
line_items | array | Echoes back the requested item (id, quantity). |
checkout_url | string | The real Stripe-hosted checkout URL — redirect the buyer (human or agent-driven browser) here to pay. |
messages | array | Reserved for ACP protocol messages; empty on success. |
Error responses
Empty items → 400 Bad Request:
{ "detail": "items must be a non-empty list" }
Unknown plan name (items[0].id not a real plan) → 400 Bad Request:
{
"detail": {
"messages": [
{
"type": "error",
"code": "invalid_item",
"content": "'NotARealPlan' is not a real AINative plan. Valid plans: ['Business', 'Cody — Your Virtual CTO', 'Enterprise', 'Hobbyist', 'Pro', 'Starter']"
}
]
}
}
This is a live, verified response — it was captured with a real curl -X POST against production, not hand-written. The valid-plans list is read from the same STRIPE_PRICE_IDS map the real checkout flow uses, so it cannot drift from what actually exists.
Missing/malformed body → 422 Unprocessable Entity (standard FastAPI/Pydantic validation error), e.g. missing items:
{
"detail": [
{ "type": "missing", "loc": ["body", "items"], "msg": "Field required", "input": {} }
],
"error_code": "VALIDATION_ERROR",
"next_action": "fix_request"
}
Stripe failure (e.g. provider outage) → 502 Bad Gateway:
{ "detail": "Payment provider error: <stripe error message>" }
POST /agentic_commerce/delegate_payment
Vaults an already-tokenized Stripe payment method into a real, constrained SetupIntent, returning a vault token an agent can reference for a later, allowance-limited charge.
payment_method.id must already be a Stripe-tokenized PaymentMethod id (pm_...), created client-side via Stripe.js or Stripe Elements — the same PCI-compliance boundary every other checkout flow on this platform respects. This endpoint never accepts or handles a raw card number server-side.
Request
POST /agentic_commerce/delegate_payment
Content-Type: application/json
| Field | Type | Required | Description |
|---|---|---|---|
payment_method.type | string | Yes | Must be exactly "CARD" (uppercase) — any other value is rejected. |
payment_method.id | string | Yes | A Stripe PaymentMethod id (pm_...), already tokenized client-side. |
allowance.max_amount | integer | Yes | Maximum amount this delegated payment method may later be charged, in minor currency units (cents). Must be > 0. |
allowance.currency | string | Yes | Currency code, e.g. "usd". |
allowance.expires_at | string | Yes | ISO-8601 timestamp the allowance expires. |
allowance.reason | string | No (default "one_time") | Freeform reason code. |
risk_signals | array | No | Freeform list of risk-signal objects, stored but not currently evaluated server-side. |
metadata | object of string | No | Freeform key/value metadata, merged into the real Stripe SetupIntent.metadata. |
billing_address | object | No | Accepted but not currently used. |
{
"payment_method": {
"type": "CARD",
"id": "pm_1AbCdEfGhIjKlMnO"
},
"allowance": {
"max_amount": 4900,
"currency": "usd",
"expires_at": "2026-12-31T23:59:59Z",
"reason": "subscription_renewal"
},
"metadata": {
"agent_id": "agent-42"
}
}
Response — 201 Created
{
"id": "vt_seti_1AbCdEfGhIjKlMnOpQrStUvW",
"created": 1793577600,
"metadata": {
"agent_id": "agent-42"
}
}
| Field | Type | Description |
|---|---|---|
id | string | The vault token id — the real Stripe SetupIntent id prefixed with vt_ per the ACP spec (vt_<setup_intent_id>). |
created | integer | Unix timestamp from the real Stripe SetupIntent.created. |
metadata | object | Echoes back the metadata you sent (not the full, internal Stripe metadata — which also carries acp_max_amount/acp_currency/acp_expires_at/acp_reason for later allowance validation). |
The vault-token concept
A vault token (vt_...) is not a payment itself — it's a durable, amount-and-currency-constrained reference to a tokenized payment method that an agent can hold and present later to authorize a charge, without ever re-collecting card details or exceeding the allowance it was created with. AINative backs this with a real Stripe SetupIntent (usage: "off_session", confirm: false) rather than an opaque internal record — the allowance's max_amount, currency, expires_at, and reason are written into the SetupIntent's own metadata (as acp_max_amount, acp_currency, acp_expires_at, acp_reason), so a later charge attempt against this vault token can be validated against the real constraint that was actually recorded, not a value that was merely echoed back and never persisted.
Error responses
Wrong payment_method.type → 400 Bad Request:
{
"detail": {
"type": "error",
"code": "unsupported_payment_method_type",
"message": "payment_method.type must be 'CARD', got 'BANK_ACCOUNT'."
}
}
Missing required fields → 422 Unprocessable Entity. This is a live, verified response, captured with a real curl -X POST {} against production:
{
"detail": [
{ "type": "missing", "loc": ["body", "payment_method"], "msg": "Field required", "input": {} },
{ "type": "missing", "loc": ["body", "allowance"], "msg": "Field required", "input": {} }
],
"error_code": "VALIDATION_ERROR",
"next_action": "fix_request"
}
Stripe failure → 502 Bad Gateway:
{ "detail": "Payment provider error: <stripe error message>" }
A known gotcha: OPTIONS vs. the real handler
OPTIONS /checkout_sessions is answered by the APISIX gateway's CORS plugin before the request ever reaches the backend — a 204 there proves routing/CORS is configured, not that the handler, Stripe integration, or validation logic is reachable. To exercise the real FastAPI handler (and get a real Pydantic 422, or a real 400/502 from the business logic), send an actual POST, even with a deliberately invalid or empty body.
curl examples
# Real validation error (reaches the actual handler)
curl -s -X POST https://ainative-browser-builder.up.railway.app/checkout_sessions \
-H "Content-Type: application/json" -d '{}'
# Real checkout session for a real plan
curl -s -X POST https://ainative-browser-builder.up.railway.app/checkout_sessions \
-H "Content-Type: application/json" \
-d '{"items":[{"id":"Starter","quantity":1}]}'
# Real delegate_payment validation error
curl -s -X POST https://ainative-browser-builder.up.railway.app/agentic_commerce/delegate_payment \
-H "Content-Type: application/json" -d '{}'
Related
| Document | Purpose |
|---|---|
| UCP Discovery Document | GET /.well-known/ucp — lists ACP's sibling commerce capabilities (Stripe subscriptions, x402, AP2) in one discovery document |
| Agent402 API | Agent-side x402 wallets and the Cloudflare wire-format payment flow |
POST /agentic_commerce/ap2/verify | Agent Payments Protocol (Google) mandate verification — AINative's other agentic-commerce surface, registered alongside ACP in main.py |