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

Common Issues

If something isn’t working the way you expected, scan this page first. Each item is a real symptom-and-fix pair from common integration confusions — not raw API error codes (those are in Errors & Conventions).

​​ Authentication and session

​​ “My App ID returns 401 / unauthorized”

  • Confirm you’re using the App ID for the right environment (sandbox vs production) — they’re different
  • Check the x-firmly-app-id header spelling — it’s case-sensitive
  • Confirm the merchant domain you’re calling is configured with Firmly. An unconfigured domain returns 404 DomainNotFound rather than an authentication error, so check the error code before suspecting the App ID. New merchants are enabled by Firmly

​​ “My session JWT expired faster than I expected”

  • Browser session tokens have a 1-hour active lifetime, renewable while session state persists (~7 days from last activity)
  • After 1 hour, call /api/v1/browser-session again to refresh
  • For backend agents, use server-to-server auth instead — no session expiration concern

​​ “I can’t tell if I’m hitting sandbox or production”

  • General calls go to the sandbox hosts api.firmly.work (general) and cc.firmly.work (payment). Production uses different hosts, provided by Firmly at go-live
  • Which environment you’re in is determined by your App ID (sandbox vs. production) and the merchant domain you call
  • A sandbox App ID against the sandbox hosts with a test merchant (e.g., staging.luma.gift) is non-production end-to-end

​​ “I’m getting a 403 / Forbidden on calls that should work”

Some HTTP clients send a default User-Agent (for example Python-urllib, or an unlabeled agent) that the sandbox’s bot-protection blocks with a 403. curl, browsers, and mainstream SDKs usually pass as-is; setting a browser-style User-Agent header explicitly makes every client work, so it’s the reliable fix if you hit a 403.


curl -X POST https://api.firmly.work/api/v1/browser-session \
-H "x-firmly-app-id: <your-app-id>" \
-H "User-Agent: Mozilla/5.0 (compatible)"

​​ Discovery and catalog

​​ “Discovery returns an empty products array”

  • Confirm the merchant has products loaded into the catalog (Firmly can verify)
  • Confirm at least one merchant is configured in the environment you’re calling — discovery returns an empty list when none are
  • The query is searched against catalog text — overly narrow queries return nothing; broaden to test

​​ “Product images aren’t loading”

  • Product image url fields are merchant-served, not Firmly-served. Image loading depends on the merchant’s CDN
  • Some merchants serve relative URLs; treat the url field as authoritative and don’t reconstruct paths yourself

​​ Cart

​​ “My cart disappears after I expected it to persist”

  • Cart state in Firmly’s edge has a 7-day TTL
  • After 7 days, the cart is garbage-collected and GET /cart returns 404 CartNotFound — add an item to create a new cart
  • For agents that build a cart over days/weeks, store the relevant pieces (line item references, addresses) on your side

​​ “Setting a new shipping address removed my shipping method selection”

  • This is expected behavior. Shipping methods are address-specific. After changing the address, re-read the shipment’s shipping_method_options from the cart and re-select a method.

​​ “Promo code returned 200 but the cart total didn’t change”

  • The code may have been silently rejected (e.g., minimum cart value not met, category exclusion, expired)
  • Always re-read the cart after applying a promo and compare the field the promo actually moves — sub_total for line-item discounts, but total / cart_discount for order-level and free-shipping promos
  • Check cart.notices for codes like PROMO_NOT_APPLICABLE or PROMO_EXPIRED

​​ Shipping and fulfillment

​​ “I see multiple shipment_ids in one cart for the same merchant”

  • Multi-shipment is intentional for merchants whose items ship from different warehouses or have different fulfillment timelines
  • Walk each shipment_id separately, reading its shipping_method_options and recording a method — see Shipping & Fulfillment

​​ “Get-availability returns 501 NotImplemented”

  • Some merchants (V1 adapters) don’t expose this endpoint. It doesn’t block checkout: get-availability only returns delivery dates, time slots, and pickup locations — never shipping methods. Read shipping methods from cart.shipments[].shipping_method_options (populated inline after set-shipping-info) and proceed.

​​ Payment and place-order

​​ “Place-order returns 200 but the merchant says no order arrived”

  • Check cart.cart_status in the response — it should be submitted for a successful placement
  • Check platform_order_number in the cart response for the merchant’s native order number
  • Some merchants delay reflecting orders in their admin UI for a few seconds after placement; re-check shortly

​​ “I’m getting different results when I retry a place-order call”

  • The core REST endpoints don’t deduplicate on Idempotency-Key today — a retry re-runs the operation
  • Before retrying, re-read the cart and check cart_status: if it’s submitted, the original attempt succeeded and retrying would double-place
  • See Idempotency in Errors & Conventions

​​ Dropin and embedded checkout

​​ “The embedded iframe loads but I don’t see any postMessage events”

  • Confirm your event.origin check matches the Firmly dropin origin your representative provided
  • Ensure your iframe has allow="payment; clipboard-read; clipboard-write"
  • Check the browser console for cross-origin warnings

​​ “The user completed checkout in the dropin but my page didn’t update”

​​ Status-code lookup

When you get an HTTP status back and want to know where to start, use this table. The named error value in the response body is the precise signal — the full error catalog maps each name to its cause and recovery.

Status Likely cause Where to look
400 Bad request — malformed body, a missing required parameter, or an invalid query value The endpoint’s own Error Responses, plus Errors & Conventions
401 Unauthorized — missing, expired, or invalid token, or an App ID not recognized in this environment Authentication and the “My App ID returns 401” item above
403 Forbidden — the credential is recognized but not permitted for this merchant or resource (App ID scoped away from the merchant) Security Model → per-merchant isolation · Firmly to confirm scope
404 Not found — DomainNotFound (wrong domain or environment), CartNotFound (cart expired or never created), or ProductNotFound The endpoint’s Error Responses; Discovery and Cart items above
409 Conflict — a business-logic clash such as NotEnoughStockError, ShipmentNotAvailable, or InvalidPromoCode. Re-read the cart before retrying. Also payment_challenge_required, which is not a failure: the shopper must complete card verification, then you resume Errors & Conventions · Resume After Challenge
412 Precondition failed — a required step wasn’t completed or the merchant doesn’t support it: OperationNotSupported, RequiredConsentsNotSigned, CountryNotSupported, CheckoutError Errors & Conventions
422 Unprocessable — a valid body rejected downstream; most card declines (CreditCard*) and UnprocessableEntity. Don’t silently retry a decline Complete Order → Error Responses
429 Rate limited (RateLimited) — too many requests. Honor the Retry-After header and back off Rate Limits
5xx Server or merchant unavailable — 503 StoreUnavailable (merchant adapter temporarily unreachable) or 500 Unexpected. Retry with exponential backoff Errors & Conventions

​​ Payment encryption (JWE) errors

If a payment call fails with a decryption or key error, the card object was almost certainly encrypted in a way Firmly can’t unwrap. The card never left your side in cleartext (encryption happens client-side — see Security Model → Card encryption), so these are all fixable before you retry. Common causes:

  • Wrong or stale key id (kid) — the kid in your JWE protected header doesn’t match the key Firmly is currently using. Re-fetch Get Public Key, use the kid it returns (also in the x-firmly-kid response header), and re-encrypt.
  • Expired / rotated public key — Firmly rotates the payment key periodically, so a cached key can go stale. Re-fetch the key on any decryption/key error and retry once; the refetch-on-error pattern is safe regardless of cache age.
  • Algorithm mismatch — the JWE must use RSA-OAEP-256 for key wrapping and A256GCM for content encryption. A raw RSA-OAEP ciphertext (e.g. from Web Crypto subtle.encrypt) is not a JWE and won’t decrypt — use a maintained JOSE library and set both alg and enc in the protected header.
  • Malformed card number — the plaintext card object’s number (PAN) is incomplete or malformed before encryption. This decrypts fine but is rejected downstream as CreditCardInvalidNumber (422) — re-collect the card details.

​​ Still stuck

If your symptom isn’t here, check:

  • FAQ — conceptual / process questions
  • Errors & Conventions — specific HTTP / UCP error codes
  • Firmly for issues that need account-level inspection