Verify endpoint
POST /api/v1/verify — validate a single email address and get a score, verdict, and per-check breakdown.
Request
POST https://mailsentry.dev/api/v1/verify
Content-Type: application/json
X-API-Key: ms_live_your_key_here
{
"email": "user@example.com",
"depth": "standard"
}
| Field | Type | Required | Description |
|---|---|---|---|
email | string | Yes | The email address to validate. |
depth | string | No | "standard" (default) or "extended". Extended mode re-probes SMTP after a 3-second delay to resolve greylisting and confirm catch-all results. Takes 4–8 seconds but gives higher confidence. |
Response
Valid email — clean pass
{
"email": "sarah@stripe.com",
"score": 95,
"verdict": "valid",
"recommendation": "Safe to send",
"verification_level": "confirmed",
"verification_depth": "standard",
"warning": null,
"checks": {
"syntax": { "valid": true, "normalized": "sarah@stripe.com", "plus_alias": null },
"mx_records": { "valid": true, "records": ["aspmx.l.google.com"] },
"disposable": { "is_disposable": false, "list_updated": null },
"role_based": { "is_role_based": false, "role_type": null },
"free_provider": { "is_free": false, "name": null, "is_business": true, "mx_provider": "Google Workspace" },
"typo": { "has_typo": false, "suggestion": null },
"smtp": { "valid": true, "catch_all": false, "mx_host": "aspmx.l.google.com", "greylisted": false, "catch_all_confidence": null },
"gibberish": { "is_gibberish": false, "score": 100, "pattern": null },
"spam_trap": { "is_potential_trap": false, "type": null, "confidence": null },
"domain_age": { "domain_created": "2010-01-18", "age_days": 5942, "risk_flag": null },
"abuse": { "is_toxic": false, "type": null }
},
"response_time_ms": 42
}
Everything checks out — verified mailbox on Google Workspace (mx_provider), domain registered 16 years ago, no flags. The is_business: true field confirms this is a corporate email, not a free provider.
Typo detected — "Did you mean?"
{
"email": "john@gmial.com",
"score": 15,
"verdict": "invalid",
"recommendation": "Do not send",
"verification_level": "confirmed",
"verification_depth": "standard",
"warning": "Possible typo detected",
"checks": {
"syntax": { "valid": true, "normalized": "john@gmial.com", "plus_alias": null },
"mx_records": { "valid": false, "records": [] },
"disposable": { "is_disposable": false, "list_updated": null },
"role_based": { "is_role_based": false, "role_type": null },
"free_provider": { "is_free": false, "name": null, "is_business": null, "mx_provider": null },
"typo": { "has_typo": true, "suggestion": "john@gmail.com" },
"smtp": { "valid": false, "catch_all": false, "mx_host": null, "greylisted": false, "catch_all_confidence": null },
"gibberish": { "is_gibberish": false, "score": 100, "pattern": null },
"spam_trap": { "is_potential_trap": false, "type": null, "confidence": null },
"domain_age": { "domain_created": null, "age_days": null, "risk_flag": null },
"abuse": { "is_toxic": false, "type": null }
},
"response_time_ms": 38
}
The domain doesn't exist, but typo.suggestion tells you exactly what they meant. Use this to prompt "Did you mean john@gmail.com?" in your signup form.
Stacked risk signals — role-based + new domain
{
"email": "info@newstartup.io",
"score": 38,
"verdict": "risky",
"recommendation": "High risk — we recommend not sending",
"verification_level": "inferred",
"verification_depth": "standard",
"warning": "Domain is catch-all — mailbox existence could not be confirmed",
"checks": {
"syntax": { "valid": true, "normalized": "info@newstartup.io", "plus_alias": null },
"mx_records": { "valid": true, "records": ["mx.zoho.com"] },
"disposable": { "is_disposable": false, "list_updated": null },
"role_based": { "is_role_based": true, "role_type": "info" },
"free_provider": { "is_free": false, "name": null, "is_business": true, "mx_provider": "Zoho Mail" },
"typo": { "has_typo": false, "suggestion": null },
"smtp": { "valid": null, "catch_all": true, "mx_host": "mx.zoho.com", "greylisted": false, "catch_all_confidence": 0.17 },
"gibberish": { "is_gibberish": false, "score": 100, "pattern": null },
"spam_trap": { "is_potential_trap": false, "type": null, "confidence": null },
"domain_age": { "domain_created": "2026-04-01", "age_days": 25, "risk_flag": "newly_registered" },
"abuse": { "is_toxic": false, "type": null }
},
"response_time_ms": 1240
}
Four signals stack: role_type: "info" (generic address), catch_all: true with catch_all_confidence: 0.17 (the local part "info" isn't a real person), risk_flag: "newly_registered" (25 days old), and Zoho Mail hosting. Each field gives developers data to build their own threshold logic.
Response fields
| Field | Type | Description |
|---|---|---|
email | string | The email address that was verified. |
score | integer | Quality score from 0 (worst) to 100 (best). See Quality scores. |
verdict | string | One of valid, caution, risky, invalid. |
recommendation | string | Human-readable action: "Safe to send", "Send with caution", etc. |
verification_level | string | confirmed (SMTP verified), inferred (catch-all), or estimated (SMTP inconclusive). |
verification_depth | string | standard or extended. Extended re-probes SMTP to resolve greylisting and confirm catch-all. |
warning | string | null | Optional advisory, e.g. "Possible typo detected". |
checks | object | Per-layer results from all 11 validation layers. See below. |
response_time_ms | integer | Server-side processing time in milliseconds. |
checks object — per-layer fields
| Layer | Field | Type | Description |
|---|---|---|---|
| syntax | valid | boolean | Whether the email passes RFC 5322 syntax validation. |
normalized | string | null | The email with IDN domains converted to punycode. null if syntax is invalid. | |
plus_alias | string | null | The plus-alias tag if present (e.g. "newsletter" for user+newsletter@). | |
| disposable | is_disposable | boolean | Whether the domain is a known disposable email provider. |
list_updated | string | null | Date the disposable domain list was last updated (ISO date). Only present when is_disposable is true. | |
| role_based | is_role_based | boolean | Whether the local part is a generic role (info@, admin@, sales@). |
role_type | string | null | The matched role prefix (e.g. "info", "admin", "sales"). | |
| free_provider | is_free | boolean | Whether the domain is a free email provider (Gmail, Yahoo, etc.). |
name | string | null | Provider name if recognized (e.g. "Gmail", "Outlook"). | |
is_business | boolean | null | Whether the domain uses a hosted business email provider. true for custom domains on Google Workspace, Microsoft 365, etc. false for free providers. null if undetermined. | |
mx_provider | string | null | The email hosting provider detected from MX records (e.g. "Google Workspace", "Microsoft 365", "Zoho Mail"). | |
| smtp | valid | boolean | null | Mailbox exists (true), doesn't exist (false), or couldn't determine (null). |
catch_all | boolean | Whether the domain accepts all addresses. | |
mx_host | string | null | The MX server hostname used for the SMTP check. | |
greylisted | boolean | Whether the server returned a temporary rejection (greylisting). | |
catch_all_confidence | number | null | When catch_all is true, a 0–1 score estimating how likely the specific mailbox exists. Based on local-part analysis — john.smith@ scores higher than xq7z@. | |
| gibberish | is_gibberish | boolean | Whether the local part looks like random characters. |
score | integer | 0–100 naturalness score (100 = looks like a real name). | |
pattern | string | null | When gibberish is detected: keyboard_sequence, repeated_chars, numeric_only, consonant_cluster, or random_characters. | |
| spam_trap | is_potential_trap | boolean | Whether the address matches known spam trap patterns. |
type | string | null | honeypot_pattern, recycled_risk, or trap_domain. | |
confidence | number | null | 0–1 confidence in the trap detection. Pattern-based — not a definitive match. | |
| domain_age | domain_created | string | null | Domain registration date (ISO format). |
age_days | integer | null | Number of days since registration. | |
risk_flag | string | null | newly_registered (≤30 days) or young_domain (31–90 days). |