Checkout flow
POST /v1/quotes (one per item) ─▶ qt_ (price locked 15 min)
POST /v1/checkouts { items } ─▶ chk_ pending_payment ──(user pays once on Stripe)──▶ paid ─▶ placing ─▶ confirmed
│ ├─▶ partially_confirmed (failed items refunded)
├─▶ expired (link expired / session expired) └─▶ failed ─▶ refunded
└─▶ cancelled (agent cancelled before payment)
Quote
POST /v1/quotes { variantId, countryCode? } calls Snappy's live availability for the country (US today) and computes:
item + shipping + duties (Snappy, per country) + service fee (admin setting, default 0) = totalAmount
Quotes expire after 15 minutes. A checkout against an expired quote returns 409 quote_expired; create a new quote and tell the user if the price changed.
Checkout (cart)
POST /v1/checkouts takes items: [{ quoteId, quantity? }] (up to 20 lines, quantity 1-10 each, all quotes for the same country) and validates everything that can be validated before money moves: quote freshness and ownership, address completeness and Snappy verification for physical items, country consistency, E.164 phone, SMS consent, user status, maintenance mode. Only then it creates one Stripe Checkout Session with one line per item and returns paymentUrl.
Request fields:
| Field | Notes |
|---|---|
items[] |
Quotes to buy; quantity expands into that many Snappy orders of the same variant |
recipient |
Who receives the package (buyer or someone else). phone is used by carriers. |
shippingAddress |
Full address when any item is physical; { countryCode } only for digital-only carts |
notify.email, notify.sms |
Where to send confirmation and receipt. Email defaults to the buyer; null disables. SMS requires consent (below). |
smsConsent |
true only after the user agreed to the consent text; recorded with timestamp, source and client |
callbackUrl |
Optional HTTPS endpoint for signed status pushes |
idempotencyKey |
Optional; returns the existing open checkout for the same cart |
Payment
The user opens paymentUrl (Stripe Checkout). The link expires with the checkout (30 minutes). Stripe calls /webhooks/stripe; checkout.session.completed with payment_status=paid moves the checkout to paid and enqueues order placement. The paid amount and currency are checked against the quote; a mismatch is refunded automatically and never placed.
Placement
The place-order task calls POST /v3/orders once per item with idempotencyKey = <checkout id>:<item index>, so retries after a crash or a 5xx cannot double-order. Each placed item becomes an ord_ row. When every item is placed the checkout is confirmed; when some items are definitively rejected by Snappy (4xx such as out of stock) the checkout is partially_confirmed, the rejected items' amounts are refunded automatically and the confirmation email says which ones; when nothing could be placed the checkout is failed and refunded in full. Operators see every failure on the dashboard.
Watching the result
| Method | Use |
|---|---|
GET /v1/checkouts/{id} |
Poll; nextStep tells the agent what to say |
GET /v1/checkouts/{id}/events |
SSE; replays history, resumes with Last-Event-ID, ends with event: done |
GET /v1/checkouts/{id}/timeline |
Full event list |
callbackUrl |
Signed POST on checkout.confirmed / checkout.partially_confirmed / checkout.failed with the list of orders |
Callback signature
X-Snappy-Agents-Timestamp: 1733942400
X-Snappy-Agents-Signature: sha256=HMAC_SHA256(secret, timestamp + "." + body)
The secret is provided to the agent platform out of band at onboarding.
After the purchase
GET /v1/orders/{id}returnsstage(confirmed → processing → shipped → out_for_delivery → delivered, orcancelled/refunded), tracking, andnextActions(cancel,request_support,track). Snappy webhooks keep it current;?refresh=trueforces a pull.POST /v1/checkouts/{id}/receiptre-sends the receipt for the whole purchase;POST /v1/orders/{id}/receiptfor one item. SMS to a number without consent on file needssmsConsent: true.POST /v1/orders/{id}/cancelcancels that item at Snappy and refunds its amount whilecancellableis true. Once fulfilment started, the errororder_not_cancellablecarriesnextAction: request_support.
Support requests (returns, damaged, wrong item, not received)
POST /v1/orders/{id}/support-requests { type, summary, details?, preferredResolution?, contactEmail?, contactPhone? } opens a structured request. Snappy support is emailed with the full context, the user receives an acknowledgement, and operators resolve it in the admin portal: partial or full refund (issued on the original payment), replacement, or rejection, each with a note that is emailed to the user. The agent can list requests with GET /v1/orders/{id}/support-requests or GET /v1/support-requests. One open request per order.
SMS consent
Texting a US number requires prior express consent (TCPA/CTIA). The platform records who consented, when, through which agent and to which text; the first message carries "Reply STOP to opt out"; inbound STOP/START/HELP are handled on /webhooks/twilio and opted-out numbers are never texted again (sms_opted_out). Sender identity is a Twilio Messaging Service registered for A2P 10DLC (configured in Integrations).