Submit a single email for validation

Download OpenAPI spec
POST /api/v1/verify
API key required Base https://api.emailvalidator.ai 60 requests per minute per account

Submit a single email address for validation.

Synchronous mode (no callback_url): the request blocks until validation completes or the timeout elapses.

  • Completes within timeout → 200 OK with the full result.
  • Timeout exceeded → 202 Accepted with a result_url to poll.

Asynchronous mode (with callback_url): the request returns 202 Accepted immediately. When validation completes the full result is delivered to your URL as a signed webhook.

Idempotency — send an Idempotency-Key and a retry replays the original response instead of spending a second credit. The request identity is email + timeout + callback_url; the same key with a different request is a 422.

Requires the verify scope.

On this page

Parameters

header · parameters

ParameterTypeDescription
Idempotency-Key string Client-generated key (up to 255 characters, one per logical operation) that makes a retry safe. A repeat with the same request replays the stored response verbatim and adds Idempotency-Replayed: true; a repeat with a different request is a 422; a repeat while the first is still in flight is a 409. Records expire after 24 hours and are scoped to the account, the endpoint and the key's mode (test keys and live keys are independent namespaces). If the first request charged you and then died before answering, the retry gets 202 carrying the Verification that charge created, in whatever state it is in at that moment — never a second charge and never a dead 409. Omit the header and behaviour is unchanged: every call is new work. example: 00000000-0000-4000-8000-000000000001

Request body · application/json · required

FieldTypeDescription
email required string Email address to validate. example: alice@example.com
timeout integer 3–60 · default 30 Seconds to wait for a synchronous result (3–60). When the validation completes within this window the response is 200 OK with the full result. When the timeout elapses first the response is 202 Accepted with a result_url to poll. example: 15
callback_url string · uri HTTPS URL to receive the result as a signed webhook. When supplied the request returns 202 Accepted immediately and the result is delivered asynchronously. The delivery carries an X-Signature header signed with your account signing secret (whsec_…, from your dashboard) — pin that one secret rather than storing the per-job callback_secret of every request. example: https://yourapp.example.com/hooks/email

Responses

200
Validation completed synchronously — the full Verification with status: complete and a populated result. · application/json
202

Accepted — async callback mode, sync timeout exceeded, or idempotency recovery. The body is the SAME Verification object a 200 returns. On the first two it is status: pending, result and recovery null, and a result_url to poll; in async mode it also carries the one-time callback_secret.

Idempotency recovery is the same shape reached a different way: if a previous request with this Idempotency-Key charged you and then died before it could answer (a crash, a deploy, a dropped connection), the retry is handed the verification that charge already created, with Idempotency-Replayed: true. The work is never re-run and you are never charged twice.

A recovery reports the resource's CURRENT state, not the state it was in when your original request died. There is no earlier response to replay here — the request that owed you one never wrote it — so retrying an hour later returns status: complete with a populated result if that is what the verification now is, rather than a pending snapshot you would have to poll past. The status code stays 202 either way: it answers "your request was accepted", while status answers "how far has it got".

· application/json
400
Invalid email format or bad callback_url. · application/json
401
Missing or invalid API key. · application/json
402
Insufficient credits. · application/json
403
The API key authenticated but is not scoped for this endpoint (code: insufficient_scope). It is a 403 rather than a 401 on purpose — the credential is valid, so retrying with it can never help; a key holding the verify scope is needed. · application/json
404

The resource a charged Idempotency-Key paid for can no longer be read (code: idempotency_resource_gone). The charge committed and the thing it created has since become unreadable — an account erasure, or the 7-day retention delete landing inside the key's 24-hour life.

It carries its OWN code rather than the generic not_found, because the two mean opposite things: not_found is an ordinary miss you may have caused, while this is a server-side fault worth reporting. Retrying the same key cannot succeed.

· application/json
409
A request carrying this Idempotency-Key is still in flight (code: idempotency_in_progress). Retry shortly — once the first request finishes, the same key replays its response. · application/json
413
Request body exceeds the maximum allowed size. · application/json
422
This Idempotency-Key was already used for a DIFFERENT request (code: idempotency_key_reuse). The original resource is untouched and nothing was charged. · application/json
429
Rate limit exceeded. · application/json
500
Unexpected server error. The body carries code internal_error plus a request_id to quote in a support request, and never any exception detail. · application/json

Response schema · 200 · Verification

A single-email verification. This ONE object is what every representation of the resource returns: the synchronous 200, the 202 that accepts an async or timed-out request, both states of GET /api/v1/verify/{id}, every row of GET /api/v1/verify, and the verify.completed webhook payload.

Two axes. status is the LIFECYCLE (pending or complete) and never carries a verdict. The verdict is result.outcome, in the same vocabulary the CSV export and the dashboard use. result and recovery are both null while pending, because a check that has not finished has reached no conclusion — the terminal unknown is a conclusion, not an absence of one.

Test and live are separate namespaces. livemode says which one this verification belongs to, and a key only ever sees resources of its own mode: a sandbox (ev_test_) key cannot list or fetch a live verification, and a live key never sees a canned sandbox one.

FieldTypeDescription
object string Always verification — the discriminator that marks this resource type. verification example: verification
id string · uuid The verification's ID, present on every representation. Pass it to GET /api/v1/verify/{id}. example: 3fa85f64-5717-4562-b3fc-2c963f66afa6
created_at string · date-time UTC timestamp when the verification was accepted. It does not change when the verification completes, which is what makes the list's cursor stable while checks finish. example: 2025-06-10T14:02:09.180Z
livemode boolean true when the verification was submitted with a LIVE key; false when it came from an ev_test_ sandbox key and the verdict is therefore a canned test result rather than a real probe. Test and live are separate namespaces — a key only ever sees resources of its own mode — so this is constant for any one key, and it is what makes a stored response, a log line or a webhook body self-describing after the fact. example: true
status string

The verification's LIFECYCLE — and only its lifecycle:

  • pending — accepted and still running. result and recovery are both null.
  • complete — finished. result carries the verdict.

It never carries a verdict word. A client can switch on it without knowing whether the check happened to finish inside the request.

pending complete example: complete
email string The address as submitted. A recovery never replaces it. example: alice@example.com
result object · null The verdict. null while status is pending.
recovery object · null A probe-confirmed alternative address, when contact recovery found one. null while pending, and null when no recovery was surfaced.
completed_at string · date-time · null UTC timestamp when the verification finished. null while pending. example: 2025-06-10T14:02:11.432Z
credits_remaining integer · null Credit balance after this request. null for system/demo accounts, and null on list rows, webhook payloads and SSE frames — a balance is a per-request figure, so read it from GET /api/v1/credits. example: 4820
result_url string Where to fetch the finished verification. Present only while status is pending; once complete you are already holding what it would point at. example: /api/v1/verify/3fa85f64-5717-4562-b3fc-2c963f66afa6
callback_secret string

A per-verification override of the account signing secret, revealed EXACTLY ONCE — on the 202 that accepts a request carrying a callback_url. It is deliberately absent from GET /api/v1/verify/{id}, from list rows and from the webhook payload. Verifying with the account secret (whsec_…) works without it.

Shape: cs_ + 64 hex characters for a single verification (bv_ + 64 hex for a batch job). It is NOT whsec_-shaped — that prefix belongs to the ACCOUNT signing secret, a different value with a different lifetime.

example: cs_9f2a4c6e...

result · VerificationResult · object

The verdict. null while status is pending.
FieldTypeDescription
outcome string

The verification VERDICT, carried by result.outcome (and accepted by the list's outcome filter):

  • deliverable — mailbox confirmed deliverable via SMTP.
  • invalid — mailbox does not exist or hard-rejected.
  • catch-all — domain accepts all addresses; deliverability unconfirmable.
  • unknown — could not determine (DNS failure, transient error, or no SMTP probe data).
deliverable invalid catch-all unknown example: deliverable
sub_status string Additional signal within the top-level status. Empty string when none applies. Derivation is first-match: disposable > role_based > accept_all > no_dns_entries > mailbox_not_found > greylisted > mail_server_did_not_respond. "" disposable role_based accept_all no_dns_entries mailbox_not_found greylisted mail_server_did_not_respond example: ""
risk string

Deliverability risk, assessed at PROBE TIME from everything the check observed — not derived from outcome, and not derivable from the other fields in this object. It reads the SMTP conversation's real status, the domain's mail-server state, DNS transience and the address's own syntax, several of which are not reported anywhere else on this response. So it is a genuinely independent signal, and it will sometimes disagree with what outcome and details on their own would suggest.

Broadly: high for a definitive rejection, a disposable domain, a domain with no mail server, or a structurally suspect address (a quoted or commented local part); medium for an accept-all domain, a role address, a non-ASCII local part, or a mailbox whose probe was DEFERRED by the server (a 4xx temporary failure — a real, common case whose outcome is unknown); low for a free-mail domain or an address we could not verify conclusively; none for a clean, confirmed mailbox with no such signal. Treat the four values as an ordered scale rather than as a formula over the other fields.

Returned for both live and test-mode keys (a test-mode key returns the canned risk the address's local part maps to). OMITTED — not reported as none — whenever the check carries no recorded assessment. This rule is WIDER than free_email's and details': those two are withheld only when the stored probe detail cannot be read, while risk is also absent on a healthy row that has no stored detail at all (a check completed before this became a recorded value, until it is backfilled), and while a check is still pending. An absent risk means unknown, never none.

none low medium high example: none
free_email boolean Whether the domain is a known free-email provider. Derived from the stored probe detail, so it is OMITTED — not reported as false — on the rare row whose stored detail cannot be read; see details. example: false
account string Local part of the email address (before @). example: alice
domain string Domain part of the email address (after @). example: example.com
details object Low-level DNS/SMTP signal. OMITTED on the rare row whose stored probe detail exists but cannot be read — a server-side data fault. The verdict in outcome comes from a separate, intact field and stays trustworthy, so the detail is withheld rather than reported as a row of falses that would read as a confident all-clear. A row that simply has no stored detail is not that case and still returns this object with its default values.

details · VerifyDetails · object

Low-level DNS/SMTP signal. OMITTED on the rare row whose stored probe detail exists but cannot be read — a server-side data fault. The verdict in outcome comes from a separate, intact field and stays trustworthy, so the detail is withheld rather than reported as a row of falses that would read as a confident all-clear. A row that simply has no stored detail is not that case and still returns this object with its default values.
FieldTypeDescription
role boolean Whether the local part is a role alias (e.g. admin, support, noreply). example: false
disposable boolean Whether the domain belongs to a known disposable/temporary email provider. example: false
catch_all boolean Whether the domain is configured to accept all addresses. example: false
mx_found boolean Whether at least one MX record was found for the domain. example: true
mx_record string · null The highest-priority MX hostname, or null when no MX records exist. example: mail.example.com
smtp_provider string · null Detected SMTP provider name (e.g. Google, Microsoft), or null when unknown. example: Google
smtp_code integer · null SMTP response code from the final RCPT TO probe, or null when no SMTP probe was attempted. example: 250

recovery · VerificationRecovery · object

A probe-confirmed alternative address, when contact recovery found one. null while pending, and null when no recovery was surfaced.
FieldTypeDescription
email string The recovered address. Never a substitute for email — that is what you sent. example: alice@gmail.com
outcome string deliverable when a probe confirmed a mailbox at the recovered address; catch-all when the fix landed on a domain that accepts everything. Drawn from the same vocabulary as result.outcome and the CSV's Outcome column, and identical to the CSV's RecoveredOutcome cell for the same row. deliverable catch-all example: deliverable

Response schema · 202 · Verification

A single-email verification. This ONE object is what every representation of the resource returns: the synchronous 200, the 202 that accepts an async or timed-out request, both states of GET /api/v1/verify/{id}, every row of GET /api/v1/verify, and the verify.completed webhook payload.

Two axes. status is the LIFECYCLE (pending or complete) and never carries a verdict. The verdict is result.outcome, in the same vocabulary the CSV export and the dashboard use. result and recovery are both null while pending, because a check that has not finished has reached no conclusion — the terminal unknown is a conclusion, not an absence of one.

Test and live are separate namespaces. livemode says which one this verification belongs to, and a key only ever sees resources of its own mode: a sandbox (ev_test_) key cannot list or fetch a live verification, and a live key never sees a canned sandbox one.

FieldTypeDescription
object string Always verification — the discriminator that marks this resource type. verification example: verification
id string · uuid The verification's ID, present on every representation. Pass it to GET /api/v1/verify/{id}. example: 3fa85f64-5717-4562-b3fc-2c963f66afa6
created_at string · date-time UTC timestamp when the verification was accepted. It does not change when the verification completes, which is what makes the list's cursor stable while checks finish. example: 2025-06-10T14:02:09.180Z
livemode boolean true when the verification was submitted with a LIVE key; false when it came from an ev_test_ sandbox key and the verdict is therefore a canned test result rather than a real probe. Test and live are separate namespaces — a key only ever sees resources of its own mode — so this is constant for any one key, and it is what makes a stored response, a log line or a webhook body self-describing after the fact. example: true
status string

The verification's LIFECYCLE — and only its lifecycle:

  • pending — accepted and still running. result and recovery are both null.
  • complete — finished. result carries the verdict.

It never carries a verdict word. A client can switch on it without knowing whether the check happened to finish inside the request.

pending complete example: complete
email string The address as submitted. A recovery never replaces it. example: alice@example.com
result object · null The verdict. null while status is pending.
recovery object · null A probe-confirmed alternative address, when contact recovery found one. null while pending, and null when no recovery was surfaced.
completed_at string · date-time · null UTC timestamp when the verification finished. null while pending. example: 2025-06-10T14:02:11.432Z
credits_remaining integer · null Credit balance after this request. null for system/demo accounts, and null on list rows, webhook payloads and SSE frames — a balance is a per-request figure, so read it from GET /api/v1/credits. example: 4820
result_url string Where to fetch the finished verification. Present only while status is pending; once complete you are already holding what it would point at. example: /api/v1/verify/3fa85f64-5717-4562-b3fc-2c963f66afa6
callback_secret string

A per-verification override of the account signing secret, revealed EXACTLY ONCE — on the 202 that accepts a request carrying a callback_url. It is deliberately absent from GET /api/v1/verify/{id}, from list rows and from the webhook payload. Verifying with the account secret (whsec_…) works without it.

Shape: cs_ + 64 hex characters for a single verification (bv_ + 64 hex for a batch job). It is NOT whsec_-shaped — that prefix belongs to the ACCOUNT signing secret, a different value with a different lifetime.

example: cs_9f2a4c6e...

result · VerificationResult · object

The verdict. null while status is pending.
FieldTypeDescription
outcome string

The verification VERDICT, carried by result.outcome (and accepted by the list's outcome filter):

  • deliverable — mailbox confirmed deliverable via SMTP.
  • invalid — mailbox does not exist or hard-rejected.
  • catch-all — domain accepts all addresses; deliverability unconfirmable.
  • unknown — could not determine (DNS failure, transient error, or no SMTP probe data).
deliverable invalid catch-all unknown example: deliverable
sub_status string Additional signal within the top-level status. Empty string when none applies. Derivation is first-match: disposable > role_based > accept_all > no_dns_entries > mailbox_not_found > greylisted > mail_server_did_not_respond. "" disposable role_based accept_all no_dns_entries mailbox_not_found greylisted mail_server_did_not_respond example: ""
risk string

Deliverability risk, assessed at PROBE TIME from everything the check observed — not derived from outcome, and not derivable from the other fields in this object. It reads the SMTP conversation's real status, the domain's mail-server state, DNS transience and the address's own syntax, several of which are not reported anywhere else on this response. So it is a genuinely independent signal, and it will sometimes disagree with what outcome and details on their own would suggest.

Broadly: high for a definitive rejection, a disposable domain, a domain with no mail server, or a structurally suspect address (a quoted or commented local part); medium for an accept-all domain, a role address, a non-ASCII local part, or a mailbox whose probe was DEFERRED by the server (a 4xx temporary failure — a real, common case whose outcome is unknown); low for a free-mail domain or an address we could not verify conclusively; none for a clean, confirmed mailbox with no such signal. Treat the four values as an ordered scale rather than as a formula over the other fields.

Returned for both live and test-mode keys (a test-mode key returns the canned risk the address's local part maps to). OMITTED — not reported as none — whenever the check carries no recorded assessment. This rule is WIDER than free_email's and details': those two are withheld only when the stored probe detail cannot be read, while risk is also absent on a healthy row that has no stored detail at all (a check completed before this became a recorded value, until it is backfilled), and while a check is still pending. An absent risk means unknown, never none.

none low medium high example: none
free_email boolean Whether the domain is a known free-email provider. Derived from the stored probe detail, so it is OMITTED — not reported as false — on the rare row whose stored detail cannot be read; see details. example: false
account string Local part of the email address (before @). example: alice
domain string Domain part of the email address (after @). example: example.com
details object Low-level DNS/SMTP signal. OMITTED on the rare row whose stored probe detail exists but cannot be read — a server-side data fault. The verdict in outcome comes from a separate, intact field and stays trustworthy, so the detail is withheld rather than reported as a row of falses that would read as a confident all-clear. A row that simply has no stored detail is not that case and still returns this object with its default values.

details · VerifyDetails · object

Low-level DNS/SMTP signal. OMITTED on the rare row whose stored probe detail exists but cannot be read — a server-side data fault. The verdict in outcome comes from a separate, intact field and stays trustworthy, so the detail is withheld rather than reported as a row of falses that would read as a confident all-clear. A row that simply has no stored detail is not that case and still returns this object with its default values.
FieldTypeDescription
role boolean Whether the local part is a role alias (e.g. admin, support, noreply). example: false
disposable boolean Whether the domain belongs to a known disposable/temporary email provider. example: false
catch_all boolean Whether the domain is configured to accept all addresses. example: false
mx_found boolean Whether at least one MX record was found for the domain. example: true
mx_record string · null The highest-priority MX hostname, or null when no MX records exist. example: mail.example.com
smtp_provider string · null Detected SMTP provider name (e.g. Google, Microsoft), or null when unknown. example: Google
smtp_code integer · null SMTP response code from the final RCPT TO probe, or null when no SMTP probe was attempted. example: 250

recovery · VerificationRecovery · object

A probe-confirmed alternative address, when contact recovery found one. null while pending, and null when no recovery was surfaced.
FieldTypeDescription
email string The recovered address. Never a substitute for email — that is what you sent. example: alice@gmail.com
outcome string deliverable when a probe confirmed a mailbox at the recovered address; catch-all when the fix landed on a domain that accepts everything. Drawn from the same vocabulary as result.outcome and the CSV's Outcome column, and identical to the CSV's RecoveredOutcome cell for the same row. deliverable catch-all example: deliverable

Error responses

400 401 402 403 404 409 413 422 429 500 · ApiProblem · object

Standard error response shape used by all error status codes.
FieldTypeDescription
status required integer HTTP status code mirrored in the body. example: 400
code required string Stable, machine-readable error code. Branch your code on this — never on the error text, which is prose and may be reworded at any time. example: invalid_email
error required string Human-readable error message. example: Invalid email format.
param string The request field the failure is attributable to. Omitted when the failure is not about one specific field. example: email
details array of string · null Optional per-field validation messages. Null when not applicable. example: ["email: required"]
doc_url string Link to the documentation for this error code. example: https://emailvalidator.ai/docs/get-started/errors
request_id string Correlation id for this request. Quote it in a support request — it is how a single call is found in the server logs. example: 0HNCV1TQ3PLRK:00000003

Example request · application/json

Synchronous (no callback)

curl -X POST https://api.emailvalidator.ai/api/v1/verify \
  -H "X-Api-Key: YOUR_API_KEY" \
  -H "Content-Type: application/json" \
  -d '{
  "email": "alice@example.com",
  "timeout": 15
}'
{
  "email": "alice@example.com",
  "timeout": 15
}

Asynchronous (with callback)

curl -X POST https://api.emailvalidator.ai/api/v1/verify \
  -H "X-Api-Key: YOUR_API_KEY" \
  -H "Content-Type: application/json" \
  -d '{
  "email": "alice@example.com",
  "callback_url": "https://yourapp.example.com/hooks/email"
}'
{
  "email": "alice@example.com",
  "callback_url": "https://yourapp.example.com/hooks/email"
}