aexaex
Guides

Errors

Errors

Every API error is a JSON body with a machine-readable error code; most also carry a human message and the self-describing fields named below.

Typed errors in the SDK

The SDK maps every non-2xx response through one factory to a typed exception. All inherit AexApiError, which carries the HTTP status, the redacted parsed body, the server's stable apiCode (a machine-branchable identity distinct from the human message), and a requestId for support correlation. The factory dispatches to a subclass by code/status:

ClassFires forExtra fields
AexAuthError401 / 403 (unauthorized, forbidden, insufficient_scope, token_invalid/revoked/expired, malformed_token)requiredScope (on insufficient_scope)
AexIdempotencyConflictError409 idempotency_conflict
AexNotFoundError404 not_found
AexRateLimitError429 (rate_limited, workspace_concurrency_exceeded, workspace_submit_rate_exceeded)retryAfterMs (when advertised)
AexApiError (base)every other stable code (session_busy, run_not_terminal, insufficient_balance, workspace_spend_cap_exceeded, upstream_error, internal_error, …)

Branch with the exported guards instead of parsing bodies or matching status codes: isAuthError, isInsufficientScope, isIdempotencyConflict, isNotFound, and isRateLimited.

import { isInsufficientScope, isIdempotencyConflict } from "@aexhq/sdk";

try {
  await aex.run(config);
} catch (err) {
  if (isInsufficientScope(err)) {
    // err.requiredScope names the missing scope; mint a key that includes it.
  } else if (isIdempotencyConflict(err)) {
    // same key, different body — use a fresh key or resend the byte-identical body.
  }
}

The stable apiCode set the SDK types and dispatches on is: unauthorized, forbidden, insufficient_scope, token_invalid, token_revoked, token_expired, malformed_token, not_found, idempotency_conflict, session_busy, run_not_terminal, unknown_workspace, workspace_concurrency_exceeded, workspace_submit_rate_exceeded, workspace_spend_cap_exceeded, insufficient_balance, rate_limited, upstream_error, internal_error. A code outside this set (e.g. a validation error a route reports) still surfaces as an AexApiError with the status and body.error preserved; apiCode is then undefined.

Transport failures with no HTTP response (DNS, connection refused, TLS reset) surface as AexNetworkError; client-side config validation surfaces as RunConfigValidationError (err.code === "RUN_CONFIG_INVALID") before any request is sent.

401 — authentication

CodeMeaning
unauthorizedMissing or empty bearer token.
token_invalidBearer token is structurally valid but does not match a live credential, has a bad signature, or belongs to an inactive workspace.
token_revokedBearer token matched a revoked credential.
token_expiredShort-lived internal writer token is past its expiry.

Check the token value and that it has not been deleted or revoked. aex whoami is the cheapest way to validate a credential. See Authentication.

403 — authorization

CodeMeaning
insufficient_scopeThe token is valid but lacks the route's required scope. The body's requiredScope field names the missing scope.
unknown_workspaceThe token does not route to a known workspace.
forbiddenThe authenticated workspace does not own the addressed resource.
{ "error": "insufficient_scope", "requiredScope": "runs:write" }

400 — validation

CodeMeaning
bad_requestMissing or unparseable request body.
invalid_submissionThe submission failed shape validation; message names the offending field.
missing_provider_keyThe submission names a provider but carries no BYOK key for it (apiKeys[provider]).
malformed_tokenThe bearer value is not a structurally valid aex token.

400s are permanent for that request — fix the input rather than retrying. The SDK's client-side validation (RunConfigValidationError) catches most of these before the request is sent.

402 — payment required

Two distinct submit gates return 402; both bodies are self-describing.

insufficient_balance — the workspace prepaid balance is at or below the effective submit floor. Top up the balance or bind a payment method.

{
  "error": "insufficient_balance",
  "message": "Workspace balance is depleted; top up your prepaid balance or bind a payment method to submit runs.",
  "balanceUsd": 0,
  "balanceGraceFloorUsd": 0,
  "paymentMethodStatus": "none",
  "planKey": "default"
}

balanceGraceFloorUsd is the payment-method-aware floor the gate compared against (paymentMethodStatus: "active" folds a bounded card overdraft into it, so the floor can be negative).

workspace_spend_cap_exceeded — the workspace's monthly spend cap is reached. The cap resets at the start of the next UTC month; contact support to raise it.

{
  "error": "workspace_spend_cap_exceeded",
  "message": "Monthly spend cap of $250 reached ($251.13 accrued this month). The cap resets at the start of the next UTC month; contact support to raise it.",
  "capUsd": 250,
  "accruedUsd": 251.13
}

429 — rate limits

workspace_concurrency_exceeded — admitting one more live run would exceed the workspace's concurrent-run cap. Wait for a run to finish, or contact support to raise the cap.

{
  "error": "workspace_concurrency_exceeded",
  "message": "Workspace concurrency limit reached: 50 live runs at the cap of 50. Wait for a run to finish, or contact support to raise your workspace limit.",
  "cap": 50,
  "observed": 50
}

workspace_submit_rate_exceeded — too many submits in the current one-minute window. Retry shortly.

{
  "error": "workspace_submit_rate_exceeded",
  "message": "Submit rate limit of 120/minute exceeded. Retry shortly, or contact support to raise your workspace limit.",
  "perMin": 120,
  "observed": 121
}

The limit-naming fields (cap/perMin) and the observed window value make each deny self-describing, so a client can back off proportionally. To anticipate both 429s and both 402s before submitting, read the effective caps from aex.whoami().limits — the values come from the same resolution code the gates enforce. See Limits & quotas.

404 — not found

not_found: the id does not exist or belongs to another workspace (aex does not distinguish the two). The SDK raises AexNotFoundError (guard: isNotFound(err)).

409 — idempotency conflict

idempotency_conflict: the idempotencyKey was already used with a different request body. The SDK raises AexIdempotencyConflictError (guard: isIdempotencyConflict(err)).

Remedy: use a fresh idempotency key for a genuinely new request, or resubmit the byte-identical body to replay the original result (a matching retry returns the existing session rather than conflicting). Note that the SDK validates the key client-side first: an empty or whitespace-only idempotencyKey throws RunConfigValidationError before the request is sent — never pass "".

5xx — server errors

CodeMeaning
internal_error (500)Unexpected server fault. Retry with backoff; report persistent cases.
db_resuming (503)The database tier is resuming from idle. Transient — retry.

The SDK retries transient failures automatically: HTTP 429, 5xx, 529, and network errors get bounded exponential backoff with full jitter, honoring any Retry-After header. Tune or disable this with the client retry option; use isRateLimited(err) / AexRateLimitError to handle persistent throttling without parsing raw bodies. Idempotent submit retries are safe — the SDK attaches a stable idempotency key to billable session create/send requests, so a retried request never double-submits.

On this page