Verifyre

Email verification API: what it is and how to integrate one

An email verification API checks an address over SMTP and returns a verdict your code can act on. Here is the request, the JSON response, and working examples in curl, Node.js and Python.

An email verification API is an HTTP endpoint that takes an email address and returns, usually within a second, whether that address can receive mail. Behind the endpoint the service checks syntax, DNS and MX records, then opens an SMTP conversation with the receiving server to confirm the mailbox exists, without sending a message. Integrating one means calling that endpoint at the moment an address enters your system, typically from a signup handler, a CRM import or a pre-send check, and branching on the verdict that comes back.

What does an email verification API actually do?

Each request runs the same pipeline that the free email checker runs in the browser: syntax parsing, domain and MX lookup, disposable and role detection, an SMTP handshake up to RCPT TO, a catch-all probe, and scoring. The stages are described in detail in what is email verification. The difference with the API is that the result arrives as JSON, so your own code decides what happens next.

Verifyre returns one of four verdicts plus a score from 0 to 100:

Score bandUsual verdictWhat it means
90 to 100deliverableThe mail server confirmed the mailbox
60 to 89risky or unknownExists but catch-all, full or role-based, or the server was slow to answer
30 to 59unknown or riskyNo firm answer, or several negative signals
0 to 29undeliverableSyntax, DNS or the server itself said no

The score is what you threshold on when the verdict alone is too coarse. A signup form might accept anything above 60; a cold outreach tool might accept 50 and tolerate a few bounces.

How do you call it?

Authentication is a bearer token in the Authorization header. A single check is either a GET with the address in the query string or a POST with a JSON body. The simplest possible call is one line of curl:

curl "https://verifyre.co/api/v1/verify?email=sara.khan@example.com" \
  -H "Authorization: Bearer vfy_live_YOUR_API_KEY"

The response looks like this:

{
  "email": "Sara.Khan@Example.com",
  "normalized": "sara.khan@example.com",
  "status": "deliverable",
  "reason": "mailbox_exists",
  "reason_text": "The mail server confirmed the mailbox exists.",
  "score": 97,
  "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": "mx1.example.com", "priority": 10 }],
  "provider": "Google Workspace",
  "did_you_mean": null,
  "cached": false,
  "ms": 812,
  "credits_charged": 1,
  "credits_remaining": 4999
}

normalized is the lowercased, trimmed form you should store. status and score are what most integrations branch on. checks exposes each stage, so you can treat role: true differently from catch_all: true even though both land in the risky band. did_you_mean is set when the domain looks like a typo of a common provider. cached is true when the address was checked within the last 24 hours, and those repeats are free. The full field reference is in the developer docs.

A Node.js example that branches on the result

This is a server-side handler, not browser code. Never put the key in a page; for client-side form checks use the JavaScript widget, which uses a scoped key and emits a DOM event instead.

async function verifyEmail(email) {
  const url = "https://verifyre.co/api/v1/verify?email=" + encodeURIComponent(email);
  const res = await fetch(url, {
    headers: { Authorization: "Bearer " + process.env.VERIFYRE_KEY },
    signal: AbortSignal.timeout(8000),
  });
  if (res.status === 402) throw new Error("out_of_credits");
  if (!res.ok) throw new Error("verifyre_http_" + res.status);
  return res.json();
}

export async function handleSignup(req, reply) {
  let result;
  try {
    result = await verifyEmail(req.body.email);
  } catch (err) {
    // Fail open: a verification outage must never block a real customer.
    return createAccount(req.body.email, { unverified: true });
  }

  if (result.status === "undeliverable") {
    return reply.code(400).send({
      error: "That address cannot receive mail.",
      suggestion: result.did_you_mean,
    });
  }
  if (result.status === "deliverable" || result.score >= 60) {
    return createAccount(result.normalized);
  }
  // risky or unknown: let them in, but ask for a confirmation click
  return createAccount(result.normalized, { requireConfirmation: true });
}

The three-way branch is the pattern that works in practice: reject only firm undeliverable verdicts, accept clear deliverable ones, and route the middle band to a confirmation email rather than refusing it.

The same thing in Python

import os
import requests

def verify_email(email: str) -> dict:
    r = requests.post(
        "https://verifyre.co/api/v1/verify",
        json={"email": email},
        headers={"Authorization": f"Bearer {os.environ['VERIFYRE_KEY']}"},
        timeout=8,
    )
    r.raise_for_status()
    return r.json()

result = verify_email("sara.khan@example.com")
if result["status"] == "undeliverable":
    print("reject:", result["reason_text"])
elif result["score"] >= 60:
    print("accept:", result["normalized"])
else:
    print("hold for confirmation")

Where should the call live?

Three places pay for themselves, in this order.

  1. The signup form, server-side. Verify in the request handler before the row is written. This stops bad addresses at the door and is what real-time verification is built around. Pair it with the widget on the front end so the user sees a typo suggestion before submitting.
  2. CRM and list imports. When a CSV lands or a sync runs, verify each new address and write the verdict back as a field. Suppress undeliverable, tag risky, and let your segments do the rest.
  3. Before a campaign. Any list untouched for 90 days should be verified before the send. Lists lose about a fifth of their valid addresses a year, so a pre-send pass on an old segment routinely removes 5% to 15% of it.

How do you batch large lists?

For anything beyond a few hundred addresses, use the bulk endpoint rather than looping over single calls. It accepts up to 50,000 addresses per job and runs them concurrently on our side.

curl -X POST https://verifyre.co/api/v1/bulk \
  -H "Authorization: Bearer vfy_live_YOUR_API_KEY" \
  -H "Content-Type: application/json" \
  -d '{"emails": ["a@example.com", "b@example.org"], "name": "March import", "callback_url": "https://yourapp.com/hooks/verifyre"}'

The response contains a job id. Poll GET /api/v1/bulk/{id} for progress, or supply a callback_url and we will POST to it when the job finishes. Results, one object per address in the same shape as the single-check response, are at GET /api/v1/bulk/{id}/results. Duplicates within a job are charged once.

Caching, retries and timeouts

A verification call depends on a remote mail server answering an SMTP conversation, and some of them are slow or evasive. Design for that.

  • Timeouts. Set a client timeout of 8 to 10 seconds. Most calls return in under a second (the ms field says exactly how long), but a greylisting server can stretch that. On a timeout, treat the address as unknown and fail open.
  • Caching. Repeat checks within 24 hours return cached: true and cost nothing, so a short-window cache of your own is unnecessary. For longer horizons, store the verdict and timestamp and re-verify after 90 days.
  • Retries. Retry a 429 with exponential backoff starting at one second, and a 5xx once. Never retry a 400, 401, 402 or 413: those fail the same way again.
  • Error codes. 400 invalid_request is a malformed address or body, 401 unauthorized a bad key, 402 out_of_credits an empty balance, 413 too_many_emails a bulk job over 50,000, and 429 the rate limit.

Key takeaways

  • An email verification API runs syntax, DNS, SMTP and catch-all checks and returns a verdict plus a 0 to 100 score as JSON.
  • Branch three ways: reject undeliverable, accept deliverable, send the risky and unknown band to a confirmation step.
  • Put the call in the server-side signup handler first, then imports, then pre-send checks on old lists.
  • Use the bulk endpoint for anything over a few hundred addresses; it takes up to 50,000 per job.
  • Set an 8 to 10 second timeout, fail open on outages, and lean on the free 24-hour cache.

Frequently asked questions

Does calling the API send an email to the address?

No. The SMTP conversation stops after RCPT TO and never issues the DATA command, so nothing is transmitted and the mailbox owner sees nothing.

How fast is a single verification?

Typically under one second for major providers, and up to a few seconds for slow or greylisting servers. The ms field in every response reports the actual time.

What does a verification cost?

One credit per unique address. Re-checking the same address within 24 hours is free, and new accounts get 100 verifications to test with. See the email verification API page for plans.

Put it into practice.

100 free verifications on signup. Clean a list, add the widget, or call the API.

Start free See pricing