Docs
Firmly Agentic Commerce
Set theme to dark (⇧+D)

Chat to Checkout

The Advanced Checkout Guide shows the happy path: clear intent, one matching product, no errors. This page covers the messy reality of conversational checkout — ambiguous intent, multiple matches, mid-flow corrections, errors recovered through dialogue.

​​ The pattern

Every conversational purchase moves through five micro-phases that are different from the API-level phases:

  1. Intent — user expresses what they want, in natural language
  2. Disambiguation — agent narrows it down to one product, variant, quantity
  3. Confirmation — agent surfaces what’s about to happen and gets explicit consent
  4. Execution — agent calls Firmly APIs to place the order
  5. Result — agent surfaces outcome (success, error, partial recovery)

The first three happen on your side. Firmly only sees phase 4.

​​ Worked example — a real chat sequence

The dialogue below is annotated with the API calls your agent makes at each step.


User: Hey, can you order me a black hoodie?
Agent: One sec, let me look.
→ POST /api/v1/discovery/search { query: "black hoodie" }
Response: 4 products from the target merchant, varying prices
Agent: Here's what I found:
1. Luma Classic Hoodie — Black — $45
2. Luma Tech Hoodie — Black — $89
3. Luma Premium Heavy Hoodie — Black — $120
4. Luma Hoodie — Black/Gray — $55
Which one — or want me to read more details?
User: The classic one. Medium.
Agent: One Luma Classic Hoodie, Black, size Medium for $45 — shipping to
your usual address (123 Main St, Brooklyn NY). Total with shipping
and tax will be around $52. Want me to place the order?
[Internally, agent has already done the cart preview:]
→ POST /cart/line-items { add_to_cart_ref: {…size:M, color:black}, quantity: 1 }
→ POST /cart/shipping-info { …saved address } (response carries shipping_method_options per shipment)
→ POST /cart/shipment/methods { shipment_id, shipping_method_id }
Response: total = $52.13, shipping_method = "Standard $5", tax = "$2.13"
User: Yes go ahead
Agent: Placing the order now...
→ GET /cart/consents (fetch any merchant-required consents)
→ PUT /cart/consents (record the buyer's acceptance if the merchant requires it)
→ GET /payment/key (cached)
→ POST /payment/complete-order { encrypted_card, billing_info } (finalizes the cart built above)
Response: { cart_status: "submitted",
cart_id: "03798510-b778-4742-ba25-5812a552507f",
platform_order_number: "29185",
urls: { order_status_page: "https://merchant.example/orders/..." },
custom_properties: { order_token: "..." },
total: { value: 52.13, symbol: "$", currency: "USD" } }
Agent: Done! Your order is in for $52.13. It'll ship in 2–3 business
days — I'll send you the merchant's order page link if you want.

This is the pattern your agent should reach for. Six user-visible turns, eight API calls, full consent, recoverable at every step.

​​ The four hard parts

​​ 1. Disambiguation

When discovery/search returns multiple products, the user has to pick one. There’s no winning shortcut here — your agent’s job is to present options in a way the user can scan and choose.

Pattern: cap the visible list at 3–5 items, sorted by relevance or popularity. Offer to read details on any of them. Avoid auto-picking the top result unless the user explicitly delegates (“just get me whatever you think”).


const search = await callFirmly('/api/v1/discovery/search', {
query: userIntent.query,
page_size: 5
});
if (!search.products?.length) {
return reply(`I couldn't find anything matching "${userIntent.query}".`);
}
if (search.products.length === 1) {
return confirmSingle(search.products[0]);
}
return reply(formatProductList(search.products.slice(0, 5)));

​​ 2. Variant resolution

A single product may have many variants (sizes, colors, materials). The user may have specified some (“medium”, “in black”) and not others. Walk the variant axes:


function resolveVariant(product, userHints) {
// Variants carry `variant_option_list`: a flat array of option strings
// (e.g. ["Black", "Medium"]) with no axis labels. Match each hint value
// against any entry in that array.
const hintValues = Object.values(userHints);
const candidates = product.variants.filter(v =>
hintValues.every(value =>
(v.variant_option_list || []).some(opt =>
opt?.toLowerCase().includes(value.toLowerCase())
)
)
);
if (candidates.length === 1) return candidates[0];
if (candidates.length === 0) return null;
// Multiple still match — ask for the missing axis
return { needsMoreInfo: true, candidates };
}

If needsMoreInfo, ask the user the discriminating question (“Which color — black or charcoal?”).

​​ 3. Confirmation that’s real

A confirmation message that just says “OK, ordering” and proceeds is not consent — it’s an announcement. A confirmation that names the item, total, and destination and waits for an explicit “yes” is consent.

Required content (per Consent & Disclosure):

  • What (product, variant, quantity)
  • From whom (merchant brand)
  • Total cost including tax and shipping
  • Where it’s going

Optional but recommended:

  • Estimated delivery
  • Payment method (“on the Visa ending in 4242”)
  • Return policy link

After the confirmation, record the user’s response with a timestamp. This is your evidence in any dispute.

​​ 4. Recovery from errors

The most common failure modes in conversational checkout:

Error Conversational response
NotEnoughStockError (409, between confirm and complete-order) “Looks like the medium just sold out — they still have a small or a large. Want one of those?”
CreditCardDeclined (422) “The card was declined. Want to try a different one, or use PayPal?” (Do NOT say the issuer’s reason verbatim — it leaks information.)
No saved address (client-side check before set-shipping-info) “What address should this ship to?”
StoreUnavailable (503) “The merchant’s having a hiccup. Try again in a minute?” (Don’t reveal which merchant or technical detail.)
Timeout / network error Retry with the same Idempotency-Key. The user shouldn’t see this.

See Errors for the full catalog and recovery strategies.

​​ Pre-fetching for responsiveness

To keep the conversation snappy, start the cart preview as soon as the user identifies the product — before they confirm — so you can quote a total without stalling. Don’t parallelize these calls: add-line-item and set-shipping-info both mutate the same cart, and concurrent cart mutations race (last one wins). Chain them serially and let the network round-trips overlap the user’s reading time instead:


// As soon as the user identifies the product, build the cart preview serially —
// discard the work if they back out. Both calls mutate the same cart, so they run in order.
const cartResp = await callFirmly('/cart/line-items', { add_to_cart_ref, quantity });
const addressResp = await callFirmly('/cart/shipping-info', savedAddress);
// Now you can quote the user a total in the confirmation message without stalling.

When the user confirms, only complete-order remains — it finalizes the cart the preview built (body: encrypted_card + billing_info), so nothing needs to be re-sent. See Complete Order.

​​ Mid-flow corrections

Users change their minds. Common patterns:

User says Your agent does
“Make it 2 instead of 1” PUT /cart/line-items/:id { quantity: 2 }
“Actually use the black one” DELETE /cart/line-items then POST /cart/line-items with the new variant
“Add a hat too” POST /cart/line-items { add_to_cart_ref: <hat>, quantity: 1 }
“Ship it to my office instead” POST /cart/shipping-info { …office_address } (and re-do shipping method)
“Cancel that” (before complete-order) DELETE /cart/line-items, acknowledge to user

​​ When to bail to a screen

Some moments in conversational checkout are unpleasant in pure text:

  • Long address entry (5 fields × possible validation back-and-forth)
  • Payment method change (especially adding a new card)

For these, consider handing off to hosted checkout at the awkward moment — text the user a Firmly checkout URL, let them finish on a screen, then re-engage when done.

See Hosted Checkout for the handoff pattern. A hybrid agent (conversational by default, hosted for friction moments) is often the best UX.