API docs
Check addresses. Read the signals. Build with the API.
Create an API keyOverview
The CheckMX HTTP API uses JSON over HTTPS. The base URL is https://checkmx.dev.
Use an API key to verify a single address from your server, or use the CLI and MCP tool in your workflow.
Authentication
Create a key in the dashboard and send it in the authorization header:
Authorization: Bearer <key>API keys belong to the workspace where they are created. Keys are shown once at creation, stored hashed, and can be revoked anytime. The dashboard shows each key’s last-used time. A key stays valid after its creator leaves the workspace until it is revoked.
Keep your key on the server. Never put keys in browser code. The JavaScript example below is for a server environment.
Verify one address
POST /api/verify
Send {"email":"ada@example.com"} as the JSON body. to_email is also accepted in place of email.
curl --request POST https://checkmx.dev/api/verify \
--header "Authorization: Bearer $CHECKMX_API_KEY" \
--header 'Content-Type: application/json' \
--data '{"email":"ada@example.com"}'const response = await fetch('https://checkmx.dev/api/verify', {
method: 'POST',
headers: {
Authorization: `Bearer ${process.env.CHECKMX_API_KEY}`,
'Content-Type': 'application/json'
},
body: JSON.stringify({ email: 'ada@example.com' })
});
const result = await response.json();
if (!response.ok) throw new Error(result.error);
console.log(result);import os
import requests
response = requests.post(
"https://checkmx.dev/api/verify",
headers={"Authorization": f"Bearer {os.environ['CHECKMX_API_KEY']}"},
json={"email": "ada@example.com"},
)
response.raise_for_status()
print(response.json())Response fields
| Field | Meaning |
|---|---|
is_reachable | Legacy verdict field, equal to verdict.status: safe, invalid, catch_all, risky, or unknown. Catch-all domains used to be reported as risky; they are now catch_all. Prefer verdict.status. |
verdict.status | The verdict: safe, invalid, catch_all (the domain accepts every address, so the mailbox cannot be confirmed), risky (reserved, rarely returned), or unknown. |
verdict.sub_status | Why, for example smtp_accepted, smtp_rejected, catch_all_live, catch_all_cached, greylisted, gateway_skipped, or timeout. |
verdict.reason_codes | Array of additive flags such as catch_all_domain, role_account, disposable, and provider_google. |
smtp.is_catch_all | Boolean: the domain accepts any recipient. Catch-all results have verdict.status catch_all. |
mx, smtp, syntax, misc | Objects with engine detail, such as misc.is_disposable. |
credits_used | Usage counter for this response. Email checks are included free; reported for compatibility. |
cached | true when served from verification memory. |
cache_type | Present when cached: "address" or "catch_all_domain". |
checked_at | ISO time of the check; the original time for cached results. |
Every response carries a verdict object, including remembered results stored before it existed. The smtp and misc objects carry the engine detail.
Example: a remembered catch-all result
Illustrative response; engine-detail objects are abbreviated.
{
"is_reachable": "catch_all",
"verdict": {
"status": "catch_all",
"sub_status": "catch_all_cached",
"reason_codes": ["catch_all_domain"]
},
"smtp": { "is_catch_all": true },
"mx": {},
"syntax": {},
"misc": { "is_disposable": false },
"credits_used": 0,
"cached": true,
"cache_type": "catch_all_domain",
"checked_at": "2026-10-24T12:00:00.000Z"
}Usage and 7-day memory
- Email checks are included free with every plan.
credits_usedis a usage counter in the response, not a charge. unknownresults are never remembered, so you can retry them.- When the same account rechecks the same address within 7 days, the response includes
cached: true,cache_type: "address", and the originalchecked_at. - For addresses on a domain confirmed catch-all in the last 30 days, the SMTP probe is skipped and the result is
catch_all. - Duplicates inside one bulk file are removed.
A cached response is 200 OK — a normal result, not an error.
Bulk jobs
Use the same Authorization: Bearer API key as single verification, or a signed-in dashboard session. Jobs belong to the authenticated account; another account receives 404 for status and downloads.
POST /api/jobs
Submit JSON with emails (1–10,000 addresses). An optional request_key is a 16–80 character idempotency key using letters, numbers, and hyphens. Reuse it when retrying a submission. If omitted, a key is generated and returned; save the returned job id and request_key.
curl https://checkmx.dev/api/jobs \
--header "Authorization: Bearer $CHECKMX_API_KEY" \
--header 'Content-Type: application/json' \
--data '{"emails":["ada@example.com","you@company.com"],"request_key":"bulk-submission-0001"}'Alternatively, send a UTF-8 CSV body (up to 2 MB), or JSON with a csv string field instead of emails. CSV uses the column headed email, email address, or email_address; without a recognized header it uses the first column. Blank cells are skipped. For a raw CSV body, put the optional request_key in the query string.
curl 'https://checkmx.dev/api/jobs?request_key=bulk-submission-0002' \
--header "Authorization: Bearer $CHECKMX_API_KEY" \
--header 'Content-Type: text/csv' \
--data-binary @emails.csvSubmission returns 202 for a live job or 200 for an entirely remembered job, with id, status, request_key, cached, duplicates_removed, and submitted. API jobs use the same 7-day per-account email dedupe and catch-all memory as dashboard jobs.
GET /api/jobs?id=<id>&after=<cursor>
Poll status and page results using has_more and next_cursor, starting at after=0. Status may be queued, running, paused, completed, or cancelled. Each row’s result carries cached and credits_used. Recover a job with /api/jobs?request_key=<request_key>.
curl "https://checkmx.dev/api/jobs?id=$JOB_ID&after=0" \
--header "Authorization: Bearer $CHECKMX_API_KEY"GET /api/jobs?id=<id>&format=json or format=csv
Download all available results after completion or cancellation. JSON includes job metadata and the full results array. CSV columns are input,is_reachable,cached,credits_used,checked_at. Downloads start from the first row, regardless of after. A job still processing returns 409; continue polling before retrying.
curl "https://checkmx.dev/api/jobs?id=$JOB_ID&format=json" \
--header "Authorization: Bearer $CHECKMX_API_KEY" --output results.json
curl "https://checkmx.dev/api/jobs?id=$JOB_ID&format=csv" \
--header "Authorization: Bearer $CHECKMX_API_KEY" --output results.csvWebhooks
Receive bulk-job events by registering an endpoint in Settings → Webhooks or through /api/webhooks. Authenticate with Authorization: Bearer <key> or a dashboard session. Endpoints and delivery logs belong to your account. Up to 10 endpoints may be registered per account, including disabled endpoints.
Manage endpoints
GET /api/webhookslists{"endpoints":[{"id":"…","url":"…","enabled":true,"createdAt":1792843200000,"updatedAt":1792843200000}]}. Times are Unix milliseconds.POST /api/webhookswith{"action":"create","url":"https://example.com/webhooks/checkmx"}returns201with{"endpoint":{…},"secret":"…"}. Store the signing secret securely; it is shown only once.POST /api/webhookswith{"action":"disable","id":"<endpoint-id>"}stops deliveries and returns{"disabled":true}. Create a new endpoint to receive events again.POST /api/webhookswith{"action":"delete","id":"<endpoint-id>"}permanently removes the endpoint and its delivery log, returning{"deleted":true}.POST /api/webhookswith{"action":"rotate","id":"<endpoint-id>"}immediately replaces the signing secret and returns{"id":"…","secret":"…"}. The old secret stops working.GET /api/webhooks?action=deliveries&endpointId=<endpoint-id>&limit=100returns{"deliveries":[…]}, newest first. The limit is 1–100 (default 50); there is no cursor. OmitendpointIdfor all endpoints. Each record includesid,endpointId,eventId,status,attemptCount,nextAttemptAt,createdAt, andupdatedAt. Status ispending,delivering,delivered, orfailed. The dashboard pages the latest 100 records, 10 at a time.
Use a public HTTPS URL on port 443 without credentials or a fragment. Private and reserved network destinations are blocked, and redirects are not followed. Session-authenticated mutations must come from the dashboard origin.
Events and payload
job.completed reports successful completion, including jobs served entirely from memory. job.failed reports a failed or cancelled job. Events are queued for endpoints enabled when the job reaches its terminal state.
{
"id": "event-uuid",
"type": "job.completed",
"created_at": 1792843200000,
"data": {
"job_id": "0123456789abcdef0123456789abcdef",
"status": "completed",
"total": 100,
"completed": 100
}
}created_at is Unix milliseconds. Counts are included when available. Payloads contain a job summary, without email addresses or result rows; fetch results through /api/jobs. Requests use Content-Type: application/json, X-CheckMX-Event for the event type, and X-CheckMX-Delivery for the delivery ID.
Verify signatures in Node
X-CheckMX-Signature has the form t=<unix-seconds>,v1=<hex-hmac>. Compute HMAC-SHA256 using the secret as a UTF-8 string over the timestamp, a period, and the exact raw request body bytes. Verify before parsing JSON; reject timestamps more than five minutes from your server clock.
const { createHmac, timingSafeEqual } = require('node:crypto');
// rawBody must be the original Buffer, before any JSON middleware.
function verifyWebhook(rawBody, signature, secret) {
if (typeof signature !== 'string') return false;
const match = /^t=(\d+),v1=([a-f0-9]{64})$/.exec(signature);
if (!match || Math.abs(Date.now() / 1000 - Number(match[1])) > 300) return false;
const expected = createHmac('sha256', secret)
.update(match[1] + '.').update(rawBody).digest();
const received = Buffer.from(match[2], 'hex');
return received.length === expected.length && timingSafeEqual(received, expected);
}
// In your HTTP handler, after collecting the raw request body:
// if (!verifyWebhook(rawBody, req.headers['x-checkmx-signature'],
// process.env.CHECKMX_WEBHOOK_SECRET)) { /* respond 401 and return */ }
// const event = JSON.parse(rawBody.toString('utf8'));Retries and duplicate deliveries
Return a 2xx response within 10 seconds to acknowledge receipt. Network errors, timeouts, and non-2xx responses are retried, up to 8 total attempts. Retry delays use exponential backoff starting at 30 seconds, capped at one hour, with 50–100% jitter (the first retry waits 15–30 seconds). A fresh timestamp and signature are generated for each attempt.
Delivery is at least once. Deduplicate by the X-CheckMX-Delivery delivery ID, which stays the same across retries. Atomically record that ID with your durable work or queue entry before acknowledging with 2xx; acknowledge an already processed ID without repeating side effects. Different endpoints receive different delivery IDs for the same event. Use the payload id to correlate the event across endpoints.
Errors
Errors are JSON: {"error":"message"}.
| Status | Meaning / action |
|---|---|
| 400 | Missing to_email / Invalid to_email / invalid job request. Check the request body. |
| 401 | Missing or invalid key/session. |
| 403 | Request not from the dashboard origin (dashboard-only routes). |
| 404 | Job not found. |
| 405 | Method not allowed. Use POST for verification and job submission; job result paging uses GET. |
| 409 | Job is still processing. Poll status before downloading. |
| 429 | Too many requests. Honor the Retry-After header (seconds). |
| 500 | Server misconfigured. |
| 502 | Upstream returned an invalid response. |
| 503 | Service temporarily unavailable. |
| 504 | Verification timed out: the receiving server didn’t answer. Safe to retry. |
Rate limits
/api/verify allows about 80 requests per minute per client IP. Exceeding this returns 429 with a Retry-After header in seconds. Wait for that interval before retrying.
/api/jobs allows 10 POST requests per minute per API key and 120 status/download requests per minute per API key, with separate counters. These burst limits apply per warm server instance. A limit returns 429 with Retry-After in seconds.
Bulk jobs have a limited number of active jobs per account. At the limit, the service returns 429: "Too many active jobs; resume an existing job first".
CLI and MCP
Set the CHECKMX_API_KEY environment variable in your server or terminal environment, then use the CLI:
npx checkmx verify you@company.com
npx checkmx verify --file emails.txt --jsonFor assistants, the MCP tool is verify_email. Open assistant setup in the dashboard .