URL: /docs/api/errors

---
title: Errors
description: The error envelope, every status code, and the reveal_code and hold_code a refused write adds.
---

Every refusal on this API — including a `405` and a `429` — comes back as the same JSON envelope with an HTTP status code. Nothing here ever answers an API path with an HTML page.

```json
{
  "error": {
    "code": "upgrade_required",
    "message": "Your reveals for this month are used up.",
    "resets_at": "2026-10-01T00:00:00.000Z",
    "reveal_code": "QUOTA_EXHAUSTED"
  }
}
```

`error.code` and `error.message` are always present. `message` is human-readable and may change. `code` is the stable machine token: branch on it, not on the prose.

## `error.code`

The HTTP-level reason. These eight are the complete set, and they are enumerated in the [OpenAPI document](https://getrevised.com/api/v1/openapi.json) so a generated client types them as a union.

| `error.code` | Status | When |
| --- | --- | --- |
| `invalid_request` | 400 | A documented parameter carried a value the API cannot honour, or a cursor it did not issue. The message names the parameter and what it accepts. |
| `unauthorized` | 401 | Missing, malformed, unknown or revoked key. Carries `WWW-Authenticate: Bearer realm="revised"`. |
| `upgrade_required` | 402 | The plan does not include what was asked for: the `since` feed, a reveal past the monthly ceiling, a reveal of a `featured` listing on free, or a hold on a plan with none. |
| `forbidden` | 403 | The write is refused by the current state of the world, not by the plan: the listing is held by another account, your hold cap is full, your own cooldown is running, or you have not revealed the listing yet. All four become possible again with time. |
| `not_found` | 404 | No such listing or hold — or one you cannot see, which includes a listing newer than your plan's access delay. |
| `method_not_allowed` | 405 | The endpoint does not implement that method. Carries an `Allow` header. |
| `rate_limited` | 429 | Over the per-minute ceiling. Carries `Retry-After`. See [Rate limits](/docs/api/rate-limits). |
| `server_error` | 500 | A fault on our side. Always reportable; safe to retry with backoff. |

## Refused writes carry a second code

`error.code` says which HTTP category a refusal falls into. On a refused reveal or hold, that is not enough to decide what to do next — `REVEAL_REQUIRED`, `HELD_BY_OTHER` and `HOLD_COOLDOWN` are three completely different next actions behind the same status. So a refused write adds a specific code, and **that is the one to branch on**.

### `reveal_code`

On `POST /api/v1/domains/{id}/reveal`.

| `reveal_code` | Status | Meaning | What to do |
| --- | --- | --- | --- |
| `QUOTA_EXHAUSTED` | 402 | The month's reveal allowance is gone. Carries `resets_at`. | Wait until `resets_at`, or keep working from the metrics, which cost nothing. Retrying before then will not work. |
| `FEATURED_REQUIRES_PRO` | 402 | A free plan against a `featured` listing. | Filter with `tier=open`, or upgrade. |
| `HELD_BY_OTHER` | 403 | Another account is holding the listing. Carries `expires_at` — never who holds it. | Come back after `expires_at`. Only a *first* reveal is refused; a reveal already granted re-opens regardless. |
| `NOT_FOUND` | 404 | No such listing, or one that is not publicly revealable (draft, blocked, delisted). | Drop the id. |
| `SIGN_IN_REQUIRED` | 401 | No authenticated account behind the call. | Send a valid key. |
| `REVEAL_FAILED` | 500 | An unexpected server-side failure. | The one reveal code that means "try again". |

### `hold_code`

On `POST /api/v1/domains/{id}/hold`, `POST /api/v1/holds/{id}/extend` and `DELETE /api/v1/holds/{id}`.

| `hold_code` | Status | Meaning | What to do |
| --- | --- | --- | --- |
| `REVEAL_REQUIRED` | 403 | You have not revealed this listing. A hold is a reservation of something you can already see. | Call `POST /api/v1/domains/{id}/reveal` first. A name disclosed by an unlimited plan is not a reveal on record. |
| `HELD_BY_OTHER` | 403 | Somebody else's hold is running. Carries `expires_at`. | Try again after `expires_at`. |
| `HOLD_CAP_REACHED` | 403 | Every hold this plan allows is already running. Carries `limit`. | End one with `DELETE /api/v1/holds/{id}` first. |
| `HOLD_COOLDOWN` | 403 | **Your own** hold on that listing ended within the last 30 days. Carries `available_at`. | Wait until `available_at`. Anybody else may hold it right now — somebody else's ended hold never produces this code. |
| `HOLDS_REQUIRE_PRO` | 402 | The plan allows no holds at all. | Upgrade, or work without holds. |
| `ALREADY_EXTENDED` | 400 | A hold extends once, and this one has. | Nothing. It runs to `expires_at` and then ends. |
| `HOLD_OVER` | 400 | That hold has already ended — expired, released or registered. | Nothing to extend or release. Your cooldown on the listing is running. |
| `HOLD_NOT_FOUND` | 404 | No hold with that id, or not this account's. | One code for both deliberately: distinguishing them would tell a stranger that a hold id exists. |
| `NOT_FOUND` | 404 | No listing with that id, or one that has left the directory. | Drop the id. |
| `SIGN_IN_REQUIRED` | 401 | No authenticated account behind the call. | Send a valid key. |

### Extra fields

A refusal adds only the fields its code needs.

| Field | Appears on | What it is |
| --- | --- | --- |
| `resets_at` | `QUOTA_EXHAUSTED` | When the monthly reveal allowance rolls over. |
| `expires_at` | `HELD_BY_OTHER` | When the listing frees up. Never who holds it. |
| `available_at` | `HOLD_COOLDOWN` | When your own cooldown on that listing ends. |
| `limit` | `HOLD_CAP_REACHED` | The plan's ceiling on concurrent holds. |

## Reading a refusal

```python
import os, requests

BASE = "https://getrevised.com/api/v1"
AUTH = {"Authorization": f"Bearer {os.environ['REVISED_API_KEY']}"}

def reveal(listing_id: str) -> dict | None:
    res = requests.post(f"{BASE}/domains/{listing_id}/reveal", headers=AUTH, timeout=30)
    if res.ok:
        return res.json()

    err = res.json()["error"]
    code = err.get("reveal_code", err["code"])

    if code == "QUOTA_EXHAUSTED":
        raise SystemExit(f"out of reveals until {err['resets_at']}")
    if code == "HELD_BY_OTHER":
        print(f"{listing_id}: held until {err['expires_at']}, skipping")
        return None
    if code == "NOT_FOUND":
        print(f"{listing_id}: gone from the directory")
        return None
    raise RuntimeError(f"{res.status_code} {code}: {err['message']}")
```

```text Output
00aLaXEG: held until 2026-09-24T09:15:00.000Z, skipping
```

## What is refused and what is ignored

A parameter this API documents, carrying a value it cannot honour, is a `400`. The reason is that a silently dropped filter always returns **more** rows than you asked for — a bigger, plausible-looking page instead of an error, with no way to find out.

```bash
curl "https://getrevised.com/api/v1/domains?spam=clean" \
  -H "Authorization: Bearer $REVISED_API_KEY"
```

```json Response — 400
{
  "error": {
    "code": "invalid_request",
    "message": "`spam` must be one of: low, moderate."
  }
}
```

Parameters this API does not document are ignored, so analytics and cache-busting keys are safe to leave on the query string.

Two refusals worth knowing about specifically:

- **An unknown `category` code is refused**, not answered with an empty page. There is no `/api/v1/categories` endpoint; read codes off the `category` field of a page of results. See [Filters](/docs/api/filters).
- **A cursor this endpoint did not issue is a `400`** — never an empty page and never a silent resume from the middle of the feed. See [Pagination](/docs/api/pagination).

## Methods and CORS

A method an endpoint does not implement is `405` `method_not_allowed` with an `Allow` header, in this same JSON envelope. Nothing here answers an API path with an HTML page.

Cross-origin calls work. Every response carries `Access-Control-Allow-Origin: *`, `OPTIONS` answers the preflight, and the rate-limit headers are exposed to script.

<Warning>
  There is no cookie and no session on this API — only the key you send. An open origin policy therefore gives a page nothing it did not already have, which is exactly why you must never send a key from a page you do not control.
</Warning>

## Retrying

| Status | Retry? |
| --- | --- |
| 400 | No. Fix the request. |
| 401 | No. Fix the key. |
| 402 | Only after `resets_at`, or after the plan changes. |
| 403 | Not immediately. `expires_at` or `available_at` says when, or free a hold slot first. |
| 404 | No. |
| 405 | No. Read the `Allow` header. |
| 429 | Yes, after `Retry-After` seconds. |
| 500 | Yes, with backoff. |

Reveals are idempotent, so a reveal whose response you lost to a network failure is safe to repeat: the repeat returns the original `revealed_at` and costs nothing. Holds are not idempotent — check `GET /api/v1/holds` before re-sending one.
