Assess a signup
POST /v1/assess — the core ShieldSignup endpoint. Full request and response reference.
POST /v1/assess is the only endpoint you need to integrate ShieldSignup
into a signup flow. It accepts an email address and, optionally, the end
user's IP. It returns a verdict, a numeric score, machine-readable reason
codes, and underlying signals.
Send the client IP in production
email is required. ip is optional but strongly recommended.
Without a usable public IP, only email and domain signals are scored.
Loopback, private, and localhost values are accepted but ignored — see
Getting the client IP.
Endpoint
POST https://api.shieldsignup.com/v1/assessAuthentication
Bearer token in the Authorization header. See
Authentication for details.
Request
Headers
| Header | Required | Value |
|---|---|---|
Authorization | Yes | Bearer sk_live_… or Bearer sk_test_… |
Content-Type | Yes | application/json |
Body
| Field | Type | Required | Description |
|---|---|---|---|
email | string | Yes | Email address to assess. Must pass basic format validation (local@domain.tld). Trimmed and lower-cased server-side. |
ip | string | No | IPv4 or IPv6 address of the end user submitting the signup, collected server-side. Strongly recommended for production. Omitted, empty, loopback, private, or localhost values run an email-only assessment. Malformed strings return 400 invalid_ip. See Getting the client IP. |
session_id | string | No | Your internal session or request ID. Echoed back on the response and stored on the assessment record. Use it to correlate ShieldSignup assessments with your own request logs. If omitted, the response does not include a session_id field at all. |
There is no metadata field at MVP. If you need to attach extra context,
put it in your own database keyed on request_id.
Full example
{
"email": "user@example.com",
"ip": "203.0.113.42",
"session_id": "sess_abc123"
}Minimal example (email only)
Works without IP extraction — email and domain signals only:
{
"email": "user@example.com"
}Recommended example (email + client IP)
{
"email": "user@example.com",
"ip": "203.0.113.42"
}Response
Status
HTTP/1.1 200 OK
Content-Type: application/jsonBody
| Field | Type | Always present | Description |
|---|---|---|---|
request_id | string | Yes | Unique ID for this assessment, prefix req_. Use it for support lookups and to refetch the assessment with GET /v1/assess/:request_id. |
session_id | string | Only if sent | Echoed back from the request. |
verdict | string | Yes | One of "allow", "challenge", "block". |
score | integer | Yes | Risk score 0–100. Higher is riskier. |
reasons | array of objects | Yes | The reason codes that contributed to the score. Empty array on a clean signal. |
reasons[].code | string | Yes | Machine-readable reason code. See Reason codes. |
reasons[].signal | string | Yes | Source group. One of "email", "ip", "velocity". |
ip_provided | boolean | Yes | true when a usable public IP was used for scoring. |
ip_status | string | Yes | One of "ok", "missing", "ignored_loopback", "ignored_private", "ignored_localhost". |
signals | object | Yes | Underlying signal values (see below). |
signals.email | object | Yes | Email-derived signals. |
signals.email.disposable | boolean | Yes | Domain is on the disposable-domain blocklist. |
signals.email.domain | string | Yes | The domain part of the email, lower-cased. |
signals.email.domain_age_days | integer | Yes | Age of the domain in days. 3650 is the placeholder when age is unknown. |
signals.email.mx_valid | boolean | Yes | Domain has valid MX records. |
signals.email.public_domain | boolean | Yes | Email is from a free consumer provider (Gmail, Yahoo, Outlook, Hotmail). |
signals.email.role_account | boolean | Yes | Local-part is a known role account (admin, info, support, sales). |
signals.ip | object | Only when ip_provided is true | IP-derived signals. Omitted from the response when no usable IP was available. |
signals.ip.address | string | When signals.ip is present | The usable public IP used for scoring. |
signals.ip.tor | boolean | When signals.ip is present | IP is a known Tor exit node. |
signals.ip.vpn | boolean | When signals.ip is present | IP is a known VPN endpoint. |
signals.ip.proxy | boolean | When signals.ip is present | IP is a known anonymous proxy. |
signals.ip.datacenter | boolean | When signals.ip is present | IP belongs to a datacenter / hosting ASN. |
signals.ip.abuse_score | integer | When signals.ip is present | Abuse score 0–100 from upstream IP intelligence sources. |
signals.ip.country_code | string | When signals.ip is present | ISO 3166-1 alpha-2 country code. |
signals.ip.asn | string | When signals.ip is present | ASN string, e.g. "AS15169". |
signals.velocity | object | Yes | Sliding-window counters seen across all of your traffic. |
signals.velocity.ip_signups_1h | integer | Yes | Signup attempts from this IP in the last 60 minutes. 0 when ip_provided is false. |
signals.velocity.ip_signups_24h | integer | Yes | Signup attempts from this IP in the last 24 hours. 0 when ip_provided is false. |
signals.velocity.email_domain_1h | integer | Yes | Distinct signups for this email domain in the last 60 minutes. |
signals.velocity.email_domain_24h | integer | Yes | Distinct signups for this email domain in the last 24 hours. |
processed_ms | integer | Yes | Server-side processing time for this request, in milliseconds. |
assessed_at | string | Yes | RFC 3339 timestamp of when the assessment was produced (UTC). |
Response headers
Every successful response also includes the rate-limit headers documented in Rate limits:
X-RateLimit-Limit: 10
X-RateLimit-Remaining: 7
X-RateLimit-Reset: 1717200000
X-Quota-Used: 4312
X-Quota-Limit: 10000Full response example
{
"request_id": "req_01hw5k3mfvx8t2b4ncqw7ydpej",
"session_id": "sess_abc123",
"verdict": "block",
"score": 91,
"reasons": [
{
"code": "email_disposable",
"signal": "email"
},
{
"code": "ip_anonymizer",
"signal": "ip"
},
{
"code": "ip_reputation",
"signal": "ip"
},
{
"code": "velocity_ip",
"signal": "velocity"
}
],
"signals": {
"email": {
"disposable": true,
"domain": "mailinator.com",
"domain_age_days": 9201,
"mx_valid": true,
"public_domain": false,
"role_account": false
},
"ip": {
"address": "185.220.101.45",
"tor": true,
"vpn": false,
"proxy": false,
"datacenter": false,
"abuse_score": 97,
"country_code": "DE",
"asn": "AS200651"
},
"velocity": {
"ip_signups_1h": 8,
"ip_signups_24h": 22,
"email_domain_1h": 14,
"email_domain_24h": 31
}
},
"ip_provided": true,
"ip_status": "ok",
"processed_ms": 18,
"assessed_at": "2026-05-10T10:14:33Z"
}Email-only response example
When ip is omitted or not usable (e.g. 127.0.0.1 during local dev):
{
"request_id": "req_01hw5k3mfvx8t2b4ncqw7ydpej",
"verdict": "allow",
"score": 5,
"reasons": [],
"ip_provided": false,
"ip_status": "ignored_loopback",
"signals": {
"email": {
"disposable": false,
"domain": "example.com",
"domain_age_days": 3650,
"mx_valid": true,
"public_domain": false,
"role_account": false
},
"velocity": {
"ip_signups_1h": 0,
"ip_signups_24h": 0,
"email_domain_1h": 1,
"email_domain_24h": 1
}
},
"processed_ms": 12,
"assessed_at": "2026-05-10T10:14:33Z"
}Verdict logic
The verdict is derived from score against the thresholds configured for
your account or for the specific API key. Defaults:
| Score | Verdict |
|---|---|
| 0–29 | allow |
| 30–59 | challenge |
| 60–100 | block |
Thresholds are configurable in Dashboard → Settings → Thresholds, both account-wide and per API key. Your code must handle all three values; see Handling verdicts.
Reason codes
These are the public reason codes returned by the API. Switch on code for
stable UX and logging. For finer-grained policies (e.g. block Tor but allow
VPN), read signals — see Reasons vs signals.
Email signals
| Code | Meaning |
|---|---|
email_disposable | Domain is in the disposable-domain blocklist. |
email_deliverability | Domain has deliverability issues (e.g. no valid MX records). |
email_role_account | Local-part is admin, info, support, or sales. |
email_alias | Local-part contains + (plus addressing). |
email_consumer_provider | Domain is a free consumer provider (Gmail, Yahoo, Outlook, Hotmail). |
IP signals
Only emitted when ip_provided is true.
| Code | Meaning |
|---|---|
ip_anonymizer | IP is associated with anonymized traffic (Tor, VPN, proxy). |
ip_reputation | IP has elevated abuse reputation or blocklist matches. |
ip_hosting | IP belongs to a datacenter or hosting ASN. |
Velocity signals
| Code | Meaning |
|---|---|
velocity_ip | Unusual signup rate from this IP once 5+ attempts occur in the last hour. Requires a usable IP. |
velocity_domain | Unusual signup rate for this email domain once 6+ attempts occur in the last hour. Always available. |
Reasons vs signals
Two layers of detail
reasons— stable risk categories for messaging, logging, and default branching. Codes likeip_anonymizerintentionally hide specific detection rules.signals— underlying booleans and counters for advanced custom policies. Example: branch onreasons[].code === "ip_anonymizer"for generic friction, then usesignals.ip.torvssignals.ip.vpnfor different actions.
Future detectors
New internal detectors may map to these same public codes without a
breaking API change. Do not depend on removed codes such as
ip_tor_exit_node or velocity_ip_signups in the public response.
Latency
Typical processing time is well under 50ms server-side, with end-to-end p95 near 80–150ms. Always set a client-side timeout. Recommended: 3000ms. ShieldSignup is a synchronous risk check, but a slow signal source must never be allowed to break your signup.
Error responses
Common errors:
HTTP 400 — missing_field, invalid_email, invalid_ip
HTTP 401 — unauthorized, token_revoked
HTTP 429 — quota_exceeded (monthly cap), rate_limit (per-second burst)
HTTP 500 — internal_errorThe error envelope is documented in full at API reference: errors.