Skip to main content
ModuleX does not wrap every error in one schema. The API returns four coexisting error-envelope shapes, and which one you get depends on the surface you call and the failure that occurred. This page documents all four, the full HTTP status taxonomy, and how the JavaScript SDK and Python SDK normalize each status into a typed error class so you can branch on it in code. The most important distinction: a 402 / 403 / 429 can mean two very different things. On the run, Composer, Assistant, and managed-knowledge surfaces, it is a structured billing-gate denial — the flat DenialEnvelope (shape D). On plain CRUD and org-settings routes, the same status codes carry only the standard {"detail": …} shape. The two are not interchangeable, and a client that handles billing denials must branch on both.

The four envelope shapes

There is no global error wrapper and no shared error-schema module. Each shape below is emitted by a different code path. Read them top to bottom — A is by far the most common.

Shape A — {"detail": "<string>"}

The standard FastAPI / Starlette HTTPException envelope, used by essentially every router. The detail value is a plain human-readable string. Missing headers, missing body fields, not-found resources, role and scope rejections, and most server errors all arrive in this shape. This is also one of the three live 429 paths: the per-key and per-user rate limiter returns a string detail ("API key rate limit exceeded" or "User rate limit exceeded (across all API keys)") alongside the rate-limit headers. See Rate limiting.

Shape B — {"detail": {…struct…}}

A few sites pass a dictionary as detail, which the framework serializes verbatim under the detail key. There are two live producers:
A dict detail is not a DenialEnvelope. Shape B nests its fields inside detail, and its code for the org limiter is "rate_limited". The flat DenialEnvelope (shape D) has no detail wrapper and uses code: "rate_limit_exceeded". Same status code, different wire shape — see the multiple 402 and 429 shapes.

Shape C — 422 validation array

There is no custom 422 handler, so request-validation failures use the framework default: {"detail": [ {loc, msg, type}, … ]}. Each array item identifies the field, a message, and an error type. This is triggered by query/body/path constraints such as a minimum string length.
The exact field set of the 422 array is the framework’s default and was not confirmed by a backend override. Treat loc, msg, and type as the fields you can rely on, and read the array as a list of per-field problems rather than a fixed schema.

Shape D — flat DenialEnvelope

The structured billing / usage / rate / quota denial. It is flat — its fields sit at the top level of the body, not under detail. This is the envelope the usage gate raises, and it is live on the run, Composer, Assistant, and managed-knowledge surfaces.
string
required
The machine-stable denial code. Branch on this in your client. One of rate_limit_exceeded, quota_exceeded, credit_plan_exhausted, wallet_overage_disabled, wallet_insufficient, or upgrade_payment_failed.
string
required
Which gate layer denied the request: rate, quota, credit, or wallet. The layer determines the HTTP status — rate → 429, quota → 403, credit → 402, wallet → 402.
string | null
The limit or counter key that was hit, such as sync_exec or max_knowledge_bases. null when not applicable.
number | null
The observed usage or count at the moment of denial. null when the gate does not report a running total for this denial.
number | null
The limit that was reached. null when no numeric limit applies (for example, a suspended subscription).
string | null
A short reason token, for example overage_disabled, insufficient_balance, or credit_plan_exhausted. Frequently mirrors code, but may differ.

DenialEnvelope codes

Each gate layer maps to a fixed code, HTTP status, and meaning: For where these come from and how credits are metered, see Usage gating & limits and Credits & metering.

Which surface emits which shape

This is the single most important fact about ModuleX errors.
Shape D (DenialEnvelope) is live on run and managed-usage surfaces, and absent everywhere else. A 402 / 403 / 429 from a CRUD or org-settings route is never a DenialEnvelope — it carries the {"detail": …} shapes (A or B). A 402 / 403 / 429 from a gated surface can be a DenialEnvelope.
The usage gate runs on these surfaces, so they can return a flat DenialEnvelope (shape D) for 402 / 403 / 429 — in addition to the standard {"detail": …} shapes for other failures:
  • POST /workflows/run — workflow execution, with the workflow id in the JSON body (workflow_id). See Run via API.
  • The AI Composer chat and resume endpoints.
  • The Assistant chat and resume endpoints.
  • Managed-knowledge retrieval and ingest on managed knowledge bases.
On these surfaces, branch on the flat top-level code / layer fields.

HTTP status taxonomy

Every status ModuleX returns, what it means in this product, and the envelope shape(s) you should expect.
The catch-all 500 hides the original exception. Any unhandled exception is returned as a fixed-string 500. Some routers, however, catch their own exceptions and re-raise a 500 HTTPException with a descriptive detail before the catch-all sees it — so a 500 may or may not include a useful message. Do not parse 500 detail strings programmatically.

Status codes with more than one live shape

Two status codes carry more than one wire shape at the same time. A client that handles them must branch on all of the shapes below.
A 402 can be either of these, depending on the route:
Wallet paid-gate (Shape B)
Gated-surface credit/wallet denial (Shape D)
Shape B comes only from the two wallet routes; shape D comes from the usage gate on run / Composer / Assistant / managed-knowledge.

Rate-limit headers

The 429 responses may carry these headers. Each is omitted when its value is not set, so you may see only some of them on a given response.
integer (seconds)
How long to wait before retrying. Defaults to 60 on gated-surface rate denials. Both SDKs honor this for 429 backoff; the Python SDK also tolerates an HTTP-date form.
integer
The ceiling for the current window.
integer
Remaining requests in the current window.
integer (Unix epoch seconds)
When the current window resets.

How the SDKs map errors

Both SDKs are at v1.0.0 and parse every envelope shape into one class tree, so you catch a typed error instead of inspecting raw JSON. Every error exposes status, the response body, and the response headers; the structured code, layer, and reason fields are surfaced where the envelope provides them. The two SDKs differ in one important way: Python adds a BillingError family (with PaymentRequiredError, QuotaExceededError, CreditExhaustedError, and WalletError) that the JavaScript SDK does not have. In JavaScript, a 402 and a rate-layer envelope fall through to the base ModulexError — with code / layer / reason still populated.

Status → SDK class

In Python, one 429 status can raise two different classes. A header-only 429 raises RateLimitError; a 429 whose body is a layer="rate" envelope raises base BillingError. JavaScript surfaces both as RateLimitError because it discriminates on status, not on envelope shape. In Python, catch both RateLimitError and BillingError to cover every 429. The Python SDK shadows the builtin PermissionError and TimeoutError.

Catch and discriminate

The example below makes an authenticated request and handles a billing denial, a rate limit, and validation errors. Authentication uses Authorization: Bearer mx_live_… plus X-Organization-ID — see Authentication.
JavaScript has no 402 or billing class. Catch the base ModulexError for 402 and read err.code / err.layer / err.reason — they are populated from the DenialEnvelope. The same applies to 410. See Errors & retries for the full SDK error class list and the retry policy.

Retry classification

Both SDKs treat the same statuses as retryable and honor Retry-After for 429 backoff. The two SDKs differ on scope: the Python SDK retries GET and HEAD only, while the JavaScript SDK retries all methods. SSE connection errors are thrown once, with no automatic reconnect, in both SDKs. For the retry-budget details and idempotency behavior, see Errors & retries.

Rate limiting

The per-key, per-user, and per-org rate limits behind the 429 responses.

Usage gating & limits

The billing admission gate that raises the 402 / 403 / 429 DenialEnvelope.

Authentication

The auth headers behind 401 and 403 — Authorization: Bearer plus X-Organization-ID.

SDK errors & retries

The full error class tree, retry policy, and idempotency behavior in both SDKs.