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.
Recommended client-side handling
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.