Errors

Every failure on /api/v1 returns the same JSON envelope. Branch your code on the machine-readable code field — never on the error prose, which is human-facing and may be reworded at any time without a version bump (see Versioning).

On this page

The error envelope

{
  "status": 400,
  "code": "invalid_email",
  "error": "Invalid email format.",
  "param": "email",
  "details": null,
  "doc_url": "https://emailvalidator.ai/docs/get-started/errors",
  "request_id": "0HNCV1TQ3PLRK:00000003"
}
Field Always present Description
status yes The HTTP status code, mirrored into the body
code yes Stable machine-readable code — the field to branch on
error yes Human-readable message. Prose; do not match on it
doc_url yes Link to this page
request_id yes Correlation id for the request. Quote it in a support request
param no The request field the failure is attributable to, when it is about one field
details on this shape Array of per-field messages. Written as null when there is nothing to add

status, code, error, doc_url and request_id are on every error the API returns. Only those five are guaranteed everywhere — see the two typed bodies below.

request_id is also returned as the X-Request-Id response header on every /api/v1 response, success or failure — the header and the body field are always the same value.

Two bodies carry typed extras

The batch 402 and the batch 422 add machine-readable fields on top of the five guaranteed ones, so a client can act on numbers and names instead of parsing prose. In exchange they carry no param and no details — there is nothing those could add that the typed fields do not already say. Both are shown below.

Error codes

The complete vocabulary. A code is a published contract: it will not be renamed inside v1.

code Status Meaning
unauthorized 401 No API key was supplied
invalid_api_key 401 A key was supplied but is unknown, revoked, expired, or past its scheduled revocation
insufficient_scope 403 The key authenticated but is not scoped for this endpoint
invalid_request 400, 405, 415 The request is malformed — wrong content type, missing required field, unsupported method
invalid_email 400 The submitted address is not a syntactically valid email
invalid_callback_url 400 The supplied callback_url is not an allowed webhook target
needs_column_selection 422 The upload has several candidate email columns — re-send with email_column
too_many_rows 400 The upload holds more valid addresses than this key may submit in one batch
unsupported_file_type 400 The file extension is not in the allow-list (.csv, .txt, .xlsx)
payload_too_large 413 The request body or upload exceeds the maximum allowed size
insufficient_credits 402 The account cannot cover the request
not_found 404 The resource does not exist, or belongs to another account
job_not_pending 409 The batch job is no longer pending, so it cannot be started
rate_limited 429 The per-minute request budget is exhausted
idempotency_in_progress 409 An identical idempotent request is still in flight
idempotency_key_reuse 422 An Idempotency-Key was reused with a different request
idempotency_resource_gone 404 A charged Idempotency-Key named a resource that can no longer be read
internal_error 500 An unhandled server-side failure

Status codes

Status Codes it carries
400 invalid_request, invalid_email, invalid_callback_url, too_many_rows, unsupported_file_type
401 unauthorized, invalid_api_key
402 insufficient_credits
403 insufficient_scope
404 not_found, idempotency_resource_gone
405 invalid_request — the method is not supported on this path
409 job_not_pending, idempotency_in_progress
413 payload_too_large
415 invalid_request — unsupported Content-Type
422 needs_column_selection, idempotency_key_reuse
429 rate_limited
500 internal_error

Framework-level rejections carry the envelope too: an unrouted path, a wrong method, an unsupported content type and a malformed path parameter all answer with JSON, never with an empty body.

Examples

Missing API key — 401:

{
  "status": 401,
  "code": "unauthorized",
  "error": "API key required."
}

Supplied key is unknown, revoked or expired — 401:

{
  "status": 401,
  "code": "invalid_api_key",
  "error": "Invalid or revoked API key."
}

Key is not scoped for the endpoint — 403:

{
  "status": 403,
  "code": "insufficient_scope",
  "error": "This API key is not scoped for this endpoint. It requires the 'verify' scope."
}

See Authentication for the scope vocabulary.

Insufficient credits — 402:

The credit numbers are top-level fields, not prose, so you can top up by shortfall and retry without parsing the message.

{
  "status": 402,
  "code": "insufficient_credits",
  "error": "Insufficient credits.",
  "shortfall": 5,
  "balance": 5,
  "required": 10
}

POST /api/v1/verify returns the plain envelope for 402 (a single check costs one credit, so there is nothing to itemise); POST /api/v1/verify/batch returns the three numbers above.

Ambiguous email column — 422:

{
  "status": 422,
  "code": "needs_column_selection",
  "error": "Several columns could hold email addresses. Re-send the request with email_column set to the one to validate.",
  "candidate_columns": ["Column1", "Column2"]
}

candidate_columns are 1-based positional tokens, one per column in the file — Column1 is the first column, Column2 the second. Send one of them back as email_column. The field also accepts the literal header text if you would rather name the column, but the tokens are what the API returns, because a file may have no header row at all.

Rate limit exceeded — 429:

{
  "status": 429,
  "code": "rate_limited",
  "error": "Rate limit exceeded."
}

A 429 also carries Retry-After and the three X-RateLimit-* headers. See Rate limits.

Unexpected server error — 500:

{
  "status": 500,
  "code": "internal_error",
  "error": "An unexpected error occurred."
}

A 500 never carries an exception type, message or stack trace. Quote the request_id in a support request and we can find the exact call.

Idempotency errors

Both credit-spending POSTs accept an Idempotency-Key header, which adds three failure cases:

code Status When
idempotency_in_progress 409 The first request with this key has not finished yet. Retry shortly
idempotency_key_reuse 422 This key was already used for a different request
idempotency_resource_gone 404 The key's charge created a resource that can no longer be read

idempotency_resource_gone is the one of the three you should never see. It means a request charged you, and the resource that charge created has since become unreadable — an account erasure, or the 7-day retention delete landing inside the key's 24-hour life. Retrying the same key cannot help, so it carries its own code rather than the generic not_found: an ordinary miss is usually the caller's mistake and this is ours. Quote the request_id.

{
  "status": 409,
  "code": "idempotency_in_progress",
  "error": "A request with this Idempotency-Key is still in progress. Retry shortly."
}
{
  "status": 422,
  "code": "idempotency_key_reuse",
  "error": "This Idempotency-Key was already used for a different request."
}
{
  "status": 404,
  "code": "idempotency_resource_gone",
  "error": "The resource this Idempotency-Key paid for is no longer available."
}

A key longer than 255 characters is rejected as 400 invalid_request with param: "Idempotency-Key". See Idempotency.

Finding the request afterwards

Every /api/v1 response carries X-Request-Id, and every error body repeats it as request_id. Both name the same row in a per-request log you can read yourself:

  • Dashboard → API → Logs. Method, path, status, latency, live/test tag and the machine error code for every call, with the redacted request and response bodies on the detail view.
  • Retention is 7 days, matching validation-result retention.
  • Secrets are redacted before storage. An ?api_key= query value, the X-Api-Key header, a callback_secret in a response body and the signing parameters of a pre-signed download URL never reach the log. Request bodies are capped, and a batch upload's file body is never captured at all — only its name and length.
  • The log is dashboard-only. There is no public endpoint for it, and none is planned.

The log is best-effort

Rows are written off the request path, so logging can never slow down or fail an API call. That is a deliberate trade: under an extreme burst a small number of rows are dropped rather than queued, so a call you made may occasionally be missing from the Logs tab. Nothing about the call itself changes — it is served, charged and answered exactly the same way.

X-Request-Id is returned on every response either way, and it is still worth quoting in a support request when the call is not in your Logs tab: it identifies the request in our own server-side logs too.

Log the X-Request-Id of any call you make. It turns "something failed yesterday" into one row we can both look at.