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.

ErrorHTTPMeaning
unauthorized401Missing/invalid key or session
insufficient_credits402Top up in Billing
not_found404Unknown job/resource
rate_limited429Slow down
context_required422Finder needs a domain or context anchor
server_error500Retry 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.