Errors
Every error returned by the /v1/ API uses a Stripe-style envelope. The HTTP status code, the type field, and the code field together tell you what happened and what to do about it. This page is the central catalog of every code value the API can return.
Envelope shape
{
"error": {
"type": "invalid_request_error",
"code": "invalid_email",
"message": "Email is not a valid address.",
"param": "email",
"request_id": "req_2c5tQ...",
"doc_url": "https://docs.thrustlab.com/errors/invalid_email"
}
}
| Field | Notes |
|---|
type | One of the nine taxonomic types in the table below. Coarse classifier — branch on this for retry vs surface vs fix-input decisions. |
code | Stable machine-readable string identifying the specific failure. Add new codes within an existing type is non-breaking; changing the code for a given outcome is breaking. |
message | Human-readable explanation. Safe to show to end users, but copy may evolve — never branch on message text. |
param | The request field that triggered the error, when applicable. null for non-field errors. |
request_id | The req_<ksuid> correlator. Include this in any support ticket. |
doc_url | Stable docs link for the specific code. |
Some error codes carry additional fields beyond the envelope (e.g. credits_insufficient includes required_amount and available_balance). Those extensions are documented under the relevant resource page.
Error types
| Type | HTTP status | Retryable? | When |
|---|
invalid_request_error | 400 / 422 | No | Malformed input, validation failure, unknown enum value, or (422) a semantic precondition failure on an otherwise well-formed request. |
authentication_error | 401 | No | Missing, malformed, or invalid credential. |
permission_error | 403 | No | Authenticated but not authorized for this resource. |
not_found_error | 404 | No | Resource does not exist or is not visible to the caller. |
conflict_error | 409 | No (without input change) | Request conflicts with current resource state. |
idempotency_error | 409 | No (without a new key or matching body) | Idempotency-Key reused with a different request body. |
rate_limit_error | 429 | Yes (after Retry-After) | Credential bucket exhausted. |
insufficient_credits_error | 402 | No (until balance changes) | Compute action requires more compute units than available. |
api_error | 500 / 503 | Yes | Server-side fault. 503 billing_not_configured is the one expected variant. |
Code catalog
invalid_request_error (400)
code | Source |
|---|
request_validation_failed | Pydantic body/query/path validation. Generic catch-all for shape errors. |
invalid_cursor | cursor query param does not decode to a valid keyset position. |
invalid_filter_value | Bracket-syntax filter value failed numeric coercion or was non-finite. |
invalid_email | PATCH /v1/users/me with malformed email address. |
idempotency_key_too_long | Idempotency-Key header exceeds 255 characters. |
authentication_error (401)
code | Source |
|---|
missing_authorization | No Authorization header was sent. |
malformed_authorization | Header present but not in Bearer <value> form. |
invalid_credential_format | Bearer value is neither a JWT nor a key_-prefixed string. |
invalid_api_key | API key not found, revoked, or owned by an inactive user. |
invalid_jwt | JWT failed decode, signature check, or expiration. |
invalid_jwt_subject | JWT decoded, but its sub claim does not resolve to an active user. |
The same code (invalid_api_key) covers "doesn't exist", "revoked", and "owner deactivated" — this is deliberate, to prevent enumeration attacks.
permission_error (403)
code | Source |
|---|
permission_denied | Authenticated user lacks permission for the target resource. Examples: cross-user PATCH/DELETE on a private component; non-staff PATCH/DELETE on a public catalog component. |
not_found_error (404)
code | Source |
|---|
user_not_found | Defensive: authenticated user vanished mid-request. |
project_not_found | GET/PATCH/DELETE /v1/projects/{id}. Cross-user access returns this same code (anti-enumeration). |
component_not_found | GET/PATCH/DELETE /v1/components/{id}. Cross-user on a private component returns this same code. |
submission_not_found | GET/PATCH/DELETE /v1/submissions/{id}. Cross-user returns this same code. |
starred_component_not_found | DELETE /v1/starred_components/{id} for an unknown or cross-user star. |
geometry_style_not_found | GET /v1/geometry_styles/{id} (or POST /v1/geometry/preview referencing an unknown style slug). |
event_not_found | GET /v1/events/{id} for an event past 30-day retention or owned by another user. |
webhook_endpoint_not_found | Operations on an unknown or cross-user wh_<ksuid> endpoint. |
webhook_delivery_not_found | Operations on an unknown delivery row. |
conflict_error (409)
code | Source |
|---|
email_already_taken | PATCH /v1/users/me with an email already registered to another user. |
submission_already_reviewed | PATCH/DELETE /v1/submissions/{id} after the submission has moved past status="submitted". |
already_starred | POST /v1/starred_components for a (project_id, component_id) pair already starred. |
delivery_not_terminal | POST /v1/webhook_endpoints/{id}/deliveries/{whd}/retry while the source delivery is still pending. |
idempotency_error (409)
code | Source |
|---|
idempotency_key_reused_with_different_body | Idempotency-Key header reused on a write request with a request body that doesn't match the original request. |
rate_limit_error (429)
code | Source |
|---|
rate_limit_exceeded | Per-credential token bucket exhausted (1000 req/min sustained, 50 burst). Honor the Retry-After header. |
insufficient_credits_error (402)
code | Source |
|---|
credits_insufficient | Simulation or sweep submission would exceed the caller's usable balance. Includes required_amount and available_balance extension fields. |
api_error (500 / 503)
code | Source |
|---|
internal_error | 500 — unexpected server-side fault. Carries a request_id for support correlation. |
billing_not_configured | 503 — Stripe credentials not set on this server. Affects POST /v1/subscriptions, DELETE /v1/subscriptions/me, POST /v1/billing_portal/sessions. DB-only reads (/v1/compute-units/*, GET /v1/subscriptions/me) are unaffected. |
Reading errors in client code
The recommended pattern across every official SDK:
- Branch on
type for the high-level class of failure (auth vs validation vs not-found vs rate-limit etc.).
- Branch on
code only when the user-facing message or recovery path differs within that type.
- Treat the
code set as open. Adding a new code inside an existing type is non-breaking — the SDK should fall back to the type-level handler for unknown codes rather than crash.
- Always log
request_id on error. It's the fastest way to find the failing request in the server logs when you open a support ticket.
For the rate-limit and idempotency semantics that govern when retries are safe, see stability-policy.md. For the 402 insufficient-balance envelope, see compute units.