VerifyMail API v1
REST API for single verification, bulk jobs, AI reports, and the AI email finder. All endpoints live under /api/v1.
Overview
Base URL: https://your-domain.com/api/v1
Requests and responses are JSON (except CSV upload/export). Timestamps are ISO 8601. Every response includes the full 8-layer pipeline trace in checks, so you can audit exactly how a verdict was reached.
Status taxonomy: valid, invalid, catch_all, disposable, role_based, spamtrap_risk, unknown, syntax_error.
Authentication
Create a key in the dashboard (shown once — we store only its SHA-256 hash) and send it as a Bearer token:
Header
Authorization: Bearer vm_live_xxxxxxxxxxxxxxxx
The X-API-Key header is also accepted. Dashboard requests authenticate via session cookie instead. Internal keys (for your own products) skip credit deduction.
POST /verify — single email
POST/api/v1/verify
Runs the full pipeline (syntax → MX → lists → SMTP → catch-all → AI scoring). 1 credit.
Request
curl -X POST https://your-domain.com/api/v1/verify \
-H "Authorization: Bearer $VERIFYMAIL_KEY" \
-H "Content-Type: application/json" \
-d '{"email": "jane.doe@stripe.com"}'Response
{
"email": "jane.doe@stripe.com",
"status": "valid",
"sub_status": "mailbox_confirmed",
"mx_record": "smtp.stripe.com",
"smtp_response_code": 250,
"smtp_message": "250 2.1.5 Ok",
"ai_confidence_score": null,
"did_you_mean": null,
"is_free_provider": false,
"is_role": false,
"is_disposable": false,
"is_catch_all": false,
"checks": [
{ "step": "syntax", "label": "Syntax check (RFC 5322)", "result": "pass", "detail": "..." },
{ "step": "mx", "label": "Domain & MX lookup", "result": "pass", "detail": "..." },
{ "step": "smtp", "label": "SMTP mailbox probe", "result": "pass", "detail": "RCPT TO → 250 ..." },
{ "step": "catch_all", "label": "Catch-all probe", "result": "pass", "detail": "..." }
],
"duration_ms": 1840
}When SMTP is inconclusive (greylisting, probe-hostile gateways), status is unknown and the AI layer fills ai_confidence_score (0-1), predicted_status, and ai_reasoning.
ZeroBounce compatibility mode
Add ?compat=zerobounce to return ZeroBounce v2's exact response shape — a drop-in migration for existing ZeroBounce clients:
Request
curl -X POST "https://your-domain.com/api/v1/verify?compat=zerobounce" \
-H "Authorization: Bearer $VERIFYMAIL_KEY" \
-H "Content-Type: application/json" \
-d '{"email": "support@stripe.com"}'Response
{
"address": "support@stripe.com",
"status": "valid",
"sub_status": "role_mailbox_valid",
"free_email": false,
"role_based": true,
"did_you_mean": "",
"mx_found": "true",
"mx_record": "smtp.stripe.com",
"smtp_provider": "smtp.stripe.com",
"disposable": false,
"toxic": false,
"catch_all": false
}POST /bulk — CSV upload
POST/api/v1/bulk
Multipart upload (file field) or JSON body { "csv": "..." }. Max 5 MB / 10,000 rows. The email column is auto-detected; duplicates are removed before charging. 1 credit per unique email. Anomaly screening runs before any SMTP probing.
Request
curl -X POST https://your-domain.com/api/v1/bulk \ -H "Authorization: Bearer $VERIFYMAIL_KEY" \ -F "file=@contacts.csv"
Response
{
"job_id": "b7e6…",
"status": "queued",
"total_emails": 4821,
"duplicates_removed": 179,
"email_column": "Work Email",
"credits_charged": 4821,
"poll": "/api/v1/results/b7e6…"
}GET /results/:jobId — poll & export
GET/api/v1/results/:jobId
Returns the job (status, progress, summary counts, anomaly verdict, AI report) plus paginated results. Query params: ?status= (repeatable filter), ?page=, ?pageSize= (max 200), and ?format=csv for export.
CSV export
curl "https://your-domain.com/api/v1/results/b7e6…?format=csv" \ -H "Authorization: Bearer $VERIFYMAIL_KEY" \ -o verified.csv
GET /jobs — list jobs
GET/api/v1/jobs
The authenticated user's bulk jobs, newest first, with progress and summaries.
POST /ai/report/:jobId — regenerate report
POST/api/v1/ai/report/:jobId
Regenerates the plain-English AI summary for a completed job. Free. Reports are also generated automatically when a job finishes.
POST /find — AI email finder
POST/api/v1/find
Body: { full_name, domain?, company_name?, job_title?, location? }. With a domain, the finder checks the learned pattern database → crawls the site → infers the pattern with AI → verifies candidates through our own verifier. Without a domain, Step 0 resolves the employer from the context fields first.
Request
curl -X POST https://your-domain.com/api/v1/find \
-H "Authorization: Bearer $VERIFYMAIL_KEY" \
-H "Content-Type: application/json" \
-d '{"full_name": "Jane Doe", "domain": "stripe.com"}'Response
{
"status": "found",
"email": "jane.doe@stripe.com",
"method": "known_pattern",
"verification_status": "valid",
"confidence": null,
"candidates_tried": ["jane.doe@stripe.com"],
"credits_charged": 1,
"steps": [
{ "step": "step1_known_pattern", "detail": "Known pattern for stripe.com: first.last (99 confirmations)" }
]
}Guardrails: name-only requests without any context field are rejected (HTTP 422). Low-confidence employer resolutions return needs_confirmation with the disambiguation note instead of burning credits. Catch-all domains return the best candidate unconfirmed and uncharged.
API keys management
POST/api/v1/keys
Create: { "name": "signup-form", "internal": false } — the full key is returned once. Revoke: DELETE /keys?id=…. List: GET /keys.
Rate limits & errors
Rate limit: 10 requests/second per API key (429 with Retry-After when exceeded). Enforced at the CDN edge in production and again in-app.
| Error | HTTP | Meaning |
|---|---|---|
| unauthorized | 401 | Missing/invalid key or session |
| insufficient_credits | 402 | Top up in Billing |
| not_found | 404 | Unknown job/resource |
| rate_limited | 429 | Slow down |
| context_required | 422 | Finder needs a domain or context anchor |
| server_error | 500 | Retry with backoff |
LeadFlow integration (internal)
LeadFlow's src/lib/email-verify.ts can swap ZeroBounce for this API with two env vars and zero code changes:
.env in leadflow
USER_EMAIL_VERIFY_BASE_URL=https://your-domain.com/api/v1/verify?compat=zerobounce USER_EMAIL_VERIFY_API_KEY=vm_live_… # an *internal* key — no credit deduction
For leads without an email, call POST /api/v1/find with full_name + domain (the business's site) at the email-extraction step of discover.ts.
Status mapping for LeadFlow's narrower Prisma enum: disposable / spamtrap_risk / syntax_error → invalid; role_based → valid (sendable, flagged). Full detail stays in VerifyMail's results table.