/v1/emails
Screens a single email address for deliverability and abuse signals before you rely on it.
- METHOD
- POST
- BODY CAP
- 4 KiB
- CHECKS
- 14
- FINDING CODES
- 9
POST /v1/emails screens one email address. It’s for the moment a signup, contact, or
checkout form hands you an address you don’t control yet — before you send anything to it
or store it as a customer record.
Request#
{ "email": "bob@gmail.com", "ip": "81.2.69.142" }
| Field | Type | Required | Notes |
|---|---|---|---|
email |
string | yes | 1–254 characters. Not pre-validated for format — a malformed address is a normal input, not a 400: email.syntax reports it as a documented finding instead of the gate rejecting the call outright. |
ip |
string | no | The signup IP, IPv4 or IPv6, up to 45 characters. Unlocks ip.hosting_provider, ip.not_public, ip.tor_exit, ip.vpn_provider and ip.asn. |
Sending ip (the address the signup came from) unlocks the ip.* checks listed below. An
ip that isn’t a valid IPv4 or IPv6 address is a 400 invalid_request, not a finding; omit
it entirely if you don’t have it. Without it, those checks appear in checks_skipped with
reason input.
Request body cap: 4 KB — far more than an address and an IP need.
Checks this gate runs#
Every check below is pure computation except email.mx, which makes a DNS-over-HTTPS
query, and email.young_domain, which makes an RDAP query. The ip.* checks run only when you send ip; all others run on every call. There’s no way to ask for a subset (design rule 5 —
no depth parameter).
| Check | What it does |
|---|---|
email.syntax |
Structural validity — see email.invalid_syntax. |
email.role_account |
Shared/automated mailbox name — see email.role_account. |
email.mx |
DNS MX lookup — see email.no_mx, email.null_mx, email.domain_not_found. Contributes data.mx — the domain’s MX hosts, sorted by preference — only when the lookup found real mail-exchanger records; absent (never null or []) on a lookup failure, a domain with no MX records, or an RFC 7505 null MX. |
email.disposable |
Disposable-provider list lookup — see email.disposable_domain. |
email.typo_domain |
Typo similarity to a major webmail provider — see email.typo_domain. |
ip.hosting_provider |
Needs ip. Cloud-provider range lookup — see ip.hosting_provider. Contributes data.hosting_provider when the address matches. |
ip.not_public |
Needs ip. Private, loopback or otherwise non-routable address — see ip.not_public. |
ip.tor_exit |
Needs ip. Tor exit-node list lookup — see ip.tor_exit. |
email.disposable_infrastructure |
Mail servers shared with known disposable domains — see email.disposable_infrastructure. Reuses the email.mx lookup. |
email.young_domain |
Domain registered in the last 30 days (RDAP) — see email.young_domain. Skipped for public mailbox providers. |
email.provider_type |
Contributes data.free_provider (always, a boolean), and data.privacy_relay (a boolean) with data.privacy_relay_provider when it is true. Emits no findings: a Gmail address or an Apple Hide My Email address is an ordinary person. |
ip.vpn_provider |
Needs ip. Known commercial VPN range — see ip.vpn_provider. |
ip.asn |
Needs ip. Contributes data.asn and data.asn_org. Emits no findings. |
email.alias_normalisation |
Contributes data.normalised for Gmail/Outlook-family addresses. Emits no findings — a +tag or a Gmail dot variant is a normal address, not a suspicious one. |
None of these check that the specific mailbox exists — only that the domain can plausibly
receive mail. StormDoor has no SMTP mailbox-existence check in this gate; that’s a
separate, narrower component (a mailbox prober) that isn’t part of /v1/emails.
Example#
curl -s -X POST https://api.stormdoor.dev/v1/emails \
-H "Authorization: Bearer sk_live_..." \
-H "content-type: application/json" \
-d '{"email":"bob@gnail.com"}'
{
"ok": true,
"verdict": "block",
"findings": [
{
"code": "email.typo_domain",
"severity": "medium",
"message": "Domain \"gnail.com\" looks like a typo of \"gmail.com\" (edit distance 1)"
},
{
"code": "email.disposable_domain",
"severity": "high",
"message": "Domain \"gnail.com\" is a known disposable/throwaway email provider"
}
],
"checks_run": ["email.alias_normalisation", "email.disposable", "email.mx", "email.role_account", "email.syntax", "email.typo_domain"],
"checks_skipped": [],
"meta": { "gate": "emails", "ruleset": "reference-9f0e0204" }
}
This is a real response, from a domain that’s both a typo of a major provider and has since been picked up as a disposable-mail catch-all — the two checks agree independently.
checks_skipped is empty here because every check ran. See response format
for what the four skip reasons mean, and don’t proxy that array to your own end users — it’s
a map of exactly what this gate doesn’t catch.
Finding codes#
Each code links to its own page with what it looks at, why it matters and what to do.