checkmails

API Documentation

Verify email addresses programmatically via the checkmails REST API. Authentication uses Bearer tokens, each verified email costs 1 credit.

Quick start

REST API access requires a paid plan. Subscribe to get your API key.

Generate an API key from your dashboard (Profile tab → API Keys section). Keys are shown only once at creation time — copy them immediately.

Authentication

All requests must include an Authorization header with a Bearer token:

Authorization: Bearer chk_xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx

Keys start with chk_ followed by 32 hexadecimal characters.

Base URL

https://checkmails.eu

POST /api/v1/verify

Verify a single email address synchronously. Returns an enriched result with derived flags and a risk score (0–100). Costs 1 credit on success.

Request

curl -X POST https://checkmails.eu/api/v1/verify \
  -H "Authorization: Bearer chk_xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx" \
  -H "Content-Type: application/json" \
  -d '{"email":"hello@example.com"}'

Python

import requests

resp = requests.post(
    "https://checkmails.eu/api/v1/verify",
    headers={"Authorization": "Bearer chk_xxx..."},
    json={"email": "hello@example.com"},
)
print(resp.json())

Node.js

const resp = await fetch("https://checkmails.eu/api/v1/verify", {
  method: "POST",
  headers: {
    "Authorization": "Bearer chk_xxx...",
    "Content-Type": "application/json",
  },
  body: JSON.stringify({ email: "hello@example.com" }),
});
console.log(await resp.json());

Response (200)

{
  "email": "hello@example.com",
  "valid": true,
  "reason": "valide",
  "mx_host": "mx.example.com",
  "smtp_code": 250,
  "flags": ["valid"],
  "risk_score": 95
}

POST /api/v1/bulk

Submit up to 1000 emails at once for asynchronous verification. Returns a job_id; poll /api/v1/jobs/{id} until done: true. Credits are deducted upfront for syntactically valid emails.

Request

curl -X POST https://checkmails.eu/api/v1/bulk \
  -H "Authorization: Bearer chk_xxx..." \
  -H "Content-Type: application/json" \
  -d '{"emails":["a@example.com","b@example.com"]}'

Response (202)

{
  "job_id": "1a2b3c4d5e6f7890",
  "count": 2,
  "status": "processing",
  "poll": "/api/v1/jobs/1a2b3c4d5e6f7890"
}

GET /api/v1/jobs/{id}

Poll the status of a bulk job. While done: false, only progress (processed) is returned. Once done: true, the full results array is included.

Request

curl https://checkmails.eu/api/v1/jobs/1a2b3c4d5e6f7890 \
  -H "Authorization: Bearer chk_xxx..."

Response while running (200)

{
  "job_id": "1a2b3c4d5e6f7890",
  "total": 1000,
  "processed": 432,
  "done": false
}

Response when finished (200)

{
  "job_id": "1a2b3c4d5e6f7890",
  "total": 2,
  "processed": 2,
  "done": true,
  "results": [
    { "email": "a@example.com", "valid": true,  "reason": "valide", "flags": ["valid"], "risk_score": 95 },
    { "email": "b@example.com", "valid": false, "reason": "rejete: 550 user unknown", "flags": ["invalid"], "risk_score": 5 }
  ]
}

Errors

All errors return JSON in the form {"error":"<code>","message":"<explanation>"}:

HTTPError codeMeaning
400bad_requestBody is not valid JSON
400invalid_emailEmail is syntactically invalid
400empty_emailsBulk array is empty
400too_many_emailsBulk array exceeds 1000 entries
400no_valid_emailNo syntactically valid email in bulk request
401missing_bearerMissing Authorization header
401invalid_key_formatKey does not start with chk_ or is too short
401invalid_keyKey not found
401revoked_keyKey has been revoked
402payment_requiredInsufficient credits
403forbiddenJob belongs to another account
404not_foundJob ID unknown
429Rate limit exceeded

Rate limits

  • /api/v1/verify60 requests per minute per API key
  • /api/v1/bulk10 requests per minute per API key
  • /api/v1/jobs/{id}60 requests per minute per API key

When the limit is exceeded, the API responds with HTTP 429 and a JSON body {"error":"too many requests, retry later"}.

Response headers

All /api/v1/* endpoints return the following headers so the client can pace its requests:

HeaderDescription
X-RateLimit-LimitMaximum number of requests allowed per window (1 minute) for this API key.
X-RateLimit-RemainingNumber of requests remaining in the current window.
X-RateLimit-ResetUnix timestamp (UTC, seconds) at which the rate-limit window resets.

Webhooks

Configure webhooks in your account to receive HTTP POST notifications when verification jobs complete or fail. Each webhook is signed with HMAC-SHA256 for authenticity.

Payload format

POST https://yourapp.com/webhook
Content-Type: application/json
X-Checkmails-Signature: sha256=<hex>
X-Checkmails-Timestamp: 1730000000
X-Checkmails-Event: job.completed
User-Agent: checkmails-webhook/1.0

{
  "event": "job.completed",
  "timestamp": 1730000000,
  "job_id": "abc123def456",
  "user_id": 42,
  "total": 1000,
  "valid_count": 970,
  "invalid_count": 30,
  "started_at": 1729999000,
  "completed_at": 1730000000,
  "download_url": "https://checkmails.eu/api/v1/jobs/abc123def456"
}

Validate signature (Python)

import hmac, hashlib, time

def verify_webhook(secret: str, timestamp: str, body: bytes, sig_header: str) -> bool:
    # Anti-replay : reject if older than 5 minutes
    if abs(int(time.time()) - int(timestamp)) > 300:
        return False
    expected = hmac.new(secret.encode(), body, hashlib.sha256).hexdigest()
    actual = sig_header.removeprefix("sha256=")
    return hmac.compare_digest(expected, actual)

# Flask example:
# sig = request.headers.get('X-Checkmails-Signature', '')
# ts = request.headers.get('X-Checkmails-Timestamp', '')
# if not verify_webhook(SECRET, ts, request.data, sig):
#     abort(401)

Validate signature (Node.js)

const crypto = require('crypto');

function verifyWebhook(secret, timestamp, body, sigHeader) {
  if (Math.abs(Date.now()/1000 - parseInt(timestamp, 10)) > 300) return false;
  const expected = crypto.createHmac('sha256', secret).update(body).digest('hex');
  const actual = sigHeader.replace(/^sha256=/, '');
  return crypto.timingSafeEqual(Buffer.from(expected), Buffer.from(actual));
}

// Express example:
// const sig = req.headers['x-checkmails-signature'];
// const ts = req.headers['x-checkmails-timestamp'];
// const ok = verifyWebhook(SECRET, ts, req.rawBody, sig);
// if (!ok) return res.sendStatus(401);

Retry policy

Failed deliveries are retried up to 3 times with exponential backoff (immediate, +1s, +5s). Webhooks expecting a 2xx response within 5 seconds. Status codes 4xx (except 408 and 429) are treated as definitive client errors and not retried.

Replay protection

The header X-Checkmails-Timestamp contains the Unix timestamp of when the event was generated. Reject any webhook whose timestamp is older than 5 minutes to prevent replay attacks.

OpenAPI specification

The full OpenAPI 3.0 spec is available at /openapi.json for use with Swagger UI, Postman, or code generation tools.

AI integration (MCP)

checkmails exposes an MCP (Model Context Protocol) server at https://checkmails.eu/api/mcp. It lets you plug email verification directly into an AI assistant such as Claude Desktop or Cursor.

The server speaks JSON-RPC 2.0 (protocol revision 2025-06-18) and exposes three tools:

  • verify_email — verifies a single address.
  • verify_bulk — submits a batch of addresses for asynchronous verification.
  • get_job — fetches the status and results of a batch.

Authentication uses an Authorization: Bearer chk_<key> header (paid account required, like the REST API).

checkmails.eu/mcp · Claude Code · Cursor · Claude Desktop

Verdicts

Each address is classified into one of three verdicts:

  • Valid — the address exists and accepts mail. Billed 1 credit.
  • Invalid — the address does not exist or cannot receive mail. Billed 1 credit.
  • Indeterminate — verification could not be confirmed. Some providers (Apple/iCloud, sometimes Outlook) rate-limit bulk verification: the address is possibly valid — do not treat it as invalid. Not billed — credits not charged.

See also