Error codes: API v4

Errors return a JSON body with a detail key:

{ "detail": "Access denied: insufficient credits or invalid subscription" }

Every response, successful or not, carries an X-Request-ID header. Quote it when contacting support.

At a glance

Status Meaning Retry? Billed
400 Unsafe or unusable target No — fix the request No
403 Auth, subscription or credit problem No — fix the key or balance No
404 Job not found No No
405 Wrong HTTP method No — use POST No
422 Request failed validation No — fix the request No
429 Rate limited Yes — honour Retry-After No
503 Temporarily unavailable Yes — honour Retry-After No

No error response is billed. You are charged only for domains that come back with a result.

400 — unsafe target

{ "detail": "Refusing to fetch unsafe target: <reason>" }

The requested target resolves somewhere we will not fetch — a private, internal or link-local address. Not retriable: the domain itself is the problem.

On a bulk request this does not fail the batch. The affected row carries an error string instead and the call still returns 200. An async job records the same row error.

Async batch submission returns 400 for two further cases:

  • domains list cannot be empty
  • webhook_url rejected: <reason>. Webhook URLs must use http or https and point to a publicly routable address.

403 — authentication, subscription or credits

Two shapes:

{ "detail": "Missing required parameter: apiKey" }

You did not send the apiKey query parameter.

{ "detail": "Access denied: insufficient credits or invalid subscription" }

The key is not recognised, the subscription is not active, or the credit balance is exhausted. Check your balance and subscription — see Account balance information — and contact support@whoisxmlapi.com if both look correct. Do not retry automatically.

404 — job not found

{ "detail": "Job abc123 not found" }

Returned by GET /api/v4/jobs/{job_id}. Only the API key that submitted a job can read it, and an unknown job and a job belonging to someone else return the identical 404 by design. Jobs are kept for at least 24 hours after submission (the 24-hour window restarts each time a job is updated); after that they read as not found.

405 — wrong method

/api/v4/categorize and /api/v4/categorize/bulk are POST only. A GET returns 405 Method Not Allowed. The domain goes in the JSON body; apiKey stays a query parameter.

422 — validation failed

Either a structured validation array:

{
  "detail": [
    {
      "type": "string_too_long",
      "loc": ["body", "domain"],
      "msg": "String should have at most 253 characters",
      "input": "…"
    }
  ]
}

or an explicit message, for example:

{ "detail": "coverage must be a number between 0.01 and 0.99" }

Input limits:

Field / parameter Limit
domain 253 characters
url 2,048 characters
title 2,048 characters
description 4,096 characters
body_text 20,000 characters
domains (bulk) 100 entries
domains (async) 10,000 entries
limit (job results) between 1 and 10,000
limit (job list) between 1 and 100
offset (jobs) 0 or more
output_mode top1 or set
coverage between 0.01 and 0.99
pack a known pack id; an unknown pack is a 422

Over-limit input returns 422, not 413. Fix the request — it is never retriable as sent.

429 — rate limited

{ "detail": "Rate limit exceeded. Please try again later." }

Requests are limited per API key. A second, independent throttle exists on the billing side and returns a similar 429:

{ "detail": "Rate limit exceeded — please retry shortly" }

Both of these carry a Retry-After header giving the number of seconds to wait — the billing throttle sends Retry-After: 1. Honour it. Do not retry in a tight loop. Rate-limited requests are never billed. See Credits and limits for the limits themselves.

503 — temporarily unavailable

Several transient conditions share this status. All are retriable with backoff, and all carry a Retry-After header where one applies.

detail Cause
Too many concurrent classifications; retry shortly. Concurrency limit reached on the single-domain endpoint. The body also carries limit and stage (inference or crawl_slot, diagnostic only). The bulk endpoint queues instead of shedding.
Temporary DNS failure (dns_timeout): the domain's name servers did not answer in time. Retry the request. The domain's name servers did not answer our lookup in time on the single-domain endpoint. It carries Retry-After: 5. On the bulk endpoint and in an async job the row carries temporary DNS failure (dns_timeout): retry this domain instead (see below).
Classification pipeline not initialized The service is still starting. Retry after a brief delay.
HCC calibration not available: <reason> You requested output_mode=set and the calibration is unavailable. Retry, or fall back to the default output_mode=top1.
Billing service temporarily unavailable — please retry The billing backend is unavailable. Retry with backoff.
Async batch submission is temporarily unavailable — please retry, or use POST /api/v4/categorize/bulk. Async submission cannot be accepted right now. Use the synchronous bulk endpoint, or retry.
Policy pack not loaded: … / Taxonomy not loaded: … An opt-in block you requested is unavailable while the service is degraded. Retry, or drop the opt-in parameter — the base classification still works.
Job manager not initialized The async job service is still starting. Retry.

A 502 can also reach you from the gateway in front of the service rather than from the API itself. Treat it like a 503: retry with backoff.

Bulk and async jobs: per-item errors, not batch failure

POST /api/v4/categorize/bulk does not fail the whole batch when one domain fails. As long as the request itself was valid and authenticated you get 200 with a row for every domain — check each row's error field:

{
  "domain": "internal.example",
  "categories": [],
  "error": "unsafe fetch target: <reason>"
}

A completed async job's rows carry the same per-row error. When a domain's name servers do not answer in time, the row's error is temporary DNS failure (dns_timeout): retry this domain: send that domain again. To retry a domain from an async job, submit it in a new job, with a new Idempotency-Key if you send one.

Rows that fail are not charged.

Retry summary

  • Do not retry: 400, 403, 404, 405, 422. These need a fixed request, key or balance — waiting changes nothing.
  • Retry, honouring Retry-After: 429, 503, and a gateway 502.
  • Retry with a stable Idempotency-Key header. A repeat within 24 hours is processed again but normally charges credits_charged: 0 if an earlier request with that key was already billed; a retry after a 429 or 503 bills normally. On async batch submission, a repeat with the same key returns the original job.