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:
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.- Gated surfaces (Shape D live)
- CRUD & org-settings (Shape D absent)
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.
code / layer fields.HTTP status taxonomy
Every status ModuleX returns, what it means in this product, and the envelope shape(s) you should expect.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.- 402 — two live shapes
- 429 — three live shapes
A 402 can be either of these, depending on the route:Shape B comes only from the two wallet routes; shape D comes from the usage gate on
run / Composer / Assistant / managed-knowledge.
Wallet paid-gate (Shape B)
Gated-surface credit/wallet denial (Shape D)
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 exposesstatus, 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
Catch and discriminate
The example below makes an authenticated request and handles a billing denial, a rate limit, and validation errors. Authentication usesAuthorization: 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 honorRetry-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.
Related pages
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.