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 | |
|---|---|
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 whole trace is recoverable |
details | Structured 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
| 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 requests already queued |
NOT_FOUND | 404 | No | No such request — or not yours |
CONFLICT | 409 | No | Idempotency key reused with a different body |
PAYLOAD_TOO_LARGE | 413 | No | prompt over 100 000 characters, or body over 1 MiB |
RATE_LIMITED | 429 | After Retry-After | Too many API calls per minute on this key |
CHANNEL_NOT_ENTITLED | 403 | No | You named a channel your plan does not include |
SUBSCRIPTION_INACTIVE | 403 | No | No active plan. Choose one on the Billing page |
QUOTA_EXCEEDED | 429 | After details.resets_at | This month's token allowance for the channel is used up |
FEATURE_NOT_IN_PLAN | 403 | No | Dashboard only: a page or download your plan does not include |
JOB_CANCELLED | 499 | No | Cancelled by you or from the dashboard |
INTERNAL_ERROR | 500 | Yes | Ours, and it never carries internal detail |
JOB_FAILED | 502 | Maybe | Every attempt was used without a response. details.attempts says how many |
NO_DEVICE_ONLINE | 503 | Yes | Revoye Desk is not online on any of your devices |
NO_AGENT_AVAILABLE | 503 | Yes | No capacity is free right now to process the request |
PROVIDER_RATE_LIMITED | 503 | Yes | An hourly limit was reached for the model that would process the request |
JOB_TIMEOUT | 504 | Yes | Held 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 limit | What to do | |
|---|---|---|
RATE_LIMITED (429) | API calls per minute, per API key | Respect 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 request | Retry 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.