# Validate an email

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

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

`POST /v1/validate` checks one email address and returns its status, the reason for it and a recommended action.
It costs 1 credit, except when the result is `unknown` or the answer needs no mail-server check (malformed and test
addresses), which are free.

## Request

```http
POST https://api.email-validation.org/v1/validate
Authorization: Bearer ev_live_…
Content-Type: application/json

{"email": "someone@example.com", "timeout": 10}
```

| Field | Required | Description |
|---|---|---|
| `email` | yes | The address to check (up to 1,024 characters). |
| `ip_address` | no | 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` | no | Seconds you are willing to wait, 5 to 15 (default 15). |
| `fresh` | no | `true` to check the address again even when it was checked in the last 12 months (default `false`). See [repeat checks](#repeat-checks). |

`GET /v1/validate?email=…` works too, with the same fields as query parameters. Prefer `POST`: it keeps addresses
out of URLs, where proxies and logs may record them. Never put the API key in the URL.

Needs a key with the `validate` scope.

## Response

```json
{
  "email": "catch_all@sandbox.email-validation.org",
  "status": "risky",
  "reason": "catch_all",
  "action": "send_with_caution",
  "account": "catch_all",
  "domain": "sandbox.email-validation.org",
  "did_you_mean": null,
  "free_email": false,
  "role_account": false,
  "disposable": false,
  "catch_all_domain": true,
  "mx_found": true,
  "mx_record": "mx.sandbox.email-validation.org",
  "smtp_provider": "other",
  "domain_age_days": 3650,
  "processed_at": "2026-11-02T09:30:00.000Z",
  "credits_used": 0,
  "request_id": "req_7Hq2LmX9cV4bN8tR1zK6wD"
}
```

| 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. |

Fields always come in this order. A field is `null` when it does not apply or is not known; for example
`did_you_mean` is set only for likely typos, and `mx_record` is `null` for domains without mail records.

Response headers:

| Header | Meaning |
|---|---|
| `X-Request-Id` | Our id for the request, the same as `request_id`. |
| `X-Credits-Remaining` | Your balance after this request. |
| `X-RateLimit-Limit` | Requests per second allowed for this key or account. |

## Statuses, reasons and actions

| Status | What it means | Default action | Credit |
|---|---|---|---|
| [`deliverable`](https://email-validation.org/docs/statuses/deliverable/) | The mailbox exists and accepted our check, so mail sent to it should arrive. | `send` (send) | 1 |
| [`undeliverable`](https://email-validation.org/docs/statuses/undeliverable/) | Mail sent to this address will not arrive, so it should come off your list. | `remove` (remove) | 1 |
| [`risky`](https://email-validation.org/docs/statuses/risky/) | The address may accept mail, but sending to it can hurt your bounce rate or sender reputation. | `send_with_caution` (send with caution) | 1 |
| [`unknown`](https://email-validation.org/docs/statuses/unknown/) | We could not get a definite answer this time, so no credit is used and a later retry often succeeds. | `retry_later` (retry later) | Free |

Each status has its own page with every reason explained: [statuses and reasons](https://email-validation.org/docs/statuses/).

## Timeouts

Some mail servers answer slowly. We wait at most `timeout` seconds (default 15). When a check cannot
finish in time, you get `status: unknown` with `reason: timeout`, and no credit is used. Set your HTTP client's
timeout a little higher than `timeout`, for example 20 seconds.

In sign-up forms, a shorter `timeout` such as 5 or 6 seconds keeps the form fast; see the
[sign-up form guide](https://email-validation.org/docs/guides/signup-form-validation/).

## Repeat checks

When an address was checked in the last 12 months, by you or anyone else, you may get the result
of that check instead of a new one. It comes back at once and costs 1 credit, like a new check. `processed_at` always
shows when the check was made, so you can tell how old a result is; `domain_age_days` is brought up to date.

Results with status `unknown`, catch-all results (`catch_all`, `trusted_catch_all`, `role_catch_all`, and any
result with `catch_all_domain: true`) and `mailbox_full` are never reused: those addresses are always checked again.
Answers to malformed and test addresses are never stored.

To check the address again anyway, send `fresh=true`: in the query string with `GET`, or with `POST` as a form field
or JSON `"fresh": true`. A definite new result then replaces the stored one (an `unknown` result leaves it in place).

We do not keep the address for this: results are stored under a keyed hash of the address and deleted after
12 months. See the [privacy policy](https://email-validation.org/legal/privacy/#retention).

## Errors

| HTTP | Codes | Meaning |
|---|---|---|
| 400 | `invalid_request` | Missing or invalid field, or a key in the URL. |
| 401 | `unauthorized` | Missing, malformed or revoked key. |
| 402 | `insufficient_credits` | Not enough credits. |
| 403 | `forbidden`, `forbidden_origin` | Scope, account state or browser origin. |
| 429 | `rate_limited`, `abuse_throttled`, `key_daily_cap` | Slow down; see `Retry-After`. |
| 503 | `service_unavailable` | Temporarily unavailable; retry after `Retry-After`. |

No credit is used for a request that ends in an error. Details: [Errors](https://email-validation.org/docs/errors/).

## Examples

**curl**

```bash
# --retry also covers 429 and 503 and honours Retry-After (curl 7.66+); --retry-max-time stops it from waiting
# out a long Retry-After such as key_daily_cap's (until the next UTC day).
curl --fail-with-body --retry 3 --retry-max-time 30 https://api.email-validation.org/v1/validate \
  -H "Authorization: Bearer $EV_API_KEY" \
  -H "Content-Type: application/json" \
  -d '{"email": "accepted@sandbox.email-validation.org"}'
```

**JavaScript**

```js
const API = 'https://api.email-validation.org/v1/validate';
const MAX_WAIT_S = 10; // never wait out a long Retry-After (key_daily_cap lasts until the next UTC day)

async function validate(email, attempts = 3) {
  for (let attempt = 1; ; attempt++) {
    const res = await fetch(API, {
      method: 'POST',
      headers: {
        Authorization: `Bearer ${process.env.EV_API_KEY}`,
        'Content-Type': 'application/json',
      },
      body: JSON.stringify({ email }),
      signal: AbortSignal.timeout(20_000),
    });
    if (res.ok) return res.json();
    const { error } = await res.json();
    const wait = Number(res.headers.get('Retry-After')) || 1;
    const retryable =
      (res.status === 429 || res.status === 503) && error.code !== 'key_daily_cap' && wait <= MAX_WAIT_S;
    if (retryable && attempt < attempts) {
      await new Promise((resolve) => setTimeout(resolve, wait * 1000));
      continue;
    }
    throw new Error(`${error.code}: ${error.message} (${error.request_id})`);
  }
}

const result = await validate('accepted@sandbox.email-validation.org');
console.log(result.status, result.reason, result.action, result.credits_used);
```

**Python**

```python
import os
import time

import requests

API = "https://api.email-validation.org/v1/validate"
MAX_WAIT_S = 10  # never wait out a long Retry-After (key_daily_cap lasts until the next UTC day)


def validate(email: str, attempts: int = 3) -> dict:
    for attempt in range(1, attempts + 1):
        resp = requests.post(
            API,
            headers={"Authorization": f"Bearer {os.environ['EV_API_KEY']}"},
            json={"email": email},
            timeout=20,
        )
        if resp.ok:
            return resp.json()
        error = resp.json()["error"]
        wait = int(resp.headers.get("Retry-After", "1"))
        retryable = resp.status_code in (429, 503) and error["code"] != "key_daily_cap" and wait <= MAX_WAIT_S
        if retryable and attempt < attempts:
            time.sleep(wait)
            continue
        raise RuntimeError(f"{error['code']}: {error['message']} ({error['request_id']})")
    raise RuntimeError("no attempts left")


result = validate("accepted@sandbox.email-validation.org")
print(result["status"], result["reason"], result["action"], result["credits_used"])
```

**PHP**

```php
<?php
function validate_email(string $email, int $attempts = 3): array
{
    for ($attempt = 1; ; $attempt++) {
        $headers = [];
        $ch = curl_init('https://api.email-validation.org/v1/validate');
        curl_setopt_array($ch, [
            CURLOPT_POST => true,
            CURLOPT_RETURNTRANSFER => true,
            CURLOPT_TIMEOUT => 20,
            CURLOPT_HTTPHEADER => [
                'Authorization: Bearer ' . getenv('EV_API_KEY'),
                'Content-Type: application/json',
            ],
            CURLOPT_POSTFIELDS => json_encode(['email' => $email]),
            CURLOPT_HEADERFUNCTION => function ($ch, $line) use (&$headers) {
                $parts = explode(':', $line, 2);
                if (count($parts) === 2) {
                    $headers[strtolower(trim($parts[0]))] = trim($parts[1]);
                }
                return strlen($line);
            },
        ]);
        $raw = curl_exec($ch);
        if ($raw === false) {
            throw new RuntimeException(curl_error($ch));
        }
        $status = curl_getinfo($ch, CURLINFO_RESPONSE_CODE);
        $body = json_decode($raw, true);
        if ($status === 200) {
            return $body;
        }
        // Never wait out a long Retry-After: key_daily_cap lasts until the next UTC day.
        $wait = max(1, (int) ($headers['retry-after'] ?? 1));
        $code = $body['error']['code'] ?? '';
        if (in_array($status, [429, 503], true) && $code !== 'key_daily_cap' && $wait <= 10 && $attempt < $attempts) {
            sleep($wait);
            continue;
        }
        throw new RuntimeException("{$body['error']['code']}: {$body['error']['message']}");
    }
}

$result = validate_email('accepted@sandbox.email-validation.org');
echo "{$result['status']} {$result['reason']} {$result['action']}\n";
```

**Go**

```go
package main

import (
	"bytes"
	"encoding/json"
	"fmt"
	"log"
	"net/http"
	"os"
	"strconv"
	"time"
)

type Result struct {
	Email       string `json:"email"`
	Status      string `json:"status"`
	Reason      string `json:"reason"`
	Action      string `json:"action"`
	CreditsUsed int    `json:"credits_used"`
	RequestID   string `json:"request_id"`
}

type apiError struct {
	Error struct {
		Code      string `json:"code"`
		Message   string `json:"message"`
		RequestID string `json:"request_id"`
	} `json:"error"`
}

var client = &http.Client{Timeout: 20 * time.Second}

// maxWait: never wait out a long Retry-After (key_daily_cap lasts until the next UTC day).
const maxWait = 10

func validate(email string, attempts int) (*Result, error) {
	payload, err := json.Marshal(map[string]string{"email": email})
	if err != nil {
		return nil, err
	}
	for attempt := 1; ; attempt++ {
		req, err := http.NewRequest(http.MethodPost, "https://api.email-validation.org/v1/validate", bytes.NewReader(payload))
		if err != nil {
			return nil, err
		}
		req.Header.Set("Authorization", "Bearer "+os.Getenv("EV_API_KEY"))
		req.Header.Set("Content-Type", "application/json")
		res, err := client.Do(req)
		if err != nil {
			return nil, err
		}
		if res.StatusCode == http.StatusOK {
			defer res.Body.Close()
			var result Result
			if err := json.NewDecoder(res.Body).Decode(&result); err != nil {
				return nil, err
			}
			return &result, nil
		}
		var e apiError
		_ = json.NewDecoder(res.Body).Decode(&e)
		res.Body.Close()
		wait, err := strconv.Atoi(res.Header.Get("Retry-After"))
		if err != nil || wait < 1 {
			wait = 1
		}
		retryable := (res.StatusCode == http.StatusTooManyRequests || res.StatusCode == http.StatusServiceUnavailable) &&
			e.Error.Code != "key_daily_cap" && wait <= maxWait
		if retryable && attempt < attempts {
			time.Sleep(time.Duration(wait) * time.Second)
			continue
		}
		return nil, fmt.Errorf("%s: %s (%s)", e.Error.Code, e.Error.Message, e.Error.RequestID)
	}
}

func main() {
	result, err := validate("accepted@sandbox.email-validation.org", 3)
	if err != nil {
		log.Fatal(err)
	}
	fmt.Println(result.Status, result.Reason, result.Action, result.CreditsUsed)
}
```

More languages, including TypeScript, Ruby, Java, C# and Rust: [code samples](https://email-validation.org/docs/code-samples/).
