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

Deep Link / Headless

Headless is the pattern where the destination calls Firmly’s REST API for every step of the purchase. There’s no iframe, no hosted page, no redirect — the destination’s code is the checkout UI, or the checkout has no UI at all.

This is the most flexible pattern and the most code to write on the destination side. Use it when you need conversational, voice, or autonomous flows that can’t fit a visual checkout.

​​ When to use it

  • Conversational and voice surfaces where checkout happens through dialogue
  • Fully autonomous flows that complete purchases without per-step human confirmation
  • Custom surfaces where neither hosted nor embedded fits (AR/VR, in-car systems, IoT, smart speakers)
  • The destination wants total control over UX and accepts the corresponding compliance work (see Consent & Disclosure)

Don’t use this when Hosted or Embedded fits — they reduce both code and PCI scope on the destination side.

​​ The full API sequence

A complete single-product purchase via headless is the sequence below. Each row links to the canonical API reference — payloads are documented there, not duplicated here. For a runnable end-to-end script, see the Advanced Checkout Guide.

# Call Purpose
1 Browser session Bootstrap auth — get a JWT
2 Discovery search Find products (results carry a domain per product)
3 Add line item Add an item (cart created implicitly)
4 Set shipping info Set shipping address — response carries shipping_method_options per shipment
5 Get shipping availability (Optional) Delivery dates / time slots / pickup locations
6 Set shipping method Pick a method from the shipment’s shipping_method_options
7 Get consents → Set consents (If the merchant requires consents) Fetch required consents, surface each to the buyer, capture acceptance. Skipping required consents makes complete-order return RequiredConsentsNotSigned (412)
8 Get payment public key Fetch the public key for card encryption
9 (client-side) JWE-encrypt the card No network call — see Get payment public key for the encryption requirements
10 Complete order (v2) Finalize the cart built in steps 3–8 (body: encrypted_card + billing_info)

For multi-product, repeat step 3 per item. For multi-shipment, repeat step 6 per shipment. For Klarna express, swap steps 8–10 for the Klarna express sequence.

​​ Authentication choice

Headless integrations have two auth patterns:

Pattern Best for Reference
Browser session Client-side or ambient surfaces that act on behalf of a session Browser session
Server-to-server Backend services acting with a service identity Server-to-server

S2S is simpler for backend deployments. See the Authentication overview for picking between them.

​​ Payment options when going headless

Method How
Card JWE-encrypt with the payment public key, pass encrypted_card in complete-order
Klarna BNPL Use the Klarna Express Checkout sequence — start → authorize → complete-order
PayPal Express Use the PayPal Express Checkout sequence — start → authorize → complete-order
Click to Pay When the user has a saved Visa/Mastercard via Click to Pay, the flow short-circuits payment — coordinate with Firmly

​​ Retries and idempotency

Headless flows are exposed to all the failure modes of distributed systems — network blips, retries, mid-flight cancellations. How you dedupe depends on the path:

  • UCP protocol bridge — pass an Idempotency-Key (a UUID generated once per logical attempt) on each mutation. The bridge caches the first response per key for 24 hours and returns it on retry, so the underlying operation runs at most once. Reuse the same key when retrying the same attempt; generate a fresh key for a genuinely new attempt.
  • Core REST — the path this page describes (add-line-item, set-shipping-info, complete-order, etc.) does not deduplicate on the Idempotency-Key header today. A retry re-runs the operation. Guard against double-placement client-side: on a failed or timed-out complete-order, re-read cart_status before retrying — if it’s already submitted, the order landed, so don’t resubmit.

// Core REST complete-order has no server-side dedup — guard by re-reading cart state.
async function placeOrderSafely(order) {
try {
return await callFirmly(placeOrderUrl, {
method: 'POST',
headers: { ...auth },
body: JSON.stringify(order),
});
} catch (e) {
const cart = await callFirmly(getCartUrl, { headers: { ...auth } });
if (cart.cart_status === 'submitted') return cart; // already placed — do not retry
throw e; // safe to retry
}
}

See Idempotency for the canonical guidance on the UCP bridge, and the scope note in Errors & Conventions for why core REST isn’t deduplicated.

​​ Error handling shape


// A real Error subclass so stack traces and `instanceof Error` keep working.
class FirmlyError extends Error {
constructor({ status, code, error, description }) {
super(description || error);
this.name = 'FirmlyError';
this.status = status; // HTTP status
this.code = code; // numeric HTTP status from the envelope
this.error = error; // PascalCase error name, e.g. CreditCardDeclined
this.description = description;
}
}
async function callFirmly(url, options) {
const resp = await fetch(url, options);
const body = await resp.json();
if (!resp.ok) {
// Core REST envelope: { code, error, description }
throw new FirmlyError({ status: resp.status, ...body });
}
return body;
}
try {
await callFirmly(placeOrderUrl, { ... });
} catch (e) {
switch (e.error) {
case 'NotEnoughStockError':
// re-fetch product, ask user to confirm new quantity
break;
case 'CreditCardDeclined':
// ask user for different payment method
break;
case 'StoreUnavailable':
// exponential backoff retry
break;
default:
// surface to user
}
}

See Errors for the full catalog.

​​ Concurrency patterns

Headless flows often have opportunities to parallelize:

  • Catalog lookup + cart fetch (when resuming a conversation) — parallel
  • Shipping availability for multiple shipments — parallel, one call per shipment_id
  • Payment key fetch + JWE encryption — sequential, but key fetch can be cached briefly

Don’t parallelize:

  • Calls that mutate the same cart — they’ll race and the last one wins
  • Duplicate complete-order calls for the same cart — core REST doesn’t dedupe them, so concurrent submits can double-place; serialize and re-read cart_status between attempts

​​ What the destination takes on by going headless

  • The destination’s UI is the checkout UI — address forms, error messages, retry flows
  • The destination’s PCI scope includes any handling of the encrypted card payload (transit, logging)
  • The destination is responsible for pre-purchase consent and disclosure
  • The destination owns retries, idempotency, and error recovery

In exchange the destination gets the most natural conversational flow and the most surface flexibility.