Errors
- Updated
- Reading time
- 4 min
- Level
- beginner
Every error on every Revoye Cloud endpoint has the same shape, and code is the contract.
{
"error": {
"code": "NO_AGENT_AVAILABLE",
"message": "No agent can currently serve this provider, and this request asked not to wait.",
"request_id": "req_01JAY7Q2K8XYZ3M4N5P6Q7R8S9",
"details": { "job_id": "job_01JAY7Q2K8XYZ3M4N5P6Q7R8S9", "provider": "chatgpt" }
}
}| Field | |
|---|---|
code | The contract. Branch on this |
message | Written for humans and may change in any release. Never parse it |
request_id | Quote it in a support request and the request can be traced |
details | Structured context, when there is any. details.field on validation errors, details.job_id on job outcomes |
Every response, successful or not, also carries an X-Request-Id header with the same value. Log it
on failures — it is the difference between a support conversation and a guess.
Every code
| Code | HTTP | Retry? | What it means for you |
|---|---|---|---|
INVALID_REQUEST | 400 | No | Failed validation. details.field names the offender |
UNAUTHORIZED | 401 | No | Missing, malformed, expired or revoked key |
FORBIDDEN | 403 | No | Valid key, wrong scope — or 1 000 of your jobs are already queued (details.limit) |
NOT_FOUND | 404 | No | No such job — or not yours |
CONFLICT | 409 | No | Idempotency key reused with a different request |
PAYLOAD_TOO_LARGE | 413 | No | Body over 1 MiB, or chat messages flattening to more than 100 000 characters |
RATE_LIMITED | 429 | After Retry-After | Too many requests per minute for this key |
JOB_CANCELLED | 499 | No | The job was cancelled while you waited on it |
INTERNAL_ERROR | 500 | Yes | Ours, and it never carries internal detail |
JOB_FAILED | 502 | Maybe | Every attempt failed. See below |
NO_AGENT_AVAILABLE | 503 | Yes | No capacity for the model right now (only with wait: false and no callback_url) |
PROVIDER_RATE_LIMITED | 503 | Yes | The model's hourly limit is used up |
JOB_TIMEOUT | 504 | Yes | The hold ran out while the job continues — or the job's deadline_ms passed |
The ones that mean "not yet"
These describe capacity, not a fault. Treating them as outages will make you retry aggressively against a system that is behaving exactly as designed.
NO_AGENT_AVAILABLE — nothing can take a job for the model you named (or for any model, if you
named none) right now. You only see this when you asked not to wait and gave no callback_url; with
wait: true, or with a webhook, the job simply waits in the queue instead. The job was still
accepted: details.job_id is queued and will run when capacity returns. Either keep it and collect
it later with GET /v1/completions/{id}, or DELETE it if you no longer want it. Retrying with the
same Idempotency-Key returns the same job rather than queuing another.
GET /v1/status shows what is free.
PROVIDER_RATE_LIMITED — the hourly limit for the model is used up. Normally a job over the limit
just waits in the queue until the hour rolls over, so you are unlikely to see this code; if you do,
back off for minutes rather than seconds, or omit provider so another model can answer.
JOB_TIMEOUT with details.status of queued or dispatched — your wait: true hold ran out
but the job is still going. Do not resubmit: fetch GET /v1/completions/{details.job_id} later. If
details.status is expired, the job's own deadline_ms passed and it will not run again.
RATE_LIMITED versus a busy model
The distinction that matters most, and the one most often confused:
| Whose limit | What to do | |
|---|---|---|
RATE_LIMITED (429) | Requests per minute on your API key | Respect Retry-After. You are sending requests too fast — usually polling |
NO_AGENT_AVAILABLE, PROVIDER_RATE_LIMITED (503) | Capacity or the hourly limit of the model | Queue with wait: true or a webhook, or let Revoye Cloud choose the model |
JOB_FAILED and more specific codes
A 502 means every attempt the job was allowed was used and none produced an answer.
details.attempts is how many there were, and details.job_id identifies the job. The code is
JOB_FAILED or, when the last attempt ended for a more specific reason, that reason — for example
PROMPT_REJECTED when the model declined to answer the prompt. Treat any unrecognised code on a
502 as JOB_FAILED.
Retry with care. A prompt the model refuses will be refused again; a transient failure may not recur.
A retry policy that works
RETRYABLE = {
"RATE_LIMITED", "NO_AGENT_AVAILABLE", "PROVIDER_RATE_LIMITED",
"JOB_TIMEOUT", "INTERNAL_ERROR",
}
def submit(body, key):
delay = 2
for attempt in range(6):
r = post("/v1/completions", headers={"Idempotency-Key": key}, json=body)
if r.status_code < 400:
return r.json()
code = r.json()["error"]["code"]
if code not in RETRYABLE:
raise RevoyeError(r.json()["error"]) # fix the request, do not retry
if code == "RATE_LIMITED":
delay = int(r.headers.get("Retry-After", delay))
sleep(delay + random.uniform(0, 1)) # jitter, so many workers do not synchronise
delay = min(delay * 2, 60)
raise RevoyeError("retries exhausted")Note that the same idempotency key is used throughout. That is what makes the loop safe: if an earlier attempt actually succeeded and you never saw the response, the retry returns that job rather than running a second one.
Internal errors
INTERNAL_ERROR is logged on our side in full and returned to you with no internal detail. Quote the
request_id and the request can be traced.