Commerce checkout spine
The commerce API turns a product message into a paid order. This page explains the architecture behind it: one persistent cart that every channel folds into, a checkout-resolution chain that picks native in-thread payment where it exists and degrades to a hosted pay-by-link where it does not, server-side reconciliation that matches the captured payment against a cart total you cannot forge, and the agentic layer that lets an AI agent transact under signed, spend-capped consent. For the end-to-end walkthrough, see Conversational Commerce Checkout.1. One checkout spine, many surfaces
Every commerce surface is one of four things, and all four converge on the same spine:- A product surface — a WhatsApp catalog or interactive product message, an RCS catalog carousel, an Apple Messages for Business (AMB) list picker, or an Instagram DM product share about the same Meta Commerce catalog. These surfaces contribute items.
- A persistent cart — one serializable cart snapshot (
OmniCart) that contributions from any of those surfaces fold into, keyed by catalog SKU. The buyer who adds two items on WhatsApp and taps one RCS carousel card checks out once, on one cart. - A checkout resolution — picking how the buyer pays: native in-thread pay where the channel supports it (WhatsApp Pay, Apple Pay in AMB), or a hosted pay-by-link otherwise.
- A payment reconciliation — matching the payment captured on whatever rail won back to the cart total, computed server-side.
2. The cart persistence model: one cart, every channel
The cart is one object per customer, stable across channels. Create it once:browsing state. The cart is a serializable snapshot you round-trip, not a row Orbit stores: every handler recomputes the next valid snapshot and returns it; your application persists it. That one design decision is what makes a cart channel-independent — a snapshot created for a WhatsApp order can be merged from RCS, transitioned from AMB, and paid from anywhere, because the cart travels in the request body.
Fold a channel contribution in when an order arrives:
productRetailerId) across every channel: the same SKU a buyer touches on WhatsApp and later on RCS accumulates into one line, with sourceChannels recording where it came from. The 8-state checkout funnel (browsing → cart_active → checkout_initiated → awaiting_payment → paid → fulfilled, plus the recoverable abandoned and the terminal cancelled) is one state machine regardless of which surface drives it — the first item auto-advances browsing → cart_active, and /cart/transition rejects any illegal jump with VALIDATION_ERROR.
Abandonment is part of the model. A cart holding items in a recoverable, pre-payment state (cart_active, checkout_initiated, awaiting_payment) that goes quiet past your idle threshold (default 30 minutes) is flagged by /cart/abandonment-check. That Boolean is the trigger an abandoned-cart-recovery flow drives a re-engagement nudge off; the abandoned state itself recovers via RESUME back to cart_active, never lost.
3. The checkout-resolution chain: native pay first, hosted fallback
When the cart is ready to checkout, resolve how the buyer pays:- Stay on the buyer’s current surface if it can take payment — the buyer is already there; friction is lowest, so
lastChannelleads. - Native pay where it exists, in preference order — WhatsApp Pay where Meta has native payments (India UPI, Brazil Pix, Singapore PayNow/cards), then AMB Apple Pay, then RCS and Instagram — the latter two have no in-thread wallet by design, so they degrade.
- Hosted pay-by-link fallback — when no capable native rail exists, mint a channel-agnostic hosted checkout link and render it for SMS, email, Telegram, Viber, or a voice-IVR readout with a QR code; it also opens cleanly in WhatsApp’s in-app browser.
order + config in, a resolved checkout out, anchored to one PaymentRequest snapshot the shared /payment-request/reconcile route later closes out, so the buyer never leaves the conversation:
Two adjacent rails plug into the same
PaymentRequest anchor rather than bypassing it:
- Direct carrier billing (DCB) — the operator-billed checkout rail, for digital goods billed to the phone bill. Onboard the merchant with
/commerce/dcb/merchant, validate a charge against its tenant-level operator coverage with/commerce/dcb/charge, shape the carrier-billing operator request with/commerce/dcb/operator-request, then drive the lifecycle (capture / refund / chargeback / fail) and reconcile settlement with/commerce/dcb/reconcile. The operator dip rides the network-APIs carrier-billing route; the API surfaces orchestrate the lifecycle. - Payment request — the channel-agnostic hosted pay-by-link the fallback chain always lands on. Mint with
/commerce/payment-request, render for a channel with/commerce/payment-request/render, drive the lifecycle (pending → viewed → paid, orexpired/cancelled) with/commerce/payment-request/transition.
4. Server-side reconciliation: match the captured payment to a re-derived total
Captured payment comes back from whatever rail won — a PSP webhook, the Meta order, the Apple Pay token, the carrier-billing settlement. Match it against the cart:channel is an annotation.
This works because of the spine’s most load-bearing invariant: every handler re-derives totals server-side before reading them. The cart snapshot carries subtotal, itemCount, and line lineTotal fields over the wire, but those are accepted and never trusted — every handler recomputes each line total from unitPrice × quantity and the subtotal from the lines before use, so a forged subtotal smuggled in the request body cannot pass reconcilePayment. The response is the verdict: matched, delta, and a reason — ok, or the specific failure (empty_cart, mixed_currency, currency_mismatch, or amount_mismatch) with the exact signed delta.
The idempotent spine: a PaymentRequest reconciles by exact match and flips only expired/cancelled decline events past pending; the cart funnel rejects illegal transitions with VALIDATION_ERROR rather than silently corrupting state; and (as with every Orbit creating POST) retrying with the same Idempotency-Key + same body returns the original response — see Idempotency and safe retries. DCB adds a settlement level: /dcb/reconcile batches captured charges against operator settlement records and reports matched / mismatched / missing / unexpected lines with gross, refund, and net-delta totals — all derived server-side.
5. The agentic layer: mandates and storefronts on the same spine
The commerce rails above are for a human in a conversation. The agentic layer reuses the same spine for an AI shopping agent — yours, or a third-party one (ChatGPT Instant Checkout, Google AP2-style agents, Perplexity).Payment mandates — scoped, revocable, spend-capped consent
An AI agent never holds an unbounded wallet. A principal issues a mandate: an AP2-style authorization that caps the maximum per transaction, the total cumulative spend, and optionally the allowed merchants or categories, carrying a SHA-256 consent digest re-verified before every spend. Issue with/commerce/agent-mandate, dry-run a charge with /commerce/agent-mandate/authorize, commit with /commerce/agent-mandate/charge, and revoke at /commerce/agent-mandate/revoke. An authorized mandate charge is the agent’s green light to invoke one of the checkout rails above — the mandate decides may the agent spend this, then the rail actually transacts. A completed charge can also carry an AP2 CartMandate verifiable credential: a standards-conformant artifact a genuine AP2 participant verifies with off-the-shelf JOSE/VC tooling.
Storefront sessions — where the agent provides the catalog
The ACP / UCP storefront (/commerce/agentic/*, and the pilot-gated /commerce/ucp/*) is the inbound edge a foreign shopping agent drives: it publishes a discovery manifest and product feed, then opens a checkout session priced server-side against the merchant catalog — the agent’s totals are never trusted, the same forge-proof rule as the human cart. Completing the session requires a mandate: the session is re-priced from the catalog, then that re-priced total is charged against the mandate (consent digest and caps re-verified), and a charge outside it is rejected with VALIDATION_ERROR. So the agentic lane is not a separate commerce stack — it is the same spine, driven by a machine, gated by signed consent.
6. The security boundary: tenant-owned credentials, never platform payment data
The spine composes payment rails you own — it never moves money itself and never touches payment data. Tenant-owned controls:- Your PSP credentials, your hosted base. The pay-by-link is hosted under your PSP (
hostedBaseUrl— e.g. your Stripe checkout origin); the mandate’sagentIdandprincipalIdare your identifiers; the DCBmerchantIdis your operator billing relationship. Orbit orchestrates; positions, captures, and settlement happen at providers you contract with. - Payment data never lands on Orbit. The voice PCI capture surface captures masked DTMF digits and returns an opaque PSP token;
/commerce/secure-payment/chargebinds that token to a pricedPaymentRequestserver-side from the call’s persisted masked summary — the full PAN/CVV never cross. The buyer’s MSISDN carries only in the DCB operator request, stored masked on the charge snapshot. - Consent is cryptographic, not asserted. The mandate’s signed consent digest is re-verified before every charge; tamper-evidence for auditors is
/commerce/agent-mandate/verifyrecomputing the digest over the mandate’s scope. An agent spend past signed consent is aVALIDATION_ERROR, not a policy. - No platform-side hard gates. Compliance on commerce flows is tenant-configurable — the spend caps, DCB operator coverage map, and mandate scopes above are yours to set; Orbit does not own a global regulatory gate here.
Putting it together
- Create one cart per customer (
/commerce/cart) — the snapshot you round-trip in every later call. - Fold every channel’s orders into it (
/cart/merge), keyed by SKU; fold late additions the same way. - Resolve the checkout rail (
/cart/checkout-channel) — native pay on the buyer’s surface first, hosted pay-by-link fallback always available. - Capture on that rail, then reconcile server-side (
/cart/reconcile-payment) — the verdict holds because totals are re-derived, not trusted. - Watch abandonment (
/cart/abandonment-check) and re-engage a stalled cart withRESUME. - For an AI shopper, wrap the same rails in a mandate (
/agent-mandate) and let a storefront session (/agentic/checkout/complete) close under signed, capped consent.