Developer docs
Decision API
Send an email address, get back a risk score and a resolved ALLOW / VERIFY / BLOCK decision — not just a boolean.
Introduction
POST /api/assess is Mailsurity's decision engine: instead of a plain disposable/legitimate boolean, it returns a 0-100 risk score, a paired confidence score, and a resolved ALLOW / VERIFY / BLOCK decision, so you branch directly on a decision the engine already made instead of writing your own thresholds around a boolean.
It runs on the same detection cascade as /api/check-email: curated allow/deny lists, a global disposable-domains list, a verdict cache, and live heuristics with an AI classifier for borderline cases, plus additional signals (domain age, role-account detection, network reputation) folded into a single score.
404 Not Found.Quickstart
Same auth and request shape as /api/check-email: bearer token, one JSON field. Pick your language:
curl -X POST https://mailsurity.com/api/assess \
-H "Authorization: Bearer YOUR_API_KEY" \
-H "Content-Type: application/json" \
-d '{"email": "abc@temporary-domain.example"}'POST /api/assess
Evaluates a single email address and returns a full risk assessment. Costs one credit per successful call, same as /api/check-email.
Request
Headers
| Header | Required | Value |
|---|---|---|
Authorization | Yes | Bearer YOUR_API_KEY |
Content-Type | Yes | application/json |
Body parameters
| Field | Type | Required | Description |
|---|---|---|---|
email | string | Yes | The full email address to assess. Only the domain is used for the verdict; the local part is never stored. |
{ "email": "abc@temporary-domain.example" }Response
{
"check_id": "0f3c1e6a-1234-4a11-9c2e-8b6f2a5d9e10",
"email": "abc@temporary-domain.example",
"risk_score": 93,
"confidence": 88,
"decision": "BLOCK",
"signals": {
"disposable": true,
"mx_valid": true,
"domain_age_days": 4,
"role_account": false,
"free_provider": false,
"network": null
},
"reasons": ["Disposable email provider", "Domain registered 4 days ago"],
"applied_rule": null,
"scoring_version": "decision-v1"
}| Field | Type | Description |
|---|---|---|
check_id | string (uuid) | Correlate a later /api/feedback call by passing this back as check_id. |
risk_score | 0–100 | How likely this identity is to be bad. |
confidence | 0–100 | How much evidence supports risk_score — a separate axis from the score itself. Low confidence means treat the score with caution. |
decision | 'ALLOW' | 'VERIFY' | 'BLOCK' | The resolved action. See Decision bands. |
signals | object | Raw evidence behind the score. See Signals object. |
reasons | string[] | Human-readable justifications, ordered by contribution, capped at 5. |
applied_rule | object | null | Present only when one of your own decision rules overrode the threshold decision. |
scoring_version | string | Identifies the weight table that produced this score (currently "decision-v1"). |
Decision bands
The score maps to a decision through two bands, evaluated on top of risk_score:
| risk_score | Default decision |
|---|---|
| 0–29 | ALLOW |
| 30–69 | VERIFY |
| 70–100 | BLOCK |
These 30/70 thresholds are overridable per team from the dashboard (Settings → Decision Engine), and further overridable by your own field → operator → value → action rules (first match wins, evaluated before the threshold bands). When a rule fires, applied_rule in the response names it.
Signals object
Always present, with null/false values rather than omitted keys when a signal couldn't be resolved (a timeout, an unsupported TLD, etc.) — never a guess.
| Field | Type | Description |
|---|---|---|
disposable | boolean | Same disposable classification as /api/check-email. |
mx_valid | boolean | The domain publishes valid MX records. |
domain_age_days | number | null | RDAP-derived domain registration age. Null when unresolved. |
role_account | boolean | The local part looks like a role account (e.g. admin@, support@) rather than a person. |
free_provider | boolean | The domain is a known free consumer email provider (Gmail, Outlook, etc.). |
network | object | null | Reserved for cross-signal network reputation. Currently always null. |
Team rules & thresholds
Beyond the default 30/70 bands, two team-level overrides layer on top of the same underlying score, without changing risk_score itself:
- Thresholds — raise or lower the ALLOW/VERIFY and VERIFY/BLOCK cut points for your team from the dashboard.
- Decision rules — condition → action overrides (e.g. “always BLOCK when
domain_age_days< 2”), evaluated priority-ascending, first match wins. A matched rule is reported back inapplied_rule.
Errors & cost
Identical to /api/check-email: standard HTTP status codes, a JSON body of the shape { "error": "message" }, and 1 credit per successful call.
| Status | Meaning |
|---|---|
200 | The assessment is returned. |
400 | Bad request: the email is missing or not a valid address. |
401 | Unauthorized: missing/malformed header or unrecognized key. |
402 | Payment required: the team is out of credits. |
404 | Not found: the decision engine isn't live in this deployment yet. |
500 | Server error: something failed on our side. Safe to retry. |
POST /api/feedback
Closes the calibration loop: report the actual outcome of a signup (fraud or legitimate) so scoring improves against real results over time. This isn't a rating of /api/assess's decision — it's ground truth about the identity, and it costs 0 credits since the label matters more than the credit.
curl -X POST https://mailsurity.com/api/feedback \
-H "Authorization: Bearer YOUR_API_KEY" \
-H "Content-Type: application/json" \
-d '{"check_id": "0f3c1e6a-1234-4a11-9c2e-8b6f2a5d9e10", "outcome": "fraud"}'Or, without a check_id on hand:
{ "email": "user@example.com", "outcome": "legitimate", "note": "optional, max 500 chars" }Exactly one of check_id or email must be present. A successful call returns:
{ "recorded": true, "id": 123 }| Aspect | Detail |
|---|---|
| Cost | 0 credits |
| Rate limit | 500 requests/hour per team, 429 beyond that |
| Auth | Same bearer token as /api/assess |
| Storage | Only the domain and a hash of the address are ever persisted — the raw address is not stored |
Best practices
- Branch on
decision, notrisk_scoredirectly — the threshold and rule logic already accounts for your team's tuning, so re-deriving your own cutoffs from the raw score just duplicates that work. - Treat
VERIFYas a real third state, not a soft ALLOW or BLOCK: route it to step-up verification or a review queue instead of collapsing it into one of the other two. - Store
check_idalongside the signup so you can call/api/feedbacklater once you know the real outcome — that's what calibrates future scores. - Fail open: on a timeout, network error, or unexpected status, default to
ALLOWrather than blocking a real user because the API was unreachable.
Need just a boolean?
If a plain disposable/legitimate boolean is all your integration needs, /api/check-email is the simpler contract — same cascade, same cost, smaller response. It isn't going anywhere; see the full reference.