Email verify
The Email verify API checks whether addresses are syntactically valid, reachable, and appropriate for your use case. It applies layered checks (syntax, disposable domains, MX records, SMTP probing where allowed, and major-provider rules) and returns structured fields such as valid, status, confidence, catch_all, and smtp_blocked so clients and agents can decide how strictly to treat each result.
- Base URL:
https://api.truval.dev - Auth:
Authorization: Bearer sk_live_...(standard API keys;sk_test_...may be used in non-prod) POST /v1/email/verify— one address per request; synchronous JSON response, or202+ background delivery via optionalwebhookPOST /v1/email/verify/batch— up to 50 addresses; JSON body withresultsin the same order as inputPOST /v1/email/verify/stream— up to 50 addresses; NDJSON (application/x-ndjson), lines may arrive in completion order
Import paths, variables, and tooling are covered next. Request and response fields for the single-email endpoint follow; Async webhook, Batch verification, and Streaming verification document batch and stream behavior and limits.
OpenAPI Specification
Section titled “OpenAPI Specification”https://api.truval.dev/openapi.json
Note: Import this URL into Cursor, Postman, or any OpenAPI-compatible tool.
Postman and Bruno
Section titled “Postman and Bruno”The saved collection includes every public route and these variables:
| Variable | Purpose |
|---|---|
TRUVAL_BASE_URL | API host (default https://api.truval.dev; no trailing slash) |
TRUVAL_API_KEY | Your sk_live_... key (sent as the Bearer token on verify routes) |
TRUVAL_MGMT_KEY | Your sk_mgmt_... provisioning key (sent as the Bearer token on /v1/management/* routes) |
Import via URL — in Postman or Bruno, choose Import → Link, then paste:
https://docs.truval.dev/postman/truval.jsonThat URL is served by this documentation site and stays in sync with API releases.
The Run in Postman button forks the collection into your workspace (Postman may ask you to sign in). For Bruno, Cursor, or a raw file, use Import → Link with https://docs.truval.dev/postman/truval.json above.
Endpoint
Section titled “Endpoint”POST https://api.truval.dev/v1/email/verifyRequest
Section titled “Request”Headers
Section titled “Headers”| Header | Value |
|---|---|
Authorization | Bearer sk_live_... |
Content-Type | application/json |
| Field | Type | Required | Description |
|---|---|---|---|
email | string | Yes | The email address to verify |
webhook | string | No | URL to receive the async callback. See Async webhook |
webhook_secret | string | No | Optional shared secret for webhook signing. When provided, truval.dev includes X-Truval-Signature: sha256=... on the webhook callback (HMAC-SHA256 over the raw JSON body). |
Example
Section titled “Example”curl https://api.truval.dev/v1/email/verify \ -H "Authorization: Bearer sk_live_..." \ -H "Content-Type: application/json" \ -d '{"email":"user@example.com"}'Response
Section titled “Response”Fields
Section titled “Fields”| Field | Type | Description |
|---|---|---|
email | string | The email address that was verified |
valid | boolean | true when SMTP result is deliverable or catch-all. false for invalid addresses, SMTP timeout (smtp_timeout), undeliverable, or unknown (including major providers with smtp_blocked — use confidence, not valid alone) |
status | string | See status values below |
confidence | float | Ordinal 0–1: how decisive the check was (not a statistical probability the mailbox exists). See confidence ladder below |
failed_check | string|null | Which layer failed first: syntax · disposable · no_mx · smtp · smtp_timeout, or null when verification finished without failing a layer (deliverable, catch-all, unknown including smtp_blocked, inconclusive SMTP). Authoritative enum: OpenAPI schema VerifyEmailResult (failed_check). |
disposable | boolean | Matched against 50k+ throwaway domain blocklist |
role | boolean | Role address — admin@, info@, noreply@ etc |
free_provider | boolean | Gmail, Yahoo, Hotmail etc |
catch_all | boolean | Server accepts all addresses; mailbox existence is not confirmable |
smtp_blocked | boolean | Provider blocks SMTP probing — Gmail, Outlook, Yahoo |
mx_found | boolean | MX records exist for the domain |
mx_host | string|null | Primary MX hostname |
suggestion | string|null | Typo correction e.g. "gmail.com" when input was "gnail.com" |
latency_ms | number | End-to-end processing time in milliseconds |
How agents should interpret fields
Section titled “How agents should interpret fields”When an agent consumes this API, it should not blindly rely on the valid boolean. Use status, failed_check, mx_found, and smtp_blocked first, then use confidence as a summary band.
Decision order (recommended):
- If
suggestionis set → offer correction. - If
disposable→ reject throwaway addresses. - If
statusisinvalidorundeliverable→ reject or ask for another address. - If
catch_allorstatus === "catch_all"→ do not treatvalid: trueas mailbox proof; confirm or add risk controls. - If
smtp_blocked→ domain checks passed;confidencearound 0.75 is expected; do not treatvalid: falseas failure by itself. - If
status === "unknown"andmx_foundand notsmtp_blocked→ SMTP was inconclusive (common on org/M365); typicalconfidenceis 0.50 — ask the user to confirm or use secondary verification, not the same as “fake email.” - Otherwise use confidence bands from the Agent decision guide.
confidence: Ordinal summary of how decisive the pipeline was — not P(mailbox exists). See the confidence ladder.suggestion: If present, the agent should ask the user: “Did you mean [suggestion]?”smtp_blocked: For Gmail, Outlook, and Yahoo, we cannot run SMTP probes. Ifsmtp_blockedistrue, aconfidenceof around 0.75 is normal and should be accepted by the agent.catch_all: Ifcatch_allistrue(orstatusiscatch_all), do not treatvalid: trueas mailbox proof. Catch-all is returned with lower confidence (0.65) because the domain may still hard-bounce specific recipients later.disposable: Iftrue, the agent should reject the email and ask for a non-throwaway address.role: Iftrue, the agent knows it’s talking to an inbox likeadmin@rather than a person.
For full examples, see the Agent decision guide.
Confidence ladder
Section titled “Confidence ladder”Typical confidence values (see OpenAPI for the full field description):
| Value | When |
|---|---|
| 0.97 | deliverable — SMTP accepted the mailbox |
| 0.75 | unknown with smtp_blocked — domain/MX OK, major provider blocks probing |
| 0.65 | catch_all — server accepts arbitrary recipients |
| 0.50 | unknown, mx_found, not smtp_blocked — SMTP inconclusive (e.g. policy/greylist/Exchange behavior) |
| 0.02 | undeliverable |
| 0.0 | Invalid path: syntax, disposable, or no MX |
Status values
Section titled “Status values”| Value | Meaning |
|---|---|
deliverable | SMTP probe returned 250 — mailbox exists |
undeliverable | SMTP probe returned 550 — mailbox does not exist |
unknown | Could not confirm — smtp_blocked, SMTP timeout, or ambiguous/non-interpretable SMTP on the MX (common for some org/M365 hosts); check mx_found and smtp_blocked |
catch_all | Server accepts all addresses — mailbox existence unconfirmable; treat as higher bounce risk (confidence around 0.65) |
invalid | Failed syntax, disposable, or MX check |
Example response
Section titled “Example response”{ "email": "user@example.com", "valid": true, "status": "deliverable", "confidence": 0.97, "failed_check": null, "disposable": false, "role": false, "free_provider": false, "catch_all": false, "smtp_blocked": false, "mx_found": true, "mx_host": "mail.example.com", "suggestion": null, "latency_ms": 187}Webhook
Section titled “Webhook”For agents or workflows that shouldn’t block waiting for the SMTP probe, you can submit the verification asynchronously.
When you include a webhook URL in the request body, the endpoint returns 202 Accepted immediately with a job_id. The verification runs in the background and POSTs the full result to your webhook when complete. For automatic retries, HMAC verification (multiple languages), and IP allow-listing guidance, see the Webhooks reference. For HMAC, SSRF-safe callbacks, and data residency in one place, see Security.
Webhook URL rules (SSRF-safe): https only (not http). The host must be a hostname, not an IP address (IPv4 or IPv6). Usernames and passwords in the URL are not allowed. localhost and *.local hosts are rejected. The callback request does not follow HTTP redirects—your endpoint must respond on the exact URL you provide.
Residual risk: Hostnames that resolve to private networks (DNS rebinding) are not blocked by this check alone.
sequenceDiagram
participant C as Client
participant A as truval.dev API
participant W as Webhook Server
Note over C,A: Synchronous (Default)
C->>A: POST /v1/email/verify {"email": "..."}
activate A
Note right of A: Full pipeline runs<br/>(blocks up to 3s)
A-->>C: 200 OK {"valid": true, ...}
deactivate A
Note over C,W: Asynchronous (Webhook)
C->>A: POST /v1/email/verify {"email": "...", "webhook": "https://..."}
A-->>C: 202 Accepted {"job_id": "...", "status": "pending"}
Note right of A: Background execution
A-)W: POST callback {"job_id": "...", "valid": true, ...}
W-->>A: 2xx Success (Optional)
Request with webhook
Section titled “Request with webhook”curl https://api.truval.dev/v1/email/verify \ -H "Authorization: Bearer sk_live_..." \ -H "Content-Type: application/json" \ -d '{"email":"user@example.com", "webhook":"https://your-agent.com/truval-callback", "webhook_secret":"whsec_..."}'Immediate response
Section titled “Immediate response”{ "job_id": "job_1234567890abcdef", "status": "pending"}Webhook payload
Section titled “Webhook payload”When the background job completes, it makes a POST request to your webhook URL with the full result object, plus the job_id for correlation.
{ "job_id": "job_1234567890abcdef", "email": "user@example.com", "valid": true, "status": "deliverable", "confidence": 0.97, "...": "..."}Webhook signature verification
Section titled “Webhook signature verification”If you provide webhook_secret, truval.dev signs the callback with X-Truval-Signature: sha256=<hex> (HMAC-SHA256 over the exact raw UTF-8 JSON body). For retry timing, delivery rules, and signature verification examples in Node.js, Python, Go, Ruby, and PHP, see the dedicated Webhooks reference.
Verify up to 50 addresses in one request. Each address runs the same five layers as the single-email endpoint; results are returned in the same order as emails. Verifications run in parallel on the server.
Rate limits: Usage is counted per email. A batch of 50 emails consumes 50 units of your per-minute quota. If the batch would exceed your limit, the API returns 429 before processing any address in the batch.
POST https://api.truval.dev/v1/email/verify/batchHeaders
Section titled “Headers”Same as single verify: Authorization and Content-Type: application/json.
| Field | Type | Required | Description |
|---|---|---|---|
emails | string[] | Yes | 1–50 email addresses |
Example
Section titled “Example”curl https://api.truval.dev/v1/email/verify/batch \ -H "Authorization: Bearer sk_live_..." \ -H "Content-Type: application/json" \ -d '{"emails":["a@b.com","c@d.com","e@f.com"]}'Response
Section titled “Response”| Field | Type | Description |
|---|---|---|
results | array | One object per input email (same shape as single-email response), in request order |
count | number | Length of results |
latency_ms | number | Wall-clock time for the entire batch request |
Example response
Section titled “Example response”{ "results": [ { "email": "a@b.com", "valid": true, "status": "deliverable", "confidence": 0.97, "failed_check": null, "disposable": false, "role": false, "free_provider": false, "catch_all": false, "smtp_blocked": false, "mx_found": true, "mx_host": "mail.example.com", "suggestion": null, "latency_ms": 120 }, { "email": "c@d.com", "valid": false, "status": "invalid", "confidence": 0.0, "failed_check": "syntax", "disposable": false, "role": false, "free_provider": false, "catch_all": false, "smtp_blocked": false, "mx_found": false, "mx_host": null, "suggestion": null, "latency_ms": 0 }, { "email": "e@f.com", "valid": false, "status": "invalid", "confidence": 0.0, "failed_check": "syntax", "disposable": false, "role": false, "free_provider": false, "catch_all": false, "smtp_blocked": false, "mx_found": false, "mx_host": null, "suggestion": null, "latency_ms": 0 } ], "count": 3, "latency_ms": 312}Streaming
Section titled “Streaming”Verify 1–50 addresses in one request; each result is sent as one JSON object per line (NDJSON) with Content-Type: application/x-ndjson. Verifications run in parallel; lines arrive in completion order (whichever address finishes first), not necessarily the same order as emails.
Rate limits: Same as batch — usage is counted per email. If the request would exceed your limit, the API returns 429 before streaming (no lines are written).
POST https://api.truval.dev/v1/email/verify/streamHeaders
Section titled “Headers”Same as batch: Authorization and Content-Type: application/json.
| Field | Type | Required | Description |
|---|---|---|---|
emails | string[] | Yes | 1–50 email addresses |
Response body
Section titled “Response body”Each line is a complete JSON object with the same fields as the single-email response (see Response above). There is no wrapper object and no trailing batch count — only one result object per line, ending with a newline.
Example (curl)
Section titled “Example (curl)”Use --no-buffer (-N) so lines print as they arrive:
curl -N https://api.truval.dev/v1/email/verify/stream \ -H "Authorization: Bearer sk_live_..." \ -H "Content-Type: application/json" \ -d '{"emails":["a@b.com","c@d.com","e@f.com"]}'Example (fetch — read NDJSON incrementally)
Section titled “Example (fetch — read NDJSON incrementally)”const res = await fetch('https://api.truval.dev/v1/email/verify/stream', { method: 'POST', headers: { Authorization: 'Bearer sk_live_...', 'Content-Type': 'application/json', }, body: JSON.stringify({ emails: ['a@b.com', 'c@d.com'] }),})
if (!res.ok) throw new Error(await res.text())
const reader = res.body!.getReader()const decoder = new TextDecoder()let buffer = ''
while (true) { const { done, value } = await reader.read() if (done) break buffer += decoder.decode(value, { stream: true }) const lines = buffer.split('\n') buffer = lines.pop() ?? '' for (const line of lines) { if (!line) continue const result = JSON.parse(line) as Record<string, unknown> console.log(result.email, result.valid) }}if (buffer.trim()) { const result = JSON.parse(buffer) as Record<string, unknown> console.log(result.email, result.valid)}Stream request errors
Section titled “Stream request errors”Invalid body (empty array, more than 50 emails, or malformed JSON) returns 400 with docs pointing to this section:
{ "error": "invalid_request", "message": "Request body must be JSON with an emails array (1–50 addresses).", "action": "Send: {\"emails\":[\"a@b.com\",\"c@d.com\"]}", "docs": "https://docs.truval.dev/api/email-verify#streaming"}Batch request errors
Section titled “Batch request errors”Invalid body (empty array, more than 50 emails, or malformed JSON) returns 400:
{ "error": "invalid_request", "message": "Request body must be JSON with an emails array (1–50 addresses).", "action": "Send: {\"emails\":[\"a@b.com\",\"c@d.com\"]}", "docs": "https://docs.truval.dev/api/email-verify#batch"}Error responses
Section titled “Error responses”For a searchable catalog of every error code, HTTP status, fix guidance, and how values map to dashboard usage_log.error_class, see Error reference.
| Status | Error | Meaning |
|---|---|---|
401 | missing_api_key | No Authorization header provided |
401 | invalid_api_key | Key not found or revoked |
400 | invalid_request | Request body is missing or malformed (single: email field; batch/stream: emails array 1–50) |
413 | payload_too_large | Request body exceeds the 2MB payload limit |
429 | rate_limit_exceeded | Per-minute rate limit exceeded (batch/stream: one unit per email) |
429 | monthly_quota_exceeded | Free tier only: included monthly verification units for the current UTC calendar month would be exceeded (details) |
503 | quota_check_failed | Temporary failure checking monthly quota — retry shortly (details) |
402 | payment_required | Hard EUR spend cap for the billing period would be exceeded — adjust the cap in Billing & Limits or upgrade (details) |
404 | not_found | Endpoint path is invalid |
Payload size limit (minimum latency): The API rejects requests over 2MB when the client provides an accurate Content-Length header. This is implemented as a fast header check to avoid adding latency by pre-reading request bodies.
Authentication
Section titled “Authentication”Missing API key (401):
{ "error": "missing_api_key", "message": "No Authorization header provided.", "action": "Include your API key as: Authorization: Bearer sk_live_...", "docs": "https://docs.truval.dev/api/email-verify#authentication"}Invalid API key (401):
{ "error": "invalid_api_key", "message": "API key not found or revoked.", "action": "Generate a new key at https://dash.truval.dev.", "docs": "https://docs.truval.dev/api/email-verify#authentication"}Request
Section titled “Request”Invalid request (400):
{ "error": "invalid_request", "message": "Request body must be JSON with an email field.", "action": "Send: {\"email\":\"user@example.com\"}", "docs": "https://docs.truval.dev/api/email-verify#request"}Rate limits
Section titled “Rate limits”Per-minute caps apply to all tiers. For batch and stream endpoints, usage is counted per email (e.g. 50 addresses → 50 units toward the per-minute limit).
| Tier | Rate limit (req/min) |
|---|---|
| Free | 10 |
| Builder | 100 |
| Scale | 1,000 |
Monthly quota
Section titled “Monthly quota”On the free tier, the API enforces a hard cap on verification units per UTC calendar month (aligned with public pricing: 500 by default). Enforcement uses the same rule as Supabase count_usage_for_customer: rows in usage_log for your account in the half-open window [first day of month 00:00:00 UTC, first day of next month 00:00:00 UTC), except rows whose failed_check is rate_limit_exceeded or monthly_quota_exceeded (those responses are still logged for the dashboard but do not consume quota).
What counts as one unit (each adds a quota row; all outcomes below are still visible in Concrete logs):
POST /v1/email/verify— one unit per request after the JSON body is accepted (including400responses for invalid body after auth).POST /v1/email/verify/batchandPOST /v1/email/verify/stream— one unit per email in theemailsarray when the body validates (same as rate-limit cost).- Successful verifications log one row per email; syntax failures inside verification still log a row and count.
429rate_limit_exceededand429monthly_quota_exceeded— logged, not counted toward the monthly cap.
When the cap would be exceeded, the API returns 429 with error monthly_quota_exceeded (no window field):
{ "error": "monthly_quota_exceeded", "message": "Monthly free tier limit of 500 verification units reached for this UTC month.", "action": "Upgrade your plan or wait until the next UTC month.", "docs": "https://docs.truval.dev/api/email-verify#monthly-quota", "limit": 500, "used": 500, "reset_at": "2026-04-01T00:00:00.000Z"}reset_at is the start of the next UTC month (when the quota resets). This is not the same timestamp as per-minute rate limiting.
Clients should retry after reset_at (add a small cushion for clock skew/latency).
If the usage database cannot be queried temporarily, verify endpoints may return 503:
{ "error": "quota_check_failed", "message": "Could not verify monthly usage quota. Retry shortly.", "action": "If this persists, contact support.", "docs": "https://docs.truval.dev/api/email-verify#monthly-quota"}Billing and billable verifications
Section titled “Billing and billable verifications”Paid tiers (Builder / Scale): Billable verification counts toward your plan’s included volume, Stripe meter overage, and dashboard Billing & Limits counters. It means one email for which the API returned a result after layer 1 (syntax). Concretely:
- Not billable: the request fails only at syntax (layer 1) inside verification —
failed_checkissyntaxon a successful HTTP200response. Malformed addresses rejected by request validation (400) are not billable. - Billable (one unit per email): outcomes with
failed_checkofdisposable,no_mx,smtp, orsmtp_timeout, orfailed_checknull(deliverable / catch-all / unknown with full pipeline).
For POST /v1/email/verify/batch and POST /v1/email/verify/stream, billable units are summed per email in the payload (not per HTTP request). Async mode with a webhook bills when the background verification completes, using the same rules.
Free tier: Monthly enforcement uses the quota definition above (monthly quota), not the paid billable definition. Stripe meter events are not emitted for free tier accounts.
Hard spend cap (402): Before running work, the API checks your cap using the number of emails in the request (worst case if every address were billable). Actual recorded usage may be lower if many addresses exit at syntax only.
| Tier | Included volume (reference) |
|---|---|
| Free | 500 verification units / UTC calendar month (monthly quota) |
| Builder | 5,000 billable verifications / subscription billing period |
| Scale | 100,000 billable verifications / subscription billing period |
When the per-minute rate limit is exceeded, the API returns 429 with error rate_limit_exceeded:
{ "error": "rate_limit_exceeded", "message": "Rate limit of 100 req/min exceeded.", "action": "Wait until reset_at (plus a small cushion) before retrying.", "limit": 100, "window": "1m", "reset_at": "2026-03-24T12:01:00.000Z", "docs": "https://docs.truval.dev/api/email-verify#rate-limits"}Spend cap
Section titled “Spend cap”The dashboard (Billing & Limits tab) lets you configure two independent EUR caps per billing period:
Soft cap: An advisory threshold. When spending reaches the soft cap, a transactional alert email is sent to the account email address (see Email alerts). API calls continue — the soft cap does not block requests.
Hard cap: A hard enforcement threshold. When the projected overage in the current billing period would exceed the configured hard cap, verify endpoints return 402 Payment Required before running any verification (applies to single, batch, and stream routes; batch/stream use the total email count as the worst-case pre-check).
{ "error": "payment_required", "message": "Hard spend cap reached. Upgrade or raise your cap to continue.", "action": "Adjust your hard cap in Billing & Limits, or upgrade your plan.", "docs": "https://docs.truval.dev/api/email-verify#spend-cap", "hard_cap_eur": 50, "current_overage_eur": 52.5}hard_cap_eur is null when no numeric cap is set but the request is still blocked (rare); otherwise it is the configured cap in euros. current_overage_eur is the projected overage for the period at the time of the check.
To adjust caps: dashboard → Billing & Limits. Changes take effect on the next API request.
Retry strategy
Section titled “Retry strategy”On 429, inspect error in the JSON body:
rate_limit_exceeded— per-minute cap. Wait untilreset_at(usually within the same minute window).monthly_quota_exceeded— free-tier monthly cap. Wait untilreset_at(start of the next UTC month) or upgrade; do not spin in a tight loop. In both cases, retry afterreset_at(add a small cushion for clock skew/latency).
Example in Python:
import timefrom datetime import datetime, timezone
if response.status_code == 429: error_data = response.json() err = error_data.get("error") reset_at = error_data.get("reset_at") if not reset_at: raise RuntimeError("429 without reset_at") reset_time = datetime.fromisoformat(reset_at.replace("Z", "+00:00")) wait_seconds = (reset_time - datetime.now(timezone.utc)).total_seconds() wait_seconds += 0.05 # small cushion after reset_at to avoid racing the window edge if wait_seconds > 0: label = "Monthly quota" if err == "monthly_quota_exceeded" else "Rate limit" print(f"{label}: waiting {wait_seconds:.2f}s...") time.sleep(wait_seconds)Email alerts
Section titled “Email alerts”Truval sends transactional alert emails to your account email address at the following thresholds:
| Threshold | Trigger |
|---|---|
| 75% | Approaching cap — verify integrations are handling volume as expected |
| 90% | Near limit — consider upgrading or reviewing usage |
| 100% | Cap reached — quota exhausted (free) or soft cap hit (paid) |
Alerts are sent for:
- Free tier: 75%, 90%, and 100% of the monthly 500-unit quota cap
- Paid tiers: 75%, 90%, and 100% of the included billable volume for the billing period
- Soft spend cap: when EUR spend reaches the configured soft cap threshold
- Hard spend cap: when the hard cap is enforced and API calls begin returning
402
Alerts are deduplicated per threshold per period — you receive each alert at most once per billing cycle crossing. To manage alert preferences, go to dashboard → Profile → Notifications.
Usage log retention
Section titled “Usage log retention”Concrete logs (per-call rows visible in the Usage tab) are retained for:
| Tier | Retention |
|---|---|
| Free | 7 calendar days (UTC) |
| Builder | 90 days |
| Scale | 90 days |
Not found
Section titled “Not found”Unknown route (404):
{ "error": "not_found", "message": "The requested endpoint does not exist.", "action": "Use POST /v1/email/verify, POST /v1/email/verify/batch, POST /v1/email/verify/stream, or inspect https://api.truval.dev/openapi.json.", "docs": "https://docs.truval.dev/api/email-verify#not-found"}Verification layers
Section titled “Verification layers”Each request runs through five layers in order. The first failure exits immediately — no wasted compute.
- Syntax — regex + RFC 5321 checks (local part length, consecutive dots, etc). Returns in 0ms.
- Disposable — KV lookup against 50k+ throwaway domains. Returns in under 1ms.
- MX lookup — DNS-over-HTTPS query for MX records, cached 6 hours. Returns in 20–80ms.
- SMTP probe — TCP connection to the mail server, RCPT TO check without sending email. Overall timeout 3000ms, plus per-read timeout 1000ms for reliability.
- Signals — role detection, free provider flag, typo suggestion. 0ms, pure local logic. Does not introduce a
failed_checkby itself; it refines the response after layers 1–4.
failed_check values (layer → code)
Section titled “failed_check values (layer → code)”| Outcome | failed_check |
|---|---|
| Syntax invalid (layer 1) | syntax |
| Disposable domain (layer 2) | disposable |
| No MX records (layer 3) | no_mx |
| SMTP RCPT undeliverable or other SMTP failure before timeout (layer 4) | smtp |
| SMTP phase hits overall or read timeout (layer 4) | smtp_timeout |
Completed without any of the above (deliverable, catch-all, unknown with smtp_blocked, SMTP inconclusive / unknown, etc.) | null |
A note on Gmail and major providers
Section titled “A note on Gmail and major providers”Gmail, Outlook, Yahoo, and iCloud block SMTP probing from all third-party services. This is not a truval.dev limitation — it affects every email verification provider.
For these domains, truval.dev verifies the MX records and domain authenticity (layers 1–3), then returns:
{ "valid": false, "status": "unknown", "confidence": 0.75, "smtp_blocked": true}A confidence of 0.75 means: the domain is real, not disposable, has valid MX records, but we cannot confirm the specific mailbox. This is the most honest answer possible.
Public Repositories
Section titled “Public Repositories”- SDK: truval-dev/truval-sdk
- MCP Server: truval-dev/truval-mcp-server
- Agent Skills: truval-dev/truval-skills