Skip to main content
Assisted ordering is the contract an AI phone agent and a customer’s browser share when they are both editing the same cart. Everything here is the ordinary Storefront API — there is no separate assisted surface, no privileged route and no second base URL. What the contract adds is a set of guarantees about concurrency, retries and recovery that a conversational caller needs and a single browser session mostly does not. Two things make assisted ordering different from a normal storefront session:
  • Two writers. The caller speaks while the customer taps. Every cart mutation is therefore concurrency-checked, and the loser is told rather than silently overwritten.
  • An unreliable channel. A call drops, a socket resets, a response is lost after the write landed. So every mutation is idempotent, and every outcome is readable after the fact.
The contract version is 2026-08-27. Event payloads and headers are pinned to it; see Outbound Webhooks for the delivery envelope itself.

Base URL

Cart, ordering-session, cart-item, cart-event, submission and checkout-handoff routes are all mounted under /api/v1/storefront. A base URL that stops at /api/v1 reaches a different route set entirely and will answer 404 in a way that reads like a missing cart.

Opening a session

channel is what marks the order as assisted. It flows to the order record and onward to webhooks and merchant surfaces, so set it at session creation rather than trying to attach it later.
An assisted channel cannot be self-declared. nomly_assisted and phone are claims that someone other than this browser took the order, so they are granted only to a merchant-issued API key scoped to the location. Send it as X-API-Key alongside the request.Without it the session is refused with 403, not silently downgraded to web. That is deliberate: a misconfigured integration recording assisted calls as web traffic would produce a plausible and wrong pilot conversion rate, and nobody would have a reason to look. An unassisted channel needs no key — a browser saying it is web is only describing itself.
The response carries the cart and a cartAccessToken. Send that token as X-Cart-Token on every subsequent request for the cart.

Concurrency: If-Match and ETag

Every cart mutation requires If-Match carrying the revision the caller last saw. Every cart response returns the current revision in the ETag.
A 409 from a revision conflict carries the current revision, so recovery is one read rather than a full resynchronisation.
Never retry a 409 by dropping If-Match. That converts a reported conflict into a silent overwrite, which is the failure the header exists to prevent.

Idempotency

Idempotency-Key is required on every mutation. Within its retention window:
  • The same key with the same payload returns the original result. Safe to retry as often as the transport requires.
  • The same key with a different payload is refused with 409. This is deliberate: the alternative silently discards one of two writes that the caller believed were both applied.
Generate one key per logical operation, not per HTTP attempt. A caller that regenerates the key on retry has an at-least-once operation, not an idempotent one.

When the response is lost

The most common assisted failure is not an error — it is a write that succeeded whose response never arrived. The contract’s answer is that you never have to guess, and you never resubmit to find out:
  1. Re-read the cart. If the revision advanced, the write landed.
  2. For a submission, look it up by whichever identity you still hold:
A voice caller who never saw the reply still knows the cart. A browser that reached payment knows the intent. A client that read the response before losing it knows the attempt. All three resolve to the same canonical answer — submission id, order id, cart revision, lifecycle, settlement state and next action. nextAction never tells you to resubmit. A submission that cannot progress on its own is either waiting on someone — the customer, or a merchant tender — or finished, and saying so is what stops a lost response from becoming a second order. Resubmitting to discover the outcome is the one thing that can turn an ambiguous result into a duplicate order.

The cart event stream

Server-sent events, authenticated by the same X-Cart-Token header as everything else. The token goes in the header, never in a query parameter. Event frames are cart.created, cart.updated, cart.locked, cart.completed and cart.expired, each carrying a monotonic id, the cartId, the resulting revision and occurredAt. Two control frames matter more than the events: A replay gap is always declared. If you resume from an id the server cannot replay from, you get resync_required rather than silence — so a consumer never has to infer that it missed something.

Submitting

quoteFingerprint and the two evidence fields pin the submission to what the caller was actually quoted. If the catalog or a discount moved underneath, the submission is refused and requoted rather than committed at the old price. Read the cart again, tell the caller the new total, and submit against the new fingerprint. OrderSubmissionOutcome reports the lifecycle, including CUSTOMER_ACTION_REQUIRED when payment needs the customer to act. Read the attempt rather than inferring state from the absence of a response.

Webhook correlation

Assisted callers reconcile against outbound events rather than by polling. The correlation model: order.created is what ties a cart to an order id. settlement.updated also carries cartId, so a settlement that arrives before order.created still resolves to its cart. Delivery semantics, all pinned to 2026-08-27:
  • Signed HMAC-SHA256 over "{timestamp}.{body}", base64url, in Crave-Webhook-Signature as v1=…. Two comma-separated signatures may appear during secret rotation; accept either.
  • Timestamps outside a 300 second tolerance must be rejected.
  • Retried on 408, 409, 425, 429 and 5xx only — up to 12 attempts, full-jitter backoff from 30 seconds to a 6 hour cap, honouring Retry-After up to 1 hour.
  • Every other 4xx is terminal. A delivery your endpoint refuses with 400 is never retried.
If you validate payloads with a strict schema, an added field is a breaking change on your side. Crave may add fields within a contract version; a strict validator rejects the unknown key, answers 400, and the delivery is dropped permanently rather than retried. Accept unknown fields, or track the registry and deploy your schema change first.

Verifying an integration

Crave ships a black-box conformance suite that exercises this contract end to end — normal session, simultaneous conflict, identical retry, changed-payload retry, response loss, replay gap, stale evidence, ambiguous lookup, and delivery signing and routing:
The report is redacted before it is written: neither the credential nor the receiver URL appears in it, so it is safe to attach to a review.