Developer docs

Decision API

Send an email address, get back a risk score and a resolved ALLOW / VERIFY / BLOCK decision — not just a boolean.

0-100 risk scoreTeam-tunable thresholds500 free credits

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.

Rollout status: this endpoint is gated behind internal flags as it rolls out. While it isn't live in a given deployment, it responds 404 Not Found.
Base for every example: a free account ships with 500 credits, no card required. Generate a key from your API Keys dashboard and you can run every snippet below as-is.

Quickstart

Same auth and request shape as /api/check-email: bearer token, one JSON field. Pick your language:

request
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

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

HeaderRequiredValue
AuthorizationYesBearer YOUR_API_KEY
Content-TypeYesapplication/json

Body parameters

FieldTypeRequiredDescription
emailstringYesThe full email address to assess. Only the domain is used for the verdict; the local part is never stored.
200 OKresponse
{ "email": "abc@temporary-domain.example" }

Response

200 OKresponse
{
  "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"
}
FieldTypeDescription
check_idstring (uuid)Correlate a later /api/feedback call by passing this back as check_id.
risk_score0–100How likely this identity is to be bad.
confidence0–100How 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.
signalsobjectRaw evidence behind the score. See Signals object.
reasonsstring[]Human-readable justifications, ordered by contribution, capped at 5.
applied_ruleobject | nullPresent only when one of your own decision rules overrode the threshold decision.
scoring_versionstringIdentifies 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_scoreDefault decision
0–29ALLOW
30–69VERIFY
70–100BLOCK

These 30/70 thresholds are overridable per team from the dashboard (Settings → Decision Engine), and further overridable by your own field operatorvalue → 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.

FieldTypeDescription
disposablebooleanSame disposable classification as /api/check-email.
mx_validbooleanThe domain publishes valid MX records.
domain_age_daysnumber | nullRDAP-derived domain registration age. Null when unresolved.
role_accountbooleanThe local part looks like a role account (e.g. admin@, support@) rather than a person.
free_providerbooleanThe domain is a known free consumer email provider (Gmail, Outlook, etc.).
networkobject | nullReserved 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 in applied_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.

StatusMeaning
200The assessment is returned.
400Bad request: the email is missing or not a valid address.
401Unauthorized: missing/malformed header or unrecognized key.
402Payment required: the team is out of credits.
404Not found: the decision engine isn't live in this deployment yet.
500Server error: something failed on our side. Safe to retry.

POST /api/feedback

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.

request
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:

200 OKresponse
{ "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:

200 OKresponse
{ "recorded": true, "id": 123 }
AspectDetail
Cost0 credits
Rate limit500 requests/hour per team, 429 beyond that
AuthSame bearer token as /api/assess
StorageOnly the domain and a hash of the address are ever persisted — the raw address is not stored

Best practices

  • Branch on decision, not risk_score directly — 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 VERIFY as 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_id alongside the signup so you can call /api/feedback later once you know the real outcome — that's what calibrates future scores.
  • Fail open: on a timeout, network error, or unexpected status, default to ALLOW rather 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.