Skip to content

Errors

Updated
Reading time
5 min
Level
beginner

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

{
  "error": {
    "code": "QUOTA_EXCEEDED",
    "message": "Monthly token limit reached for Revoye Cloud. Resets on Nov 13, 2026.",
    "request_id": "req_01JAY7Q2K8",
    "details": {
      "channel": "cloud",
      "limit": 1000000,
      "used": 1000000,
      "resets_at": "2026-11-13T00:00:00.000Z"
    }
  }
}
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.job_id whenever a request was already accepted

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 — or 1 000 requests already queued
NOT_FOUND404NoNo such request — 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-AfterToo many API calls per minute on this key
CHANNEL_NOT_ENTITLED403NoYou named a channel your plan does not include
SUBSCRIPTION_INACTIVE403NoNo active plan. Choose one on the Billing page
QUOTA_EXCEEDED429After details.resets_atThis month's token allowance for the channel is used up
FEATURE_NOT_IN_PLAN403NoDashboard only: a page or download your plan does not include
JOB_CANCELLED499NoCancelled by you or from the dashboard
INTERNAL_ERROR500YesOurs, and it never carries internal detail
JOB_FAILED502MaybeEvery attempt was used without a response. details.attempts says how many
NO_DEVICE_ONLINE503YesRevoye Desk is not online on any of your devices
NO_AGENT_AVAILABLE503YesNo capacity is free right now to process the request
PROVIDER_RATE_LIMITED503YesAn hourly limit was reached for the model that would process the request
JOB_TIMEOUT504YesHeld as long as you asked; the request continues

The three that are about capacity

These describe whether a request can be processed right now. Treating them as outages will make you retry aggressively against a system that is behaving exactly as designed.

NO_DEVICE_ONLINE — Revoye Desk is not online on any of your devices. You only see this when you asked not to wait, gave no callback_url, and no fallback to Revoye Cloud is possible; with wait: true, or with a webhook, the request waits in your queue instead. details.job_id names the request, which stays queued until Revoye Desk is back. The fix is to start Revoye Desk, or to stop asking for an immediate response when it is offline.

NO_AGENT_AVAILABLE — no capacity is free right now to process the request, for the model you asked for. Like NO_DEVICE_ONLINE, it only appears when you asked not to wait, and the request stays queued under details.job_id. Back off and retry, or omit provider so Revoye can choose. GET /v1/status shows what is available.

PROVIDER_RATE_LIMITED — an hourly limit was reached: one you set yourself for Revoye Desk, or, on Revoye Cloud, the model is temporarily at capacity. Retry later, or let Revoye choose the model. Do not raise a limit you set without thinking about why you set it.

Your plan: CHANNEL_NOT_ENTITLED, QUOTA_EXCEEDED, SUBSCRIPTION_INACTIVE

These come from your subscription, and retrying sooner changes nothing.

CHANNEL_NOT_ENTITLED — you sent channel: "cloud" (or desk) and your plan does not include it. Send auto, or change plan. Revoye refuses rather than re-routing, so a request that named a channel is never processed somewhere you did not ask for.

QUOTA_EXCEEDED — the monthly token allowance for the channel is used up, or the request would not fit in what is left. details carries channel, limit, used and resets_at; the allowance renews on your subscription date, not on the calendar month. With auto on a plan with both channels, Revoye uses the other channel first and only refuses when both are used up.

SUBSCRIPTION_INACTIVE — there is no active plan on the account: never chosen, or ended after its grace period. Nothing will be processed until there is one.

RATE_LIMITED versus PROVIDER_RATE_LIMITED

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

Whose limitWhat to do
RATE_LIMITED (429)API calls per minute, per API keyRespect Retry-After. You are calling the API too fast — usually by polling
PROVIDER_RATE_LIMITED (503)An hourly limit on the model that would process the requestRetry later, or let Revoye choose the model

JOB_FAILED and details.attempts

JOB_FAILED means every attempt was used and none produced a response. details carries job_id, status and attempts, the number of attempts made. When the AI model declined to answer the request, the message says so — retrying the same request will not help, so change it first.

Retry other failures once, with the same idempotency key. A request that fails on every attempt several times in a row will fail again.

A retry policy that works

RETRYABLE = {
    "RATE_LIMITED", "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"])          # fix the request or the plan; do not retry
 
        if code == "RATE_LIMITED":
            delay = int(r.headers.get("Retry-After", delay))
 
        sleep(delay + random.uniform(0, 1))               # jitter, so many clients 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 was accepted and you never saw the response, the retry returns that request rather than creating a second one.

Internal errors

INTERNAL_ERROR is logged on our side and returned with no internal detail. Nothing about a file path, a query or a hostname reaches a caller. Quote the request_id and the trace is recoverable.