LeadlistVerifier

Email Verification API: A Developer's Guide

Jeremy Dixon, Founder of Elevate Clients Inc.9 min read

In short

A verification API is worth integrating when the check has to happen inside a system rather than as a task someone performs. Evaluate the auth model, rate-limit headers, error contract, async bulk pattern and webhook signing before writing any code.

Most verification integrations are written twice. The first version calls an endpoint, gets a boolean-shaped answer, and works fine until a rate limit, a partial failure or a result value nobody planned for arrives in production. This guide is about writing the second version first.

API or bulk upload?

The distinction is not volume, it is whether a human is in the loop.

If somebody exports a file, cleans it and re-imports the results, the dashboard already does that and an integration adds nothing. If the check has to happen when a user submits a form, when a CRM record is created, or on a schedule with nobody watching, you need an API. Both run the same engine and return the same result values, so the choice is purely about where the work is triggered.

What to evaluate before you integrate

Six properties determine how much of your integration you will rewrite later. Ask them of any vendor, ours included.

  • Auth model. A bearer token in a header is the expected shape. Keys in query strings end up in logs and browser history.
  • Rate-limit headers. The limit, the remaining allowance and the reset time should come back on every response. If you have to count requests yourself, you will get it wrong under concurrency.
  • Error contract. Stable machine-readable codes, not prose you have to string-match. You want to branch on a code and log the message, never the reverse.
  • Async bulk pattern. Submitting ten thousand addresses should return a job identifier immediately, not hold a connection open. Anything that blocks will time out somewhere in your stack.
  • Webhook signing. An unsigned webhook is an unauthenticated POST from the internet. Look for HMAC signing over the raw body, and check the documentation says raw body rather than parsed JSON.
  • Result schema stability. A small closed set of verdicts you can exhaustively handle beats thirty sub-statuses that grow over time.

For comparing verification products rather than their interfaces, the verifier comparisons cover that separately. The rest of this guide is a worked integration against our documented API.

Authentication

Keys are created in the dashboard and sent as a bearer token. The base URL is https://www.leadlistverifier.com/api/v1.

curl: authenticated request
curl "https://www.leadlistverifier.com/api/v1/credits" \
  -H "Authorization: Bearer $LLV_API_KEY"

Keep the key server-side. A verification key can spend credits, so it belongs in an environment variable and never in client-side code where a browser can read it.

Verifying one address

The single endpoint accepts both GET with a query parameter and POST with a JSON body. It deducts one credit on success, and refunds automatically if the engine itself fails.

curl
curl "https://www.leadlistverifier.com/api/v1/verify?email=alex@orbital.tech" \
  -H "Authorization: Bearer $LLV_API_KEY"
python
import os, requests

res = requests.post(
    "https://www.leadlistverifier.com/api/v1/verify",
    json={"email": "alex@orbital.tech"},
    headers={"Authorization": f"Bearer {os.environ['LLV_API_KEY']}"},
)
res.raise_for_status()
print(res.json())
node
const res = await fetch(
  "https://www.leadlistverifier.com/api/v1/verify",
  {
    method: "POST",
    headers: {
      Authorization: `Bearer ${process.env.LLV_API_KEY}`,
      "Content-Type": "application/json",
    },
    body: JSON.stringify({ email: "alex@orbital.tech" }),
  },
);
const result = await res.json();

The response:

200 OK
{
  "email": "alex@orbital.tech",
  "result": "deliverable",
  "score": 91,
  "engine": "two_pass",
  "is_role": false,
  "is_free": false,
  "is_disposable": false,
  "is_catch_all": true,
  "did_pass_two": true,
  "credits_remaining": 4932
}

Two fields repay attention. engine is either primary or two_pass, telling you which pass produced the verdict, and is_catch_all being true alongside a deliverable result is exactly the case a second pass exists to produce. That combination is unavailable from a single-pass verifier, which would have returned this address as unresolved.

Handling the four verdicts

The result field is one of deliverable, undeliverable, risky or unknown. Handle all four explicitly. The common bug is collapsing them into a boolean, which silently assigns risky and unknown to whichever branch the author happened to prefer.

python: exhaustive branching
def decide(result: dict) -> str:
    match result["result"]:
        case "deliverable":
            return "accept"
        case "undeliverable":
            return "reject"          # and add to suppression
        case "risky":
            # Neither. Use the score to set your own threshold,
            # and consider the flags: is_role, is_disposable.
            return "accept" if result["score"] >= 70 else "review"
        case "unknown":
            # No verdict was obtainable. Do not treat as invalid.
            return "review"

Treating unknown as invalid is the most expensive mistake available here, because it silently deletes addresses that were never established to be bad.

Bulk verification

For lists, submit a job rather than looping over the single endpoint. The bulk endpoint accepts up to 10,000 addresses per call and returns immediately with a job identifier.

submit a job
curl -X POST "https://www.leadlistverifier.com/api/v1/verify-bulk" \
  -H "Authorization: Bearer $LLV_API_KEY" \
  -H "Content-Type: application/json" \
  -d '{"emails": ["alex@orbital.tech", "sarah.chen@northbeam.io"]}'

Then poll the job status endpoint, or receive a webhook when it completes. Polling is simpler to build and a reasonable place to start:

python: poll with a ceiling
import time, os, requests

HEADERS = {"Authorization": f"Bearer {os.environ['LLV_API_KEY']}"}
BASE = "https://www.leadlistverifier.com/api/v1"

def wait_for(job_id: str, timeout: int = 3600):
    deadline = time.time() + timeout
    delay = 5
    while time.time() < deadline:
        job = requests.get(f"{BASE}/verify-bulk/{job_id}", headers=HEADERS).json()
        if job["status"] in ("completed", "failed"):
            return job
        time.sleep(delay)
        delay = min(delay * 2, 60)      # back off, cap at a minute
    raise TimeoutError(job_id)

Note the backoff and the ceiling. A fixed one-second poll on a job that takes fifteen minutes spends most of its requests learning nothing and eats your rate limit doing it.

Webhooks

Configure a webhook URL in the dashboard to be notified on completion instead of polling. Deliveries are signed with HMAC-SHA256 and retried up to five times with exponential backoff on any non-2xx response, so your endpoint should be idempotent.

Delivery headers look like this:

request headers
Content-Type: application/json
User-Agent: LeadListVerifier-Webhook/1
X-LLV-Event: bulk.completed
X-LLV-Timestamp: 1745510400
X-LLV-Signature: sha256=8f4e2c...

Verify the signature before trusting the payload. Compute HMAC-SHA256 of the raw request body using your signing secret and compare against the value after sha256=, using a timing-safe comparison.

node: signature verification
import { createHmac, timingSafeEqual } from "node:crypto";

export function verify(secret, body, sigHeader) {
  const received = sigHeader.startsWith("sha256=")
    ? sigHeader.slice(7)
    : sigHeader;
  const expected = createHmac("sha256", secret).update(body).digest("hex");
  const a = Buffer.from(received, "hex");
  const b = Buffer.from(expected, "hex");
  if (a.length !== b.length) return false;
  return timingSafeEqual(a, b);
}

The detail that catches people out: sign the raw body bytes. Frameworks that parse JSON before your handler runs will have re-serialised it, and key ordering or whitespace differences produce a different digest and a valid webhook that fails verification. In most frameworks this means capturing the raw body explicitly on the webhook route.

Rate limits and errors

The limit is 60 requests per minute, and every response carries the current state:

response headers
X-RateLimit-Limit: 60
X-RateLimit-Remaining: 58
X-RateLimit-Reset: 1745510460

Read X-RateLimit-Remaining rather than counting your own requests, which is unreliable the moment more than one process shares a key. On rate_limit_exceeded, wait for the interval the response indicates rather than retrying immediately.

Errors return machine-readable codes: invalid_request, unauthorized, forbidden, not_found, insufficient_credits, rate_limit_exceeded and internal_server_error. Branch on the code and log the message, never the other way round, since messages are written for people and may be reworded.

insufficient_credits deserves its own handling. Treating it as a generic failure means a job stops for a billing reason your code never surfaces. Check the credits endpoint on a schedule and alert before you run out rather than discovering it mid-run.

Caching and result freshness

Verification results are stable over days and weeks rather than seconds, so caching is both safe and worth doing. Store the verdict, the score and the timestamp against the address, and read from cache rather than re-verifying on every access.

Choose an expiry that matches why you are checking. A signup-time verdict rarely needs refreshing at all, since you verified the address at the moment it mattered. A CRM record you intend to mail again should be re-verified on the order of months, because decay is the reason to re-check and decay is slow.

Testing without burning credits

Verification calls cost money and hit real mail servers, which makes the usual habit of running the integration test suite on every commit a bad fit. Three practices keep the cost near zero.

Record once, replay forever. Capture real responses for a handful of addresses covering all four verdicts, then serve them from fixtures in unit tests. The response schema is small and stable, so fixtures stay valid far longer than they would against a sprawling API.

Test your branching, not their engine. The interesting logic is what your code does with risky and unknown, and that is testable entirely against fixtures. You are not responsible for verifying that verification works.

Reserve live calls for a smoke test. One real call against a known address on deploy confirms credentials, network path and schema in a single credit. Run that, not the suite.

Where to put verification in a signup flow

The most common real-time use is a registration form, and the placement decision matters more than the integration.

Verify on submit rather than on every keystroke. Per-keystroke checks burn credits on partial addresses that were never going to be valid, and the user has not finished typing anyway. Debouncing helps and does not solve it.

Decide in advance what a risky verdict does to your flow. Blocking signup on anything short of undeliverable will reject real users, particularly those at organisations whose domains accept everything. Most products accept the address and flag the account rather than refusing it, which keeps the funnel intact while surfacing the uncertainty somewhere useful.

Fail open, not closed. If the API is unreachable, allowing signup and verifying asynchronously afterwards is almost always better than blocking registration on a dependency the user cannot see or fix.

Mistakes worth avoiding

  • Collapsing four verdicts into a boolean. The single most common integration bug, and it silently discards the distinction the second pass exists to create.
  • Looping single verifications over a list. Slower, harder on the rate limit, and it gives up the job semantics that let you resume after a failure.
  • Verifying the same address repeatedly. Results are stable over days and weeks. Cache them and re-verify on a schedule rather than on every read.
  • Parsing the webhook body before verifying it. The signature covers the raw bytes, so parsing first breaks verification and skipping verification makes the endpoint an open door.
  • Ignoring rate-limit headers until a 429 arrives. Reacting to failure works, but the information to avoid it entirely was in every response you already received.

The full reference, including the list endpoints and filtered export, is on the API documentation page. Pricing is credit-based with no subscription, so an integration that runs occasionally costs nothing between runs, and a free account includes 100 credits and an API key to build against.

Frequently asked questions

When should I use the API instead of uploading a CSV?

Use the API when verification has to happen inside a system rather than as a task somebody performs. Signup forms, CRM enrichment on record creation, and scheduled jobs that clean a list without anyone logging in are all API cases. If a person exports a file, verifies it and re-imports the results, the dashboard is less work and the outcome is identical.

How should I handle the risky verdict in code?

Do not collapse it into either deliverable or undeliverable, because it means neither. Treat it as a third branch and use the accompanying score to decide the threshold that suits your application. A signup form might accept risky addresses while flagging the account for review; a cold outreach pipeline might route them to a separate segment sent from a different domain.

What happens if verification fails partway through a bulk job?

Credits are refunded automatically rather than consumed for work that did not complete, and the job's status reflects the failure. For single verification, a credit is deducted on success and refunded automatically if the engine fails, so a transient outage does not quietly cost you balance.

Do I need webhooks, or can I poll?

Polling the job status endpoint works and is simpler to build, which makes it a reasonable starting point. Webhooks are better once volume grows, because you stop making requests that mostly return the same in-progress status and you find out about completion immediately rather than on your next interval. Both paths are documented and you can start with polling and add webhooks later.

How do I avoid hitting the rate limit?

Read the headers on every response rather than counting requests yourself. The limit, remaining allowance and reset time all come back on each call, so a client that respects the remaining count degrades smoothly instead of hitting a wall. For bulk work, submit a job rather than looping over single verifications, which is both faster and easier on the limit.

Try it on your own list.

100 free credits on signup, no card required. Or check a single address with the free verifier, no account needed.