Skip to main content
Successful responses return the resource JSON directly, no envelope. Errors are equally plain: one flat, opaque body, every time. It never carries a traceback, a stack frame, an internal field name, or anything that maps out the private request model. What it always carries is a stable slug you branch on, a human message, and a correlation id.

Error body

Every error is a flat JSON object. The minimum, always-present shape:

Branch on the slug, not the message

Two internal layers can produce an error, and they populate the slug field differently. A business-rule error from a /v1 endpoint (a bad amount, a missing scope, a firm that is not connected) sets error to the constant "request_error" and puts the discriminating slug in code. A transport or framework error (an oversized body, an unmatched route, an internal fault) puts the slug directly in error and has no code. One line reads the right slug in both cases:
Never key logic on message. It is generic and its wording can change without notice.

The slug vocabulary

Every slug maps to exactly one HTTP status. Handle the status class; treat the slug set as open and default anything you do not recognise.
Two slugs appear for 422 and 500 because either layer can raise them: the trading surface emits unprocessable / internal, the transport layer emits validation_error / internal_error. Reading code ?? error and switching on the shared HTTP status keeps a client correct regardless of which layer answered.

The request id is on every response

Every response, success or error, carries an X-Request-ID header: the id inbound from your gateway if you sent one, otherwise a fresh one ScripX generates. It stamps the matching server-side log line, so one id maps a client-observed error to its server trace.
On transport and framework errors the same id is also echoed in the body as request_id. Read it from the header when you want it unconditionally, log it on every non-2xx, and quote it in any support request. It identifies exactly one request and contains nothing sensitive.

Body-size limit

A request body over 1 MiB is rejected up front, before any parsing, with 413 request_too_large:
The one place this bites in normal use is POST /v1/orders:batch: keep a batch within the 500-order cap and its body stays well under the limit.

Rate-limit errors

A 429 carries Retry-After: 60 plus the full X-RateLimit-* header set, so a correct client never guesses. The header table and back-off guidance are in Rate limits. The public surface is metered separately per IP and returns a retry_after_s hint in its detail.

Partial success on batches

POST /v1/orders:batch does not fail wholesale: valid orders submit, invalid ones return per-item {index, ok, status, error} results in the same 200 response. Inspect each result rather than the top-level status.

Handling errors well

  1. Read code ?? error for the stable slug; switch on it and the HTTP status, never on message.
  2. Log X-Request-ID on every non-2xx, and quote it when you contact support.
  3. Retry only 429 (after Retry-After) and 500 (with backoff). Treat 4xx other than 429 as a bug to fix, not to retry.
  4. Make retries safe: send an Idempotency-Key on every order so a retried write can never double-place.