Email verification in one POST request.
Send an email address, get back a clear verdict, a 0–100 score, and every check we ran. Bearer-token auth, JSON in and out, no SDK required.
Authentication
Every request needs an Authorization: Bearer <token> header. Generate a token from API keys in your dashboard. Tokens are scoped to a single account.
Verify an email
POST /api/v1/verify with a JSON body containing an email. One verification spends one credit, whatever the verdict.
Reading the result
Branch on result, not on score. result is one of four values and is always authoritative. score (0–100) is a confidence number for ranking addresses within a verdict — an unknown is deliberately scored mid-range, so a score threshold on its own would misfile it. reason gives the single deciding signal and checks gives all of them.
For completeness: once a verdict is decidable, the score thresholds are 80 for deliverable and 50 for risky. But unknown is assigned before those thresholds are ever reached, which is exactly why a score comparison is not a substitute for reading result.
deliverable additionally requires SMTP mailbox confirmation on a non-catch-all domain — a valid MX record alone caps the score at 75 and returns risky / mailbox_unconfirmed. (Breaking change note: the former valid_mx reason is retired; confirmed mailboxes return mailbox_exists.)
A live SMTP probe confirmed this exact mailbox on a domain that is not a catch-all. Safe to send.
Send at your own risk. Disposable domain, role account, catch-all domain, or a mailbox we could not confirm (mailbox_unconfirmed). Free-provider inboxes usually land here too, because those providers block mailbox probes.
Hard failure — it will bounce. Invalid syntax, no mail servers for the domain, or the server rejected the mailbox outright.
We could not reach a verdict: greylisting, an SMTP or DNS timeout, or a probe the server refused. Retry later; an honest unknown beats a fabricated verdict.
The checks object
Every check we ran, always present. Disposable, role and catch-all are risk signals: they deduct from the score and can never make an address deliverable.
Errors
A syntactically invalid email is a normal result (200 + undeliverable), not an error. Errors are reserved for request problems, and every one is a JSON body with an error key. None of the responses below spends a credit.
Volume and limits
Requests are limited to 120 per minute per API key (counted on the bearer token; unauthenticated calls are counted per IP). Request 121 inside a minute returns HTTP 429 with {"error":"Too many requests"} — back off and retry after the window. Two more things throttle in practice: live mailbox probes are rate-limited per destination domain (so hammering one domain returns smtp_skipped: "rate_limited" rather than a verdict), and large one-off lists are far better served by a CSV bulk import in the dashboard than by thousands of concurrent single calls. Bulk verification is dashboard-only for now — there is no bulk API endpoint yet.
Usage
GET /api/v1/usage returns your account's request counts, error counts and credits spent over a period (?period=24h|7d|30d), per token — names and prefixes only, never raw keys.
curl https://clearmx.app/api/v1/usage?period=7d \ -H "Authorization: Bearer [REDACTED]"
Bounce intelligence
Verification happens before a send. These endpoints close the loop after it, and ClearMX still never carries your mail: you keep your ESP, we hand you a return path per message, and whatever bounces comes back to us. The address lands on your suppression list, your endpoint hears about it, and the next verify call for that address answers "suppressed": true.
10 tracked messages cost one credit. The suppression list, webhooks and sender-domain health are free. Setup, feedback loops and ESP settings: Set up bounce handling.
Track a message
POST /api/v1/tracked_messages before or right after you send, then use verp_return_path as the message's Return-Path (envelope sender). external_ref is yours and unique per account — re-posting it returns the same row with 200 and costs nothing, so a retried send is idempotent on our side. Single tracks run down a prepaid counter of 10 and debit one credit when it is empty; POST /api/v1/tracked_messages/batch with {"messages":[…]} (up to 1000) pays ceil(n/10) up front and is all-or-nothing: a malformed item or an unaffordable batch writes no rows. A recipient already on your suppression list still gets a return path, with "suppressed": true so you can skip the send.
Suppression list
Free, per account, never shared. GET /api/v1/suppressions with optional since (ISO 8601, filters on last_seen_at), page and per_page (up to 500) returns {"suppressions":[{"email","reason","source","first_seen_at","last_seen_at","hit_count"}],"page","per_page","total"}. POST {"email","reason","notes"} adds one (reason defaults to manual; hard_bounce and complaint are accepted so an existing list can be imported; a complaint is never downgraded). DELETE /api/v1/suppressions/:email removes one. Hard bounces suppress at once, complaints at once, soft bounces after 3 in a row to the same address; a 5.7.x reputation block is reported with "policy": true and suppresses nobody — it is about the sender, not the address.
Webhooks
POST /api/v1/webhook_endpoints with {"url":"https://…","events":["bounce.hard","bounce.soft","complaint"]} returns the signing secret once. GET lists, DELETE /:id removes, POST /:id/rotate_secret issues a new secret, POST /:id/test sends a ping. Each bounce or complaint is one JSON POST to your URL:
X-ClearMX-Signature is v1= followed by the hex HMAC-SHA256 of "<timestamp>.<raw body>" under your secret. Verify it over the raw bytes before parsing, compare in constant time, and refuse a timestamp more than five minutes old. Retries on a non-2xx answer or a 5-second timeout come at 1 min, 5 min, 30 min, 2 h, 12 h and 24 h; after the sixth failure the endpoint is marked failing in your dashboard. We may deliver an event twice — be idempotent on event_id.
Sender domain health
One DNS pass over the domain you send from, free, cached for an hour per domain, selector and IP. SPF missing or +all, a missing or unparseable DKIM key, a missing DMARC record, or a blacklist listing grade red; ~all, more than ten SPF lookups, a DKIM key under 1024 bits, DMARC p=none or a policy bounce this week grade amber. A lookup that fails grades unknown — never green. Pass sending_ip when you send from shared infrastructure: that is what the IP lists are actually about.
API FAQ
Yes, without ever carrying your mail. Ask POST /api/v1/tracked_messages for a return path before you send, use it as the message's Return-Path, and whatever bounces comes to us: the address lands on your suppression list, your webhook endpoint receives a signed bounce.hard, bounce.soft or complaint event, and the next verify call for that address says suppressed: true. 10 tracked messages cost one credit; the list, the webhooks and sender-domain health are free.
Create a free ClearMX account, open API keys in the dashboard, and generate a token. Each token is scoped to one account.
Yes. A single POST returns the full result in the response body — no polling, no callback. Syntax and DNS answers come from cache where possible; when a live mailbox probe runs it adds a round trip to the receiving mail server, bounded by a 5-second timeout.
The API returns HTTP 402 with {"error":"Out of credits"} and an upgrade_url pointing at pricing. Every new account starts with 100 free verification credits, and credits never expire.
No. ClearMX never sends email. The optional SMTP check talks to the receiving mail server and stops before DATA — no message is ever transmitted.
See also: Email verification API guide · Bulk verification