Getting started
Errors & the response envelope
Every route answers with the same two shapes. Successes carry a data object (or array); failures carry an errors list. Both carry a request_id you can quote to support.
Success
200 OKAPISingleResponse
{"request_id": "req_5d1e9b3c","timestamp": "2026-10-11T16:20:03.118402+00:00","data": {"plan": "Starter","billing_status": "active","credits_remaining": 61218,"credits_per_month": 80000,"credits_reset_at": "2026-11-04T09:12:40+00:00","current_period_end": "2026-11-05T09:10:17+00:00","cancel_at_period_end": false,"rate_limit_per_second": 30,"max_api_keys": 5}}
A bulk lookup uses the same envelope, with its per-item results as an array inside data. The HTTP status is always 200 on success.
Failure
422 Unprocessable Entityvalidation
{"errors": [{"code": "INVALID_FORMAT","message": "Invalid IP address","field": "ip","details": null}],"request_id": "req_8f2c1a7e"}
401 Unauthorizedauthentication
{"errors": [{"code": "UNAUTHORIZED","message": "Authentication required. Pass a Bearer token or API key (X-API-Key header or ?key= query param).","field": null,"details": null}],"request_id": "req_8f2c1a7e"}
402 Payment Requiredcredits
{"errors": [{"code": "INSUFFICIENT_CREDITS","message": "Insufficient credits. This request costs 1 credit(s).","field": null,"details": {"cost": 1}}],"request_id": "req_8f2c1a7e"}
errors is a list because validation can fail on several fields at once. Each entry has a stable machine-readable code, a human message, the offending field when there is one, and optional details. Branch on code, show message.
Codes by status
| Status | Codes | Meaning |
|---|---|---|
| 400 / 422 | VALIDATION_ERROR · INVALID_FORMAT · INVALID_FIELD · MISSING_FIELD · INVALID_EMAIL · VALUE_OUT_OF_RANGE · VALUE_TOO_LONG · VALUE_TOO_SHORT | The request is malformed. Fix the field named in the error; never retry as-is. |
| 401 | UNAUTHORIZED · INVALID_TOKEN | No key, or an invalid or revoked one. |
| 402 | INSUFFICIENT_CREDITS | Balance too low for this call. Top up or wait for the monthly reset. |
| 403 | FORBIDDEN · INSUFFICIENT_PERMISSIONS | Authenticated but not allowed (e.g. a deactivated account). |
| 404 | NOT_FOUND | Unknown route; check the path and the method. |
| 429 | RATE_LIMIT_EXCEEDED | Over the plan's request rate or concurrency limit. Wait for Retry-After, then retry. |
| 500 | INTERNAL_ERROR · UNKNOWN_ERROR · DATABASE_* | Our fault. Safe to retry with backoff; include the request_id if it persists. |
| 503 | EXTERNAL_SERVICE_ERROR · EXTERNAL_SERVICE_TIMEOUT · EXTERNAL_SERVICE_UNAVAILABLE | An upstream (DNS resolver, WHOIS server) failed. Retry with backoff. |
Handling errors well
- Check
res.okfirst, then readerrors[0].code. Don't parse messages; they are for humans and may change. - Retry only
429and5xx, with exponential backoff.4xxresponses will keep failing until you change the request, the key, or the balance. - Log the
request_idwith every failure; it links the call to our request logs. - An engine that is unavailable during a lookup does not fail the request; the remaining engines still answer.