Code samples

Copy-paste examples that validate one address, send the API key in the Authorization header, retry 429 and 503 answers after Retry-After when the wait is at most 10 seconds (never key_daily_cap, which lasts until the next UTC day), and report other errors with their code and request id. Each uses the free test address accepted@sandbox.email-validation.org; replace it with a real address when you are ready.

Set your key first: export EV_API_KEY="ev_live_…".

Validate an address

curl

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

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);

TypeScript

type Status = 'deliverable' | 'undeliverable' | 'risky' | 'unknown';
type Action = 'send' | 'send_with_caution' | 'remove' | 'retry_later';

interface ValidationResult {
  email: string;
  status: Status;
  reason: string;
  action: Action;
  did_you_mean: string | null;
  credits_used: 0 | 1;
  request_id: string;
}

interface ApiError {
  error: { code: string; message: string; request_id: string };
}

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

export async function validate(email: string, attempts = 3): Promise<ValidationResult> {
  for (let attempt = 1; ; attempt++) {
    const res = await fetch('https://api.email-validation.org/v1/validate', {
      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 (await res.json()) as ValidationResult;
    const { error } = (await res.json()) as ApiError;
    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})`);
  }
}

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
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";

Ruby

require "json"
require "net/http"
require "uri"

API = URI("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, attempts: 3)
  attempt = 0
  loop do
    attempt += 1
    req = Net::HTTP::Post.new(API)
    req["Authorization"] = "Bearer #{ENV.fetch("EV_API_KEY")}"
    req["Content-Type"] = "application/json"
    req.body = JSON.generate(email: email)
    res = Net::HTTP.start(API.host, API.port, use_ssl: true, read_timeout: 20) { |http| http.request(req) }
    return JSON.parse(res.body) if res.is_a?(Net::HTTPSuccess)

    error = JSON.parse(res.body).fetch("error")
    wait = [Integer(res["Retry-After"] || "1"), 1].max
    if %w[429 503].include?(res.code) && error["code"] != "key_daily_cap" && wait <= MAX_WAIT_S && attempt < attempts
      sleep(wait)
      next
    end
    raise "#{error["code"]}: #{error["message"]} (#{error["request_id"]})"
  end
end

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

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)
}

Java

import java.io.IOException;
import java.net.URI;
import java.net.http.HttpClient;
import java.net.http.HttpRequest;
import java.net.http.HttpResponse;
import java.time.Duration;

public class ValidateEmail {
    private static final HttpClient CLIENT =
            HttpClient.newBuilder().connectTimeout(Duration.ofSeconds(10)).build();

    static String validate(String email, int attempts) throws IOException, InterruptedException {
        // Build the JSON with your JSON library in real code; this address needs no escaping.
        String json = "{\"email\":\"" + email + "\"}";
        for (int attempt = 1; ; attempt++) {
            HttpRequest request = HttpRequest.newBuilder(URI.create("https://api.email-validation.org/v1/validate"))
                    .timeout(Duration.ofSeconds(20))
                    .header("Authorization", "Bearer " + System.getenv("EV_API_KEY"))
                    .header("Content-Type", "application/json")
                    .POST(HttpRequest.BodyPublishers.ofString(json))
                    .build();
            HttpResponse<String> response = CLIENT.send(request, HttpResponse.BodyHandlers.ofString());
            int status = response.statusCode();
            if (status == 200) {
                return response.body();
            }
            // Never wait out a long Retry-After: key_daily_cap lasts until the next UTC day.
            long wait = Math.max(1, response.headers().firstValueAsLong("Retry-After").orElse(1));
            boolean dailyCap = response.body().contains("\"key_daily_cap\"");
            if ((status == 429 || status == 503) && !dailyCap && wait <= 10 && attempt < attempts) {
                Thread.sleep(wait * 1000);
                continue;
            }
            throw new IOException("HTTP " + status + ": " + response.body());
        }
    }

    public static void main(String[] args) throws Exception {
        // The body is JSON: read status, reason, action and credits_used with Jackson, Gson or similar.
        System.out.println(validate("accepted@sandbox.email-validation.org", 3));
    }
}

C#

using System.Net;
using System.Net.Http.Headers;
using System.Net.Http.Json;
using System.Text.Json.Serialization;

using var http = new HttpClient { Timeout = TimeSpan.FromSeconds(20) };
http.DefaultRequestHeaders.Authorization =
    new AuthenticationHeaderValue("Bearer", Environment.GetEnvironmentVariable("EV_API_KEY"));

var result = await Validate(http, "accepted@sandbox.email-validation.org");
Console.WriteLine($"{result.Status} {result.Reason} {result.Action} {result.CreditsUsed}");

static async Task<ValidationResult> Validate(HttpClient http, string email, int attempts = 3)
{
    for (var attempt = 1; ; attempt++)
    {
        using var response = await http.PostAsJsonAsync("https://api.email-validation.org/v1/validate", new { email });
        if (response.IsSuccessStatusCode)
            return (await response.Content.ReadFromJsonAsync<ValidationResult>())!;

        var body = await response.Content.ReadFromJsonAsync<ErrorEnvelope>();
        // Never wait out a long Retry-After: key_daily_cap lasts until the next UTC day.
        var wait = response.Headers.RetryAfter?.Delta ?? TimeSpan.FromSeconds(1);
        var retryable = (response.StatusCode is HttpStatusCode.TooManyRequests or HttpStatusCode.ServiceUnavailable)
            && body?.Error.Code != "key_daily_cap" && wait <= TimeSpan.FromSeconds(10);
        if (retryable && attempt < attempts)
        {
            await Task.Delay(wait);
            continue;
        }
        throw new HttpRequestException($"{body?.Error.Code}: {body?.Error.Message} ({body?.Error.RequestId})");
    }
}

record ValidationResult(
    [property: JsonPropertyName("email")] string Email,
    [property: JsonPropertyName("status")] string Status,
    [property: JsonPropertyName("reason")] string Reason,
    [property: JsonPropertyName("action")] string Action,
    [property: JsonPropertyName("credits_used")] int CreditsUsed,
    [property: JsonPropertyName("request_id")] string RequestId);

record ApiError(
    [property: JsonPropertyName("code")] string Code,
    [property: JsonPropertyName("message")] string Message,
    [property: JsonPropertyName("request_id")] string RequestId);

record ErrorEnvelope([property: JsonPropertyName("error")] ApiError Error);

Rust

// Cargo.toml: reqwest = { version = "0.12", features = ["json"] }, serde = { version = "1", features = ["derive"] },
//             serde_json = "1", tokio = { version = "1", features = ["macros", "rt-multi-thread"] }
use std::time::Duration;

use reqwest::StatusCode;
use serde::Deserialize;

#[derive(Debug, Deserialize)]
struct ValidationResult {
    email: String,
    status: String,
    reason: String,
    action: String,
    credits_used: u8,
}

#[derive(Debug, Deserialize)]
struct ApiError {
    code: String,
    message: String,
    request_id: String,
}

#[derive(Debug, Deserialize)]
struct ErrorEnvelope {
    error: ApiError,
}

async fn validate(
    client: &reqwest::Client,
    key: &str,
    email: &str,
    attempts: u32,
) -> Result<ValidationResult, Box<dyn std::error::Error>> {
    let mut attempt = 0;
    loop {
        attempt += 1;
        let res = client
            .post("https://api.email-validation.org/v1/validate")
            .bearer_auth(key)
            .json(&serde_json::json!({ "email": email }))
            .send()
            .await?;
        let status = res.status();
        if status.is_success() {
            return Ok(res.json().await?);
        }
        let wait = res
            .headers()
            .get("retry-after")
            .and_then(|v| v.to_str().ok())
            .and_then(|v| v.parse::<u64>().ok())
            .unwrap_or(1)
            .max(1);
        let e = res.json::<ErrorEnvelope>().await?.error;
        // Never wait out a long Retry-After: key_daily_cap lasts until the next UTC day.
        let retryable = (status == StatusCode::TOO_MANY_REQUESTS || status == StatusCode::SERVICE_UNAVAILABLE)
            && e.code != "key_daily_cap"
            && wait <= 10;
        if retryable && attempt < attempts {
            tokio::time::sleep(Duration::from_secs(wait)).await;
            continue;
        }
        return Err(format!("{}: {} ({})", e.code, e.message, e.request_id).into());
    }
}

#[tokio::main]
async fn main() -> Result<(), Box<dyn std::error::Error>> {
    let key = std::env::var("EV_API_KEY")?;
    let client = reqwest::Client::builder().timeout(Duration::from_secs(20)).build()?;
    let r = validate(&client, &key, "accepted@sandbox.email-validation.org", 3).await?;
    println!("{} {} {} {} {}", r.email, r.status, r.reason, r.action, r.credits_used);
    Ok(())
}

Read the credit balance

curl

curl https://api.email-validation.org/v1/credits -H "Authorization: Bearer $EV_API_KEY"

JavaScript

const res = await fetch('https://api.email-validation.org/v1/credits', {
  headers: { Authorization: `Bearer ${process.env.EV_API_KEY}` },
});
const credits = await res.json();
if (!res.ok) throw new Error(`${credits.error.code}: ${credits.error.message}`);
console.log(`Balance: ${credits.balance}`);

Python

import os

import requests

resp = requests.get(
    "https://api.email-validation.org/v1/credits",
    headers={"Authorization": f"Bearer {os.environ['EV_API_KEY']}"},
    timeout=10,
)
resp.raise_for_status()
credits = resp.json()
print("Balance:", credits["balance"])
for lot in credits["lots"]:
    print(lot["remaining"], "credits expire on", lot["expires_at"][:10])

Create a bulk job

curl

# A CSV file with a header row; the email column is found automatically.
curl https://api.email-validation.org/v1/bulk \
  -H "Authorization: Bearer $EV_API_KEY" \
  -H "Content-Type: text/csv" \
  --data-binary @customers.csv

# Or a JSON list, without starting it yet:
curl "https://api.email-validation.org/v1/bulk?start=false" \
  -H "Authorization: Bearer $EV_API_KEY" \
  -H "Content-Type: application/json" \
  -d '{"emails": ["accepted@sandbox.email-validation.org", "catch_all@sandbox.email-validation.org"]}'

JavaScript

import { readFile } from 'node:fs/promises';

const csv = await readFile('customers.csv');
const res = await fetch('https://api.email-validation.org/v1/bulk?column=email&has_header=true', {
  method: 'POST',
  headers: {
    Authorization: `Bearer ${process.env.EV_API_KEY}`,
    'Content-Type': 'text/csv',
  },
  body: csv,
});
const job = await res.json();
if (!res.ok) throw new Error(`${job.error.code}: ${job.error.message}`);
console.log(job.id, job.status);

Python

import os

import requests

with open("customers.csv", "rb") as f:
    resp = requests.post(
        "https://api.email-validation.org/v1/bulk",
        params={"column": "email", "has_header": "true"},
        headers={"Authorization": f"Bearer {os.environ['EV_API_KEY']}", "Content-Type": "text/csv"},
        data=f.read(),
        timeout=120,
    )
job = resp.json()
if not resp.ok:
    raise RuntimeError(f"{job['error']['code']}: {job['error']['message']}")
print(job["id"], job["status"])

Verify a webhook

JavaScript

// Node.js (Express shown): use express.raw() so the body stays byte-for-byte as sent.
import { createHmac, timingSafeEqual } from 'node:crypto';
import express from 'express';

function verifyWebhook(rawBody, headers, secret, toleranceSeconds = 300) {
  const id = headers['webhook-id'];
  const timestamp = headers['webhook-timestamp'];
  const signatures = headers['webhook-signature'];
  if (!id || !timestamp || !signatures) throw new Error('Missing webhook headers');
  const ts = Number(timestamp);
  if (!Number.isInteger(ts) || Math.abs(Date.now() / 1000 - ts) > toleranceSeconds) {
    throw new Error('Timestamp outside tolerance');
  }
  const key = Buffer.from(secret.replace(/^whsec_/, ''), 'base64');
  const expected = createHmac('sha256', key).update(`${id}.${timestamp}.`).update(rawBody).digest();
  for (const entry of signatures.split(' ')) {
    const [version, sig] = entry.split(',');
    if (version !== 'v1' || !sig) continue;
    const given = Buffer.from(sig, 'base64');
    if (given.length === expected.length && timingSafeEqual(given, expected)) return JSON.parse(rawBody);
  }
  throw new Error('No valid signature');
}

const app = express();
app.post('/webhooks/email-validation', express.raw({ type: 'application/json' }), (req, res) => {
  let event;
  try {
    event = verifyWebhook(req.body, req.headers, process.env.EV_WEBHOOK_SECRET);
  } catch {
    return res.sendStatus(400);
  }
  // Deduplicate on req.headers['webhook-id'], then handle event.type.
  res.sendStatus(204);
});

Python

import base64
import hashlib
import hmac
import json
import time


def verify_webhook(raw_body: bytes, headers, secret: str, tolerance: int = 300) -> dict:
    """headers: any case-insensitive mapping, e.g. request.headers in Flask or Django."""
    msg_id = headers.get("webhook-id")
    timestamp = headers.get("webhook-timestamp")
    signatures = headers.get("webhook-signature")
    if not (msg_id and timestamp and signatures):
        raise ValueError("missing webhook headers")
    if not timestamp.isdigit() or abs(time.time() - int(timestamp)) > tolerance:
        raise ValueError("timestamp outside tolerance")

    key = base64.b64decode(secret.removeprefix("whsec_"))
    signed = f"{msg_id}.{timestamp}.".encode() + raw_body
    expected = hmac.new(key, signed, hashlib.sha256).digest()
    for entry in signatures.split(" "):
        version, _, sig = entry.partition(",")
        if version != "v1" or not sig:
            continue
        try:
            given = base64.b64decode(sig)
        except ValueError:
            continue
        if hmac.compare_digest(given, expected):
            return json.loads(raw_body)
    raise ValueError("no valid signature")


# Flask: event = verify_webhook(request.get_data(), request.headers, os.environ["EV_WEBHOOK_SECRET"])

PHP

<?php
function verify_webhook(string $rawBody, array $headers, string $secret, int $tolerance = 300): array
{
    $id = $headers['webhook-id'] ?? '';
    $timestamp = $headers['webhook-timestamp'] ?? '';
    $signatures = $headers['webhook-signature'] ?? '';
    if ($id === '' || $timestamp === '' || $signatures === '') {
        throw new RuntimeException('Missing webhook headers');
    }
    if (!ctype_digit($timestamp) || abs(time() - (int) $timestamp) > $tolerance) {
        throw new RuntimeException('Timestamp outside tolerance');
    }
    $key = base64_decode(preg_replace('/^whsec_/', '', $secret), true);
    if ($key === false) {
        throw new RuntimeException('Invalid webhook secret');
    }
    $expected = base64_encode(hash_hmac('sha256', "$id.$timestamp.$rawBody", $key, true));
    foreach (explode(' ', $signatures) as $entry) {
        [$version, $sig] = array_pad(explode(',', $entry, 2), 2, '');
        if ($version === 'v1' && hash_equals($expected, $sig)) {
            return json_decode($rawBody, true, 512, JSON_THROW_ON_ERROR);
        }
    }
    throw new RuntimeException('No valid signature');
}

// Read the raw body before any framework parses it.
$event = verify_webhook(
    file_get_contents('php://input'),
    array_change_key_case(getallheaders(), CASE_LOWER),
    getenv('EV_WEBHOOK_SECRET'),
);

Go

package webhooks

import (
	"crypto/hmac"
	"crypto/sha256"
	"encoding/base64"
	"errors"
	"math"
	"net/http"
	"strconv"
	"strings"
	"time"
)

// Verify checks a delivery. Read the body with io.ReadAll(r.Body) before calling it.
func Verify(body []byte, h http.Header, secret string, tolerance time.Duration) error {
	id, ts, sigs := h.Get("webhook-id"), h.Get("webhook-timestamp"), h.Get("webhook-signature")
	if id == "" || ts == "" || sigs == "" {
		return errors.New("missing webhook headers")
	}
	sec, err := strconv.ParseInt(ts, 10, 64)
	if err != nil || math.Abs(float64(time.Now().Unix()-sec)) > tolerance.Seconds() {
		return errors.New("timestamp outside tolerance")
	}
	key, err := base64.StdEncoding.DecodeString(strings.TrimPrefix(secret, "whsec_"))
	if err != nil {
		return err
	}
	mac := hmac.New(sha256.New, key)
	mac.Write([]byte(id + "." + ts + "."))
	mac.Write(body)
	expected := mac.Sum(nil)
	for _, entry := range strings.Split(sigs, " ") {
		version, sig, ok := strings.Cut(entry, ",")
		if !ok || version != "v1" {
			continue
		}
		given, err := base64.StdEncoding.DecodeString(sig)
		if err == nil && hmac.Equal(given, expected) {
			return nil
		}
	}
	return errors.New("no valid signature")
}

More languages for webhook verification are on Webhooks.

Notes

  • Use POST with a JSON body; it keeps addresses out of URLs and logs.
  • Set the HTTP timeout to about 20 seconds: a check may take up to 15 seconds.
  • A 200 answer with the status unknown is a normal, free result, not an error.
  • For JavaScript, TypeScript and Python, the official SDKs do all of this for you.

Updated · Maintained by the email-validation.org team