CHECKMX / DEVELOPERS / HTTP API

API docs

Check addresses. Read the signals. Build with the API.

Create an API key
01 / START HERE

Overview

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.

02 / ACCESS

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.

03 / REQUEST

Verify one address

POST /api/verify

Send {"email":"ada@example.com"} as the JSON body. to_email is also accepted in place of email.

Terminal
curl --request POST https://checkmx.dev/api/verify \
  --header "Authorization: Bearer $CHECKMX_API_KEY" \
  --header 'Content-Type: application/json' \
  --data '{"email":"ada@example.com"}'

04 / RESULTS

Response fields

FieldMeaning
is_reachableLegacy 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.statusThe verdict: safe, invalid, catch_all (the domain accepts every address, so the mailbox cannot be confirmed), risky (reserved, rarely returned), or unknown.
verdict.sub_statusWhy, for example smtp_accepted, smtp_rejected, catch_all_live, catch_all_cached, greylisted, gateway_skipped, or timeout.
verdict.reason_codesArray of additive flags such as catch_all_domain, role_account, disposable, and provider_google.
smtp.is_catch_allBoolean: the domain accepts any recipient. Catch-all results have verdict.status catch_all.
mx, smtp, syntax, miscObjects with engine detail, such as misc.is_disposable.
credits_usedUsage counter for this response. Email checks are included free; reported for compatibility.
cachedtrue when served from verification memory.
cache_typePresent when cached: "address" or "catch_all_domain".
checked_atISO 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"
}
05 / USAGE

Usage and 7-day memory

  • Email checks are included free with every plan. credits_used is a usage counter in the response, not a charge.
  • unknown results 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 original checked_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.

06 / BULK API

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.

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

Submit CSV
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.csv

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

Download results
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.csv
07 / EVENTS

Webhooks

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/webhooks lists {"endpoints":[{"id":"…","url":"…","enabled":true,"createdAt":1792843200000,"updatedAt":1792843200000}]}. Times are Unix milliseconds.
  • POST /api/webhooks with {"action":"create","url":"https://example.com/webhooks/checkmx"} returns 201 with {"endpoint":{…},"secret":"…"}. Store the signing secret securely; it is shown only once.
  • POST /api/webhooks with {"action":"disable","id":"<endpoint-id>"} stops deliveries and returns {"disabled":true}. Create a new endpoint to receive events again.
  • POST /api/webhooks with {"action":"delete","id":"<endpoint-id>"} permanently removes the endpoint and its delivery log, returning {"deleted":true}.
  • POST /api/webhooks with {"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=100 returns {"deliveries":[…]}, newest first. The limit is 1–100 (default 50); there is no cursor. Omit endpointId for all endpoints. Each record includes id, endpointId, eventId, status, attemptCount, nextAttemptAt, createdAt, and updatedAt. Status is pending, delivering, delivered, or failed. 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.

Node · signature verification
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.

08 / RECOVERY

Errors

Errors are JSON: {"error":"message"}.

StatusMeaning / action
400Missing to_email / Invalid to_email / invalid job request. Check the request body.
401Missing or invalid key/session.
403Request not from the dashboard origin (dashboard-only routes).
404Job not found.
405Method not allowed. Use POST for verification and job submission; job result paging uses GET.
409Job is still processing. Poll status before downloading.
429Too many requests. Honor the Retry-After header (seconds).
500Server misconfigured.
502Upstream returned an invalid response.
503Service temporarily unavailable.
504Verification timed out: the receiving server didn’t answer. Safe to retry.
09 / THROUGHPUT

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

10 / OTHER WAYS TO BUILD

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

For assistants, the MCP tool is verify_email. Open assistant setup in the dashboard .