API documentation
One request to verify any address.
REST over HTTPS, JSON in and out, a bearer key, and a verdict in about a second. Single checks, bulk jobs up to 50,000 addresses, signed webhooks and a drop-in form widget.
Quick start
Create a free account, open Settings, then API keys, and create a key. Every new account carries 100 credits; one credit buys one live verification, and a repeat check of the same address inside 24 hours is free and flagged cached.
Base URL: https://verifyre.co/api/v1. All responses are JSON with Cache-Control: no-store. The API is CORS-enabled, but keep your key on a server: a browser can only safely use the widget key flow.
Verify one address
curl "https://verifyre.co/api/v1/verify?email=jane.doe%40example.com" \ -H "Authorization: Bearer vfy_live_YOUR_API_KEY"
{
"email": "jane.doe@example.com",
"normalized": "jane.doe@example.com",
"status": "deliverable",
"reason": "accepted_email",
"reason_text": "The mail server confirmed this mailbox exists and accepts mail.",
"score": 99,
"deliverable": true,
"checks": {
"syntax": true, "domain": true, "mx": true, "smtp": true, "mailbox": true,
"catch_all": false, "disposable": false, "free": false, "role": false,
"spf": true, "dmarc": true
},
"mx": [{ "exchange": "aspmx.l.google.com", "priority": 1 }],
"provider": "Google Workspace",
"did_you_mean": null,
"cached": false,
"ms": 1240,
"credits_charged": 1,
"credits_remaining": 9871
}Authentication
Send the key as a bearer token, or in an X-Api-Key header if your HTTP client makes that easier. Keys start with vfy_live_, are shown once when created, and are stored on our side only as a SHA-256 hash. Revoke and rotate them from the account at any time; a revoked key answers 401 invalid_key immediately.
GET https://verifyre.co/api/v1/account HTTP/1.1 Authorization: Bearer vfy_live_YOUR_API_KEY # or X-Api-Key: vfy_live_YOUR_API_KEY
Give each system its own key (website widget, CRM sync, data warehouse) so usage is attributable and a leak is contained to one surface.
Rate limits
Each key may make 120 requests per minute by default (higher on request for enterprise accounts). The widget endpoint is limited separately to 30 requests per minute per key and visitor IP. Every response carries the current window:
X-RateLimit-Limit: 120
X-RateLimit-Remaining: 117
# when exceeded
HTTP/1.1 429 Too Many Requests
Retry-After: 23
{ "error": { "code": "rate_limited", "message": "Rate limit of 120 requests per minute reached. Retry in 23s.", "retry_after": 23 } }Bulk jobs are the right tool for anything over a few hundred addresses: one request creates the job, the worker verifies with per-domain politeness, and a webhook or callback tells you when it is done.
Errors
Errors use a conventional HTTP status and a JSON body with a stable code you can branch on and a message written for a human.
{
"error": {
"code": "out_of_credits",
"message": "Your account has no credits left. Buy a pack at https://verifyre.co/pricing to continue."
}
}| Status | Code | Meaning |
|---|---|---|
| 400 | invalid_request | A parameter is missing or malformed (no email, an empty emails array, a bad callback_url). |
| 401 | missing_key / invalid_key | No key was sent, it does not start with vfy_, or it has been revoked. |
| 402 | out_of_credits | The balance cannot cover the request. For bulk jobs the body includes credits_needed and credits_available. |
| 403 | account_suspended | The account behind the key is suspended. |
| 404 | not_found | No bulk job with that id belongs to this account. |
| 413 | too_many_emails | A bulk request had more than 50,000 addresses. |
| 429 | rate_limited | The per-key limit was reached. Honour Retry-After (seconds). |
| 500 | verification_failed | The check failed on our side. Nothing was charged; retry. |
Verify one address
Runs the full pipeline: syntax, DNS and MX, disposable and role lists, an SMTP handshake with the mailbox's own server, catch-all detection and scoring. Typical latency is 0.5 to 2 seconds; providers that refuse handshakes (some Yahoo and Outlook hosts) answer faster with unknown / unverifiable.
| Parameter | Type | Description |
|---|---|---|
| string, required | The address to verify. GET takes it as a query parameter, POST as a JSON body {"email": "..."}. |
Response fields
| Parameter | Type | Description |
|---|---|---|
| status | string | deliverable, undeliverable, risky or unknown. |
| reason | string | The reason code behind the status, e.g. accepted_email, mailbox_not_found, catch_all. See the reference below. |
| reason_text | string | The reason explained in one sentence, suitable for showing to users. |
| score | integer 0-100 | Confidence the address will deliver; 0 is certainly dead, 100 is confirmed. |
| deliverable | boolean | Shorthand for status == deliverable. |
| checks | object | Each pipeline stage: syntax, domain, mx, smtp, mailbox, catch_all, disposable, free, role, spf, dmarc. null means not determinable. |
| mx | array | The domain's mail exchangers, highest priority first. |
| provider | string | null | The mailbox provider when recognised (Google Workspace, Microsoft 365, Gmail, ...). |
| did_you_mean | string | null | A corrected domain when the given one looks like a typo of a well-known provider (gmai.com becomes gmail.com). |
| normalized | string | Lower-cased, provider-aware canonical form (Gmail dots and plus tags removed) for de-duplication. |
| cached | boolean | true when the verdict was reused from a check inside the last 24 hours; no credit charged. |
| ms | integer | Time the check took, in milliseconds. |
| credits_charged / credits_remaining | integer | What this call cost and the balance after it. |
curl "https://verifyre.co/api/v1/verify?email=jane.doe%40example.com" \ -H "Authorization: Bearer vfy_live_YOUR_API_KEY"
Create a bulk job
Submits up to 50,000 addresses in one request. Addresses are de-duplicated on their normalised form; duplicates and syntactically invalid rows are free, and the unique, well-formed remainder is charged when the job is created. The job runs in the background; poll the status URL or supply a callback_url.
| Parameter | Type | Description |
|---|---|---|
| emails | string[], required | Up to 50,000 addresses. Objects of the form {"email": "..."} are accepted too. |
| name | string | A label shown in your dashboard. Defaults to "API list" plus the timestamp. |
| callback_url | string | A public https URL that receives a POST {event: "job.completed", job: {...}} when the job finishes. For signed delivery use webhooks instead. |
curl -X POST "https://verifyre.co/api/v1/bulk" \
-H "Authorization: Bearer vfy_live_YOUR_API_KEY" \
-H "Content-Type: application/json" \
-d '{"name":"October newsletter","emails":["jane.doe@example.com","info@acme.co","typo@gmai.com"],"callback_url":"https://example.com/hooks/verifyre"}'HTTP/1.1 202 Accepted
{
"id": "cmh3k9x1a0001v8l2q7u4d9fz",
"name": "October newsletter",
"status": "queued",
"total": 3,
"duplicates": 0,
"credits_charged": 3,
"callback_url": "https://example.com/hooks/verifyre",
"status_url": "https://verifyre.co/api/v1/bulk/cmh3k9x1a0001v8l2q7u4d9fz",
"results_url": "https://verifyre.co/api/v1/bulk/cmh3k9x1a0001v8l2q7u4d9fz/results",
"created_at": "2026-10-22T09:14:03.000Z"
}Job status
Counts update as the worker progresses. status is one of queued, running, paused, done, failed or cancelled; progress is a percentage. Poll no faster than every few seconds, or let a webhook tell you.
curl "https://verifyre.co/api/v1/bulk/cmh3k9x1a0001v8l2q7u4d9fz" \ -H "Authorization: Bearer vfy_live_YOUR_API_KEY"
{
"id": "cmh3k9x1a0001v8l2q7u4d9fz",
"name": "October newsletter",
"status": "done",
"total": 3,
"processed": 3,
"progress": 100,
"counts": { "deliverable": 2, "undeliverable": 1, "risky": 0, "unknown": 0 },
"duplicates": 0,
"credits_charged": 3,
"error": null,
"results_url": "https://verifyre.co/api/v1/bulk/cmh3k9x1a0001v8l2q7u4d9fz/results",
"created_at": "2026-10-22T09:14:03.000Z",
"started_at": "2026-10-22T09:14:04.000Z",
"finished_at": "2026-10-22T09:14:09.000Z"
}Job results
Rows in their original order. JSON is paginated; ?format=csv streams every row as a file with the same columns. Rows not yet processed show status: "pending", so you can read a running job.
| Parameter | Type | Description |
|---|---|---|
| page | integer | 1-based page number for JSON (default 1). |
| limit | integer | Rows per page, 1 to 1000 (default 1000). |
| status | string | Only rows with this status: deliverable, undeliverable, risky or unknown. |
| format | csv | Return every matching row as CSV instead of JSON. |
curl "https://verifyre.co/api/v1/bulk/cmh3k9x1a0001v8l2q7u4d9fz/results?page=1&limit=1000" \ -H "Authorization: Bearer vfy_live_YOUR_API_KEY"
{
"job_id": "cmh3k9x1a0001v8l2q7u4d9fz",
"job_status": "done",
"page": 1,
"limit": 1000,
"total": 3,
"pages": 1,
"results": [
{
"row": 1,
"email": "jane.doe@example.com",
"status": "deliverable",
"reason": "accepted_email",
"reason_text": "The mail server confirmed this mailbox exists and accepts mail.",
"score": 99,
"normalized": "jane.doe@example.com",
"provider": "Google Workspace",
"did_you_mean": null,
"disposable": false, "role": false, "free": false, "catch_all": false,
"checked_at": "2026-10-22T09:14:06.000Z"
}
]
}Account
The balance, this month's usage and the key that made the call. Handy for a low-balance alert in your own monitoring.
curl "https://verifyre.co/api/v1/account" \ -H "Authorization: Bearer vfy_live_YOUR_API_KEY"
{
"email": "you@company.com",
"credits": 9871,
"usage": {
"month": { "since": "2026-10-01T00:00:00.000Z", "single_checks": 412, "bulk_jobs": 3, "bulk_credits": 14200, "credits_spent": 14612 },
"all_time": { "single_checks": 2210, "bulk_jobs": 19, "bulk_credits": 91000 }
},
"key": { "name": "Production", "prefix": "vfy_live_a8Kd3", "calls": 2622, "created_at": "2026-09-02T10:00:00.000Z" },
"active_keys": 2,
"rate_limit": { "per_minute": 120, "remaining": 119 }
}Status codes
No key required. Returns every status with its label and blurb, every reason code with its sentence, and the score bands, so your own UI can stay in step with ours without hard-coding copy.
curl https://verifyre.co/api/v1/status-codes
Webhooks
Add endpoints under Settings, then Webhooks. Each endpoint gets a secret; every delivery is a POST with the JSON body below, signed with HMAC-SHA256 over the raw bytes and sent in X-Verifyre-Signature (lower-case hex). Compare in constant time and reject anything that does not match. Deliveries time out after 8 seconds and are not retried, so answer quickly (2xx) and do the work afterwards.
| Parameter | Type | Description |
|---|---|---|
| job.completed | event | A bulk job finished. data carries the counts and a link to the list in the dashboard. |
POST https://example.com/hooks/verifyre
Content-Type: application/json
X-Verifyre-Event: job.completed
X-Verifyre-Signature: 4f6c0c2a... (hex HMAC-SHA256 of the raw body, keyed with your webhook secret)
{
"event": "job.completed",
"data": {
"id": "cmh3k9x1a0001v8l2q7u4d9fz",
"name": "October newsletter",
"total": 14200,
"deliverable": 12910,
"undeliverable": 740,
"risky": 410,
"unknown": 140,
"url": "https://verifyre.co/app/lists/cmh3k9x1a0001v8l2q7u4d9fz"
},
"sentAt": "2026-10-22T09:14:09.000Z"
}Verify the signature
import { createHmac, timingSafeEqual } from "node:crypto";
// Express: use express.raw({ type: "application/json" }) so req.body is the raw bytes.
app.post("/hooks/verifyre", (req, res) => {
const expected = createHmac("sha256", process.env.VERIFYRE_WEBHOOK_SECRET).update(req.body).digest("hex");
const given = req.get("X-Verifyre-Signature") || "";
if (given.length !== expected.length || !timingSafeEqual(Buffer.from(given), Buffer.from(expected))) {
return res.status(401).end();
}
const { event, data } = JSON.parse(req.body);
if (event === "job.completed") console.log("List done:", data.id, data.deliverable, "deliverable");
res.status(204).end();
});Form widget
One script tag adds live validation to any email field on any site: a green tick for a confirmed mailbox, a "Did you mean" prompt for typos, and a red warning for addresses that cannot receive mail. It writes a hidden verifyre_status input into the form and, with data-block="true", stops the form submitting an undeliverable address. The widget is 9 KB, has no dependencies and works with React, Vue, Shopify, WordPress and plain HTML alike.
<script src="https://verifyre.co/widget.js" data-key="vfy_live_YOUR_API_KEY" data-selector="input[type=email]" data-block="true" async></script>
| Parameter | Type | Description |
|---|---|---|
| data-key | required | An API key from your account. Make one just for the widget: it is visible in your page source, so keep it separate from server keys and revoke it if it leaks. |
| data-selector | default input[type=email] | Which inputs to watch. Any CSS selector; new matching inputs (modals, SPAs) are picked up automatically. |
| data-block | true | false | Prevent submission when the address is undeliverable. The visitor sees the hint and can correct it. |
| data-debounce | default 600 | Milliseconds after typing stops before a check runs. |
| data-text-good, data-text-bad, data-text-suggest, data-text-use | strings | Override the hint copy, for translations. |
Each uncached check spends one credit from the key's account; the widget never checks a field that does not look like a complete address, and repeated checks of the same address are free. Listen for the verifyre event or call Verifyre.verify() from your own code:
document.addEventListener("verifyre", (e) => {
// e.target is the input; e.detail is { status, reason, reason_text, did_you_mean, score }
if (e.detail.status === "undeliverable") analytics.track("bad_email_caught");
});
// Programmatic use: returns the same object (or null if the check failed).
const verdict = await Verifyre.verify("someone@example.com");The widget endpoint is limited to 30 checks a minute per key and visitor, and a key can be restricted to the widget by revoking it the moment it is misused: it is a normal key with the normal controls.
Status reference
Four statuses, fifteen reason codes, one score. The full table with what to do about each is on the status codes page.
Deliverable
Safe to send. The mailbox exists and accepts mail.
Undeliverable
Do not send. Mail to this address will bounce.
Risky
Might deliver, might bounce. Send with care or suppress.
Unknown
The mail server would not confirm either way. Re-check later.