# API reference

URL: https://email-validation.org/docs/api-reference/
Updated: 2026-10-20

> Documentation index: https://email-validation.org/llms.txt

Every endpoint, parameter and schema of the email-validation.org REST API, generated from our OpenAPI 3.1 document. Download
the document as [JSON](https://api.email-validation.org/openapi.json) or [YAML](https://api.email-validation.org/openapi.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>` (or `X-Api-Key`). See [Authentication](https://email-validation.org/docs/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](https://email-validation.org/docs/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 `timeout` seconds (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_at` shows when). Results with status `unknown`, catch-all results (including any with `catch_all_domain: true`) and `mailbox_full` are always checked again. Send `fresh=true` to 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.org` addresses: 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](https://email-validation.org/docs/api-reference/#schema-validationresult) |
| 400 | The request is not valid. Codes: `invalid_request`. | application/json: [Error](https://email-validation.org/docs/api-reference/#schema-error) |
| 401 | The API key is missing, malformed or no longer valid. Codes: `unauthorized`. | application/json: [Error](https://email-validation.org/docs/api-reference/#schema-error) |
| 402 | Not enough credits. Codes: `insufficient_credits`. | application/json: [Error](https://email-validation.org/docs/api-reference/#schema-error) |
| 403 | The key may not do this. Codes: `forbidden`, `forbidden_origin`. | application/json: [Error](https://email-validation.org/docs/api-reference/#schema-error) |
| 429 | Slow down and retry after `Retry-After` seconds. Codes: `rate_limited`, `abuse_throttled`, `key_daily_cap`. | application/json: [Error](https://email-validation.org/docs/api-reference/#schema-error) |
| 500 | Something failed on our side. No credit was used. Codes: `internal_error`. | application/json: [Error](https://email-validation.org/docs/api-reference/#schema-error) |
| 503 | Temporarily unavailable. No credit was used; retry after `Retry-After` seconds. Codes: `service_unavailable`, `upstream_capacity`. | application/json: [Error](https://email-validation.org/docs/api-reference/#schema-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 `timeout` seconds (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_at` shows when). Results with status `unknown`, catch-all results (including any with `catch_all_domain: true`) and `mailbox_full` are always checked again. Send `fresh=true` to 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.org` addresses: 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](https://email-validation.org/docs/api-reference/#schema-validaterequest)

**Responses**

| Status | Meaning | Body |
|---|---|---|
| 200 | The result. HTTP 200 also covers `unknown` results, which are free. | application/json: [ValidationResult](https://email-validation.org/docs/api-reference/#schema-validationresult) |
| 400 | The request is not valid. Codes: `invalid_request`. | application/json: [Error](https://email-validation.org/docs/api-reference/#schema-error) |
| 401 | The API key is missing, malformed or no longer valid. Codes: `unauthorized`. | application/json: [Error](https://email-validation.org/docs/api-reference/#schema-error) |
| 402 | Not enough credits. Codes: `insufficient_credits`. | application/json: [Error](https://email-validation.org/docs/api-reference/#schema-error) |
| 403 | The key may not do this. Codes: `forbidden`, `forbidden_origin`. | application/json: [Error](https://email-validation.org/docs/api-reference/#schema-error) |
| 413 | The body is too large. Codes: `payload_too_large`. | application/json: [Error](https://email-validation.org/docs/api-reference/#schema-error) |
| 429 | Slow down and retry after `Retry-After` seconds. Codes: `rate_limited`, `abuse_throttled`, `key_daily_cap`. | application/json: [Error](https://email-validation.org/docs/api-reference/#schema-error) |
| 500 | Something failed on our side. No credit was used. Codes: `internal_error`. | application/json: [Error](https://email-validation.org/docs/api-reference/#schema-error) |
| 503 | Temporarily unavailable. No credit was used; retry after `Retry-After` seconds. Codes: `service_unavailable`, `upstream_capacity`. | application/json: [Error](https://email-validation.org/docs/api-reference/#schema-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](https://email-validation.org/docs/api-reference/#schema-credits) |
| 400 | The request is not valid. Codes: `invalid_request`. | application/json: [Error](https://email-validation.org/docs/api-reference/#schema-error) |
| 401 | The API key is missing, malformed or no longer valid. Codes: `unauthorized`. | application/json: [Error](https://email-validation.org/docs/api-reference/#schema-error) |
| 403 | The key may not do this. Codes: `forbidden`, `forbidden_origin`. | application/json: [Error](https://email-validation.org/docs/api-reference/#schema-error) |
| 429 | Slow down and retry after `Retry-After` seconds. Codes: `rate_limited`, `abuse_throttled`, `key_daily_cap`. | application/json: [Error](https://email-validation.org/docs/api-reference/#schema-error) |
| 500 | Something failed on our side. No credit was used. Codes: `internal_error`. | application/json: [Error](https://email-validation.org/docs/api-reference/#schema-error) |
| 503 | Temporarily unavailable. No credit was used; retry after `Retry-After` seconds. Codes: `service_unavailable`, `upstream_capacity`. | application/json: [Error](https://email-validation.org/docs/api-reference/#schema-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](https://email-validation.org/docs/api-reference/#schema-usageresponse) |
| 400 | The request is not valid. Codes: `invalid_request`. | application/json: [Error](https://email-validation.org/docs/api-reference/#schema-error) |
| 401 | The API key is missing, malformed or no longer valid. Codes: `unauthorized`. | application/json: [Error](https://email-validation.org/docs/api-reference/#schema-error) |
| 403 | The key may not do this. Codes: `forbidden`, `forbidden_origin`. | application/json: [Error](https://email-validation.org/docs/api-reference/#schema-error) |
| 429 | Slow down and retry after `Retry-After` seconds. Codes: `rate_limited`, `abuse_throttled`, `key_daily_cap`. | application/json: [Error](https://email-validation.org/docs/api-reference/#schema-error) |
| 500 | Something failed on our side. No credit was used. Codes: `internal_error`. | application/json: [Error](https://email-validation.org/docs/api-reference/#schema-error) |
| 503 | Temporarily unavailable. No credit was used; retry after `Retry-After` seconds. Codes: `service_unavailable`, `upstream_capacity`. | application/json: [Error](https://email-validation.org/docs/api-reference/#schema-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`: string
- `text/plain`: string
- `application/json`: [BulkCreateJson](https://email-validation.org/docs/api-reference/#schema-bulkcreatejson)

**Responses**

| Status | Meaning | Body |
|---|---|---|
| 202 | Job created and being analysed. | application/json: [BulkJob](https://email-validation.org/docs/api-reference/#schema-bulkjob) |
| 400 | The request is not valid. Codes: `invalid_request`. | application/json: [Error](https://email-validation.org/docs/api-reference/#schema-error) |
| 401 | The API key is missing, malformed or no longer valid. Codes: `unauthorized`. | application/json: [Error](https://email-validation.org/docs/api-reference/#schema-error) |
| 403 | The key may not do this. Codes: `forbidden`, `forbidden_origin`. | application/json: [Error](https://email-validation.org/docs/api-reference/#schema-error) |
| 404 | No such resource for this account. Codes: `not_found`. | application/json: [Error](https://email-validation.org/docs/api-reference/#schema-error) |
| 409 | The resource is not in a state that allows this. Codes: `conflict`. | application/json: [Error](https://email-validation.org/docs/api-reference/#schema-error) |
| 411 | `Content-Length` is required. Codes: `length_required`. | application/json: [Error](https://email-validation.org/docs/api-reference/#schema-error) |
| 413 | The body is too large. Codes: `payload_too_large`. | application/json: [Error](https://email-validation.org/docs/api-reference/#schema-error) |
| 429 | Slow down and retry after `Retry-After` seconds. Codes: `rate_limited`, `abuse_throttled`, `key_daily_cap`. | application/json: [Error](https://email-validation.org/docs/api-reference/#schema-error) |
| 500 | Something failed on our side. No credit was used. Codes: `internal_error`. | application/json: [Error](https://email-validation.org/docs/api-reference/#schema-error) |
| 503 | Temporarily unavailable. No credit was used; retry after `Retry-After` seconds. Codes: `service_unavailable`, `upstream_capacity`. | application/json: [Error](https://email-validation.org/docs/api-reference/#schema-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](https://email-validation.org/docs/api-reference/#schema-bulkjob) |
| 400 | The request is not valid. Codes: `invalid_request`. | application/json: [Error](https://email-validation.org/docs/api-reference/#schema-error) |
| 401 | The API key is missing, malformed or no longer valid. Codes: `unauthorized`. | application/json: [Error](https://email-validation.org/docs/api-reference/#schema-error) |
| 403 | The key may not do this. Codes: `forbidden`, `forbidden_origin`. | application/json: [Error](https://email-validation.org/docs/api-reference/#schema-error) |
| 404 | No such resource for this account. Codes: `not_found`. | application/json: [Error](https://email-validation.org/docs/api-reference/#schema-error) |
| 429 | Slow down and retry after `Retry-After` seconds. Codes: `rate_limited`, `abuse_throttled`, `key_daily_cap`. | application/json: [Error](https://email-validation.org/docs/api-reference/#schema-error) |
| 500 | Something failed on our side. No credit was used. Codes: `internal_error`. | application/json: [Error](https://email-validation.org/docs/api-reference/#schema-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](https://email-validation.org/docs/api-reference/#schema-bulkjob) |
| 400 | The request is not valid. Codes: `invalid_request`. | application/json: [Error](https://email-validation.org/docs/api-reference/#schema-error) |
| 401 | The API key is missing, malformed or no longer valid. Codes: `unauthorized`. | application/json: [Error](https://email-validation.org/docs/api-reference/#schema-error) |
| 403 | The key may not do this. Codes: `forbidden`, `forbidden_origin`. | application/json: [Error](https://email-validation.org/docs/api-reference/#schema-error) |
| 404 | No such resource for this account. Codes: `not_found`. | application/json: [Error](https://email-validation.org/docs/api-reference/#schema-error) |
| 409 | The resource is not in a state that allows this. Codes: `conflict`. | application/json: [Error](https://email-validation.org/docs/api-reference/#schema-error) |
| 429 | Slow down and retry after `Retry-After` seconds. Codes: `rate_limited`, `abuse_throttled`, `key_daily_cap`. | application/json: [Error](https://email-validation.org/docs/api-reference/#schema-error) |
| 500 | Something failed on our side. No credit was used. Codes: `internal_error`. | application/json: [Error](https://email-validation.org/docs/api-reference/#schema-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](https://email-validation.org/docs/api-reference/#schema-bulkjob) |
| 400 | The request is not valid. Codes: `invalid_request`. | application/json: [Error](https://email-validation.org/docs/api-reference/#schema-error) |
| 401 | The API key is missing, malformed or no longer valid. Codes: `unauthorized`. | application/json: [Error](https://email-validation.org/docs/api-reference/#schema-error) |
| 402 | Not enough credits. Codes: `insufficient_credits`. | application/json: [Error](https://email-validation.org/docs/api-reference/#schema-error) |
| 403 | The key may not do this. Codes: `forbidden`, `forbidden_origin`. | application/json: [Error](https://email-validation.org/docs/api-reference/#schema-error) |
| 404 | No such resource for this account. Codes: `not_found`. | application/json: [Error](https://email-validation.org/docs/api-reference/#schema-error) |
| 409 | The resource is not in a state that allows this. Codes: `conflict`. | application/json: [Error](https://email-validation.org/docs/api-reference/#schema-error) |
| 429 | Slow down and retry after `Retry-After` seconds. Codes: `rate_limited`, `abuse_throttled`, `key_daily_cap`. | application/json: [Error](https://email-validation.org/docs/api-reference/#schema-error) |
| 500 | Something failed on our side. No credit was used. Codes: `internal_error`. | application/json: [Error](https://email-validation.org/docs/api-reference/#schema-error) |
| 503 | Temporarily unavailable. No credit was used; retry after `Retry-After` seconds. Codes: `service_unavailable`, `upstream_capacity`. | application/json: [Error](https://email-validation.org/docs/api-reference/#schema-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](https://email-validation.org/docs/api-reference/#schema-error) |
| 401 | The API key is missing, malformed or no longer valid. Codes: `unauthorized`. | application/json: [Error](https://email-validation.org/docs/api-reference/#schema-error) |
| 403 | The key may not do this. Codes: `forbidden`, `forbidden_origin`. | application/json: [Error](https://email-validation.org/docs/api-reference/#schema-error) |
| 404 | No such resource for this account. Codes: `not_found`. | application/json: [Error](https://email-validation.org/docs/api-reference/#schema-error) |
| 409 | The resource is not in a state that allows this. Codes: `conflict`. | application/json: [Error](https://email-validation.org/docs/api-reference/#schema-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](https://email-validation.org/docs/api-reference/#schema-error) |
| 500 | Something failed on our side. No credit was used. Codes: `internal_error`. | application/json: [Error](https://email-validation.org/docs/api-reference/#schema-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](https://email-validation.org/docs/api-reference/#schema-servicestatus) |
| 503 | Temporarily unavailable. No credit was used; retry after `Retry-After` seconds. Codes: `service_unavailable`, `upstream_capacity`. | application/json: [Error](https://email-validation.org/docs/api-reference/#schema-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](https://email-validation.org/docs/api-reference/#schema-error) |
| 401 | The API key is missing, malformed or no longer valid. Codes: `unauthorized`. | application/json: [Error](https://email-validation.org/docs/api-reference/#schema-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](https://email-validation.org/docs/api-reference/#schema-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](https://email-validation.org/docs/api-reference/#schema-error) |
| 401 | The API key is missing, malformed or no longer valid. Codes: `unauthorized`. | application/json: [Error](https://email-validation.org/docs/api-reference/#schema-error) |
| 429 | Slow down and retry after `Retry-After` seconds. Codes: `rate_limited`, `abuse_throttled`, `key_daily_cap`. | application/json: [Error](https://email-validation.org/docs/api-reference/#schema-error) |

### A2A endpoint (POST only)

`GET /a2a`

The endpoint takes JSON-RPC requests with `POST`; `GET` answers 405.

**Authentication:** none.

**Responses**

| Status | Meaning | Body |
|---|---|---|
| 405 | Method not allowed. | (none) |

### A2A endpoint (JSON-RPC 2.0)

`POST /a2a`

Agent2Agent protocol endpoint (A2A 1.0 JSON-RPC binding; send the header `A2A-Version: 1.0`, or no header for A2A 0.3). Skills: `validate_email` (one address per message, in a text part or a data part `{"email": …}`; the result comes back as a data part of a completed task) and `get_credits`. Authenticate with the same API key as the REST API; a validation uses the same credits, limits and rules as `POST /v1/validate`. Tasks finish inside the request and are not stored. JSON-RPC errors use the A2A codes (for example -32602 invalid parameters); throttling and outages are answered with HTTP 429 and 503 and `Retry-After`, like the REST API. Agent card: https://api.email-validation.org/.well-known/agent-card.json. Guide: https://email-validation.org/docs/ai-agents/

**Authentication:** API key (`Authorization: Bearer …` or `X-Api-Key`).

**Parameters**

| Name | In | Type | Required | Description |
|---|---|---|---|---|
| `A2A-Version` | header | string: `1.0`, `0.3` | no | A2A protocol version (Major.Minor). No header means 0.3. |

**Request body**

- `application/json`: object
- `application/a2a+json`: object

**Responses**

| Status | Meaning | Body |
|---|---|---|
| 200 | A JSON-RPC response: a result (for example `{"task": {…}}`) or an error. | application/json: object |
| 400 | The request is not valid. Codes: `invalid_request`. | application/json: [Error](https://email-validation.org/docs/api-reference/#schema-error) |
| 401 | The API key is missing, malformed or no longer valid. Codes: `unauthorized`. | application/json: [Error](https://email-validation.org/docs/api-reference/#schema-error) |
| 413 | The body is too large. Codes: `payload_too_large`. | application/json: [Error](https://email-validation.org/docs/api-reference/#schema-error) |
| 429 | Slow down and retry after `Retry-After` seconds. Codes: `rate_limited`, `abuse_throttled`, `key_daily_cap`. | application/json: [Error](https://email-validation.org/docs/api-reference/#schema-error) |
| 503 | Temporarily unavailable. No credit was used; retry after `Retry-After` seconds. Codes: `service_unavailable`, `upstream_capacity`. | application/json: [Error](https://email-validation.org/docs/api-reference/#schema-error) |

### Get the A2A agent card

`GET /.well-known/agent-card.json`

The A2A agent card: endpoint, protocol versions, the bearer key scheme and the skills. No key needed. The same card is published at https://email-validation.org/.well-known/agent-card.json.

**Authentication:** none.

**Responses**

| Status | Meaning | Body |
|---|---|---|
| 200 | The agent card. | application/json: object |

## Webhooks

### `bulk.completed`

Body: [BulkWebhookEvent](https://email-validation.org/docs/api-reference/#schema-bulkwebhookevent).

### `bulk.failed`

Body: [BulkWebhookEvent](https://email-validation.org/docs/api-reference/#schema-bulkwebhookevent).

### `credits.low`

Body: [CreditsLowEvent](https://email-validation.org/docs/api-reference/#schema-creditslowevent).

### `credits.expiring`

Body: [CreditsExpiringEvent](https://email-validation.org/docs/api-reference/#schema-creditsexpiringevent).

### `webhook.test`

Body: [WebhookTestEvent](https://email-validation.org/docs/api-reference/#schema-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](https://email-validation.org/docs/api-reference/#schema-status) |  |
| `reason` | [Reason](https://email-validation.org/docs/api-reference/#schema-reason) |  |
| `action` | [Action](https://email-validation.org/docs/api-reference/#schema-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](https://email-validation.org/docs/api-reference/#schema-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](https://email-validation.org/docs/api-reference/#schema-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](https://email-validation.org/docs/api-reference/#schema-usagetotals) |  |
| `items` | array of [UsageDay](https://email-validation.org/docs/api-reference/#schema-usageday) or [UsageByKey](https://email-validation.org/docs/api-reference/#schema-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](https://email-validation.org/docs/api-reference/#schema-statuscountsapprox) |  |

### UsageDay

| Field | Type | Description |
|---|---|---|
| `date` | string (date) |  |
| `credits_used` | integer | Credits used that day, from the credit ledger (exact). |
| `by_status` | [StatusCountsApprox](https://email-validation.org/docs/api-reference/#schema-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](https://email-validation.org/docs/api-reference/#schema-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](https://email-validation.org/docs/api-reference/#schema-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. |
