The verification API

One POST for a single address. Another POST and a webhook for a list. Everything else is billing and pagination.

Shell
curl -s -X POST "https://api.zapbounce.com/v1/verify" \
  -H "Authorization: Bearer $ZAPBOUNCE_KEY" \
  -H "Content-Type: application/json" \
  -d '{"email":"ada@example.com"}'

{
  "email": "ada@example.com",
  "result": "catch_all",
  "reason": "accept_all_domain",
  "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"
}

Verification is POST only, and the address travels in a JSON body. We don't offer a GET with the address in the URL, because a query string ends up in access logs, proxy logs and browser history, and those are places your contacts' addresses shouldn't sit.

What comes back

Branch on result. The reason underneath it is a fixed list that can grow, so treat one you don't recognize as extra detail.

FieldWhat it holds
emailThe address as you sent it, normalized.
resultOne of valid, invalid, catch_all, unknown. It answers one question: does this mailbox exist?
reasonThe narrower cause, from a closed list: mailbox_not_found, accept_all_domain, greylisted and so on.
domain, mx_found, mx_hostThe domain we checked, whether it has a mail server, and the host we spoke to.
smtp_codeThe literal reply to RCPT TO, so you can audit our reading. null if the handshake never got that far.
role, disposable, free_providerBooleans about the address itself. They are flags, never results, and they are set even when the result is unknown.
did_you_meanA typo suggestion or null. We never apply it for you.
billedtrue for valid, invalid and catch_all. false for unknown and for a duplicate inside a batch.
checked_atWhen we ran the check, ISO 8601 in UTC.

A list, and a webhook when it finishes

Shell
curl -s -X POST "https://api.zapbounce.com/v1/batches" \
  -H "Authorization: Bearer $ZAPBOUNCE_KEY" \
  -H "Content-Type: application/json" \
  -d '{"emails":["ada@example.com","grace@example.org"],
       "webhook_url":"https://yourapp.com/hooks/zapbounce"}'

{ "batch_id": "bat_8f2c1d", "status": "queued", "submitted": 2, "unique": 2 }

The limits you'll hit first

SurfaceDefaultWhat to do about it
Single checks100 requests a second per keyRead X-RateLimit-Remaining and slow down before it reaches zero. A 429 carries Retry-After in seconds.
Batch submissions10 a minute, up to 100,000 addresses eachSend one large batch instead of many small ones. Past 100,000 you get a 413, so upload a file.
File uploads5 a minuteCSV or TXT. Gzip anything over about 50 MB.
Results pages1 to 1,000 rows, default 100Loop on has_more and pass next_cursor back. There are no page numbers.
Idempotency window24 hoursReuse one Idempotency-Key per unit of work, so a lost reply can't create a second batch.

Two errors need different handling from the rest. A 429 means wait for Retry-After and carry on, with jitter if you run more than one worker. A 402 means the balance is too low, and retrying won't help until someone tops up. Neither costs a credit. No error does, because an error means the address never reached the verification queue.

At a signup form, budget three seconds for the common case and set your client timeout at ten. A greylisting server can hold the connection that long, and when the timeout fires your code should let the signup through.

Test every branch before you spend a credit

A key that starts zb_test_ runs the same API in sandbox mode. Nothing connects to a mail server, nothing is billed, and the response has the same shape, so your parsing code gets a real workout in CI.

Reserved addresses at sandbox.zapbounce.com force each outcome. unknown@ returns the result most pipelines never handle, because in production it only appears when a mail host refuses to answer. slow@ replies after about nine seconds, which tells you whether your timeout works. ratelimit@ returns a 429 so you can watch your backoff do its job.

Before you build against this

Questions

Am I charged for an unknown result?

No. Unknown means we could not finish the work, and billing for it would make the honest verdict the expensive one.

Is a catch-all result billed?

Yes. One credit buys one definitive result, and valid, invalid and catch_all all count. A catch-all took a full handshake plus a second probe, and it tells you something true about the domain. Unknown results, duplicates inside a batch, errors and sandbox calls cost nothing.

Is there a sandbox?

Yes. Sandbox addresses return each verdict deterministically, so you can test the unknown and catch-all branches without hunting for a real example.

What happens on a timeout?

Fail open in your code. A verification service should never be the reason a signup fails.

Can I re-check an address?

Yes, and it costs a credit, because the answer genuinely can change between checks.

Get a key

100 free checks a month, no card.