The verification API
One POST for a handful of addresses. Another POST for a list, which you poll and download. Everything else is billing and pagination.
curl -s -X POST "https://app.zapbounce.com/api/v1/verify" \
-H "X-API-Key: $ZB_API_KEY" \
-H "Content-Type: application/json" \
-d '{"emails":["ada@example.com"]}'
{
"node": "verify-1",
"results": [
{
"id": "5821",
"email": "ada@example.com",
"status": "catch_all",
"reasonCode": "CATCH_ALL",
"provider": "other",
"flags": [],
"smtpCode": 250,
"checkedAt": "2026-10-26T10:02:41.000Z"
}
]
}Verification is one POST to /v1/verify, with up to 20 addresses in a JSON body. The body keeps your contacts' addresses out of access logs, proxy logs and browser history, which is where a URL query string gets written down.
What comes back
Branch on status. The reason code underneath it is a closed list that can grow, so treat one you don't recognize as extra detail.
| Field | What it holds |
|---|---|
| node, results | The node that did the work, and one object per address you sent. |
| The address as you sent it. | |
| status | One of valid, invalid, catch_all, unknown. It answers one question: does this mailbox exist? |
| reasonCode | The narrower cause, from a closed list: MAILBOX_NOT_FOUND, CATCH_ALL, TEMPORARY_FAILURE and so on. |
| provider | The mailbox provider we detected from the domain's mail servers. |
| flags | An array of role, disposable and free_provider. They are flags, never statuses, and they are set even when the status is unknown. |
| smtpCode | The literal reply code to RCPT TO, so you can audit our reading. null if the handshake never got that far. |
| checkedAt | When we ran the check, ISO 8601 in UTC. |
A list, and a job you poll
curl -s -X POST "https://app.zapbounce.com/api/v1/jobs" \
-H "X-API-Key: $ZB_API_KEY" \
-H "Content-Type: application/json" \
-d '{"emails":["ada@example.com","grace@example.org"],"source":"api"}'
{ "id": "0b6f2d3e-6a52-4c58-9d0e-3f6a1c9a7e10", "status": "pending", "submittedEmails": 2, "totalEmails": 2 }
# Poll until status is completed, then download
curl -s "https://app.zapbounce.com/api/v1/jobs/0b6f2d3e-6a52-4c58-9d0e-3f6a1c9a7e10/export" \
-H "X-API-Key: $ZB_API_KEY" -o results.csvRate limits and job sizes
| Surface | Default | What to do about it |
|---|---|---|
| All requests | 120 requests per 60-second window | Read X-RateLimit-Limit and X-RateLimit-Window-Seconds. A 429 has no Retry-After, so wait out the window. |
| Synchronous check | 1 to 20 addresses a call | More than that is rejected with VERIFY_TOO_MANY. Use a job. |
| Job creation | Up to 50,000 addresses each | Send one large job instead of many small ones. |
| Result pages | pageSize 1 to 100, default 20 | Loop on page up to meta.totalPages, or download the CSV. |
| Idempotency window | 24 hours | Reuse one Idempotency-Key per unit of work, so a lost reply can't create a second job. |
Two errors need different handling from the rest. A 429 means wait out the window and carry on, with jitter if you run more than one worker. A 422 CREDITS_INSUFFICIENT means the balance is too low, and retrying won't help until credits are added. Neither costs a credit. No error does, because an error means the request never produced a verdict.
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.
Read next
Authentication
API keys, scopes, rotation, and why the key belongs on your server rather than in the browser.
Result codes
Every verdict and reason, and what each one means for what you do next.
Rate limits
The limit headers, and what a 429 tells you to do.
Job status and results
Poll a job, read per-address rows and download the CSV.
SDKs
Python, Node, PHP, Ruby, Go, C# and more, with real code per language.
At a signup form
Which verdicts should block a signup, and which should never.
Before you build against this
Questions
Am I charged for an unknown result?
No. Unknown means the receiving server would not give a straight answer, and billing for it would make the honest verdict the expensive one. Its credit is refunded.
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 and failed requests cost nothing.
How do I know a job finished?
Poll GET /v1/jobs/{id} until status is completed, partial, failed or cancelled. There is no webhook for customers today.
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.