Reference

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

4xx / 5xx error shape
// 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.codeHTTPCause
cart_not_found404Cart id is invalid or belongs to another workspace.
cart_not_open409A 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_out409POST /carts/{id}/checkout was called twice on the same cart.
cart_already_converted409Cart is in status=converted (linked order is paid). Read-only.
customer_not_found404Customer id is invalid or belongs to another workspace.
application_not_found404Application UUID is invalid or belongs to another workspace.
application_required400Endpoint requires X-Throttle-Application-Id header or applicationId in the body.
application_mismatch403API key is scoped to a different application than the request target.
application_key_workspace_route403API keys cannot access workspace-level routes (Clerk JWT required).
missing_application_id400POST /carts called without applicationId in the request body.
idempotency_key_reused409Same Idempotency-Key sent with a different request body within 24h.
invalid_origin400Origin in allowedOrigins isn't https://<host> or http://localhost:<port>.
invalid_quote_token400pk_ token expired, scope-mismatched, or signed with a stale secret.
invalid_signature401Webhook signature failed verification (HMAC mismatch or stale timestamp).
embed_token_invalid401Provider embed JWT expired or was tampered with. Re-mint via the proxy endpoint.
embed_token_failed502The payment provider refused to mint an embed token (auth/permissions issue upstream).
permission_denied403Caller authenticated but lacks the required role / scope.
workspace_access_denied403Caller is not a member of the workspace in :workspaceId.
workspace_not_entitled_for_live402A production-environment credential was used without active subscription or partner status.
environment_mismatch403A 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_found404Requested workspace environment does not exist, is archived, or is not accessible to the workspace.
member_env_forbidden403Clerk-authenticated member tried to use an environment outside their environment grants.
environment_slug_taken409A workspace environment with that slug already exists.
system_environment_locked400Production or another system environment cannot be archived.
validation_error400Request body failed zod validation. See details[] for field-level errors.
rate_limit_exceeded429Per-workspace rate limit hit. Retry-After header indicates the cooldown.
clerk_required403Endpoint requires a Clerk JWT (API key auth rejected).
workspace_mismatch403Clerk JWT workspace does not match the workspace in the URL.
no_file400Multipart upload route received a request without a file part.
mime_not_allowed415Upload file type is outside the route’s ALLOWED_MIME set.
logo_too_big413Logo upload exceeded the per-route byte cap.
asset_too_big413Email asset upload exceeded the per-route byte cap.
file_too_big413Digital-fulfillment upload exceeded the per-route byte cap.
fulfillment_not_found404Digital-upload route’s :id is not a fulfillment in this application.
fulfillment_not_digital404Upload route targeted a non-digital fulfillment type.
missing_token400Buyer download URL missing ?token= query param.
invalid_token401Buyer download token failed JWT verification.
token_mismatch401Buyer download token was minted for a different fulfillment.
download_expired410Buyer download window has elapsed.
download_limit_reached410Buyer download count has reached the configured limit for this delivery.
invalid_logo_url400logoUrl on /embed-config is not a valid https URL.
invalid_primary_color400primaryColor on /embed-config is not a #rrggbb hex string.
invalid_storefront_base_url400storefrontBaseUrl on /embed-config is not an absolute https URL.
image_url_unresolvable400A 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:

validation_error
{
  "error": {
    "code": "validation_error",
    "message": "Request body validation failed",
    "details": [
      {
        "path": "",
        "message": "Unsupported field(s): unexpectedField",
        "code": "unrecognized_keys",
        "received": ["unexpectedField"]
      }
    ]
  }
}
Discover field names from error.details
When in doubt about a field name, send the request, then read 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.

Catch every Throttle SDK error with one check
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).

Branch on a stale cart without sniffing error codes
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.