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

Single Product Purchase

This page is the flow-level walkthrough. For a runnable script that executes this exact flow end-to-end, see the Advanced Checkout Guide — both pages describe the same multi-step /api/v2 sequence; this one is annotated for clarity, the other is meant to run.

​​ The five canonical phases

Every Firmly purchase moves through five phases. The single-product flow is the simplest instance.

Phase What happens What you do
1. Hosting Your surface is reachable (your domain in production, or a Firmly UAT environment for testing) One-time configuration with Firmly
2. Authentication A Firmly session is established POST /api/v1/browser-session (or use S2S directly)
3. Discovery Resolve user intent to a real merchant SKU with live price and stock POST /api/v1/discovery/search or GET /api/v1/domains-products/{domain}/{handle}
4. Checkout Cart, address, shipping, payment, place order A short sequence of calls (see below)
5. Order placement Order lands in the merchant’s OMS with destination attribution POST /payment/complete-order

​​ Sequence diagram

​​ The implementation steps (Phase 4 broken out)

​​ Intent

User tells your agent what they want. Your agent extracts product, options, quantity. No API calls yet.

​​ Discover (Phase 3)


curl -X POST https://api.firmly.work/api/v1/discovery/search \
-H "x-firmly-authorization: $TOKEN" \
-H "Content-Type: application/json" \
-d '{"query": "gift card", "page_size": 5}'

Response contains products[] (across all merchants your App ID can reach). Each product has a domain field identifying its merchant, plus variants[]. Each variant has an add_to_cart_ref — keep it as an opaque handle.

​​ Add to cart (Phase 4 begins)


curl -X POST https://api.firmly.work/api/v2/domains/staging.luma.gift/cart/line-items \
-H "x-firmly-authorization: $TOKEN" \
-H "Content-Type: application/json" \
-d '{"add_to_cart_ref": {...}, "quantity": 1}'

Cart is created implicitly. Response is the full cart object.

​​ Set shipping address (Phase 4)


curl -X POST https://api.firmly.work/api/v2/domains/staging.luma.gift/cart/shipping-info \
-H "x-firmly-authorization: $TOKEN" \
-H "Content-Type: application/json" \
-d '{
"first_name": "Agent", "last_name": "Tester",
"email": "agent@example.com", "phone": "+15551234567",
"address1": "500 Howard St", "city": "San Francisco",
"state_or_province": "CA", "country": "US", "postal_code": "94105"
}'

Response includes updated shipments[] with shipment_ids.

Optimization tip: if you know the user’s postal code before you have their full address (e.g. from a saved profile), set it earlier via Set Postal Code — Firmly stores it session-wide and the merchant often has shipping rates pre-warmed by the time you set the full address.

​​ Pick a shipping method (Phase 4)


# Read available methods from cart.shipments[].shipping_method_options (set in step 4),
# then record the choice
curl -X POST https://api.firmly.work/api/v2/domains/staging.luma.gift/cart/shipment/methods \
-H "x-firmly-authorization: $TOKEN" \
-H "Content-Type: application/json" \
-d '{"shipment_id": "01e232ec-6202-5cb1-25fa-6fe46cc569a9", "shipping_method_id": "method_standard"}'
# Optional: for scheduled-delivery / pickup, fetch delivery dates & time slots first
curl -X POST https://api.firmly.work/api/v2/domains/staging.luma.gift/cart/shipments/get-availability \
-H "x-firmly-authorization: $TOKEN" \
-H "Content-Type: application/json" \
-d '{"shipment_id": "01e232ec-6202-5cb1-25fa-6fe46cc569a9"}'

​​ (If required) Capture merchant consents

Some merchants require the user to agree to terms-of-sale, warranty, or delivery acknowledgments before order placement (this is merchant-required consent, distinct from your agent’s own user-confirmation step). Before complete-order:

  1. GET /api/v2/domains/{domain}/cart/consents — returns an array of required consents with id, html/text to display, required, explicit flags
  2. If the array is non-empty, surface each text to the user, get explicit agreement, then PUT /api/v2/domains/{domain}/cart/consents with {consents: [{id, revoke: false}, ...]}

If you skip this and the merchant required consents, complete-order returns RequiredConsentsNotSigned (412). See Get Consents and Set Consents.

​​ Encrypt payment + complete order (Phase 4 ends)

Get the payment public key, JWE-encrypt the card, then submit. See the Advanced Checkout Guide for the full JWE encryption code.

Note the host change: every call up to this point used https://api.firmly.work; payment calls (get public key and complete-order) go to the dedicated payment host https://cc.firmly.work. Switch hosts for the payment steps to avoid a copy-paste 404.


curl -X POST https://cc.firmly.work/api/v2/payment/domains/staging.luma.gift/complete-order \
-H "x-firmly-authorization: $TOKEN" \
-H "Idempotency-Key: $(uuidgen)" \
-H "Content-Type: application/json" \
-d '{
"encrypted_card": "eyJh...",
"billing_info": { ... }
}'

Why complete-order and not place-order? The cart was built over the previous calls (add line item → shipping info → shipping method → consents), so complete-order finalizes that existing cart — the body carries only encrypted_card and billing_info (shipping was already set in the earlier steps). Reach for place-order instead only on the one-shot path: it creates a cart and places the order in a single call, and therefore requires an items[] array plus shipping_info in the body because there’s no prior cart to finalize. Verify the request body against Complete Order.

Response includes cart_status: "submitted", cart_id, and urls.order_status_page for customer-facing tracking. The merchant’s native order number comes back in the first-class platform_order_number field (some merchants also echo platform-specific identifiers in custom_properties).

​​ Surface the placed order to the user

Firmly’s scope ends at order placement. After complete-order succeeds, surface the order confirmation to the user using the response:

  • urls.thank_you_page — the merchant’s own confirmation URL; route the user there to continue their relationship with the merchant
  • cart_id and the merchant’s native order number from platform_order_number — for the user’s reference

Anything post-order — order status, tracking, returns, refunds, cancellations — lives in the merchant’s native order management system. The user contacts the merchant directly through the merchant’s existing post-order support channels.

​​ What can go wrong at each step

Step Most common failure Recovery
Discover Empty results, StoreUnavailable Check domain spelling, confirm merchant is enabled for agentic
Add to cart ProductNotFound, NotEnoughStockError Re-fetch product or pick another variant
Set shipping InvalidInputBody Validate address fields client-side first
Shipping methods No methods returned Address may not be deliverable — ask user for an alternative
Place order CreditCardDeclined, NotEnoughStockError (race with another buyer) Ask user for different payment method, or re-quote with new stock
Post-order status read 404 (rare, transient) Retry with backoff

See Errors for the full catalog.

​​ What’s in a cart response

Every cart-mutation endpoint returns the full cart object in the response, so your agent doesn’t have to re-fetch. Real shape (anonymized from a QA trace):


{
"cart_id": "00000000-0000-0000-0000-000000000000",
"cart_status": "active",
"schema_version": "2.0",
"display_name": "Example Merchant",
"shop_id": "merchant.example",
"platform_id": "example_commerce",
"shop_properties": { "paypal": { "clientId": "...", "merchantId": "..." } },
"line_items": [
{
"line_item_id": "00000000-0000-0000-0000-000000000000",
"sku": "variant-123",
"base_sku": "product-123",
"quantity": 1,
"msrp": { "currency": "USD", "value": 25.00, "number": 2500, "symbol": "$" },
"price": { "currency": "USD", "value": 25.00, "number": 2500, "symbol": "$" },
"line_price": { "currency": "USD", "value": 25.00, "number": 2500, "symbol": "$" },
"requires_shipping": true,
"image": { "type": "large", "url": "https://cdn.example.com/..." },
"platform_line_item_id": "...",
"description": "Example Product",
"variant_description": "Default"
}
],
"shipments": [],
"addons": { "offers": [], "selections": [] },
"sub_total": { "currency": "USD", "value": 25.00, "number": 2500, "symbol": "$" },
"shipping_total": { "currency": "USD", "value": 0.00, "number": 0, "symbol": "$" },
"tax_total": { "currency": "USD", "value": 0.00, "number": 0, "symbol": "$" },
"fees": [{ "description": "Recycle fee", "currency": "USD", "value": 2.00, "number": 200, "symbol": "$" }],
"fee_total": { "currency": "USD", "value": 2.00, "number": 200, "symbol": "$" },
"addon_total": { "currency": "USD", "value": 0.00, "number": 0, "symbol": "$" },
"total": { "currency": "USD", "value": 27.00, "number": 2700, "symbol": "$" },
"session": {
"requires_login": false,
"is_email_registered": false,
"is_logged_in": false,
"cookies": []
},
"urls": {},
"payment_method_options": [
{ "type": "CreditCard", "wallet": "user" },
{ "type": "PayPal", "wallet": "paypal" }
]
}

​​ Field reference

Field What it is
cart_id UUID for this cart. Stable across cart-mutation calls within the same session.
cart_status Cart state: active while building, submitted after the order is placed.
schema_version Cart envelope schema version ("2.0").
display_name / shop_id / platform_id Merchant identity. platform_id is the literal identifier of the merchant’s underlying commerce platform.
shop_properties Merchant-configured properties — most commonly the PayPal client/merchant IDs needed if you’re using PayPal Express.
line_items[] Each item in the cart with quantity, price, image, and a stable line_item_id you use in subsequent PUT/DELETE calls.
shipments[] Populated only after set-shipping-info. Each shipment has its own shipment_id for shipping-method selection.
addons Merchant add-on offers (warranties, gift wrap, etc.) and the user’s selections.
sub_total / shipping_total / tax_total / addon_total / total Money values. All money objects use the shape {currency, value, number, symbol} — value is a decimal, number is the integer in minor units (e.g. cents).
session Session state including login/registration flags and cookies.
urls Filled in after the order is placed with thank_you_page and order_status_page URLs (empty before placement).
payment_method_options Available payment methods for this cart. Each entry has a type and wallet.

After the order is placed, the same shape comes back with cart_status: "submitted", populated urls, a first-class platform_order_number (the merchant’s native order number), and a custom_properties block with any additional platform-specific identifiers.

​​ Runnable end-to-end

The Advanced Checkout Guide is this exact flow — the multi-step /api/v2 cart — as a complete Node script you can run today. Start there if you want working code, return here for the conceptual walkthrough.