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-idheader spelling — it’s case-sensitive - Confirm the merchant domain you’re calling is configured with Firmly. An unconfigured domain returns
404 DomainNotFoundrather 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-sessionagain 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) andcc.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
queryis searched against catalog text — overly narrow queries return nothing; broaden to test
“Product images aren’t loading”
- Product image
urlfields are merchant-served, not Firmly-served. Image loading depends on the merchant’s CDN - Some merchants serve relative URLs; treat the
urlfield 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 /cartreturns404 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_optionsfrom 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_totalfor line-item discounts, buttotal/cart_discountfor order-level and free-shipping promos - Check
cart.noticesfor codes likePROMO_NOT_APPLICABLEorPROMO_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_idseparately, reading itsshipping_method_optionsand 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-availabilityonly returns delivery dates, time slots, and pickup locations — never shipping methods. Read shipping methods fromcart.shipments[].shipping_method_options(populated inline afterset-shipping-info) and proceed.
Payment and place-order
“Place-order returns 200 but the merchant says no order arrived”
- Check
cart.cart_statusin the response — it should besubmittedfor a successful placement - Check
platform_order_numberin 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-Keytoday — a retry re-runs the operation - Before retrying, re-read the cart and check
cart_status: if it’ssubmitted, 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.origincheck 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”
- Listen for
firmly::OrderPlacedpostMessage events — this is the canonical signal - See Embedded Checkout — postMessage events
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) — thekidin your JWE protected header doesn’t match the key Firmly is currently using. Re-fetch Get Public Key, use thekidit returns (also in thex-firmly-kidresponse 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-256for key wrapping andA256GCMfor content encryption. A raw RSA-OAEP ciphertext (e.g. from Web Cryptosubtle.encrypt) is not a JWE and won’t decrypt — use a maintained JOSE library and set bothalgandencin 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 asCreditCardInvalidNumber(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