GET /batches/{id} answers two questions: how far along is this, and what did it find. While a batch runs you get a processed count against the total. Once it finishes you get the full breakdown by verdict.
The summary is the product's argument in one object. Alongside valid and invalid you get the catch-all count and the unknown count, and neither is folded into anything else. A verifier that quietly maps unknowns into valid would show you a prettier summary and the same bounce rate six days later.
Poll every ten seconds if you must, but a webhook costs you nothing and removes the loop entirely.
curl https://api.zapbounce.com/v1/batches/bat_8f2c1d \
-H "Authorization: Bearer zb_live_..."{
"batch_id": "bat_8f2c1d",
"name": "q4-webinar-list",
"status": "complete",
"submitted": 12000,
"unique": 11480,
"processed": 11480,
"summary": {
"valid": 7380,
"invalid": 1430,
"catch_all": 2410,
"unknown": 260,
"duplicates": 520,
"role": 310,
"disposable": 90,
"billed": 11220
},
"coverage": 0.977,
"results_url": "https://api.zapbounce.com/v1/batches/bat_8f2c1d/results",
"completed_at": "2026-09-18T10:19:12Z"
}Fields
| Field | Type | What it carries |
|---|---|---|
| status | string | queued, running, complete, canceled or failed. |
| summary.valid, invalid, catch_all, unknown | integer | One count per result. The four add up to unique, because every distinct address gets exactly one result. |
| summary.duplicates | integer | Rows collapsed before checking. Equals submitted minus unique, and none of them are billed. |
| summary.role, summary.disposable | integer | How many addresses carry each flag. These overlap the four results and are not added to them. |
| summary.billed | integer | Credits actually spent. Equals unique minus unknown: valid, invalid and catch_all cost one each, and unknowns are free. |
| coverage | float | Share of unique addresses that got a definitive result. Unknowns are what pull this below 1. |
| results_url | string | Signed, paginated download of the per-address rows. Expires; re-read the status call for a fresh one. |
Reference
| status | What it means | What to do |
|---|---|---|
| queued | Accepted, not yet started. | Wait. Nothing is billed yet. |
| running | Probes in flight. processed is climbing. | Poll, or wait for the webhook. |
| complete | Every unique address has a verdict. | Download from results_url. |
| canceled | You stopped it. Partial results are kept. | Download what finished. |
| failed | We could not finish and have refunded the batch. | Check error and resubmit. |
Reading a 20,000-address summary before you send
Say a finished batch reports unique: 20000 with this summary: 12,500 valid, 2,300 invalid, 4,600 catch-all and 600 unknown. Those four add up to 20,000. The same summary flags 500 of those addresses as role and 100 as disposable, and those two counts overlap the four, so don't add them in. billed reads 19,400, which is unique minus the 600 unknowns, and coverage reads 0.97.
Start with invalid. At 2,300 of 20,000 it's 11.5%, so mailing this list unchecked would have put you well past the point where mailbox providers start throttling. Next, look at catch-all. At 23% of the list, it sets the size of the separate segment you'll want to send and measure alone.
Coverage of 0.97 counts catch-all as a verdict, because the domain did give a clear answer. The share of mailboxes you can prove is a different number: 12,500 of 20,000, or 62.5%. Carry both into your send plan.
A polling loop that knows when to stop
If you can't receive webhooks, your loop needs four exits and most first drafts have one. Stop on complete, but also stop on cancelled and on failed. A failed batch has been refunded, so read error, fix what it names and resubmit. Add a deadline of your own as the fourth exit, and have it page a person when it fires.
Mind the budget while you poll. The limit on this call is 60 a minute per key, so seven batches polled every ten seconds will start drawing 429s. Poll a shared list of open batches from one place and you won't have every worker running a loop of its own.
Watch processed between polls. A count that climbs slowly near the end is normal, because the slowest mail hosts finish last. When it sits completely still for a long stretch, check the status page before you cancel anything.
Don't save results_url in your database. It's signed and short-lived, so store the batch id and ask for a fresh link when you need one.
Questions developers ask
Why is coverage not 1.0?
Because some servers refused to answer. Those addresses come back unknown, they lower coverage, and you are not billed for them.
How long does results_url stay valid?
It is a signed URL with a short life. Call the status endpoint again to mint a new one whenever you need it.
Does a canceled batch refund anything?
You pay for the addresses that were already resolved when the cancel landed. Nothing else.