Developer docs
Email Verification API
One endpoint, bearer-token auth, JSON in and out. Send an email address, get a disposable / legitimate verdict in milliseconds, from whatever stack you already run.
Introduction
The Mailsurity API tells you, in a single request, whether an email address is disposable (a throwaway, burner, or temporary inbox) or a legitimate address worth keeping in your funnel. It is a JSON-over-HTTPS API: one endpoint, bearer-token auth, no SDK required.
Every verdict flows through a layered cascade: curated allow/deny lists, a global disposable-domains list, a 30-day verdict cache, and finally live heuristics (MX records, local-part entropy, brand similarity, mail-server reputation) with an AI classifier for borderline cases. The response tells you which layer produced the answer via the source field.
Base URL
All requests go to a single HTTPS base. There is no versioned path prefix.
Always call the apex host shown above, not www.mailsurity.com. The www host issues a 308 redirect to the apex, and curl (with -L) and many HTTP clients drop the Authorization header on that cross-host redirect, which surfaces as a confusing Invalid authorization header error.
HTTPS is required; plain-HTTP requests are rejected. All request and response bodies are application/json encoded as UTF-8.
Authentication
Authenticate every request with your team's API key as a bearer token in the Authorization header. Keys are issued per team and can be revoked at any time from the dashboard.
- Treat your key like a password: keep it server-side, in an environment variable, never in client-side code or a public repo.
- Requests with a missing or malformed header get
401 Unauthorized; an unrecognized key also returns401. - Rotate a key by issuing a new one and revoking the old one from API Keys. Revocation is immediate.
Quickstart
Send a POST with one JSON field (email) and read isDisposable off the response. Pick your language:
// npm i mailsurity (server-side only, keep your key secret)
import { Mailsurity } from "mailsurity";
const ms = new Mailsurity({ apiKey: process.env.MAILSURITY_API_KEY! });
// Convenience boolean, fail-open: a transient outage never blocks a signup
if (await ms.isDisposable("user@mailinator.com")) {
// reject the signup
}
// ...or the full result
const verdict = await ms.checkEmail("user@mailinator.com");
console.log(verdict.isDisposable); // trueA throwaway address comes back like this:
{
"email": "user@mailinator.com",
"isDisposable": true,
"source": "list"
}Check an email
Evaluates a single email address and returns a disposable verdict. This is the core, metered endpoint: each successful call costs one credit (see Credits & limits).
Request
Headers
| Header | Required | Value |
|---|---|---|
Authorization | Yes | Bearer YOUR_API_KEY |
Content-Type | Yes | application/json |
Body parameters
| Field | Type | Required | Description |
|---|---|---|---|
email | string | Yes | The full email address to evaluate. Must be syntactically valid (local@domain.tld). Only the domain is used for the verdict; the local part is never stored. |
A minimal request body:
{ "email": "user@mailinator.com" }Response
A 200 OK returns a JSON verdict. Which fields are present depends on how the verdict was resolved: email, isDisposable, and source are always there; the rest are added only when a live detection runs.
| Field | Type | Always | Description |
|---|---|---|---|
email | string | Yes | Echo of the submitted address. |
isDisposable | boolean | null | Yes | true if the address is disposable, false if legitimate, null when the verdict is genuinely unknown. |
source | string | Yes | Which cascade layer answered. See Verdict sources. |
classification | string | No | disposable, legitimate, or unknown. Present on live/cached verdicts. |
confidence | number | No | Model confidence from 0 to 1. Present on live/cached verdicts. |
reason | string | No | Human-readable explanation of the verdict. |
signals | object | No | Feature breakdown behind the verdict (see below). Present on live/cached verdicts. |
matchedRule | object | No | Present only when one of your team allow/deny rules matched: { kind, value }. |
The signals object
| Field | Type | Description |
|---|---|---|
mxValid | boolean | The domain publishes valid MX (mail-exchange) records. |
entropy | number | Shannon entropy of the local part. High values suggest a randomly generated inbox. |
levenshteinMatch | string | null | Nearest known throwaway-mail brand if the domain is a close lookalike, else null. |
mxClusterSuspicious | boolean | The domain shares mail servers with known disposable providers. |
Verdict sources
The source field reports which layer of the detection cascade produced the verdict, in precedence order (earlier layers win):
| source | Meaning |
|---|---|
allowlist | Matched one of your team allow rules, forced to legitimate. |
denylist | Matched one of your team deny rules, forced to disposable. |
legit-list | A curated, globally known-legitimate domain (e.g. gmail.com). |
list | Matched the curated global disposable-domains list. |
cache | A cached result from a recent live detection of this domain. |
heuristic | Resolved live from MX, entropy, brand similarity, and cluster reputation. |
llm | A borderline case escalated to the AI classifier. |
Response examples
Disposable: resolved live (full payload)
{
"email": "user@mailinator.com",
"isDisposable": true,
"classification": "disposable",
"confidence": 0.94,
"reason": "high-entropy local part; MX cluster flagged disposable",
"source": "heuristic",
"signals": {
"mxValid": true,
"entropy": 3.7,
"levenshteinMatch": "mailinator",
"mxClusterSuspicious": true
}
}Legitimate: fast legit-list hit
{
"email": "jane@gmail.com",
"isDisposable": false,
"source": "legit-list"
}Team rule match
When a team allow/deny rule matches, it overrides every other layer:
{
"email": "partner@trusted-partner.com",
"isDisposable": false,
"source": "allowlist",
"matchedRule": { "kind": "domain", "value": "trusted-partner.com" }
}Errors
Errors use standard HTTP status codes and return a JSON body of the shape { "error": "message" }.
| Status | Meaning | Example body |
|---|---|---|
200 | The verdict is returned. | N/A |
400 | Bad request: the email is missing or not a valid address. | { "error": "Invalid email format" } |
401 | Unauthorized: missing/malformed header or unrecognized key. | { "error": "Invalid token" } |
402 | Payment required: the team is out of credits. | { "error": "Insufficient credits" } |
500 | Server error: something failed on our side. Safe to retry. | { "error": "Internal server error" } |
402 still applies even when a team allow-rule would have matched. Top up or wait for your cycle to renew.Credits & limits
- Every verdict (list hit, cache hit, or full live detection) costs exactly one credit. Errored requests (4xx/5xx) aren't charged, except that a
402is itself the "out of credits" signal. - New accounts start with 500 free credits, no card required.
- Paid plans grant a monthly credit allotment that resets each billing cycle and doesn't roll over. See Pricing.
- Checking thousands of addresses at once? Use the Bulk Check CSV uploader instead of looping the API. It's billed per unique domain.
Decision API: /api/assess
The recommended integration path going forward. Instead of a boolean plus classification metadata, /api/assess returns a 0-100 risk score, a paired confidence score, and a resolved ALLOW / VERIFY / BLOCK decision, so you branch directly on the decision the engine already made instead of writing your own if/else around isDisposable.
404 Not Found. /api/check-email's response shape is not changing or going away; this is an additive endpoint.Request
Same one-field body as /api/check-email:
{ "email": "abc@temporary-domain.example" }Response
{
"check_id": "0f3c1e6a-1234-4a11-9c2e-8b6f2a5d9e10",
"email": "abc@temporary-domain.example",
"risk_score": 93,
"confidence": 88,
"decision": "BLOCK",
"signals": {
"disposable": true,
"mx_valid": true,
"domain_age_days": 4,
"role_account": false,
"free_provider": false,
"network": null
},
"reasons": ["Disposable email provider", "Domain registered 4 days ago"],
"applied_rule": null,
"scoring_version": "decision-v1"
}| Field | Type | Description |
|---|---|---|
check_id | string (uuid) | Correlate a later /api/feedback call by passing this back as check_id. |
risk_score | 0–100 | How likely this identity is to be bad. |
confidence | 0–100 | How much evidence supports risk_score: a separate axis from the score itself. |
decision | 'ALLOW' | 'VERIFY' | 'BLOCK' | Default bands: 0–29 ALLOW, 30–69 VERIFY, 70–100 BLOCK. Overridable per team from the dashboard, and further overridable by your own decision rules. |
signals | object | Raw evidence behind the score, always present with null/false values rather than omitted keys. |
reasons | string[] | Human-readable justifications, ordered by contribution, capped at 5. |
applied_rule | object | null | Present only when one of your own decision rules overrode the threshold decision. |
scoring_version | string | Identifies the weight table that produced this score (currently "decision-v1"). |
Cost, auth, and error behavior are identical to /api/check-email: 1 credit per successful call, same 400/401/402 bodies, plus a 404 while the endpoint isn't live in this deployment.
Outcome feedback: /api/feedback
Reports the actual outcome of a signup (fraud or legitimate) so scoring gets calibrated against real results over time. This isn't a rating of /api/assess's decision; it's ground truth about the identity.
{ "check_id": "0f3c1e6a-1234-4a11-9c2e-8b6f2a5d9e10", "outcome": "fraud" }Or, without a check_id on hand:
{ "email": "user@example.com", "outcome": "legitimate", "note": "optional, max 500 chars" }Exactly one of check_id or email must be present.
A successful call returns:
{ "recorded": true, "id": 123 }| Aspect | Detail |
|---|---|
| Cost | 0 credits: deliberately free, since the label matters more than the credit |
| Rate limit | 500 requests/hour per team, 429 beyond that |
| Auth | Same bearer token as /api/check-email |
| Storage | Only the domain and a hash of the address are ever persisted; the raw address is not stored |
Best practices
- Validate syntax client-side before you call the API, so a malformed address doesn't cost you a credit and a round trip on a
400. - Branch your logic on
isDisposable, notsource. The boolean is the contract;sourceandsignalsare for logging and tuning your own thresholds. - Treat
nullas genuinely unknown, not as false. Decide whether to allow, soft-flag, or challenge rather than hard-blocking. - Retry
5xxwith backoff, and surface402to whoever manages billing rather than retrying it. - If you re-check the same domains often, cache the verdict on your side. Verdicts are stable, and it conserves credits.
Framework guides
Wiring this into a specific stack? These guides show the exact integration point (webhook handler, Action, or API route) for the frameworks Mailsurity customers use most.
- How to block disposable emails in Next.js
- Block fake signups in Clerk and Supabase Auth
- Block disposable signups in Auth0 with a Pre User Registration Action
See all guides at /guides.
Support
Questions, edge cases, or a domain you think we're scoring wrong? Reach us through the contact page, and check the API status page for live health.