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>"}:
| HTTP | Error code | Meaning |
|---|---|---|
| 400 | bad_request | Body is not valid JSON |
| 400 | invalid_email | Email is syntactically invalid |
| 400 | empty_emails | Bulk array is empty |
| 400 | too_many_emails | Bulk array exceeds 1000 entries |
| 400 | no_valid_email | No syntactically valid email in bulk request |
| 401 | missing_bearer | Missing Authorization header |
| 401 | invalid_key_format | Key does not start with chk_ or is too short |
| 401 | invalid_key | Key not found |
| 401 | revoked_key | Key has been revoked |
| 402 | payment_required | Insufficient credits |
| 403 | forbidden | Job belongs to another account |
| 404 | not_found | Job ID unknown |
| 429 | — | Rate limit exceeded |
Rate limits
/api/v1/verify— 60 requests per minute per API key/api/v1/bulk— 10 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:
| Header | Description |
|---|---|
X-RateLimit-Limit | Maximum number of requests allowed per window (1 minute) for this API key. |
X-RateLimit-Remaining | Number of requests remaining in the current window. |
X-RateLimit-Reset | Unix 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.