Revised

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.

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",
    "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.codeStatusWhen
invalid_request400A 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.
unauthorized401Missing, malformed, unknown or revoked key. Carries WWW-Authenticate: Bearer realm="revised".
upgrade_required402The 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.
forbidden403The 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_found404No such listing or hold — or one you cannot see, which includes a listing newer than your plan’s access delay.
method_not_allowed405The endpoint does not implement that method. Carries an Allow header.
rate_limited429Over the per-minute ceiling. Carries Retry-After. See Rate limits.
server_error500A 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_codeStatusMeaningWhat to do
QUOTA_EXHAUSTED402Not 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_PRO402The 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_OTHER403Another 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_FOUND404No such listing, or one that is not publicly revealable (draft, blocked, delisted).Drop the id.
SIGN_IN_REQUIRED401No authenticated account behind the call.Send a valid key.
REVEAL_FAILED500An 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_codeStatusMeaningWhat to do
REVEAL_REQUIRED403You 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_OTHER403Somebody else’s hold is running. Carries expires_at.Try again after expires_at.
HOLD_CAP_REACHED403Every hold this plan allows is already running. Carries limit.End one with DELETE /api/v1/holds/{id} first.
HOLD_COOLDOWN403Your 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_PRO402The plan allows no holds at all.Upgrade, or work without holds.
ALREADY_EXTENDED400A hold extends once, and this one has.Nothing. It runs to expires_at and then ends.
HOLD_OVER400That hold has already ended — expired, released or registered.Nothing to extend or release. Your cooldown on the listing is running.
HOLD_FAILED500An unexpected server-side failure.The one hold code that means “try again”. Retry with backoff.
HOLD_NOT_FOUND404No 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_FOUND404No listing with that id, or one that has left the directory.Drop the id.
SIGN_IN_REQUIRED401No authenticated account behind the call.Send a valid key.

Extra fields

A refusal adds only the fields its code needs.

FieldAppears onWhat it is
resets_atQUOTA_EXHAUSTEDWhen the reveal period rolls over.
credits_neededQUOTA_EXHAUSTEDWhat this reveal would have cost, in credits.
credits_remainingQUOTA_EXHAUSTEDCredits left in the current period.
expires_atHELD_BY_OTHERWhen the listing frees up. Never who holds it.
available_atHOLD_COOLDOWNWhen your own cooldown on that listing ends.
limitHOLD_CAP_REACHEDThe 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":
        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']}")
Output
text
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?traffic=huge" \
  -H "Authorization: Bearer $REVISED_API_KEY"
Response
json
{
  "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 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.
  • 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

StatusRetry?
400No. Fix the request.
401No. Fix the key.
402Only after resets_at, or after the plan changes.
403Not immediately. expires_at or available_at says when, or free a hold slot first.
404No.
405No. Read the Allow header.
429Yes, after Retry-After seconds.
500Yes, 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.

Type to search…

↑↓ navigate↵ openesc close