API reference
Every endpoint, parameter and schema of the email-validation.org REST API, generated from our OpenAPI 3.1 document. Download the document as JSON or YAML to generate a client, import it into an API tool or give it to an AI coding assistant.
- Base URL:
https://api.email-validation.org - Authentication:
Authorization: Bearer <API key>(orX-Api-Key). See Authentication. - Format: JSON in UTF-8, except bulk uploads (CSV or plain text) and result downloads (CSV).
- Errors:
{"error": {"code", "message", "request_id"}}with the HTTP status. See Errors. - Times: ISO 8601 in UTC.
Endpoints
Validate an email address (query string)
GET /v1/validate
Checks one address and returns its status, reason and recommended action.
- Costs 1 credit, except results with status
unknown(free) and answers to malformed or sandbox addresses (free). - Waits up to
timeoutseconds (default 15). Set your HTTP client timeout a few seconds higher. - The result can come from a check of the same address made in the last 12 months (
processed_atshows when). Results with statusunknown, catch-all results (including any withcatch_all_domain: true) andmailbox_fullare always checked again. Sendfresh=trueto check again anyway. - Paid accounts may send up to 500 requests per second; accounts without a purchase up to 20.
- Test for free with
<reason>@sandbox.email-validation.orgaddresses: https://email-validation.org/docs/test-addresses/
This form puts the address in the URL. Prefer POST.
Authentication: API key (Authorization: Bearer … or X-Api-Key).
Parameters
| Name | In | Type | Required | Description |
|---|---|---|---|---|
email | query | string | yes | The address to check. Prefer POST so addresses stay out of URLs. |
ip_address | query | string | no | Optional IP address the person used when they gave you the address. |
timeout | query | integer | no | Seconds you are willing to wait (5–15). |
fresh | query | boolean | no | Set true to check the address again even when it was checked in the last 12 months. By default a definite result from such a check can be returned instead (processed_at shows when it was made); it costs 1 credit like a new check. A definite new result replaces the stored one; an unknown result leaves it in place. |
Responses
| Status | Meaning | Body |
|---|---|---|
| 200 | The result. HTTP 200 also covers unknown results, which are free. | application/json: ValidationResult |
| 400 | The request is not valid. Codes: invalid_request. | application/json: Error |
| 401 | The API key is missing, malformed or no longer valid. Codes: unauthorized. | application/json: Error |
| 402 | Not enough credits. Codes: insufficient_credits. | application/json: Error |
| 403 | The key may not do this. Codes: forbidden, forbidden_origin. | application/json: Error |
| 429 | Slow down and retry after Retry-After seconds. Codes: rate_limited, abuse_throttled, key_daily_cap. | application/json: Error |
| 500 | Something failed on our side. No credit was used. Codes: internal_error. | application/json: Error |
| 503 | Temporarily unavailable. No credit was used; retry after Retry-After seconds. Codes: service_unavailable, upstream_capacity. | application/json: Error |
Validate an email address
POST /v1/validate
Checks one address and returns its status, reason and recommended action.
- Costs 1 credit, except results with status
unknown(free) and answers to malformed or sandbox addresses (free). - Waits up to
timeoutseconds (default 15). Set your HTTP client timeout a few seconds higher. - The result can come from a check of the same address made in the last 12 months (
processed_atshows when). Results with statusunknown, catch-all results (including any withcatch_all_domain: true) andmailbox_fullare always checked again. Sendfresh=trueto check again anyway. - Paid accounts may send up to 500 requests per second; accounts without a purchase up to 20.
- Test for free with
<reason>@sandbox.email-validation.orgaddresses: https://email-validation.org/docs/test-addresses/
The body (JSON or form data) is at most 4 KB (413 above) and must arrive within 2 seconds (400 otherwise).
Authentication: API key (Authorization: Bearer … or X-Api-Key).
Request body
application/json: ValidateRequest
Responses
| Status | Meaning | Body |
|---|---|---|
| 200 | The result. HTTP 200 also covers unknown results, which are free. | application/json: ValidationResult |
| 400 | The request is not valid. Codes: invalid_request. | application/json: Error |
| 401 | The API key is missing, malformed or no longer valid. Codes: unauthorized. | application/json: Error |
| 402 | Not enough credits. Codes: insufficient_credits. | application/json: Error |
| 403 | The key may not do this. Codes: forbidden, forbidden_origin. | application/json: Error |
| 413 | The body is too large. Codes: payload_too_large. | application/json: Error |
| 429 | Slow down and retry after Retry-After seconds. Codes: rate_limited, abuse_throttled, key_daily_cap. | application/json: Error |
| 500 | Something failed on our side. No credit was used. Codes: internal_error. | application/json: Error |
| 503 | Temporarily unavailable. No credit was used; retry after Retry-After seconds. Codes: service_unavailable, upstream_capacity. | application/json: Error |
Get your credit balance
GET /v1/credits
Returns the spendable balance, any debt, credits still being applied, and the credit lots with their expiry dates. Needs the account:read scope.
Authentication: API key (Authorization: Bearer … or X-Api-Key).
Responses
| Status | Meaning | Body |
|---|---|---|
| 200 | Your credits. | application/json: Credits |
| 400 | The request is not valid. Codes: invalid_request. | application/json: Error |
| 401 | The API key is missing, malformed or no longer valid. Codes: unauthorized. | application/json: Error |
| 403 | The key may not do this. Codes: forbidden, forbidden_origin. | application/json: Error |
| 429 | Slow down and retry after Retry-After seconds. Codes: rate_limited, abuse_throttled, key_daily_cap. | application/json: Error |
| 500 | Something failed on our side. No credit was used. Codes: internal_error. | application/json: Error |
| 503 | Temporarily unavailable. No credit was used; retry after Retry-After seconds. Codes: service_unavailable, upstream_capacity. | application/json: Error |
Get credit usage by day or by key
GET /v1/usage
Credit usage for any window within the last 90 days. Daily credit totals come from the credit ledger and are exact; the split by status (API, MCP, dashboard and bulk results) is compiled from request records and is approximate (it can lag by up to an hour). totals are always the whole account; with group_by=key the items cover only credits spent through an API key. Needs the account:read scope.
Authentication: API key (Authorization: Bearer … or X-Api-Key).
Parameters
| Name | In | Type | Required | Description |
|---|---|---|---|---|
from | query | string (date) | no | First day (UTC, YYYY-MM-DD), no earlier than 89 days before today: usage covers the last 90 days. Defaults to the start of the 90-day range that ends on to. |
to | query | string (date) | no | Last day (UTC). Defaults to today. The whole window must lie within the last 90 days (today included); an earlier day is a 400. |
group_by | query | string: day, key | no | Group the usage by day or by API key. |
Responses
| Status | Meaning | Body |
|---|---|---|
| 200 | Usage for the range. | application/json: UsageResponse |
| 400 | The request is not valid. Codes: invalid_request. | application/json: Error |
| 401 | The API key is missing, malformed or no longer valid. Codes: unauthorized. | application/json: Error |
| 403 | The key may not do this. Codes: forbidden, forbidden_origin. | application/json: Error |
| 429 | Slow down and retry after Retry-After seconds. Codes: rate_limited, abuse_throttled, key_daily_cap. | application/json: Error |
| 500 | Something failed on our side. No credit was used. Codes: internal_error. | application/json: Error |
| 503 | Temporarily unavailable. No credit was used; retry after Retry-After seconds. Codes: service_unavailable, upstream_capacity. | application/json: Error |
Create a bulk job
POST /v1/bulk
Uploads a list and creates a job. Send either a CSV or plain-text file (one address per line) as the raw body with Content-Type: text/csv or text/plain (up to 100 MB and 1,000,000 rows), or a JSON body {"emails": [...]} with up to 100,000 addresses. Content-Length is required. The job starts in analyzing: we read the file, remove duplicates and answer malformed addresses for free. Then it either starts on its own (start=true, the default) or waits in ready until you start it. Credits are held for every unique address that needs a check when the job starts, and anything not used is returned when it ends. Files and results are deleted after 30 days. Needs a key with the bulk scope. Quotas: an account without a purchase may have at most 2 lists that are analyzing or ready (jobs that start on their own count while they are analysed); another upload is a 409 conflict until one starts or is cancelled. Stored lists may take up to 2 GB without a purchase and 20 GB with one; an upload over the quota is a 413 payload_too_large.
Authentication: API key (Authorization: Bearer … or X-Api-Key).
Parameters
| Name | In | Type | Required | Description |
|---|---|---|---|---|
column | query | integer or string | no | The column that holds the addresses: a 1-based index (up to 512) or the header name. When omitted we detect it. |
has_header | query | boolean | no | Whether the first row is a header. When omitted we detect it. |
start | query | boolean | no | Start checking as soon as the file has been analysed. Set false to review the cost first and call the start endpoint later. |
webhook_endpoint_id | query | string | no | Id of a webhook endpoint registered in the dashboard. We notify it when the job finishes, even if it did not subscribe to bulk.completed / bulk.failed. |
Request body
text/csv: stringtext/plain: stringapplication/json: BulkCreateJson
Responses
| Status | Meaning | Body |
|---|---|---|
| 202 | Job created and being analysed. | application/json: BulkJob |
| 400 | The request is not valid. Codes: invalid_request. | application/json: Error |
| 401 | The API key is missing, malformed or no longer valid. Codes: unauthorized. | application/json: Error |
| 403 | The key may not do this. Codes: forbidden, forbidden_origin. | application/json: Error |
| 404 | No such resource for this account. Codes: not_found. | application/json: Error |
| 409 | The resource is not in a state that allows this. Codes: conflict. | application/json: Error |
| 411 | Content-Length is required. Codes: length_required. | application/json: Error |
| 413 | The body is too large. Codes: payload_too_large. | application/json: Error |
| 429 | Slow down and retry after Retry-After seconds. Codes: rate_limited, abuse_throttled, key_daily_cap. | application/json: Error |
| 500 | Something failed on our side. No credit was used. Codes: internal_error. | application/json: Error |
| 503 | Temporarily unavailable. No credit was used; retry after Retry-After seconds. Codes: service_unavailable, upstream_capacity. | application/json: Error |
Get a bulk job
GET /v1/bulk/{id}
Returns the job with its progress and counts. Poll every few seconds, or use a webhook.
Authentication: API key (Authorization: Bearer … or X-Api-Key).
Parameters
| Name | In | Type | Required | Description |
|---|---|---|---|---|
id | path | string | yes |
Responses
| Status | Meaning | Body |
|---|---|---|
| 200 | The job. | application/json: BulkJob |
| 400 | The request is not valid. Codes: invalid_request. | application/json: Error |
| 401 | The API key is missing, malformed or no longer valid. Codes: unauthorized. | application/json: Error |
| 403 | The key may not do this. Codes: forbidden, forbidden_origin. | application/json: Error |
| 404 | No such resource for this account. Codes: not_found. | application/json: Error |
| 429 | Slow down and retry after Retry-After seconds. Codes: rate_limited, abuse_throttled, key_daily_cap. | application/json: Error |
| 500 | Something failed on our side. No credit was used. Codes: internal_error. | application/json: Error |
Cancel a bulk job
DELETE /v1/bulk/{id}
Cancels the job. Work in flight finishes (cancelling), then the job becomes cancelled, unused credits are returned and its files are deleted. Cancelling a finished job is a conflict.
Authentication: API key (Authorization: Bearer … or X-Api-Key).
Parameters
| Name | In | Type | Required | Description |
|---|---|---|---|---|
id | path | string | yes |
Responses
| Status | Meaning | Body |
|---|---|---|
| 200 | The job after the cancel request. | application/json: BulkJob |
| 400 | The request is not valid. Codes: invalid_request. | application/json: Error |
| 401 | The API key is missing, malformed or no longer valid. Codes: unauthorized. | application/json: Error |
| 403 | The key may not do this. Codes: forbidden, forbidden_origin. | application/json: Error |
| 404 | No such resource for this account. Codes: not_found. | application/json: Error |
| 409 | The resource is not in a state that allows this. Codes: conflict. | application/json: Error |
| 429 | Slow down and retry after Retry-After seconds. Codes: rate_limited, abuse_throttled, key_daily_cap. | application/json: Error |
| 500 | Something failed on our side. No credit was used. Codes: internal_error. | application/json: Error |
Start a bulk job
POST /v1/bulk/{id}/start
Starts a job that is ready. Credits for every unique address that needs a check are held now and count toward the daily credit cap of the API key that starts it. Answers insufficient_credits when the balance is too low and key_daily_cap (429) when the job would pass the key's daily cap (the job stays ready either way).
Authentication: API key (Authorization: Bearer … or X-Api-Key).
Parameters
| Name | In | Type | Required | Description |
|---|---|---|---|---|
id | path | string | yes |
Responses
| Status | Meaning | Body |
|---|---|---|
| 200 | The started job. | application/json: BulkJob |
| 400 | The request is not valid. Codes: invalid_request. | application/json: Error |
| 401 | The API key is missing, malformed or no longer valid. Codes: unauthorized. | application/json: Error |
| 402 | Not enough credits. Codes: insufficient_credits. | application/json: Error |
| 403 | The key may not do this. Codes: forbidden, forbidden_origin. | application/json: Error |
| 404 | No such resource for this account. Codes: not_found. | application/json: Error |
| 409 | The resource is not in a state that allows this. Codes: conflict. | application/json: Error |
| 429 | Slow down and retry after Retry-After seconds. Codes: rate_limited, abuse_throttled, key_daily_cap. | application/json: Error |
| 500 | Something failed on our side. No credit was used. Codes: internal_error. | application/json: Error |
| 503 | Temporarily unavailable. No credit was used; retry after Retry-After seconds. Codes: service_unavailable, upstream_capacity. | application/json: Error |
Download bulk results (CSV)
GET /v1/bulk/{id}/results
Streams the results of a completed job as CSV (UTF-8). Our columns are appended to yours in this order: ev_status, ev_reason, ev_action, ev_free_email, ev_role_account, ev_disposable, ev_catch_all_domain, ev_did_you_mean, ev_mx_found, ev_mx_record, ev_smtp_provider, ev_domain_age_days. Rows that repeat an address get the same result. To resume a download with Range, request columns=original with filter=all (the default, with or without escape_formulas) or with a single status and escape_formulas=false, and check for 206.
Authentication: API key (Authorization: Bearer … or X-Api-Key).
Parameters
| Name | In | Type | Required | Description |
|---|---|---|---|---|
id | path | string | yes | |
filter | query | string | no | all, one status, or a comma-separated list of statuses (for example deliverable,risky). |
columns | query | string: original, email_only | no | original keeps every column of your file; email_only returns the address and our columns. |
escape_formulas | query | boolean | no | Prefix cells from your file that start with =, +, - or @ (also after a comma, semicolon, tab or line break inside a cell, for spreadsheets set to another list separator) so spreadsheets do not run them as formulas. Our own columns and the address column are always escaped. |
Range | header | string | no | Byte range of a stored result file, to resume an interrupted download. Honoured only with columns=original and either filter=all (with or without escape_formulas) or a single status with escape_formulas=false: those answers carry Accept-Ranges: bytes and a 206. Other answers carry Accept-Ranges: none and ignore the header (200, the whole file). |
Responses
| Status | Meaning | Body |
|---|---|---|
| 200 | The CSV file. | text/csv: string |
| 206 | Part of the CSV file (Range requests). | text/csv: string |
| 400 | The request is not valid. Codes: invalid_request. | application/json: Error |
| 401 | The API key is missing, malformed or no longer valid. Codes: unauthorized. | application/json: Error |
| 403 | The key may not do this. Codes: forbidden, forbidden_origin. | application/json: Error |
| 404 | No such resource for this account. Codes: not_found. | application/json: Error |
| 409 | The resource is not in a state that allows this. Codes: conflict. | application/json: Error |
| 416 | The Range lies outside the file. The body is empty; Content-Range: bytes */<size> gives the file size. | (none) |
| 429 | Slow down and retry after Retry-After seconds. Codes: rate_limited, abuse_throttled, key_daily_cap. | application/json: Error |
| 500 | Something failed on our side. No credit was used. Codes: internal_error. | application/json: Error |
Get service status
GET /v1/status
Public status of each component. No key needed. Updated every minute; answers may be up to 30 seconds old. When no status information is available the answer is 503 with Retry-After: 60.
Authentication: none.
Responses
| Status | Meaning | Body |
|---|---|---|
| 200 | Current status. | application/json: ServiceStatus |
| 503 | Temporarily unavailable. No credit was used; retry after Retry-After seconds. Codes: service_unavailable, upstream_capacity. | application/json: Error |
Get this OpenAPI document
GET /openapi.json
This document as JSON. The same document is served as YAML at /openapi.yaml.
Authentication: none.
Responses
| Status | Meaning | Body |
|---|---|---|
| 200 | The OpenAPI 3.1 document. | application/json: object |
MCP event stream (not offered)
GET /mcp
The server is stateless and does not open server-initiated streams, so GET answers 405. The key is checked first: without a valid key the answer is 401 (or 400 for a key in the URL).
Authentication: API key (Authorization: Bearer … or X-Api-Key).
Responses
| Status | Meaning | Body |
|---|---|---|
| 400 | The request is not valid. Codes: invalid_request. | application/json: Error |
| 401 | The API key is missing, malformed or no longer valid. Codes: unauthorized. | application/json: Error |
| 405 | Method not allowed. | (none) |
| 429 | Slow down and retry after Retry-After seconds. Codes: rate_limited, abuse_throttled, key_daily_cap. | application/json: Error |
MCP endpoint (Streamable HTTP)
POST /mcp
Model Context Protocol endpoint for AI agents and MCP clients (stateless Streamable HTTP, JSON-RPC 2.0). Tools: validate_email, get_credits, create_bulk_job (up to 10,000 addresses inline) and get_bulk_job. Authenticate with the same API key as the REST API; tool calls use the same credits, limits and rules. Setup: https://email-validation.org/docs/mcp/
Authentication: API key (Authorization: Bearer … or X-Api-Key).
Request body
application/json: object
Responses
| Status | Meaning | Body |
|---|---|---|
| 200 | A JSON-RPC response, as JSON or as a server-sent event stream. | application/json: object; text/event-stream: string |
| 202 | A notification was accepted. | (none) |
| 400 | The request is not valid. Codes: invalid_request. | application/json: Error |
| 401 | The API key is missing, malformed or no longer valid. Codes: unauthorized. | application/json: Error |
| 429 | Slow down and retry after Retry-After seconds. Codes: rate_limited, abuse_throttled, key_daily_cap. | application/json: Error |
Webhooks
bulk.completed
Body: BulkWebhookEvent.
bulk.failed
Body: BulkWebhookEvent.
credits.low
Body: CreditsLowEvent.
credits.expiring
Body: CreditsExpiringEvent.
webhook.test
Body: WebhookTestEvent.
Schemas
ValidationResult
The result of one check. Fields always appear in this order; values are null when they do not apply or are not known.
| Field | Type | Description |
|---|---|---|
email | string | The address that was checked, in normalised form. |
status | Status | |
reason | Reason | |
action | Action | |
account | string or null | The part before the @. |
domain | string or null | The part after the @ (international domains in ASCII form). |
did_you_mean | string or null | A suggested correction when the address looks like a typo of a common domain. |
free_email | boolean or null | True when the domain is a free consumer mailbox service. |
role_account | boolean or null | True when the address belongs to a team or function (such as info@ or sales@) rather than a person. |
disposable | boolean or null | True when the address comes from a throwaway inbox service. |
catch_all_domain | boolean or null | True when the domain accepts mail for any address. |
mx_found | boolean or null | True when the domain publishes mail (MX) records. |
mx_record | string or null | The preferred mail server host for the domain. |
smtp_provider | SmtpProvider or null | |
domain_age_days | integer or null | Age of the domain registration in days, when known. |
processed_at | string (date-time) | When the check was made (UTC). A result reused from a check of the same address made in the last 12 months keeps that check's time. |
credits_used | integer: 0, 1 | 1 when this result cost a credit. Results with status unknown, answers to malformed addresses and sandbox addresses are free. |
request_id | string | Our id for this request. Quote it when you contact support. |
ValidateRequest
| Field | Type | Description |
|---|---|---|
email | string | The address to check. |
ip_address (optional) | string | Optional IPv4 or IPv6 address the person used when they gave you the address (for example at sign-up). Ignored when it is not a valid IP. |
timeout (optional) | integer | How many seconds you are willing to wait (5–15). When the check cannot finish in time the result is unknown with reason timeout, and it is free. |
fresh (optional) | boolean | Set true to check the address again even when it was checked in the last 12 months. By default a definite result from such a check can be returned instead (processed_at shows when it was made); it costs 1 credit like a new check. A definite new result replaces the stored one; an unknown result leaves it in place. |
Credits
| Field | Type | Description |
|---|---|---|
balance | integer | Credits you can spend now. |
debt | integer | Credits owed after a refund or reversal; new credits pay this off first. |
pending | integer | Credits granted moments ago that are still being applied. They become spendable shortly. |
lots | array of CreditLot | Credit lots with credits left, soonest-expiring first. Credits are spent in this order. |
CreditLot
| Field | Type | Description |
|---|---|---|
remaining | integer | Credits left in this lot. |
expires_at | string (date-time) | When the credits in this lot expire. |
UsageResponse
| Field | Type | Description |
|---|---|---|
from | string (date) | First day included (UTC). |
to | string (date) | Last day included (UTC). |
group_by | string: day, key | |
totals | UsageTotals | |
items | array of UsageDay or UsageByKey | One item per day (group_by=day) or per API key (group_by=key). |
UsageTotals
Totals of the whole account for the range, whatever group_by: API, MCP, dashboard and bulk usage. With group_by=key they can be larger than the sum of the items, because bulk jobs and dashboard checks are not tied to a key.
| Field | Type | Description |
|---|---|---|
credits_used | integer | Credits used in the range, from the credit ledger (exact). |
by_status | StatusCountsApprox |
UsageDay
| Field | Type | Description |
|---|---|---|
date | string (date) | |
credits_used | integer | Credits used that day, from the credit ledger (exact). |
by_status | StatusCountsApprox |
UsageByKey
| Field | Type | Description |
|---|---|---|
key_id | string | API key id (never the key itself). |
name | string or null | The name you gave the key (null for a deleted key). |
last4 | string or null | Last 4 characters of the key, as shown in the dashboard. |
credits_used | integer | Credits used with this key in the range, from the credit ledger (exact). Bulk jobs and dashboard checks are not attributed to a key. |
StatusCountsApprox
Results by status (approximate; may lag by up to an hour).
| Field | Type | Description |
|---|---|---|
deliverable | integer | |
undeliverable | integer | |
risky | integer | |
unknown | integer |
BulkJob
A bulk job. Counters are null until the file has been read (analyzing).
| Field | Type | Description |
|---|---|---|
id | string | Bulk job id. |
status | BulkJobStatus | |
filename | string or null | |
total_rows | integer or null | Data rows in the file (header excluded). |
unique_emails | integer or null | Distinct addresses after normalisation. |
duplicates | integer or null | Rows that repeat an address seen earlier in the file. |
syntax_invalid | integer or null | Addresses that are not well formed (answered for free). |
local_answered | integer or null | Addresses answered without a mail-server check (malformed and sandbox addresses), all free. |
processed | integer | Unique addresses with a final result so far. |
credits_reserved | integer | Credits held when the job started: one per unique address that needs a check. |
credits_charged | integer | Credits actually used. When the job ends, the difference to credits_reserved is returned. |
counts | object | Results so far by status. |
error_code | string or null | Why a job failed or is waiting in ready, for example insufficient_credits (the job stays ready) or invalid_csv. Every value: https://email-validation.org/docs/api/bulk/#errors |
webhook_endpoint_id | string or null | Registered webhook endpoint notified about this job. |
created_at | string (date-time) | When the job was created. |
started_at | string or null (date-time) | |
finished_at | string or null (date-time) | |
expires_at | string (date-time) | When the uploaded file and the results are deleted (30 days). |
BulkCreateJson
| Field | Type | Description |
|---|---|---|
emails | array of string | Up to 100,000 addresses; the JSON body is at most 4 MB (send larger lists as CSV). |
ServiceStatus
| Field | Type | Description |
|---|---|---|
status | ComponentState | |
components | object | |
updated_at | string (date-time) | When this status was computed. |
Error
Every error uses this envelope. No credit is ever charged for a request that ends in an error.
| Field | Type | Description |
|---|---|---|
error | object |
BulkWebhookEvent
A bulk job finished or failed.
| Field | Type | Description |
|---|---|---|
type | string: bulk.completed, bulk.failed | |
timestamp | string (date-time) | When the event happened. |
data | object |
CreditsLowEvent
Your balance fell below the low-balance threshold set in the dashboard.
| Field | Type | Description |
|---|---|---|
type | string: credits.low | |
timestamp | string (date-time) | When the event happened. |
data | object |
CreditsExpiringEvent
Some of your credits expire in 30 days.
| Field | Type | Description |
|---|---|---|
type | string: credits.expiring | |
timestamp | string (date-time) | When the event happened. |
data | object |
WebhookTestEvent
A test delivery from "Send test" in the dashboard.
| Field | Type | Description |
|---|---|---|
type | string: webhook.test | |
timestamp | string (date-time) | When the event happened. |
data | object |
Status
The verdict for the address. unknown never costs a credit; every other status costs 1 credit when it comes from a completed check.
| Status | Meaning | Default action | Credit |
|---|---|---|---|
deliverable | The mailbox exists and accepted our check, so mail sent to it should arrive. | send | 1 |
undeliverable | Mail sent to this address will not arrive, so it should come off your list. | remove | 1 |
risky | The address may accept mail, but sending to it can hurt your bounce rate or sender reputation. | send_with_caution | 1 |
unknown | We could not get a definite answer this time, so no credit is used and a later retry often succeeds. | retry_later | free |
Reason
Why the address got its status. Each reason belongs to exactly one status (see x-reasons-by-status). Details: https://email-validation.org/docs/statuses/
| Reason | Status | Meaning |
|---|---|---|
accepted | deliverable | The mail server confirmed that this mailbox exists and accepts messages. |
alias | deliverable | The address is an alias that forwards to another mailbox, and mail sent to it is delivered. |
normalized | deliverable | We corrected a harmless formatting detail in the address, and the corrected address accepts mail. |
low_engagement | deliverable | The mailbox works, but it looks like a secondary address that its owner seldom reads. |
high_engagement | deliverable | The mailbox works and shows signs of a person who reads mail regularly. |
trusted_catch_all | deliverable | The domain accepts mail for any address, but mail sent to it has a strong record of arriving. |
ai_agent | deliverable | The mailbox accepts mail and appears to be handled by an automated AI agent rather than a person. |
no_mailbox | undeliverable | The domain receives mail, but this particular mailbox does not exist. |
no_inbound_mail | undeliverable | The domain is set up to send mail only and turns away incoming messages. |
invalid_syntax | undeliverable | The text is not a well-formed email address, so no mail server could ever accept it. |
likely_typo | undeliverable | The domain looks like a misspelling of a common mail provider, so check the suggested correction. |
no_dns | undeliverable | The domain has no DNS records that tell senders where to deliver mail. |
mailbox_full | undeliverable | The mailbox exists but is over its storage limit and is turning away new mail. |
unroutable | undeliverable | The domain points its mail to a server address that cannot be reached on the public internet. |
catch_all | risky | The domain accepts mail for every address, so this specific mailbox cannot be confirmed. |
role_account | risky | The address belongs to a team or function, such as info@ or sales@, rather than to one person. |
role_catch_all | risky | A team or function address on a domain that accepts mail for every address. |
disposable | risky | The address comes from a throwaway mail service whose inboxes are usually abandoned quickly. |
toxic | risky | The address is associated with spam, abuse or automated sign-ups. |
forwarding_domain | risky | The domain only relays mail to mailboxes elsewhere, which hides whether the final mailbox exists. |
suppressed | risky | The address appears on widely shared do-not-contact lists, often after earlier bounces or complaints. |
possible_trap | risky | The address follows patterns that traps often use, so mailing it can damage your sender reputation. |
spamtrap | risky | The address is believed to exist only to catch senders who mail lists without consent. |
complainer | risky | The person behind this address often reports legitimate mail as spam. |
server_unavailable | unknown | The mail server refused or dropped our connection, so the mailbox could not be checked. |
temporary_error | unknown | The mail server reported a temporary problem while we were checking the address. |
greylisted | unknown | The mail server asked us to come back later, which is a common anti-spam delay. |
anti_spam_block | unknown | An anti-spam system in front of the mail server stopped our check before it could finish. |
timeout | unknown | The check did not finish within the time limit, so we stopped waiting and charged nothing. |
internal_error | unknown | Something went wrong on our side while checking this address, so no credit is used. |
Action
What we recommend doing with the address.
| Action | Meaning |
|---|---|
send | Keep the address and send to it as usual. |
send_with_caution | Send only if the address matters to you, and watch bounces and complaints for this segment. |
remove | Remove the address from your list or ask for another one. |
retry_later | Check the address again later; the answer often becomes definite on a second try. |
SmtpProvider
The mail provider that hosts the domain's mailboxes: google (Google), microsoft (Microsoft), yahoo (Yahoo), apple (Apple iCloud), zoho (Zoho), proton (Proton), fastmail (Fastmail), gmx (GMX / Web.de), yandex (Yandex), mailru (Mail.ru), amazon (Amazon), mimecast (Mimecast), proofpoint (Proofpoint), barracuda (Barracuda), other (Other).
BulkJobStatus
Job lifecycle:
| Status | Meaning |
|---|---|
analyzing | Reading the file, removing duplicates and answering malformed addresses. |
ready | Analysis done. Waiting for a start (or for credits when error_code is insufficient_credits). |
queued | Started and waiting for its turn. |
running | Addresses are being checked. |
cancelling | A cancel was requested; work in flight is finishing. |
finalizing | Writing the result files. |
completed | Results are ready to download. |
failed | The job could not finish; see error_code. Unused credits are returned. |
cancelled | The job was cancelled. Unused credits are returned. |
expired | The files were deleted after 30 days. |
ComponentState
One of: operational, degraded, outage.
ErrorCode
Stable, machine-readable error code. Branch on this, not on the message.
| Code | HTTP | When |
|---|---|---|
invalid_request | 400 | A parameter is missing, has the wrong type or is out of range, or the API key was sent in the URL. The message names the fields at fault. |
unauthorized | 401 | No API key was sent, the key is malformed, or it has been revoked or rolled. |
insufficient_credits | 402 | The account has no credits left for this request (or not enough for a bulk job). |
forbidden | 403 | The key lacks the scope this endpoint needs (for example bulk), or the account is suspended or on a billing hold. |
forbidden_origin | 403 | The key is restricted to a list of browser origins and the request came from another Origin. |
not_found | 404 | The resource does not exist or belongs to another account. |
conflict | 409 | The request does not fit the current state of the resource, such as starting a bulk job that is already running or downloading results before a job has finished. |
length_required | 411 | A bulk upload was sent without a Content-Length header (for example as a chunked stream). |
payload_too_large | 413 | The request body is over the limit: 100 MB for a bulk upload, 100,000 addresses for a JSON list. |
rate_limited | 429 | The account or key sent more requests per second than its limit allows. |
abuse_throttled | 429 | Too many recent requests ended without a definite answer (free outcomes) compared with charged ones, so the account is slowed down for a while. |
key_daily_cap | 429 | This key reached the daily credit cap set for it in the dashboard. |
internal_error | 500 | Something failed on our side. No credit was used. |
service_unavailable | 503 | Validation is paused for a short time, for example while a dependency recovers. No credit was used. |
upstream_capacity | 503 | A bulk job could not be admitted right now because validation capacity is limited. No credit was used. |