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