Skip to content

Errors

The error envelopes the API actually returns, per status code

Updated View as Markdown

The API has no custom exception handler. Failures raised by the framework come back in its default shapes; failures raised by an endpoint come back in that endpoint’s own shape. The result is three different error envelopes on one API. Handle all three.


Send Accept: application/json

Successful responses are JSON regardless of headers. Error responses are not.

The framework only serialises an exception to JSON when the request asked for JSON. Without Accept: application/json:

Failure With the header Without the header
Unauthenticated 401 JSON 302 redirect to https://app.firearmcart.com/login
Validation failure 422 JSON with errors 302 redirect, errors flashed to a session you cannot read
Everything else JSON HTML error page

Set it globally in your HTTP client and never think about it again. Every example in this reference includes it.


The three envelopes

1. Framework envelope — message only

Produced by the framework for authentication, authorization, routing, throttling and unhandled exceptions.

{
    "message": "Unauthenticated."
}

Validation failures add an errors object:

{
    "message": "The email field is required.",
    "errors": {
        "email": ["The email field is required."]
    }
}

2. Gate and lookup envelope — error + message

Produced by the API access middleware and by the “not found” branch of most controllers. error is a stable machine-readable slug; message is human-readable and may change.

{
    "error": "not_found",
    "message": "Order not found."
}

3. Operation envelope — success + error + message

Produced by the write endpoints — payments, upsells, and subscription updates. Same two keys as above, plus success: false. Card declines add a details object.

{
    "success": false,
    "error": "payment_declined",
    "message": "Insufficient funds.",
    "details": {
        "response_code": "51",
        "response_message": "Insufficient funds."
    }
}

Note: Do not branch on the presence of success. It is inconsistent even within this envelope: POST /v1/payments/charge and POST /v1/orders/{order_id}/upsell return success: true when they succeed, but the subscription endpoints return a bare {"data": …} on success and only add success: false when they fail. Branch on the HTTP status first, then read error if it is present.

There is no validation_error envelope and no rate_limit_exceeded envelope. Older FirearmCart material documents both; neither exists.


By status code

401 Unauthorized

Two distinct bodies, from two different layers.

Missing, malformed, revoked or unknown token — rejected at the auth guard:

{
    "message": "Unauthenticated."
}

Token authenticated but did not resolve to a store — rejected at the API access gate:

{
    "error": "unauthorized",
    "message": "Invalid API token."
}

Neither is retryable. Check the token value, then check that it has not been deleted in the dashboard.

403 Forbidden

Three bodies.

Token is missing the ability the route requires:

{
    "message": "Invalid ability provided."
}

The body does not name the missing ability. Cross-reference the endpoint against the abilities table, then mint a replacement token — abilities cannot be added to an existing token.

Store’s plan does not include API access:

{
    "error": "forbidden",
    "message": "API access is not enabled for this team."
}

Store’s subscription is suspended or past its cancellation date:

{
    "error": "subscription_expired",
    "message": "Your subscription has expired. Please renew to continue using the API."
}

Both 403s from the gate are billing problems, not code problems. See Authentication.

404 Not Found

Resource does not exist, or belongs to another store. Every lookup is scoped to the token’s store, so a valid id from a different store is indistinguishable from an id that does not exist:

{
    "error": "not_found",
    "message": "Order not found."
}

The message names the resource: Customer not found., Order not found., Product not found., Fulfillment not found., Subscription not found.

URL does not match any route — framework envelope, and the path is echoed back:

{
    "message": "The route v1/order/1042 could not be found."
}

If you see the second shape, you have a typo in the path, not a missing record.

405 Method Not Allowed

{
    "message": "The GET method is not supported for this route. Supported methods: POST."
}

409 Conflict

Only from POST /v1/orders/{order_id}/upsell, when another add-on for the same order is mid-flight:

{
    "success": false,
    "error": "in_progress",
    "message": "Another add-on for this order is currently processing. Please retry."
}

This one is retryable. Back off briefly and retry with the same idempotency_key.

422 Unprocessable Entity

Two very different meanings share this code.

Request validation failed — framework envelope. errors maps each rejected field to an array of messages, using dot notation for nested and array fields:

{
    "message": "The items.0.quantity field is required.",
    "errors": {
        "items.0.quantity": ["Each item must have a quantity."],
        "payment_type": ["The selected payment type is invalid."]
    }
}

Business rule rejected — operation envelope, with a slug in error:

error Endpoint Meaning
payment_declined charge, upsell Gateway declined the card. Carries details.response_code.
order_not_paid upsell Add-ons require an order that has actually been paid.
upsell_invalid upsell Product, variant, card or merchant account could not be resolved.
address_not_found subscription update shipping_address_id does not belong to this subscription’s customer.
payment_method_not_found subscription update payment_method_id does not belong to this subscription’s customer.

Important: A 422 payment_declined means a failed transaction was recorded against the order. The order exists and is in a failed state — do not treat the decline as “nothing happened”.

400 Bad Request

Only from POST /v1/payments/charge, for order-construction problems found before the card is touched. No money moved and no order was created.

error Meaning
payment_method_not_found No usable card on file, or the supplied token did not resolve
product_not_found A product_id in items does not exist in this store
variant_not_found A variant_id does not belong to the given product
subscription_not_allowed A serialized firearm cannot be sold on a recurring cadence
subscription_not_available The product is not subscription-eligible
cadence_not_offered The product does not offer the requested cadence
subscription_required The product is subscription-only; supply a cadence
{
    "success": false,
    "error": "product_not_found",
    "message": "Product with ID 9999 not found."
}

404 from POST /v1/payments/charge

Charge also uses 404 for one case — the customer could not be matched by customer_id or email:

{
    "success": false,
    "error": "customer_not_found",
    "message": "Customer not found."
}

429 Too Many Requests

{
    "message": "Too Many Attempts."
}

The retry delay is in the Retry-After header, never in the body. See Rate limits.

500 Internal Server Error

Two bodies.

Unhandled exception — framework envelope, with a fixed message:

{
    "message": "Server Error"
}

Payment or upsell processing failed — operation envelope. The underlying exception is reported to error monitoring; the body deliberately reveals nothing:

{
    "success": false,
    "error": "processing_error",
    "message": "An error occurred while processing the payment."
}

Warning: A 500 from POST /v1/payments/charge is not proof that no charge occurred. The endpoint has no idempotency key, so blind retries can double-charge. Reconcile with GET /v1/orders?customer_id=… before retrying. POST /v1/orders/{order_id}/upsell requires an idempotency_key and is safe to retry with the same key.


Handling errors

status=$(curl -s -o /tmp/body -w '%{http_code}' \
  "https://api.firearmcart.com/v1/orders/1042" \
  -H "Authorization: Bearer $FIREARMCART_TOKEN" \
  -H "Accept: application/json")

case "$status" in
  2*)   jq '.data' /tmp/body ;;
  401)  echo "Token is missing, wrong, or revoked." ;;
  403)  jq -r '.error // "missing_ability"' /tmp/body ;;
  404)  echo "Not in this store." ;;
  409|429|5*) echo "Retryable — back off." ;;
  4*)   jq -r '.error // .message' /tmp/body ;;
esac

A reasonable retry policy:

Status Retry?
401, 403, 404, 405 Never — fix the request or the token
422 Never for validation; never for a decline
409 Yes, after a short pause, same idempotency key
429 Yes, after Retry-After seconds
500, 502, 503, 504 Yes with backoff — except on POST /v1/payments/charge, which is not idempotent

Navigation

Type to search…

↑↓ navigate↵ selectEsc close