Docs menu
Guides
API reference
POST /api/v1/verify
Screen a sign-up. Screen a sign-up from your backend. Generated from the OpenAPI file.
Last updated
Overview
POST https://api.inlineshield.io/api/v1/verify
Screens one sign-up and returns the verdict, the action to take and the reasons. Call it from your backend before you create the account.
action:allow(create the account),hold(create it but keep it on hold until the review is decided),block(refuse it).recommendedAction: advice next toaction, never instead of it. ALLOWED withriskScore25 or more givesallow_no_bonus; QUARANTINED givesreview; BLOCKED givesblock.- A live hold has a
reviewId; a test hold has none.
With the header X-Response-Mode: http-status (or ?mode=http-status) a BLOCKED screening is answered with HTTP 403 and the same body.
POST /api/v1/evaluate is a deprecated alias with identical behaviour; use /api/v1/verify.
Authentication
Send your secret API key in the X-Api-Key header.
Your secret API key (bs_live_… or bs_test_…), from the console under Settings › Keys. Never put it in a browser or an app. The header name X-ApiKey is also accepted (deprecated).
Parameters
| Name | In | Type | Required | Description |
|---|---|---|---|---|
X-Response-Mode | header | string: http-status | no | Answer a BLOCKED screening with HTTP 403 instead of 200. |
mode | query | string: http-status | no | Same as the X-Response-Mode header. |
Request body
JSON (Content-Type: application/json), required.
Other fields are accepted and ignored.
| Field | Type | Required | Description |
|---|---|---|---|
email | string | yes | The address the player signed up with.at least 1 character |
ip | string | no | The player's IP address (IPv4 or IPv6). Without it, the IP of the calling connection is used, which is your server's. Anything that is not an IP address gets 400 INVALID_IP. |
userAgent | string | no | The player's browser User-Agent. Only this field is read; the request's own User-Agent header (your HTTP client) is ignored. Without it the User-Agent counts as empty. |
username | string | no | The username the player chose, if any. |
clientDwellMs | number | no | Milliseconds the sign-up form was open before it was submitted, if you measure it. Not measured means unknown, never a fast submission. |
honeypot | string | no | The value of a hidden form field that people leave empty. A non-empty value is a bot signal. |
Request examples
{
"email": "allow@test.inlineshield.io",
"ip": "203.0.113.24",
"userAgent": "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/130.0.0.0 Safari/537.36",
"clientDwellMs": 8200
}{
"email": "nobonus@test.inlineshield.io",
"ip": "203.0.113.24",
"userAgent": "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/130.0.0.0 Safari/537.36",
"clientDwellMs": 8200
}{
"email": "review@test.inlineshield.io",
"ip": "203.0.113.24",
"userAgent": "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/130.0.0.0 Safari/537.36",
"clientDwellMs": 8200
}{
"email": "kyc@test.inlineshield.io",
"ip": "203.0.113.24",
"userAgent": "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/130.0.0.0 Safari/537.36",
"clientDwellMs": 8200
}{
"email": "block@test.inlineshield.io",
"ip": "203.0.113.24",
"userAgent": "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/130.0.0.0 Safari/537.36",
"clientDwellMs": 8200
}The first example as a cURL command:
curl -X POST https://api.inlineshield.io/api/v1/verify \
-H "X-Api-Key: $INLINESHIELD_API_KEY" \
-H "Content-Type: application/json" \
-d '{
"email": "allow@test.inlineshield.io",
"ip": "203.0.113.24",
"userAgent": "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/130.0.0.0 Safari/537.36",
"clientDwellMs": 8200
}'Responses
200 OK
The screening result. A live hold also has reviewId. Live responses carry the quota headers; test responses do not.
Headers: X-RateLimit-Limit, X-RateLimit-Remaining, X-RateLimit-Reset, X-Quota-Limit (optional), X-Quota-Used (optional), X-Quota-Reset (optional), X-Quota-State (optional).
| Field | Type | Required | Description |
|---|---|---|---|
requestId | string | yes | The screening's id. Use it for feedback and support.matches ^AUD- |
decision | string | yes | ALLOWED: let the player in. QUARANTINED: hold for review. BLOCKED: refuse.Values: ALLOWED, QUARANTINED, BLOCKED |
action | string | yes | What to do with the sign-up: ALLOWED → allow, QUARANTINED → hold, BLOCKED → block.Values: allow, hold, block |
recommendedAction | string | yes | Advice next to action: allow, allow without a sign-up bonus, review, block, or ask for KYC before paying out.Values: allow, allow_no_bonus, review, block, require_kyc |
reviewId | string | no | Live holds only (action: "hold"): the review to poll or wait for.matches ^rev_ |
verdict | string | yes | Same as decision (kept for older integrations).Values: ALLOWED, QUARANTINED, BLOCKED |
riskScore | number | yes | 0 (lowest risk) to 100.0 to 100 |
reasons | array of string | yes | Plain-language reasons for the result. |
triggers | array of string | yes | Machine-readable signal codes (e.g. DISPOSABLE_DOMAIN, TEST_MAGIC_ADDRESS). New codes may be added. |
policyVersion | "1" | yes | |
processingTimeMs | number | yes | at least 0 |
forwardedToDatabase | boolean | yes | True when the decision is ALLOWED. |
emailAnalysis | object | yes | |
emailAnalysis.normalizedEmail | string | yes | |
emailAnalysis.localPart | string | yes | |
emailAnalysis.domain | string | yes | |
emailAnalysis.isDisposable | boolean | yes | |
emailAnalysis.disposableCategory | string | no | |
emailAnalysis.disposableProvider | string | no | |
emailAnalysis.isPlusAliased | boolean | yes | |
emailAnalysis.isRoleAccount | boolean | yes | |
emailAnalysis.hasTypoRisk | boolean | yes | |
emailAnalysis.suggestedCorrection | string | no | |
emailAnalysis.mxValid | boolean | yes | True only when mx.status is found. |
emailAnalysis.mx | object | yes | |
emailAnalysis.mx.status | string | yes | absent only on a definite DNS answer; unknown on any lookup failure.Values: found, absent, unknown |
emailAnalysis.mx.records | array of object | yes | |
emailAnalysis.mxRecords | array of object | yes | Same as mx.records. |
emailAnalysis.mxRecords[].exchange | string | yes | |
emailAnalysis.mxRecords[].priority | number | yes | |
emailAnalysis.mailServers | string | yes | Values: present, none, unknown |
emailAnalysis.spf | object | yes | |
emailAnalysis.spf.exists | boolean | yes | |
emailAnalysis.spf.status | string | yes | absent only on a definite DNS answer; unknown on any lookup failure.Values: found, absent, unknown |
emailAnalysis.spf.raw | string | no | |
emailAnalysis.spf.qualifier | string | yes | Values: PASS, NEUTRAL, FAIL, NONE |
emailAnalysis.spf.isPermissive | boolean | yes | |
emailAnalysis.spf.mechanisms | array of string | yes | |
emailAnalysis.dmarc | object | yes | |
emailAnalysis.dmarc.exists | boolean | yes | |
emailAnalysis.dmarc.status | string | yes | absent only on a definite DNS answer; unknown on any lookup failure.Values: found, absent, unknown |
emailAnalysis.dmarc.raw | string | no | |
emailAnalysis.dmarc.policy | string | yes | Values: reject, quarantine, none, missing |
emailAnalysis.dmarc.compliant | boolean | yes | |
emailAnalysis.dmarc.percentage | number | no | |
emailAnalysis.dkim | object | yes | |
emailAnalysis.dkim.selectorChecked | string | yes | |
emailAnalysis.dkim.exists | boolean | yes | |
emailAnalysis.dkim.raw | string | no | |
emailAnalysis.dkim.status | string | yes | Values: VERIFIED, FOUND, NOT_FOUND, UNVERIFIABLE |
emailAnalysis.providerType | string | no | Values: FREE_WEBMAIL, DISPOSABLE, CORPORATE_PRIVATE |
emailAnalysis.isCatchAll | boolean | no | |
emailAnalysis.localPartRandomness | object or null | no | |
emailAnalysis.localPartRandomness.random | boolean | yes | |
emailAnalysis.localPartRandomness.score | number | yes | 0 to 1 |
botAnalysis | object | yes | |
botAnalysis.dwellTimeMs | number or null | yes | null when not measured (never a fast submission). |
botAnalysis.isFastSubmission | boolean | yes | |
botAnalysis.honeypotTriggered | boolean | yes | |
botAnalysis.trapFieldTriggered | boolean | no | |
botAnalysis.trapFieldValue | string | no | |
botAnalysis.isHeadlessBrowser | boolean | yes | |
botAnalysis.headlessUserAgent | boolean | no | |
botAnalysis.automationSignals | array of string | no | |
botAnalysis.isAutomationAgent | boolean | yes | |
botAnalysis.scriptedUserAgent | boolean | yes | The User-Agent is a known scripted HTTP client (curl, python-requests, …). |
botAnalysis.scriptedClient | string or null | yes | |
botAnalysis.entropyScore | number | yes | |
ipRisk | object | yes | |
ipRisk.ip | string | yes | |
ipRisk.isDatacenter | boolean or null | yes | null = unknown. |
ipRisk.isTorExit | boolean or null | yes | null = unknown. |
ipRisk.isVpnOrProxy | boolean | yes | isDatacenter or isTorExit is true. |
ipRisk.isVpn | boolean or null | yes | On the VPN provider list; null = unknown. |
ipRisk.country | string | yes | Country name, or "Unknown". |
ipRisk.countryCode | string or null | yes | |
ipRisk.countryName | string or null | no | |
ipRisk.city | string or null | no | |
ipRisk.region | string or null | no | |
ipRisk.flagEmoji | string | no | |
ipRisk.org | string | yes | The network owner, or "Unknown". |
ipRisk.asOrg | string or null | no | |
ipRisk.asn | string or null | yes | "AS<number>", or null. |
ipRisk.isp | string or null | no |
{
"requestId": "AUD-MHH3K2QZ-4F1C9B2E7D10",
"decision": "ALLOWED",
"action": "allow",
"recommendedAction": "allow",
"verdict": "ALLOWED",
"riskScore": 0,
"reasons": [
"Test mode: allow@test.inlineshield.io always gives ALLOWED / allow."
],
"triggers": [
"TEST_MAGIC_ADDRESS"
],
"policyVersion": "1",
"processingTimeMs": 0,
"forwardedToDatabase": true,
"emailAnalysis": {
"normalizedEmail": "allow@test.inlineshield.io",
"localPart": "allow",
"domain": "test.inlineshield.io",
"isDisposable": false,
"isPlusAliased": false,
"isRoleAccount": false,
"hasTypoRisk": false,
"mxValid": false,
"mx": {
"status": "unknown",
"records": []
},
"mxRecords": [],
"mailServers": "unknown",
"spf": {
"exists": false,
"status": "unknown",
"qualifier": "NONE",
"isPermissive": false,
"mechanisms": []
},
"dmarc": {
"exists": false,
"status": "unknown",
"policy": "missing",
"compliant": false
},
"dkim": {
"selectorChecked": "",
"exists": false,
"status": "UNVERIFIABLE"
},
"localPartRandomness": null
},
"botAnalysis": {
"dwellTimeMs": 8200,
"isFastSubmission": false,
"honeypotTriggered": false,
"isHeadlessBrowser": false,
"isAutomationAgent": false,
"scriptedUserAgent": false,
"scriptedClient": null,
"entropyScore": 0
},
"ipRisk": {
"ip": "203.0.113.24",
"isDatacenter": null,
"isTorExit": null,
"isVpnOrProxy": false,
"isVpn": null,
"country": "Unknown",
"countryCode": null,
"org": "Unknown",
"asn": null
}
}{
"requestId": "AUD-MHH3K30A-91B7C2D40E55",
"decision": "ALLOWED",
"action": "allow",
"recommendedAction": "allow_no_bonus",
"verdict": "ALLOWED",
"riskScore": 30,
"reasons": [
"Test mode: nobonus@test.inlineshield.io always gives ALLOWED / allow_no_bonus."
],
"triggers": [
"TEST_MAGIC_ADDRESS"
],
"policyVersion": "1",
"processingTimeMs": 0,
"forwardedToDatabase": true,
"emailAnalysis": {
"normalizedEmail": "nobonus@test.inlineshield.io",
"localPart": "nobonus",
"domain": "test.inlineshield.io",
"isDisposable": false,
"isPlusAliased": false,
"isRoleAccount": false,
"hasTypoRisk": false,
"mxValid": false,
"mx": {
"status": "unknown",
"records": []
},
"mxRecords": [],
"mailServers": "unknown",
"spf": {
"exists": false,
"status": "unknown",
"qualifier": "NONE",
"isPermissive": false,
"mechanisms": []
},
"dmarc": {
"exists": false,
"status": "unknown",
"policy": "missing",
"compliant": false
},
"dkim": {
"selectorChecked": "",
"exists": false,
"status": "UNVERIFIABLE"
},
"localPartRandomness": null
},
"botAnalysis": {
"dwellTimeMs": 8200,
"isFastSubmission": false,
"honeypotTriggered": false,
"isHeadlessBrowser": false,
"isAutomationAgent": false,
"scriptedUserAgent": false,
"scriptedClient": null,
"entropyScore": 0
},
"ipRisk": {
"ip": "203.0.113.24",
"isDatacenter": null,
"isTorExit": null,
"isVpnOrProxy": false,
"isVpn": null,
"country": "Unknown",
"countryCode": null,
"org": "Unknown",
"asn": null
}
}{
"requestId": "AUD-MHH3K31C-0C3D5E7F9A21",
"decision": "QUARANTINED",
"action": "hold",
"recommendedAction": "review",
"verdict": "QUARANTINED",
"riskScore": 50,
"reasons": [
"Test mode: review@test.inlineshield.io always gives QUARANTINED / review."
],
"triggers": [
"TEST_MAGIC_ADDRESS"
],
"policyVersion": "1",
"processingTimeMs": 0,
"forwardedToDatabase": false,
"emailAnalysis": {
"normalizedEmail": "review@test.inlineshield.io",
"localPart": "review",
"domain": "test.inlineshield.io",
"isDisposable": false,
"isPlusAliased": false,
"isRoleAccount": false,
"hasTypoRisk": false,
"mxValid": false,
"mx": {
"status": "unknown",
"records": []
},
"mxRecords": [],
"mailServers": "unknown",
"spf": {
"exists": false,
"status": "unknown",
"qualifier": "NONE",
"isPermissive": false,
"mechanisms": []
},
"dmarc": {
"exists": false,
"status": "unknown",
"policy": "missing",
"compliant": false
},
"dkim": {
"selectorChecked": "",
"exists": false,
"status": "UNVERIFIABLE"
},
"localPartRandomness": null
},
"botAnalysis": {
"dwellTimeMs": 8200,
"isFastSubmission": false,
"honeypotTriggered": false,
"isHeadlessBrowser": false,
"isAutomationAgent": false,
"scriptedUserAgent": false,
"scriptedClient": null,
"entropyScore": 0
},
"ipRisk": {
"ip": "203.0.113.24",
"isDatacenter": null,
"isTorExit": null,
"isVpnOrProxy": false,
"isVpn": null,
"country": "Unknown",
"countryCode": null,
"org": "Unknown",
"asn": null
}
}{
"requestId": "AUD-MHH3K32F-7E2A4C6B8D90",
"decision": "QUARANTINED",
"action": "hold",
"recommendedAction": "require_kyc",
"verdict": "QUARANTINED",
"riskScore": 60,
"reasons": [
"Test mode: kyc@test.inlineshield.io always gives QUARANTINED / require_kyc."
],
"triggers": [
"TEST_MAGIC_ADDRESS"
],
"policyVersion": "1",
"processingTimeMs": 0,
"forwardedToDatabase": false,
"emailAnalysis": {
"normalizedEmail": "kyc@test.inlineshield.io",
"localPart": "kyc",
"domain": "test.inlineshield.io",
"isDisposable": false,
"isPlusAliased": false,
"isRoleAccount": false,
"hasTypoRisk": false,
"mxValid": false,
"mx": {
"status": "unknown",
"records": []
},
"mxRecords": [],
"mailServers": "unknown",
"spf": {
"exists": false,
"status": "unknown",
"qualifier": "NONE",
"isPermissive": false,
"mechanisms": []
},
"dmarc": {
"exists": false,
"status": "unknown",
"policy": "missing",
"compliant": false
},
"dkim": {
"selectorChecked": "",
"exists": false,
"status": "UNVERIFIABLE"
},
"localPartRandomness": null
},
"botAnalysis": {
"dwellTimeMs": 8200,
"isFastSubmission": false,
"honeypotTriggered": false,
"isHeadlessBrowser": false,
"isAutomationAgent": false,
"scriptedUserAgent": false,
"scriptedClient": null,
"entropyScore": 0
},
"ipRisk": {
"ip": "203.0.113.24",
"isDatacenter": null,
"isTorExit": null,
"isVpnOrProxy": false,
"isVpn": null,
"country": "Unknown",
"countryCode": null,
"org": "Unknown",
"asn": null
}
}{
"requestId": "AUD-MHH3K33H-5B9D1F3A7C02",
"decision": "BLOCKED",
"action": "block",
"recommendedAction": "block",
"verdict": "BLOCKED",
"riskScore": 95,
"reasons": [
"Test mode: block@test.inlineshield.io always gives BLOCKED / block."
],
"triggers": [
"TEST_MAGIC_ADDRESS"
],
"policyVersion": "1",
"processingTimeMs": 0,
"forwardedToDatabase": false,
"emailAnalysis": {
"normalizedEmail": "block@test.inlineshield.io",
"localPart": "block",
"domain": "test.inlineshield.io",
"isDisposable": false,
"isPlusAliased": false,
"isRoleAccount": false,
"hasTypoRisk": false,
"mxValid": false,
"mx": {
"status": "unknown",
"records": []
},
"mxRecords": [],
"mailServers": "unknown",
"spf": {
"exists": false,
"status": "unknown",
"qualifier": "NONE",
"isPermissive": false,
"mechanisms": []
},
"dmarc": {
"exists": false,
"status": "unknown",
"policy": "missing",
"compliant": false
},
"dkim": {
"selectorChecked": "",
"exists": false,
"status": "UNVERIFIABLE"
},
"localPartRandomness": null
},
"botAnalysis": {
"dwellTimeMs": 8200,
"isFastSubmission": false,
"honeypotTriggered": false,
"isHeadlessBrowser": false,
"isAutomationAgent": false,
"scriptedUserAgent": false,
"scriptedClient": null,
"entropyScore": 0
},
"ipRisk": {
"ip": "203.0.113.24",
"isDatacenter": null,
"isTorExit": null,
"isVpnOrProxy": false,
"isVpn": null,
"country": "Unknown",
"countryCode": null,
"org": "Unknown",
"asn": null
}
}400 Bad Request
email missing, empty or not a string; an ip that is not an IP address; or a body that is not valid JSON.
| Field | Type | Required | Description |
|---|---|---|---|
error | string | yes | Values: BAD_REQUEST, INVALID_IP |
message | string | yes |
{
"error": "BAD_REQUEST",
"message": "Field \"email\" is required for registration verification."
}{
"error": "INVALID_IP",
"message": "ip must be a valid IPv4 or IPv6 address."
}401 Unauthorized
No X-Api-Key header, or an unknown, revoked or expired key.
| Field | Type | Required | Description |
|---|---|---|---|
error | string | yes | Values: UNAUTHORIZED, INVALID_API_KEY, EXPIRED_API_KEY, ORGANIZATION_NOT_FOUND |
message | string | yes |
{
"error": "UNAUTHORIZED",
"message": "Access denied: Missing required X-Api-Key header."
}{
"error": "INVALID_API_KEY",
"message": "Invalid API key provided."
}402 Payment Required
The monthly quota is used up. Test requests of an account over its quota get this too (they are still not counted).
Headers: X-RateLimit-Limit, X-RateLimit-Remaining, X-RateLimit-Reset.
| Field | Type | Required | Description |
|---|---|---|---|
error | "QUOTA_EXCEEDED" | yes | |
message | string | yes | |
used | integer | yes | at least 0 |
limit | integer | yes | at least 0 |
resetsAt | string (date-time) | yes | The start of the next UTC month. |
{
"error": "QUOTA_EXCEEDED",
"message": "Monthly check limit reached for this account. Checks resume at 2026-12-01T00:00:00.000Z.",
"used": 27500,
"limit": 25000,
"resetsAt": "2026-12-01T00:00:00.000Z"
}403 Forbidden
Either a BLOCKED screening with X-Response-Mode: http-status (the body is the screening result), or TENANT_SUSPENDED.
Headers: X-RateLimit-Limit (optional), X-RateLimit-Remaining (optional), X-RateLimit-Reset (optional), X-Quota-Limit (optional), X-Quota-Used (optional), X-Quota-Reset (optional), X-Quota-State (optional).
One of 2 shapes:
VerifyResponse
| Field | Type | Required | Description |
|---|---|---|---|
requestId | string | yes | The screening's id. Use it for feedback and support.matches ^AUD- |
decision | string | yes | ALLOWED: let the player in. QUARANTINED: hold for review. BLOCKED: refuse.Values: ALLOWED, QUARANTINED, BLOCKED |
action | string | yes | What to do with the sign-up: ALLOWED → allow, QUARANTINED → hold, BLOCKED → block.Values: allow, hold, block |
recommendedAction | string | yes | Advice next to action: allow, allow without a sign-up bonus, review, block, or ask for KYC before paying out.Values: allow, allow_no_bonus, review, block, require_kyc |
reviewId | string | no | Live holds only (action: "hold"): the review to poll or wait for.matches ^rev_ |
verdict | string | yes | Same as decision (kept for older integrations).Values: ALLOWED, QUARANTINED, BLOCKED |
riskScore | number | yes | 0 (lowest risk) to 100.0 to 100 |
reasons | array of string | yes | Plain-language reasons for the result. |
triggers | array of string | yes | Machine-readable signal codes (e.g. DISPOSABLE_DOMAIN, TEST_MAGIC_ADDRESS). New codes may be added. |
policyVersion | "1" | yes | |
processingTimeMs | number | yes | at least 0 |
forwardedToDatabase | boolean | yes | True when the decision is ALLOWED. |
emailAnalysis | object | yes | |
emailAnalysis.normalizedEmail | string | yes | |
emailAnalysis.localPart | string | yes | |
emailAnalysis.domain | string | yes | |
emailAnalysis.isDisposable | boolean | yes | |
emailAnalysis.disposableCategory | string | no | |
emailAnalysis.disposableProvider | string | no | |
emailAnalysis.isPlusAliased | boolean | yes | |
emailAnalysis.isRoleAccount | boolean | yes | |
emailAnalysis.hasTypoRisk | boolean | yes | |
emailAnalysis.suggestedCorrection | string | no | |
emailAnalysis.mxValid | boolean | yes | True only when mx.status is found. |
emailAnalysis.mx | object | yes | |
emailAnalysis.mx.status | string | yes | absent only on a definite DNS answer; unknown on any lookup failure.Values: found, absent, unknown |
emailAnalysis.mx.records | array of object | yes | |
emailAnalysis.mxRecords | array of object | yes | Same as mx.records. |
emailAnalysis.mxRecords[].exchange | string | yes | |
emailAnalysis.mxRecords[].priority | number | yes | |
emailAnalysis.mailServers | string | yes | Values: present, none, unknown |
emailAnalysis.spf | object | yes | |
emailAnalysis.spf.exists | boolean | yes | |
emailAnalysis.spf.status | string | yes | absent only on a definite DNS answer; unknown on any lookup failure.Values: found, absent, unknown |
emailAnalysis.spf.raw | string | no | |
emailAnalysis.spf.qualifier | string | yes | Values: PASS, NEUTRAL, FAIL, NONE |
emailAnalysis.spf.isPermissive | boolean | yes | |
emailAnalysis.spf.mechanisms | array of string | yes | |
emailAnalysis.dmarc | object | yes | |
emailAnalysis.dmarc.exists | boolean | yes | |
emailAnalysis.dmarc.status | string | yes | absent only on a definite DNS answer; unknown on any lookup failure.Values: found, absent, unknown |
emailAnalysis.dmarc.raw | string | no | |
emailAnalysis.dmarc.policy | string | yes | Values: reject, quarantine, none, missing |
emailAnalysis.dmarc.compliant | boolean | yes | |
emailAnalysis.dmarc.percentage | number | no | |
emailAnalysis.dkim | object | yes | |
emailAnalysis.dkim.selectorChecked | string | yes | |
emailAnalysis.dkim.exists | boolean | yes | |
emailAnalysis.dkim.raw | string | no | |
emailAnalysis.dkim.status | string | yes | Values: VERIFIED, FOUND, NOT_FOUND, UNVERIFIABLE |
emailAnalysis.providerType | string | no | Values: FREE_WEBMAIL, DISPOSABLE, CORPORATE_PRIVATE |
emailAnalysis.isCatchAll | boolean | no | |
emailAnalysis.localPartRandomness | object or null | no | |
emailAnalysis.localPartRandomness.random | boolean | yes | |
emailAnalysis.localPartRandomness.score | number | yes | 0 to 1 |
botAnalysis | object | yes | |
botAnalysis.dwellTimeMs | number or null | yes | null when not measured (never a fast submission). |
botAnalysis.isFastSubmission | boolean | yes | |
botAnalysis.honeypotTriggered | boolean | yes | |
botAnalysis.trapFieldTriggered | boolean | no | |
botAnalysis.trapFieldValue | string | no | |
botAnalysis.isHeadlessBrowser | boolean | yes | |
botAnalysis.headlessUserAgent | boolean | no | |
botAnalysis.automationSignals | array of string | no | |
botAnalysis.isAutomationAgent | boolean | yes | |
botAnalysis.scriptedUserAgent | boolean | yes | The User-Agent is a known scripted HTTP client (curl, python-requests, …). |
botAnalysis.scriptedClient | string or null | yes | |
botAnalysis.entropyScore | number | yes | |
ipRisk | object | yes | |
ipRisk.ip | string | yes | |
ipRisk.isDatacenter | boolean or null | yes | null = unknown. |
ipRisk.isTorExit | boolean or null | yes | null = unknown. |
ipRisk.isVpnOrProxy | boolean | yes | isDatacenter or isTorExit is true. |
ipRisk.isVpn | boolean or null | yes | On the VPN provider list; null = unknown. |
ipRisk.country | string | yes | Country name, or "Unknown". |
ipRisk.countryCode | string or null | yes | |
ipRisk.countryName | string or null | no | |
ipRisk.city | string or null | no | |
ipRisk.region | string or null | no | |
ipRisk.flagEmoji | string | no | |
ipRisk.org | string | yes | The network owner, or "Unknown". |
ipRisk.asOrg | string or null | no | |
ipRisk.asn | string or null | yes | "AS<number>", or null. |
ipRisk.isp | string or null | no |
SuspendedError
| Field | Type | Required | Description |
|---|---|---|---|
error | string | yes | Values: TENANT_SUSPENDED |
message | string | yes |
429 Too Many Requests
RATE_LIMIT_EXCEEDED: the per-minute limit (your plan's; 300 for test keys). TEST_LIMIT: 10,000 test requests per account per UTC day; Retry-After is the time to the next UTC midnight.
Headers: Retry-After, X-RateLimit-Limit, X-RateLimit-Remaining, X-RateLimit-Reset.
| Field | Type | Required | Description |
|---|---|---|---|
error | string | yes | Values: RATE_LIMIT_EXCEEDED, TEST_LIMIT |
message | string | yes |
{
"error": "RATE_LIMIT_EXCEEDED",
"message": "Tenant verification rate limit of 600 req/min exceeded. Try again in 12 seconds."
}{
"error": "TEST_LIMIT",
"message": "Test mode is limited to 10,000 test requests per account per day (UTC). Use a live key for production traffic."
}500 Internal Server Error
The screening failed or could not be stored. Your integration decides whether to fail open or closed.
| Field | Type | Required | Description |
|---|---|---|---|
error | string | yes | Values: VERIFICATION_ERROR, PERSISTENCE_FAILURE, AUTH_ERROR |
message | string | yes |
503 Service Unavailable
The key could not be checked right now. Try again.
| Field | Type | Required | Description |
|---|---|---|---|
error | string | yes | Values: DATABASE_ERROR |
message | string | yes |
Errors
Errors are JSON, { "error": "CODE", "message": "…" }. Branch on error, never on message.
| HTTP status | error | When |
|---|---|---|
400 | BAD_REQUEST, INVALID_IP | email missing, empty or not a string; an ip that is not an IP address; or a body that is not valid JSON. |
401 | UNAUTHORIZED, INVALID_API_KEY, EXPIRED_API_KEY, ORGANIZATION_NOT_FOUND | No X-Api-Key header, or an unknown, revoked or expired key. |
402 | QUOTA_EXCEEDED | The monthly quota is used up. Test requests of an account over its quota get this too (they are still not counted). |
403 | TENANT_SUSPENDED | Either a BLOCKED screening with X-Response-Mode: http-status (the body is the screening result), or TENANT_SUSPENDED. |
429 | RATE_LIMIT_EXCEEDED, TEST_LIMIT | RATE_LIMIT_EXCEEDED: the per-minute limit (your plan's; 300 for test keys). TEST_LIMIT: 10,000 test requests per account per UTC day; Retry-After is the time to the next UTC midnight. |
500 | VERIFICATION_ERROR, PERSISTENCE_FAILURE, AUTH_ERROR | The screening failed or could not be stored. Your integration decides whether to fail open or closed. |
503 | DATABASE_ERROR | The key could not be checked right now. Try again. |
See all errors for every endpoint.
Headers and limits
| Response header | Meaning |
|---|---|
X-RateLimit-Limit | Requests allowed in the current one-minute window. |
X-RateLimit-Remaining | Requests left in the current window. |
X-RateLimit-Reset | Seconds until the current window ends. |
X-Quota-Limit | Live requests only: the monthly quota of the plan. |
X-Quota-Used | Live requests only: screenings counted this month, this one included. |
X-Quota-Reset | Live requests only: when the count resets (the start of the next UTC month). |
X-Quota-State | Live requests only: ok, warn (80% used), limit (100%), grace (over 100%, still served) or blocked. |
Retry-After | Seconds to wait before trying again. |
The limits per endpoint and plan are on Rate limits and quotas.