Verification for the engineer wiring it up
Developers get handed this ticket with a sentence of context: stop bad emails getting into the database. The interesting decisions are all in what happens after the API answers.
What follows is the design detail that matters when you are the one holding the pager, rather than a feature list.
The branch nobody writes is the one that fires at 2am
Four results come back, plus three boolean flags, and most integrations handle two of the results. Valid goes forward, invalid gets rejected, and catch-all and unknown fall into an else that was written in a hurry, along with every flag nobody read.
Unknown is the one that hurts. It means a receiving server refused to answer, which happens when Microsoft-hosted domains throttle or when greylisting kicks in, and it almost never fires in local testing. Then a batch runs on a Tuesday when one large host is being difficult and a few thousand real customers land in whatever your else branch does.
That is why unknown is a result of its own rather than an error, and why the status field is a string rather than a boolean. A boolean would have made your integration shorter and your incident longer.
How the work gets done
The order that matters for developers, rather than a generic checklist.
Model the result as a closed set
Four values on the status field: valid, invalid, catch_all, unknown. Role, disposable and free_provider arrive in a separate flags array, so read them on their own and never as a fifth case. In TypeScript or Rust, an exhaustive match on the four means a missed branch becomes a compile error rather than a silent fallthrough.
Fail open at the signup path
A two-second timeout, and a timeout counts as a pass. A verifier having a slow second should never cost you a registration, and that decision is easier to make now than during the incident.
Poll the job, do not hold a request open
POST /v1/jobs returns immediately with a job id. Poll GET /v1/jobs/{id} with a growing delay until status is completed, partial, failed or cancelled, then page /emails or download /export. Send an Idempotency-Key so a retried submission does not create a second job.
Give CI its own key
Create a separate API key with only the verification.run scope. Test keys behave like live keys and calls spend credits, so keep CI to a handful of addresses and stub the rest.
The hard cases in developers, and how they come back
What it costs
A hundred free credits a month, no card, is enough to develop against. Unknown results are refunded. Production starts at $5 for a thousand addresses.
Every tier works the same way. Unknown results and duplicates are never billed, and credits do not expire.
Questions we get asked
Is there an SDK?
No, and one would wrap four HTTP calls in a dependency you would then have to keep pinned. Every language page carries a working client in that language's own idiom.
How do I test the unknown branch?
Stub a results[0] with status unknown in a unit test. A live call returns it only when a mail host refuses to answer, which you cannot trigger on demand.
Does verification send an email?
No. The connection closes before DATA, so nothing is delivered to the address being checked.
What happens to my credits on an error?
Nothing. A credit is spent only on a valid, invalid or catch-all result. Errors, rate limits, duplicates and unknown results never cost one.
Try it on your own list first
100 free checks a month, no card. Run a sample and read the unresolved count before you decide anything.