Skip to main content

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)

Live & Public

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_sessions creates an actual stripe.checkout.Session (mode="subscription") for one of AINative's real plans, the same plan catalog (plan_catalog_service.py's STRIPE_PRICE_IDS) the platform's own pricing page checks out against.
  • POST /agentic_commerce/delegate_payment vaults an already-tokenized Stripe PaymentMethod id into a real, single-use stripe.SetupIntent, constrained by the allowance's max_amount/currency/expires_at recorded as SetupIntent metadata.
Scope of this increment

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
FieldTypeRequiredDescription
itemsarrayYesNon-empty list of items to purchase. Only items[0] is used — ACP checkout here is single-line-item.
items[].idstringYesA 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[].quantityintegerNo (default 1)Must be >= 1.
buyerobjectNoOptional buyer info. buyer.email, if present, is passed to Stripe as customer_email.
fulfillment_addressobjectNoAccepted 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": []
}
FieldTypeDescription
idstringThe real Stripe Checkout Session id.
payment_provider.providerstringAlways "stripe".
payment_provider.supported_payment_methodsarrayAlways ["card"] currently.
statusstring"ready_for_payment" if the Stripe session is open; otherwise "not_ready_for_payment".
currencystringAlways "usd".
line_itemsarrayEchoes back the requested item (id, quantity).
checkout_urlstringThe real Stripe-hosted checkout URL — redirect the buyer (human or agent-driven browser) here to pay.
messagesarrayReserved 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.

No raw card numbers, ever

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
FieldTypeRequiredDescription
payment_method.typestringYesMust be exactly "CARD" (uppercase) — any other value is rejected.
payment_method.idstringYesA Stripe PaymentMethod id (pm_...), already tokenized client-side.
allowance.max_amountintegerYesMaximum amount this delegated payment method may later be charged, in minor currency units (cents). Must be > 0.
allowance.currencystringYesCurrency code, e.g. "usd".
allowance.expires_atstringYesISO-8601 timestamp the allowance expires.
allowance.reasonstringNo (default "one_time")Freeform reason code.
risk_signalsarrayNoFreeform list of risk-signal objects, stored but not currently evaluated server-side.
metadataobject of stringNoFreeform key/value metadata, merged into the real Stripe SetupIntent.metadata.
billing_addressobjectNoAccepted 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"
}
}
FieldTypeDescription
idstringThe vault token id — the real Stripe SetupIntent id prefixed with vt_ per the ACP spec (vt_<setup_intent_id>).
createdintegerUnix timestamp from the real Stripe SetupIntent.created.
metadataobjectEchoes 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 '{}'
DocumentPurpose
UCP Discovery DocumentGET /.well-known/ucp — lists ACP's sibling commerce capabilities (Stripe subscriptions, x402, AP2) in one discovery document
Agent402 APIAgent-side x402 wallets and the Cloudflare wire-format payment flow
POST /agentic_commerce/ap2/verifyAgent Payments Protocol (Google) mandate verification — AINative's other agentic-commerce surface, registered alongside ACP in main.py