Error codes
Every 4xx / 5xx response carries a stable error.code. Reach for it (not the prose message) when branching in your handler — codes are versioned with the wire contract; messages are not.
Envelope
// Every Throttle 4xx / 5xx response follows this shape.
{
"error": {
"code": "cart_already_checked_out", // stable contract — use this
"message": "Cart has already been converted to a draft order.",
"details": [ // optional, field-level
{ "field": "cartId", "code": "conflict", "message": "Already checked out" }
]
}
}Common codes
The list below covers the ~20 codes you'll see most often when integrating cart, checkout, payments, and embed flows. The full set is generated from the OpenAPI spec at api.usethrottle.dev/openapi.json.
| error.code | HTTP | Cause |
|---|---|---|
cart_not_found | 404 | Cart id is invalid or belongs to another workspace. |
cart_not_open | 409 | A mutating cart operation targeted a cart no longer in status=open (converted by a completed order, or abandoned). In the cart SDK this surfaces as CartNotOpenError. |
cart_already_checked_out | 409 | POST /carts/{id}/checkout was called twice on the same cart. |
cart_already_converted | 409 | Cart is in status=converted (linked order is paid). Read-only. |
customer_not_found | 404 | Customer id is invalid or belongs to another workspace. |
application_not_found | 404 | Application UUID is invalid or belongs to another workspace. |
application_required | 400 | Endpoint requires X-Throttle-Application-Id header or applicationId in the body. |
application_mismatch | 403 | API key is scoped to a different application than the request target. |
application_key_workspace_route | 403 | API keys cannot access workspace-level routes (Clerk JWT required). |
missing_application_id | 400 | POST /carts called without applicationId in the request body. |
idempotency_key_reused | 409 | Same Idempotency-Key sent with a different request body within 24h. |
invalid_origin | 400 | Origin in allowedOrigins isn't https://<host> or http://localhost:<port>. |
invalid_quote_token | 400 | pk_ token expired, scope-mismatched, or signed with a stale secret. |
invalid_signature | 401 | Webhook signature failed verification (HMAC mismatch or stale timestamp). |
embed_token_invalid | 401 | Provider embed JWT expired or was tampered with. Re-mint via the proxy endpoint. |
embed_token_failed | 502 | The payment provider refused to mint an embed token (auth/permissions issue upstream). |
permission_denied | 403 | Caller authenticated but lacks the required role / scope. |
workspace_access_denied | 403 | Caller is not a member of the workspace in :workspaceId. |
workspace_not_entitled_for_live | 402 | A production-environment credential was used without active subscription or partner status. |
environment_mismatch | 403 | A credential or dashboard request targeted a resource from a different workspace environment. Use a key for the correct environment, or send the matching X-Throttle-Environment-Id. |
environment_not_found | 404 | Requested workspace environment does not exist, is archived, or is not accessible to the workspace. |
member_env_forbidden | 403 | Clerk-authenticated member tried to use an environment outside their environment grants. |
environment_slug_taken | 409 | A workspace environment with that slug already exists. |
system_environment_locked | 400 | Production or another system environment cannot be archived. |
validation_error | 400 | Request body failed zod validation. See details[] for field-level errors. |
rate_limit_exceeded | 429 | Per-workspace rate limit hit. Retry-After header indicates the cooldown. |
clerk_required | 403 | Endpoint requires a Clerk JWT (API key auth rejected). |
workspace_mismatch | 403 | Clerk JWT workspace does not match the workspace in the URL. |
no_file | 400 | Multipart upload route received a request without a file part. |
mime_not_allowed | 415 | Upload file type is outside the route’s ALLOWED_MIME set. |
logo_too_big | 413 | Logo upload exceeded the per-route byte cap. |
asset_too_big | 413 | Email asset upload exceeded the per-route byte cap. |
file_too_big | 413 | Digital-fulfillment upload exceeded the per-route byte cap. |
fulfillment_not_found | 404 | Digital-upload route’s :id is not a fulfillment in this application. |
fulfillment_not_digital | 404 | Upload route targeted a non-digital fulfillment type. |
missing_token | 400 | Buyer download URL missing ?token= query param. |
invalid_token | 401 | Buyer download token failed JWT verification. |
token_mismatch | 401 | Buyer download token was minted for a different fulfillment. |
download_expired | 410 | Buyer download window has elapsed. |
download_limit_reached | 410 | Buyer download count has reached the configured limit for this delivery. |
invalid_logo_url | 400 | logoUrl on /embed-config is not a valid https URL. |
invalid_primary_color | 400 | primaryColor on /embed-config is not a #rrggbb hex string. |
invalid_storefront_base_url | 400 | storefrontBaseUrl on /embed-config is not an absolute https URL. |
image_url_unresolvable | 400 | A relative line-item imageUrl was sent but the application has no storefrontBaseUrl (or allowedOrigin) to resolve it against. |
Validation errors
When a zod body validator fails, the envelope carries code: "validation_error" and a per-field details[] array. Strict-input endpoints return only the canonical camelCase contract. Field-level details identify unrecognized keys and may include a suggestion string when the server can identify the intended field:
{
"error": {
"code": "validation_error",
"message": "Request body validation failed",
"details": [
{
"path": "",
"message": "Unsupported field(s): unexpectedField",
"code": "unrecognized_keys",
"received": ["unexpectedField"]
}
]
}
}error.details[].path. The validator names the canonical key it expected.SDK errors
Every Throttle SDK throws the same error class — ThrottleError from @usethrottle/errors, re-exported from each SDK. It carries code, statusCode, message, and optional details. The historical per-SDK names still work and are subclasses, so instanceof ThrottleApiError (cart) and instanceof ThrottleCheckoutError (checkout) keep working — and a single instanceof ThrottleError catches errors from all of them.
import { ThrottleError } from '@usethrottle/cart'; // same class in every SDK
try {
await cart.items.add(cartId, item);
await checkout.completeSession(sessionId, payment);
} catch (e) {
if (e instanceof ThrottleError) {
console.error(e.statusCode, e.code, e.message);
} else {
throw e;
}
}The cart SDK additionally throws typed subclasses for the two lifecycle failures worth branching on, so you don't have to sniff error.code strings. Both extend ThrottleApiError (and therefore ThrottleError): CartNotOpenError (409 cart_not_open) and CartNotFoundError (404).
import { CartNotOpenError } from '@usethrottle/cart';
try {
await cart.shipping.select(cartId, method);
} catch (e) {
if (e instanceof CartNotOpenError) {
// The cart was converted by a completed order (terminal) or abandoned.
// Rebuild a fresh cart from your last-known line items and retry.
cartId = await rebuildCart();
} else {
throw e;
}
}HTTP code conventions
- 400 — request was malformed (validation, missing required field, invalid value).
- 401 — authentication missing or invalid (key, signature, JWT).
- 402 — request requires a paid subscription that isn't active (production entitlement).
- 403 — authenticated but not authorized for this workspace / application / role.
- 404 — resource doesn't exist (or exists in a different workspace).
- 409 — request conflicts with current state (cart already checked out, Idempotency-Key reused, slug taken).
- 410 — resource was permanently removed (revoked invitation, abandoned cart).
- 422 — semantic validation failed (e.g. payment amount exceeds order total).
- 429 — rate limit exceeded.
- 5xx — Throttle problem. Idempotency-Key replays do NOT cache 5xx, so safe to retry with the same key.