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