Skip to content

Errors

Updated
Reading time
4 min
Level
beginner

Every error on every endpoint has the same shape, and code is the contract.

{
  "error": {
    "code": "NO_AGENT_AVAILABLE",
    "message": "No idle agent is available for provider 'chatgpt'.",
    "request_id": "req_01JAY7Q2K8",
    "details": { "provider_kind": "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 whole trace is recoverable
detailsStructured context. details.field on validation errors, details.attempts on JOB_FAILED

Every response also carries X-Request-Id. 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
NOT_FOUND404NoNo such job — or not yours
CONFLICT409NoIdempotency key reused with a different body
PAYLOAD_TOO_LARGE413NoPrompt over 100 000 characters, or body over 1 MiB
RATE_LIMITED429After Retry-AfterRevoye's ingress limit
JOB_CANCELLED499NoCancelled by you or from the dashboard
INTERNAL_ERROR500YesOurs, and it never carries internal detail
JOB_FAILED502MaybeAttempts exhausted with agent-reported errors. Read details.attempts
NO_DEVICE_ONLINE503YesNo paired device is connected
NO_AGENT_AVAILABLE503YesA device is connected, but no agent is eligible right now
PROVIDER_RATE_LIMITED503YesYour own hourly cap, set in your dashboard
JOB_TIMEOUT504YesHeld as long as you asked; the job continues

The three that are not failures

These describe the state of your fleet. Treating them as outages will make you retry aggressively against a system that is behaving exactly as designed.

NO_DEVICE_ONLINE — no paired machine is connected. You only ever see this when you asked not to wait and gave no callback_url; with wait: true, or with a webhook, the job queues instead. The fix is to turn a machine on, or to stop asking for an immediate answer from a fleet that is asleep.

NO_AGENT_AVAILABLE — a device is connected, but nothing is eligible: every agent is busy, disabled, rate-limited, or has already failed this job. Back off and retry. If it persists, you need more agents, not more retries. GET /v1/status will tell you which.

PROVIDER_RATE_LIMITED — you hit a cap you configured. This is your own throttle protecting your own provider account. Honour it; do not retry immediately, and do not raise the cap without thinking about why you set it.

RATE_LIMITED versus PROVIDER_RATE_LIMITED

The distinction that matters most, and the one most often confused:

Whose limitWhat to do
RATE_LIMITED (429)Revoye's ingress limit on requests per minuteRespect Retry-After. You are sending requests too fast
PROVIDER_RATE_LIMITED (503)Yours, set in your dashboardSlow down, or route to another provider. The job may simply wait

JOB_FAILED and details.attempts

JOB_FAILED means every attempt was used and none produced an answer. details.attempts lists what happened on each one — which agent, which provider, and why it ended. That is where you find out whether your prompt is triggering a provider refusal, whether one agent is broken, or whether a provider's interface has changed.

Retry only after reading it. A prompt that fails on every agent will fail again.

A retry policy that works

RETRYABLE = {
    "NO_DEVICE_ONLINE", "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"])          # 4xx: 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 a fleet does 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 our side with a stack trace and returned with none. Nothing about a file path, a query or a hostname reaches a caller. Quote the request_id and the trace is recoverable.