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

Errors & Conventions

This is the canonical reference for how Firmly returns errors and how your code should respond to them.

​​ Error response shape

Every error response uses the same envelope (the REST envelope — used by all endpoints except the UCP protocol bridge):


{
"code": 409,
"error": "NotEnoughStockError",
"description": "The amount of the required item is not available in stock."
}
Field Meaning
code HTTP status code, repeated in the body
error Stable error name in PascalCase (e.g. CartNotFound, NotEnoughStockError)
description Human-readable message. On validation errors, often includes the failing field path (e.g. "At path: items.0.add_to_cart_ref — Expected an object, but received: undefined")

The description field is safe to log verbatim. Don’t show it raw to buyers — it can leak technical detail. Map to your own user-facing messaging.

​​ Catalog by category

​​ Authentication

Name HTTP Cause Recovery
MissingAuthHeader 400 x-firmly-authorization header missing or empty Add the header
InvalidToken 400 Token is not a valid JWT structure (malformed) Re-bootstrap via POST /api/v1/browser-session
InvalidJWTToken 401 JWT signature couldn’t be verified Bootstrap a new session, or pass the previous token to Browser Session to renew
PartnerNotFound 404 The appid claim on the JWT doesn’t map to a known partner Usually means the token was issued against a different environment — re-bootstrap with the correct App ID

​​ Routing & resource lookup

Name HTTP Cause Recovery
RouteNotFound 404 The path doesn’t match any handler Check the URL — common cause is a wrong /api/v1/ vs /api/v2/ prefix
DomainNotFound 404 The merchant {domain} parameter isn’t a recognized merchant Confirm the domain string from Firmly
CartNotFound 404 No cart exists yet for this device_id + domain Create a cart by calling /cart/line-items — cart is implicit
ProductNotFound 404 The product ID / handle doesn’t exist for this merchant Re-search via /api/v1/discovery/search
ShipmentNotFound 404 Specified shipment_id doesn’t exist on the cart Refresh the cart via GET /cart to get current shipment IDs
LineItemNotFound 404 Line item ID doesn’t exist Refresh cart to get current line_item_ids
AddonNotFound 404 Addon offer ID doesn’t exist or isn’t applicable Re-read available offers from cart.addons.offers[]

​​ Validation

Name HTTP Cause Recovery
InvalidInputQuery 400 A query-string parameter failed validation — a bad value (?flush_cart=1) or an unrecognized key (?utm_source=…), since query schemas are strict. description names the failing parameter Fix or drop the query parameter
InvalidInputBody 400 Request body failed schema validation. description includes the failing field path (incl. wrong types, missing required fields, out-of-range quantity, etc.) Fix the field and retry
InvalidInputBody 422 Same name, different layer: the body is well-formed but the merchant rejected it — most often an add-on that does not exist or is not offered on this cart. description explains which value was rejected Re-read the available add-ons and retry with a valid one
ErrorInvalidEmail 400 Email format is invalid Validate format before submission
InvalidShippingInfo 400 Shipping address validation failed (missing fields, invalid postal code, etc.) Read description for which field; fix and retry
InvalidPromoCode 409 Promo code is invalid, expired, or non-stackable Tell the user; suggest removing or replacing
CountryNotSupported 412 Merchant doesn’t ship to this country Use a supported country, or surface “not deliverable” to user
OperationNotSupported 412 The operation isn’t enabled for this merchant (e.g. setting a fulfillment type the merchant doesn’t offer). Surface to the user, or fall back to the merchant’s default.
NotImplemented 501 The merchant’s adapter doesn’t implement this operation — e.g. cart/shipments/get-availability on a V1 adapter. Skip the optional call. Shipping methods still come from cart.shipments[].shipping_method_options; get-availability only adds delivery dates / time slots / pickup. See Shipping & Fulfillment.
PreconditionError 412 Required prior step wasn’t completed (e.g. complete-order before shipping is set) Walk through the missing step

​​ Inventory & merchant state

Name HTTP Cause Recovery
NotEnoughStockError 409 Quantity exceeds current stock Lower quantity or pick a different variant. Re-fetch the product to see current availability.
ShipmentNotAvailable 409 Selected shipping method is no longer available for this address (e.g. inventory moved, region restriction) Re-fetch the cart, surface the new options
ProductNotSupported 412 Product can’t be added to cart (type restriction, region, business rule) Surface to user; check product eligibility
StoreUnavailable 503 Merchant adapter is temporarily unreachable Retry with exponential backoff (1s → 30s cap). If persistent, surface to user.

​​ Server

Name HTTP Cause Recovery
UnprocessableEntity 422 Request body was valid but couldn’t be processed (downstream merchant call rejected the payload, or a pre-condition like postal code wasn’t met) Read description; most often resolved by re-fetching the cart
Unexpected 500 Generic server error Retry once with backoff. If persistent, report to Firmly with the request ID and timestamp

​​ Place-order specific (on cc.firmly.work)

Name HTTP Cause Recovery
MissingRequiredParameters 400 Cart is missing data needed to place the order (e.g. no payment_handle because Set Shipping Info wasn’t called) Walk back through the checkout steps; ensure shipping is set
CheckoutError 412 Merchant-side checkout failure (promo re-evaluation rejected, cart total mismatch, etc.) Re-fetch the cart, surface the discrepancy to the user
RequiredConsentsNotSigned 412 A required && explicit consent from Get Consents wasn’t signed before place-order Show the consent, capture acceptance via Set Consents, retry
CreditCardDeclined 422 Card declined by the PSP. Also returned when a PayPal express payment is declined, despite the name — see Complete Order — PayPal. Don’t silently retry; ask the user for a different payment method. Log issuer reason (don’t show to user).
CreditCardInsufficientFunds 422 Issuer reported insufficient funds Surface to user; offer a different card
CreditCardInvalidNumber 422 Card number failed PSP validation Re-collect card details
CreditCardInvalidSecurityCode 422 CVV / verification_value failed PSP validation Re-collect CVV
CreditCardInvalidExpiry 422 Expiry combination is invalid or in the past (also: CreditCardInvalidExpiryMonth / …Year / …Date for finer-grained signals) Re-collect expiry
CreditCardInvalidPostalCode 422 AVS rejected the postal code in billing_info against the card on file Re-collect billing postal code
RateLimited 429 Payment host rate-limited the device for excessive place-order attempts Back off and retry

​​ Additional errors by name

Every error the endpoint pages document, beyond the grouped tables above. The last column links to the page carrying the full cause-and-recovery detail for that call.

Name HTTP Cause Documented on
AllProductsNotFound 404 No catalog snapshot is available for this merchant — the merchant has no published all-products feed. get-all-products
BadRequest 400 All of these conditions share the same error (BadRequest) and code (400), so the description is the only way to tell them apart. set-consents
CannotCreateCart 409 The merchant system rejected creation of a new cart. add-line-item
CannotTransferSession 409 The supplied handle could not be turned into a usable merchant session. session-transfer
ClearPromoCodeNotAllowed 409 The merchant’s configuration disables API-driven promo-code clearing for this store. clear-promo-codes
CreditCardInvalidExpiryDate 422 The expiry could not be parsed — the supplied month/year are malformed or do not form a valid date (as opposed to CreditCardInvalidExpiry, which is a well-formed but past expiry). complete-order-v2
CreditCardInvalidExpiryMonth 422 Expiry month was rejected (not 1–12). complete-order-v2
CreditCardInvalidExpiryYear 422 Expiry year was rejected. complete-order-v2
PaymentChallengeExpired 422 The interactive verification window (3-D Secure or equivalent) closed before the shopper completed it, or the PSP invalidated the challenge. Terminal for that attempt. Resume After Challenge
PaymentChallengeRequired 422 The merchant’s PSP demanded interactive verification, but the calling App ID has not opted in to the payment-challenge contract — so the order fails closed rather than returning a challenge the client could not present. Also returned on a post-authentication decline, or when no pending challenge exists for the session. complete-order-v2, Resume After Challenge
ErrorInvalidAppId 400 App-id-auth path: the supplied App ID is not recognized, or exceeds 512 characters. search
ErrorMissingAppId 400 App-id-auth path: x-firmly-app-id header is missing or empty. search
ErrorStartExpressCheckout 400 Firmly could not open the express-checkout session — the provider rejected the request, or the merchant adapter failed to translate it. Klarna — Start, PayPal — Start
ErrorSubmitExpressCheckout 400 Authorization with the merchant failed — the provider rejected the token, or the merchant adapter could not submit the checkout. Klarna — Authorize, PayPal — Authorize
GatewayNotFound 404 The express-checkout provider (Google Pay, Klarna or PayPal) is not enabled for this merchant. Google Pay, Klarna, PayPal
InvalidAPIToken 400 Server-to-server path: the API token is missing, invalid, or has been revoked. search
InvalidDeliveryDate 400 The supplied selected_date and/or selected_time_slot is not available for this shipment. set-shipping-method
InvalidFulfillmentType 400 The supplied fulfillment_type is not in the target shipment’s fulfillment_type_options. set-fulfillment-type
InvalidRequest 400 The url query parameter is missing or empty. get-product-from-url
InvalidShipmentType 400 The shipment’s fulfillment_type is not one this endpoint can query, or the type is not supported by the merchant. get-availability
InvalidShippingMethod 400 The supplied shipping_method_id is not in the shipment’s shipping_method_options. set-shipping-method
InvalidState 400 The state_or_province value is not valid for the given country (e.g. "ZZ" for country: "US"). set-shipping-info
LocationNotFound 404 The supplied location_id is not a valid pickup location for this merchant. set-fulfillment-type
MissingRequiredCookie 400 A cookie the merchant session depends on is absent, so the session transfer could not be completed. PayPal — Complete Order
MissingShippingInfo 412 No shipping address is set on the cart, so it has no shipments to query. get-availability
MissingShippingMethod 412 A shipping method has not been selected on the cart yet. Klarna — Complete Order
MissingTaxSync 412 Cart taxes have not been calculated yet. Klarna — Complete Order
MultipleDiscountCodesNotSupported 409 Setting shipping triggered a re-evaluation of cart promotions and the merchant rejected the combination of currently-applied discount codes. set-shipping-info
NoLineItemError 412 The merchant accepted the request but did not return a line item — typically indicates a product was silently rejected by merchant validation. add-line-item
PaymentMethodNotAvailable 409 The requested wallet payment method is not available for this merchant. wallet-complete-order
PostalCodeRequired 412 The merchant requires a postal code before line items can be modified. update-line-item
ProductDiscontinued 410 The product at the supplied url exists but has been discontinued by the merchant. get-product-from-url
ProductUrlNotImplemented 404 The merchant’s adapter does not support resolving a product from a URL. get-product-from-url
PromoNotAvailable 409 The code exists but is not available for the current cart (e.g. minimum-purchase not met, product-restricted, customer-segment-restricted). add-promo-codes
ShippingAddressNotSupported 409 The supplied address is rejected by the merchant for reasons beyond country/state validation (e.g. APO/FPO, US territories, embargoed regions). set-shipping-info
ShippingNotNeeded 400 The cart contains only items that do not require shipping (e.g. digital-only). set-shipping-info
Unauthorized 401 The Server-to-Server credentials were rejected, or no authentication method (Browser Session, App ID, or Server-to-Server) accepted the request. search
VariantNotFound 404 The url requested a ?variant= that is not among the product’s variants. get-product-from-url

​​ Rate limits

A 429 response means you’ve exceeded your tenant’s rate limit. Honor the Retry-After header.

For the full pattern, recommended retry logic, and how to design high-volume integrations, see Rate limits.

​​ Pagination

Firmly uses two pagination styles depending on the endpoint. Discovery search returns a cursor in next_page when more results are available — pass it on the next request to walk forward. Catalog listing (Get All Products) uses page-number pagination instead: pass page and size, and read page / size / totalPages from the response’s metadata object (with products as a sibling of metadata).

For both patterns, page-size guidance, and cursor-expiry handling, see Pagination.

​​ Idempotency

Pass an Idempotency-Key header (a UUID your code generates) on a mutation request routed through the UCP protocol bridge (see the scope note below). Reuse the same key when retrying the same attempt; generate a fresh key only for a new attempt.

On a UCP-routed mutation, the first time the server sees a given key the mutation runs normally and the response is cached for 24 hours; a retry with the same key returns the cached response, so the underlying operation runs at most once. This applies to the UCP bridge only — core REST endpoints do not deduplicate on the header (see the scope note below).


POST /api/v1/ucp/rest/domain/{domain}/checkout-sessions/{session_id}/complete
Idempotency-Key: 7c2a8b1e-4f3d-4a1c-9e6b-2d0a8e7c1b3a

If the same key is reused with a different request body, the UCP bridge rejects the request with the UCP envelope code idempotency_conflict (“idempotency key reused with different parameters”). Either retry with the original body, or generate a new key for the new attempt. (This is UCP-bridge-only — there is no equivalent REST error, since core REST endpoints don’t deduplicate on the header.)

For the full key format and the conflict-detection fingerprint, see Idempotency.

​​ Recovery patterns for common failure modes

​​ Stock changed between discovery and place-order

The classic race: showed the user a product, they confirmed, someone else bought it before you got to place-order.


add-line-item → OK
set-shipping → OK
place-order → 409 NotEnoughStockError

Recovery: re-fetch the product, surface the change (“only 1 left — want to proceed with 1 instead of 2?”), update the cart with the new quantity, then retry place-order.

​​ Payment declined


place-order → 422 CreditCardDeclined

Recovery: do not silently retry the same card. Tell the user the card was declined (without showing the issuer’s reason verbatim — that can leak signals about valid card numbers). Offer a different payment method. (A 412 CheckoutError is a distinct, merchant-side checkout failure — promo re-evaluation or a total mismatch — not a card decline.)

​​ Validation error with field path


set-shipping-info → 400 InvalidInputBody
"At path: phone — Expected a string with a length between 10 and 13"

The description includes the failing field. Map it to a conversational ask (“what’s the phone number with country code?”) rather than echoing the raw error.

​​ Merchant adapter down

An adapter is Firmly’s connector for a merchant’s commerce platform (see the glossary). Each implements only what its platform supports — hence OperationNotSupported / NotImplemented on some merchants.


add-line-item → 503 StoreUnavailable

Recovery: retry with exponential backoff. If persistent for >30 seconds, surface to user.

​​ get-availability not supported


get-availability → 501 NotImplemented

Some merchants (V1 adapters) don’t implement this endpoint. This doesn’t block checkout: get-availability only returns delivery dates, time slots, and pickup locations — never shipping methods. Shipping methods always come from cart.shipments[].shipping_method_options, populated inline after set-shipping-info. Treat a 501 as “no scheduled-delivery/pickup detail for this merchant” and proceed. See Shipping & Fulfillment.

​​ UCP envelope (UCP protocol layer only)

Consumers of the UCP protocol bridge see a different envelope. Documented here for completeness — most direct REST integrations never see it.


{
"messages": [
{
"type": "error",
"code": "insufficient_stock",
"content": "Product 'Example Item' has only 2 in stock",
"severity": "requires_buyer_input",
"field": "items[0].quantity"
}
],
"ucp": { "version": "1.0" }
}

​​ Severity → agent behavior (UCP only)

severity Meaning What your agent does
recoverable Transient or auto-resolvable Retry with backoff, or auto-correct
requires_buyer_input User must provide or change something Ask the user, then retry
requires_buyer_review User must confirm something risky Surface details, wait for explicit confirmation
unrecoverable Flow cannot continue on this path Tell the user, offer an alternative

​​ UCP code catalog

Category Codes
Session session_not_found, session_expired, session_not_ready, session_already_completed, session_already_canceled
Validation validation_error, invalid_product, invalid_quantity, currency_mismatch, missing_required_field, missing_email, missing_shipping_address, missing_fulfillment_method
Inventory insufficient_stock, product_unavailable
Payment payment_declined, payment_invalid, payment_required
Catalog product_not_found, catalog_unavailable, merchant_not_found, ucp_disabled
Service idempotency_conflict, service_unavailable, internal_error

​​ Quick reference

Error name HTTP Quick fix
MissingAuthHeader 400 Add the auth header
InvalidToken 400 Re-bootstrap session
InvalidJWTToken 401 Re-bootstrap session
CartNotFound 404 Create cart by adding an item
ProductNotFound 404 Re-search via discovery
NotEnoughStockError 409 Lower quantity or pick different variant
InvalidPromoCode 409 Verify code; suggest alternatives
CountryNotSupported 412 Use a supported country
OperationNotSupported 412 Surface to user, or use the merchant default
NotImplemented 501 Skip the optional call; read methods from cart.shipments[].shipping_method_options
InvalidInputBody 400 Read description, fix the failing field
CreditCardDeclined 422 Ask for a different payment method
StoreUnavailable 503 Exponential backoff retry
Unexpected 500 Retry once; if persistent report to support

​​ Need help?

Support Contact support with the request ID + timestamp