docs
stormdoor.dev
GATE

/v1/signups

Screens a new signup in one call — the email, its domain, and (if sent) the signup IP and form behaviour.

METHOD
POST
BODY CAP
8 KiB
CHECKS
21
FINDING CODES
4

POST /v1/signups is the composite gate: one call, one verdict, covering everything a new-account signup form usually wants checked. It takes the same email and optional ip fields as /v1/emails — nothing new to integrate if you’re already calling that gate.

Request#

{
  "email": "person@example.com",
  "ip": "203.0.113.4",
  "honeypot": "",
  "fill_time_ms": 6400,
  "user_agent": "Mozilla/5.0 (Macintosh; ...) Safari/605.1.15",
  "name": "Ada Lovelace"
}
Field Type Required Notes
email string yes Same bounds and validation as /v1/emails’s email field.
ip string no IPv4 or IPv6. Sending it unlocks the ip.* checks — same as /v1/emails’s optional ip field. An unparseable value is a 400 invalid_request.

| honeypot | string | no | The value of a hidden form field real users never see. Unlocks signup.honeypot_filled. Send "" when it was empty. | | fill_time_ms | integer | no | Milliseconds from form render to submit, measured by you. Unlocks signup.too_fast. Omit it when unknown. | | user_agent | string | no | The end user’s User-Agent header, forwarded by your backend. Unlocks signup.automation_user_agent. | | name | string | no | The name field from the form. Unlocks signup.suspicious_name. |

There is deliberately no password field: StormDoor never needs one, and a password sent here would be a secret we handled for nothing.

Each optional field unlocks the check named beside it; without it the check appears in checks_skipped with reason input.

Request body cap: 8 KB.

Checks this gate runs#

Most checks here belong to another gate — /v1/signups runs them all in one call rather than requiring three separate ones. The domain.* checks run against the domain half of email, not a separately submitted domain. The four signup.* checks are this gate’s own.

Check Home gate What it does
email.syntax, email.role_account, email.mx, email.alias_normalisation, email.disposable, email.disposable_infrastructure, email.provider_type, email.typo_domain /v1/emails See that gate’s docs.
domain.dns, domain.rdap, domain.parked, domain.typosquat /v1/domains Run against email’s domain — see that gate’s docs.
ip.not_public, ip.hosting_provider, ip.tor_exit, ip.vpn_provider, ip.asn /v1/ip Only when ip is sent — see that gate’s docs.
signup.honeypot_filled /v1/signups Only when honeypot is sent — see signup.honeypot_filled.
signup.too_fast /v1/signups Only when fill_time_ms is sent — see signup.too_fast.
signup.automation_user_agent /v1/signups Only when user_agent is sent — see signup.automation_user_agent.
signup.suspicious_name /v1/signups Only when name is sent — see signup.suspicious_name.

email.young_domain is not run here: domain.rdap already reports a recently registered domain as domain.newly_registered, and one fact should not be reported twice.

All of them run concurrently in one executor call — one check erroring or timing out never fails the others; it lands in checks_skipped with reason: "error" and the rest of the response is unaffected.

Example#

curl -s -X POST https://api.stormdoor.dev/v1/signups \
  -H "Authorization: Bearer sk_live_..." \
  -H "content-type: application/json" \
  -d '{"email":"person@10minutemail.com","ip":"3.5.140.1"}'
{
  "ok": true,
  "verdict": "block",
  "findings": [
    { "code": "email.disposable", "severity": "high", "message": "..." },
    { "code": "ip.hosting_provider", "severity": "medium", "message": "..." }
  ],
  "data": { "hosting_provider": "aws", "asn": 16509, "asn_org": "AMAZON-02", "free_provider": false, "privacy_relay": false },
  "checks_run": ["domain.dns", "domain.parked", "domain.rdap", "domain.typosquat", "email.alias_normalisation", "email.disposable", "email.disposable_infrastructure", "email.mx", "email.provider_type", "email.role_account", "email.syntax", "email.typo_domain", "ip.asn", "ip.hosting_provider", "ip.not_public", "ip.tor_exit", "ip.vpn_provider"],
  "checks_skipped": [],
  "meta": { "gate": "signups", "ruleset": "reference-..." }
}

See response format for how checks_skipped reasons work, and don’t proxy that array to your own end users.

Finding codes#

Each code links to its own page with what it looks at, why it matters and what to do.

signup.automation_user_agentmediumsignup.automation_user_agentsuspect
signup.honeypot_filledhighsignup.honeypot_filledblock
signup.suspicious_namelowsignup.suspicious_namesuspect
signup.too_fastmediumsignup.too_fastsuspect