Errors
Every MobileValidate API error code with its HTTP status, whether to retry, what causes it and how to fix it, plus the error response format.
When a whole request can't be processed, the API returns a non-2xx status and one JSON error object. Each error has a stable code to branch on, a retryable flag, and a request_id to quote to support. Problems with single rows, such as invalid, duplicate, suppressed, unknown or unsupported numbers, are reported inside results instead. They never fail the request and are never billed.
What does an error response look like?
{
"error": {
"code": "service_disabled",
"message": "The check 'signal.registered' is available in bulk jobs only (POST /v1/jobs).",
"status": 403,
"retryable": false,
"param": "checks[0]",
"doc_url": "https://mobilevalidate.com/docs/errors#service_disabled",
"request_id": "req_0VWF4BN8PIsgkUh3EvUj",
"suggestion": "Create a bulk job (POST /v1/jobs) for this check, or remove it from checks."
}
}codeis stable. Branch on it, not onmessage, whose wording may change.parampoints to the field at fault (checks[1],numbers,emails[3],max_cost.amount), or isnull. Your values are never echoed back.retryablesays whether sending the same request again later can succeed.request_idalso comes back as theX-Request-Idheader on every response.suggestion(optional) is a plain-English hint on how to fix the request, for example a missing country code or a likely typo in an e-mail domain. It never repeats the value you sent. Show it to developers; don't parse it.
Which codes can the API return?
| Code | HTTP | Retryable |
|---|---|---|
invalid_request | 400 | no |
too_many_numbers | 400 | no |
test_number_only | 400 | no |
invalid_cursor | 400 | no |
unauthorized | 401 | no |
insufficient_balance | 402 | no |
cost_limit_exceeded | 402 | no |
insufficient_scope | 403 | no |
service_disabled | 403 | no |
sandbox_magic_only | 403 | no |
suspected_enumeration | 403 | no |
not_found | 404 | no |
idempotency_key_reused | 409 | no |
idempotency_request_in_progress | 409 | yes |
test_key_exists | 409 | no |
payload_too_large | 413 | no |
rate_limited | 429 | yes |
daily_cap_reached | 429 | yes (after the reset) |
spend_cap_reached | 429 | no |
internal_error | 500 | yes |
temporarily_unavailable | 503 | yes |
What does each code mean?
invalid_request
400, not retryable. The body is malformed JSON, breaks the schema (missing or unknown field, wrong type), or uses an unknown check code. It is also returned when e-mails are sent without an e-mail check or numbers without a phone check (param: "checks"), for more than 20 checks, and when identifiers × applicable checks exceed 2,000 per lookup or 100,000 per job. Fix: read param and message, and correct that field. GET /v1/services lists valid codes.
too_many_numbers
400, not retryable. More than 100 numbers + e-mails on POST /v1/lookup, more than 50,000 on POST /v1/jobs (or in an uploaded CSV, param: "file"), or more than 10 in a job or estimate with the public sandbox key. Fix: split the list, or use a bulk job for anything over 100.
test_number_only
400, not retryable. A live key was used with a test number ("Test numbers can only be used with test keys.") or a test.mobilevalidate.com address ("Test e-mail addresses can only be used with test keys."). Fix: use your test key for test data and real identifiers with live keys.
invalid_cursor
400, not retryable. The after cursor for job results is malformed. Fix: use next_cursor from the previous page exactly as returned, or start again without after.
unauthorized
401, not retryable. The key is missing, malformed, unknown, revoked or expired, or was used from an IP address outside its allowlist. All cases look the same on purpose. Fix: check the Authorization: Bearer header, the key prefix and your outgoing IP address, including IPv6 (see authentication).
insufficient_balance
402, not retryable. Your balance can't cover the maximum possible cost of the request, which is reserved before checking. Fix: top up, or send fewer identifiers or checks. Cached and inconclusive answers are released afterwards, so the final charge is often lower than the reservation.
cost_limit_exceeded
402, not retryable. The maximum possible cost is higher than the max_cost you sent. Fix: raise max_cost or shrink the request. POST /v1/jobs/estimate shows the maximum cost for free.
insufficient_scope
403, not retryable. The key doesn't have the scope this endpoint needs, for example webhooks:manage or account:read. Fix: use a key with that scope, or ask for one.
service_disabled
403, not retryable. One of the checks is switched off, isn't enabled for your account, or is bulk only and was sent to POST /v1/lookup ("… is available in bulk jobs only (POST /v1/jobs)"). param names the check (checks[i]); it is null when real-time checks are not enabled for the whole account. Fix: run bulk-only services in a job, or remove the check. Services marked coming soon, such as number.hlr, give this answer until they launch.
sandbox_magic_only
403, not retryable. The public sandbox key (the key in the docs examples) accepts only the documented test numbers and e-mail addresses. param names the first other identifier (numbers[2], emails[0]); an invalid number counts as another identifier. The sandbox's other limits have their own codes: jobs over 10 rows return too_many_numbers, and webhooks return invalid_request. Fix: use a test value, or get a personal test key to test your own data. The suggestion field links both.
suspected_enumeration
403, not retryable. The request looks like a scan: 20 or more consecutive numbers (param: "numbers"), 20 or more addresses on one domain differing only by digits or separators, or (live keys) 50 or more such addresses from your account in one UTC day (param: "emails"). Fix: only check identifiers you actually hold, such as customers, sign-ups and leads. See rate limits and abuse.
not_found
404, not retryable. The id doesn't exist, belongs to another account, or its results were purged ("Results for this job were purged."). For privacy, we don't say which. An unknown path also returns not_found, before the key is checked; the suggestion names the closest endpoint. Fix: check the id and the path, and fetch results within your retention period.
idempotency_key_reused
409, not retryable. The Idempotency-Key was already used within 24 hours with a different request body. Fix: use a new key for each distinct request, and reuse a key only for exact retries.
idempotency_request_in_progress
409, retryable. The first request with this Idempotency-Key is still running. Fix: wait briefly and retry with the same key and body. You'll get the stored response.
test_key_exists
409, not retryable. POST /v1/test-keys (the "get a test key" form): no more test keys can be issued for this e-mail address right now (at most 3 in 30 days). Keys already issued are never shown again. Fix: use a key you received earlier, or request access and we'll set you up.
payload_too_large
413, not retryable. The body is larger than 1 MB (4 MB for POST /v1/jobs and /v1/jobs/estimate). Fix: split the list into several jobs.
rate_limited
429, retryable. More than 10 requests per second (burst 20) on this key, or, for the public sandbox key, more than 30 requests per minute or 1,000 per day from your IP address. Fix: wait for the Retry-After seconds, then retry with backoff. Batch numbers into one request instead of sending one request per number.
daily_cap_reached
429, retryable after the reset. Your account's daily cap on identifiers is used up. Caps reset at 00:00 UTC, and Retry-After counts down to it. Fix: retry after the reset shown in GET /v1/limits, or ask for a higher cap.
spend_cap_reached
429, not retryable. The key's daily spend cap (agent keys) has been reached. It resets at 00:00 UTC. Fix: wait for the reset, raise the cap or use another key. Retrying right away won't help.
internal_error
500, retryable. Something failed on our side. Fix: retry with backoff, using the same Idempotency-Key so the retry is safe. If it keeps happening, send us the request_id.
temporarily_unavailable
503, retryable. The service is overloaded or every route for a check is down. Fix: retry after Retry-After. A request that fails this way has not been charged.
Are there codes outside the HTTP API?
- MCP server:
confirmation_requiredmeans a tool call would spend more than the confirmation threshold (default USD 1.00) or covers more than 100 numbers. The agent should show the amount to the user and call again withconfirm_max_cost. See MCP. - SDK: client-side codes
missing_api_key,connection_error,timeout,invalid_responseandinvalid_argumentnever come from the API itself. See SDK.
Frequently asked questions
Is an unknown result an error?
No. Per-row problems (invalid, duplicate, suppressed, unknown, unsupported country) never fail the request and are never billed. Errors are only for problems with the request as a whole.
Which errors should my client retry?
Only those with retryable: true: rate_limited, daily_cap_reached (after the reset), idempotency_request_in_progress, internal_error and temporarily_unavailable. Honour Retry-After and use backoff with jitter.

