On this page

REST errors and retries

A Wexa refusal tells you three things: which HTTP status, which stage of the governed lifecycle refused, and which lifecycle record to read for the rest. Learn the error body once and every route on this surface becomes readable.

The SDKs turn everything on this page into typed exceptions with retryability already decided. If you are using one, read error handling instead — this page is for callers working over raw HTTP.

The error body

{
  "error": "S4:validate-input",
  "error_description": "cypher rejected: query must start with a read clause (MATCH/OPTIONAL MATCH/WITH/UNWIND/RETURN/CALL), got \"CREATE\"",
  "lifecycle_id": "qlc_000002"
}
FieldMeaning
errorA stable code. For a lifecycle refusal it is the stage id — S3:rate-quota, S4:validate-input, S5:policy, S6:approval, S8:execute. Outside the lifecycle it is a name: invalid_token, invalid_request, not_found, conflict, unconfigured, unavailable.
error_descriptionHuman-readable detail. Safe to log; do not parse.
lifecycle_idPresent once the call has a lifecycle. Absent on a 401, which is refused before one exists.

Two response headers matter. Retry-After appears on a 429 and carries the real remaining seconds of the quota window. X-Trace-Id appears when the request carried a traceparent header or the deployment has its tracing exporter configured — send your own traceparent and the gateway echoes that trace id straight back:

curl -s -i -X POST "$BASE/v1/docs" \
  -H "Authorization: Bearer $WEXA_API_KEY" -H 'Content-Type: application/json' \
  -H 'traceparent: 00-4bf92f3577b34da6a3ce929d0e0e4736-00f067aa0ba902b7-01' \
  -d '{"topic":"governance"}'
X-Trace-Id: 4bf92f3577b34da6a3ce929d0e0e4736
Server-Timing: traceparent;desc="4bf92f3577b34da6a3ce929d0e0e4736"

Status codes

StatusWhenRetry?
400The body is not valid JSON, or a required field is missing.No
401Missing, malformed, revoked or wrong-kind credential.No
403The credential lacks the grant (S2:resolve-scope), or a policy rule refused (S5:policy), or a resume token was rejected (S6:approval).No
404No such approval, flow or execution — or the path names no tool.No
405Right path, wrong method. Tools are POST only; the response carries Allow.No
409State conflict: an approval that is no longer pending, an agent with no promoted version.Re-read state first
422The request parsed but failed validation or a dry run (S4:validate-input, S7:dry-run).No
429Over the request window (S3:rate-quota). Carries Retry-After.Yes
502A downstream service refused or was unreachable (S8:execute), or the deployment is misconfigured (unconfigured).Only when unlabelled
503unavailable — a deployment fault.No
504An upstream deadline was exceeded.Yes

Worked refusals

Each of these is real output from a running gateway.

No credential

HTTP/1.1 401 Unauthorized
Www-Authenticate: Bearer resource_metadata="http://localhost:7123/.well-known/oauth-protected-resource"

{"error":"invalid_token","error_description":"missing bearer credential"}

Malformed JSON

HTTP/1.1 400 Bad Request

{"error":"invalid_request","error_description":"invalid character 'o' looking for beginning of object key string"}

A write clause sent to a read-only tool

HTTP/1.1 422 Unprocessable Entity

{"error":"S4:validate-input","error_description":"cypher rejected: query must start with a read clause (MATCH/OPTIONAL MATCH/WITH/UNWIND/RETURN/CALL), got \"CREATE\"","lifecycle_id":"qlc_000002"}

A query that does not constrain itself to a project

HTTP/1.1 422 Unprocessable Entity

{"error":"S4:validate-input","error_description":"cypher rejected: query must filter on project_id: use `project_id = $project_id` for this project, or `project_id IN $project_ids` to include projects you were granted","lifecycle_id":"qlc_000003"}

Over quota

HTTP/1.1 429 Too Many Requests
Retry-After: 14

{"error":"S3:rate-quota","error_description":"quota exceeded for project proj_c10 (retry after 14s)","lifecycle_id":"qlc_000152"}

Reading the lifecycle record

When an error is not self-explanatory, fetch its lifecycle. It lists every stage the call reached, in order, with the one that refused marked failed:

curl -s "$BASE/v1/lifecycles/qlc_000002" -H "Authorization: Bearer $WEXA_API_KEY"
{
  "lifecycle_id": "qlc_000002",
  "tool": "query-context",
  "status": "denied",
  "stages": [
    { "stage": "S1:authenticate",   "status": "ok",     "detail": "credential verified by transport (api_key)" },
    { "stage": "S2:resolve-scope",  "status": "ok",     "detail": "org=org_c10 project=proj_c10 role=OWNER" },
    { "stage": "S3:rate-quota",     "status": "ok",     "detail": "within limits" },
    { "stage": "S4:validate-input", "status": "failed", "detail": "cypher rejected: query must start with a read clause …" }
  ]
}

This is the fastest way to tell "my credential is wrong" from "my payload is wrong": the first failing stage answers it.

What to retry

Only two failures are worth retrying without changing anything: 429 and an unlabelled 5xx. Everything else will refuse identically on the second attempt.

Honour Retry-After

On a 429 the gateway sends the quota window's real remaining seconds. Returning before that window resets only spends another 429. Add a small random offset so a fleet of clients told to wait the same number of seconds does not return in lockstep.

Where there is no header, back off with full jitter — a uniform draw from [0, 2^attempt) seconds — capped at 60, which is the length of the quota window. The default ceiling is not arbitrary: waiting longer than a window cannot help, and waiting less than the window cannot succeed.

The 502 distinction

A 502 carrying "error":"S8:execute", harness_unreachable, data_service_unreachable or upstream_unreachable is a downstream service having a bad moment. Retry it.

A 502 or 503 carrying "error":"unconfigured" or "error":"unavailable" is a deployment fault — a service that is not wired up at all. Retrying will never fix it. Tell an operator. Both SDKs encode exactly this distinction by mapping the second group to a class that is excluded from the retry set despite its 5xx status.

Quota

The defaults are 120 requests per project and 600 per organization, in a sliding 60-second window. POST /v1/docs reports your live headroom without costing you a call's worth of guesswork:

"quota": { "org_window_max": 600, "org_window_used": 12, "project_window_max": 120, "project_window_used": 12 }

A value of 0 for a maximum means unlimited. See quota and credits for how the windows are configured.