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>; or it is a wildcard sent to the production environment, or a wildcard over a shared or public suffix. The message says which.
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_mismatch403An API key or dashboard request contradicted its own environment: an API key sent an X-Throttle-Environment-Id for a different environment than the key belongs to, or a storefront session token was minted for a different environment. Use a key for the correct environment, or send the matching header. Fetching another environment’s resource by id answers 404 not_found, not this.
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.
invalid_interval400Subscription cadence (create, change-plan, or a checkout recurring intent) named an out-of-vocabulary interval or unit, or a count of 0, negative, fractional, or above the unit max.
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.

Subscription and recurring-checkout codes

Codes returned by the subscription routes and by checkout sessions that carry a plan. See Managing Subscriptions and Starting checkout from an existing cart for the behavior behind them.

error.codeHTTPCause
subscription_terms_undeterminable422A checkout session was created from an existing cart (hosted /cart/<id>, recovery link, cartId hand-off) whose subscription plan line has no usable plan reference or cadence, and no earlier session on the cart carries the intent. Refused before any charge.
recurring_customer_required422A recurring checkout session has no customer email or identifier, and none to inherit from an earlier session on the cart.
quantity_unchanged400change-quantity was called with the quantity the subscription already has. Checked before any charge.
invalid_subscription_state400An immediate or scheduled change that alters nothing (same plan, cadence and amount) ("Nothing to change"); pause on a subscription that is not active; resume on a cancelled or completed one.
invalid_state400 / 409 / 422The subscription (or invoice) is in a state that refuses the action. 400: recording usage on an installment plan. 409: cancelling with a refund a subscription that is already cancelled or completed, or scheduling a change while cancelAtPeriodEnd is true. 422: changing or reporting usage on a cancelled or completed subscription, an immediate change while paused, retry-charge / waive-period when not past due, or a prorated cancel with no unused time left.
installment_plan_locked409Amount, cadence, quantity or plan change on an installment plan (totalPayments set), or a manual period renew of a scheduled plan.
invalid_schedule400A custom installment schedule is malformed: not 2–60 rows, a gap on row 0, a later row without a valid gap (hour 1–8760, day 1–365, week 1–52, month 1–12), or an amount that is not a positive integer. The message names the row, e.g. "schedule[2].intervalCount: …".
schedule_first_payment_mismatch400A line unitPrice (or the POST /subscriptions amount) differs from schedule[0].amount.
installment_plan_merchant_only403A buyer session (or the subscription proxy) tried to pause, resume or cancel an installment plan.
renewal_needs_attention409An immediate change, retry-charge or waive-period on a subscription holding an unresolved orphan_charge renewalIssue. Scheduled changes and cancellation are still accepted.
currency_mismatch422POST /api/v1/subscriptions sent a currency that differs from the application currency.
payment_failed402The stored card declined during an immediate plan or seat change. Nothing changed.

Order line-item edit codes

Codes returned by PATCH /api/v1/orders/:id/line-items, which adds, updates or removes line items and settles the change on an order with a captured payment: an increase is charged to the stored card, a decrease refunded. On an order whose balance is on an outstanding Net-N invoice the invoice takes the change instead — its amount rises or falls, no card is charged — and the response reports it in data.adjustment.invoice. Every code below refuses the edit before anything is written, charged or refunded.

error.codeHTTPCause
no_changes400The body has no add, update or remove entries.
invalid_line_item400An update lineItemId or a remove id is not a line item on this order.
empty_order400The edit would remove every line item. An order keeps at least one.
not_found404No such order in the application and environment of the credential.
order_not_editable409The order is cancelled, refunded or voided.
subscription_order_not_editable409The order is backed by a subscription. Change the subscription plan instead.
no_customer409The edit raises the total of an order with a captured payment, and the order has no customer whose stored card could be charged.
payment_failed402Charging the increase to the customer’s stored card failed. Nothing changed.
tax_origin_missing422The application calculates tax with a provider (taxMode: "app_based", e.g. Avalara) and has no origin address. Set it under Tax → Setup → Origin.
tax_address_missing422The application calculates tax with a provider, the order has no address its tax trigger can use, and the tax fallback policy is strict. Under lenient the edit goes through untaxed with a tax_address_missing warning.
tax_provider_unavailable503The tax provider could not answer and the tax fallback policy is strict (the default). Retry once the provider is reachable. Under lenient your native tax rules price the edit and the response carries a provider_fallback warning.
not_implemented501The edit raises the total of an order with a captured payment on a server that has no stored-card charging configured.

When the application calculates tax with a provider, the edit re-quotes tax with it. Under the lenient fallback policy a failure the strict policy refuses goes through instead, and the 200 response carries data.warnings: [{ code, message }]: provider_fallback (the provider could not answer, so your native tax rules priced the edit), tax_address_missing (no address to tax from, so no tax was charged), or tax_zero_from_provider (the provider returned no tax for the address). The field is absent when there is nothing to report. A refund that fails after the items changed does not error either: it comes back in data.adjustment.refundError, and the refund can be retried from the order. Likewise, a Net-N invoice the edit leaves owing nothing is voided, and a void that fails comes back in data.adjustment.invoice.voidError.

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.