Single verification

POST /v1/verify

POST /verify takes one address and runs the whole pipeline against it: syntax, MX lookup, an SMTP handshake to the mail server, then a catch-all probe against the same domain. You get a verdict back on the same connection. The address travels in the JSON body and never in the URL, so it stays out of access logs and browser history.

Timing depends almost entirely on the receiving server. A well-behaved MX answers a RCPT TO in under a second. A greylisting server holds the connection, and Microsoft-hosted domains sometimes throttle mid-probe. Budget three seconds for the common case and set your client timeout at ten.

The connection is closed before the DATA command, so no message is sent to the address you check. Nobody receives anything. This is worth saying out loud because it is the first question every buyer asks.

Shell
curl https://api.zapbounce.com/v1/verify \
  -H "Authorization: Bearer zb_live_..." \
  -H "Content-Type: application/json" \
  -d '{"email":"ada@example.com"}'
JSON
{
  "email": "ada@example.com",
  "result": "valid",
  "reason": "accepted_email",
  "domain": "example.com",
  "mx_found": true,
  "mx_host": "mx1.example.com",
  "smtp_code": "250",
  "role": false,
  "disposable": false,
  "free_provider": false,
  "did_you_mean": null,
  "billed": true,
  "checked_at": "2026-09-18T10:02:41Z"
}

Fields

FieldTypeWhat it carries
resultstringOne of valid, invalid, catch_all, unknown. It answers one question: does this mailbox exist?
reasonstringMachine-readable cause behind the result, from the closed list on the result codes page. New values can be added, so branch on result.
domainstringThe domain part of the address, as we checked it.
mx_foundbooleanWhether the domain has a mail server at all. False means we stopped before opening a connection.
mx_hoststring or nullThe mail host we spoke to.
smtp_codestring or nullThe literal response the receiving server gave to RCPT TO, so you can audit our reading of it. Null if the handshake never got that far.
rolebooleanA flag, not a result. True when the local part is a function such as info@ or sales@.
disposablebooleanA flag, not a result. True when the domain is on the throwaway-provider list.
free_providerbooleanA flag, not a result. True for consumer mailbox providers such as gmail.com or yahoo.com.
did_you_meanstring or nullA typo suggestion when the domain is one edit away from a common provider. Never applied automatically.
billedbooleanTrue for valid, invalid and catch_all. False for unknown and for a duplicate you already checked in the same batch.
checked_atstringWhen the check ran, ISO 8601 in UTC.

A checkout field, handled verdict by verdict

Say you run a store and check the email field when the shopper tabs out of it. Your server calls POST /verify and branches on result. A valid verdict needs no action. An invalid one shows a short inline message asking the shopper to look again.

Suppose the shopper typed ada@gmial.com and did_you_mean comes back as ada@gmail.com. Show that as a one-click offer. Don't swap it silently, because the person might own the odd-looking address.

Accept the order on catch_all, and when the role flag is true. They're caveats for your marketing sends, and neither is a reason to refuse a sale. Accept it on unknown and on a timeout too, since the order confirmation will tell you soon enough whether the mailbox works. Then store result, smtp_code and checked_at next to the customer record. Anyone reading it in six months can see how old the verdict is.

Mistakes that show up in the first week

The most expensive one is treating unknown as invalid. Gmail and Microsoft 365 often refuse probes, so that rule turns away people on the two largest mailbox providers. Write the unknown branch on day one and make it accept.

A close second is branching on reason alone. The list of reasons is fixed today, and new values can be added without a version bump. Key your logic on result, and keep reason for your logs and for the finer rules that can live with a default branch.

Watch your timeout as well. Ten seconds is right for a background job and far too long for a person staring at a form. A signup flow wants a shorter budget of its own, and a pass when it runs out.

Last, think about repeats. The duplicate rule covers the same address appearing twice inside one batch. It says nothing about calling /verify for the same address every morning. Cache the verdict with its checked_at and pick your own refresh age. Read the billed field on each response if you'd like to see what a call cost.

Questions developers ask

Does checking an address send it an email?

No. We open the connection, get an answer to RCPT TO, and hang up before DATA. There is no message to deliver.

Why did a real address come back unknown?

The receiving server would not answer cleanly. Greylisting, throttling and outright blocks on probe traffic all produce that. We report it rather than guessing, and we do not bill it.

How long does one check take?

Usually under a second when the MX responds normally. Greylisted and rate-limited hosts take longer, which is why the client timeout matters more than the average.

Why is there no GET with the address in the URL?

Because a query string gets written down. It lands in access logs, proxy logs and browser history, and an email address is personal data. A POST body stays out of all three.

Can I verify at signup without slowing the form?

Call it after the field blurs rather than on submit, and treat a timeout as a pass. Blocking a signup on a verifier that is having a slow second costs more than it saves.

Try it against a sandbox key

Scripted verdicts, no SMTP connections, no credits. Live keys come with 100 free checks a month and no card.