API errors: status codes, reveal codes and hold codes
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, never as an HTML page.
{
"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",
"credits_needed": 1,
"credits_remaining": 0
}
}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 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 that needs more credits than are left, 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. |
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 | Not enough credits left for this reveal. Carries resets_at, credits_needed and credits_remaining. A featured reveal can be refused with credits still left, for example when it needs 3 and you have 2. | If credits_remaining is above 0, an ordinary listing may still fit. Otherwise wait until resets_at, or keep working from the metrics, which cost nothing. |
FEATURED_REQUIRES_PRO | 402 | The Free plan against a featured listing. The name is kept for compatibility. Any paid plan can reveal featured listings. | 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_FAILED | 500 | An unexpected server-side failure. | The one hold code that means “try again”. Retry with backoff. |
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 reveal period rolls over. |
credits_needed | QUOTA_EXHAUSTED | What this reveal would have cost, in credits. |
credits_remaining | QUOTA_EXHAUSTED | Credits left in the current period. |
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
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":
if err["credits_remaining"] > 0:
print(f"{listing_id}: needs {err['credits_needed']} credits, skipping")
return None
raise SystemExit(f"out of credits 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']}")00aLaXEG: held until 2026-09-24T09:15:00.000Z, skippingWhat 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.
curl "https://getrevised.com/api/v1/domains?traffic=huge" \
-H "Authorization: Bearer $REVISED_API_KEY"{
"error": {
"code": "invalid_request",
"message": "`traffic` must be one of: any, 100k, 1m."
}
}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
categorycode is refused, not answered with an empty page. There is no/api/v1/categoriesendpoint; read codes off thecategoryfield of a page of results. See 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.
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.
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. Taking a hold is safe to repeat too — asking for a hold you already have returns the existing one, not a second hold. Extending is not: a hold extends once, so a repeated extend after a lost response comes back ALREADY_EXTENDED. Check GET /api/v1/holds before retrying one.