Skip to content

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
codeThe contract. Branch on this
messageWritten for humans and may change in any release. Never parse it
request_idQuote it in a support request and the request can be traced
detailsStructured 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

CodeHTTPRetry?What it means for you
INVALID_REQUEST400NoFailed validation. details.field names the offender
UNAUTHORIZED401NoMissing, malformed, expired or revoked key
FORBIDDEN403NoValid key, wrong scope — or 1 000 of your jobs are already queued (details.limit)
NOT_FOUND404NoNo such job — or not yours
CONFLICT409NoIdempotency key reused with a different request
PAYLOAD_TOO_LARGE413NoBody over 1 MiB, or chat messages flattening to more than 100 000 characters
RATE_LIMITED429After Retry-AfterToo many requests per minute for this key
JOB_CANCELLED499NoThe job was cancelled while you waited on it
INTERNAL_ERROR500YesOurs, and it never carries internal detail
JOB_FAILED502MaybeEvery attempt failed. See below
NO_AGENT_AVAILABLE503YesNo capacity for the model right now (only with wait: false and no callback_url)
PROVIDER_RATE_LIMITED503YesThe model's hourly limit is used up
JOB_TIMEOUT504YesThe 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 limitWhat to do
RATE_LIMITED (429)Requests per minute on your API keyRespect Retry-After. You are sending requests too fast — usually polling
NO_AGENT_AVAILABLE, PROVIDER_RATE_LIMITED (503)Capacity or the hourly limit of the modelQueue 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.