Errors

Every error response is a JSON body with at least an error field (machine-readable code) and usually a message (human-readable text). The HTTP status code carries the broad category.

HTTP status codes

Code When What you should do
200 Success Process the response
202 Accepted (async batch queued) Poll the status_url or wait for the webhook
400 Bad request — malformed body or unsupported parameter Fix the request
401 Authentication failed Check the Authorization header
402 Payment required — monthly quota exhausted Upgrade plan or wait for cycle reset
404 Resource not found (batch ID, route…) Verify the URL / ID
419 CSRF token expired (dashboard only) Reload the form
422 Validation error on input Fix the input field reported in error
429 Rate-limited Retry after Retry-After seconds
500 Server error (rare) Retry with backoff; if persistent, contact us
503 Service unavailable (maintenance) Retry with backoff

Common error codes

401 Unauthorized

{ "error": "unauthorized", "message": "Missing or invalid API key" }

Causes:

  • No Authorization: Bearer ... header
  • API key revoked
  • Typo in the key

402 quota_exceeded

{
  "error": "quota_exceeded",
  "message": "Monthly quota exhausted. Upgrade your plan or wait for the next cycle.",
  "plan_code": "free",
  "plan_quota": 100,
  "plan_used": 100,
  "plan_remaining": 0,
  "credits_needed": 1,
  "resets_at": "2026-06-01T00:00:00Z",
  "upgrade_url": "https://phonevalidationapi.com/pricing"
}

Use resets_at to schedule retries; upgrade_url if you want to send your team there.

422 phone_required

{ "error": "phone_required" }

The phone field is missing or empty in your request body.

422 invalid_country

{ "error": "invalid_country", "detail": "country must be ISO-3166-1 alpha-2 (e.g. FR, US)" }

Country code is not a valid ISO-3166 alpha-2 (FR, US, GB, …).

400 level_not_available

{
  "error": "level_not_available",
  "detail": "Level 'hlr' is not available yet. Available: basic"
}

You requested a level that's not enabled on this account or not yet shipped (e.g. hlr while we're still in v1).

429 rate_limited

{ "error": "rate_limited" }

With headers:

X-RateLimit-Limit: 120
X-RateLimit-Remaining: 0
X-RateLimit-Reset: 1714850460
Retry-After: 17

Wait Retry-After seconds and retry.

Async-batch-specific errors

422 phones_required

phones field is missing, empty, or not an array.

422 too_many_phones

{ "error": "too_many_phones", "limit": 10000, "received": 12345 }

Split your batch into chunks of ≤10,000.

422 webhook_url_invalid / webhook_url_private

The webhook_url you provided is malformed or points to a private/loopback IP. We block private IPs to prevent SSRF.

404 batch_not_found

The bat_<32hex> ID doesn't exist or doesn't belong to your account.

import requests, time

def call_with_retry(payload, max_retries=3):
    for attempt in range(max_retries):
        r = requests.post(URL, headers=HEADERS, json=payload, timeout=10)

        if r.status_code == 200:
            return r.json()

        if r.status_code == 429:
            wait = int(r.headers.get('Retry-After', 2 ** attempt))
            time.sleep(wait)
            continue

        if r.status_code == 402:
            data = r.json()
            raise RuntimeError(f"Quota exhausted, resets at {data['resets_at']}")

        if r.status_code in (500, 503):
            time.sleep(2 ** attempt)
            continue

        # 4xx other than 429/402 are fatal — input error, not transient
        r.raise_for_status()

    raise RuntimeError("max retries exceeded")

When in doubt, log everything

The error field is stable and intended for branching. The message is human-friendly but may evolve — never write production code that parses it.