docs
stormdoor.dev
GATE

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

email.disposable_domainhighemail.disposableblock
email.disposable_infrastructuremediumemail.disposable_infrastructuresuspect
email.domain_not_foundhighemail.mxblock
email.invalid_syntaxhighemail.syntaxblock
email.no_mxhighemail.mxblock
email.null_mxhighemail.mxblock
email.role_accountmediumemail.role_accountsuspect
email.typo_domainmediumemail.typo_domainsuspect
email.young_domainlowemail.young_domainsuspect