POST /v1/jobs takes an array of up to 50,000 addresses and returns at once with a job. The job is a record that moves through states while verification runs in the background, paced against each receiving domain so our probe traffic does not get blocked, which would make your results worse.
The job reports two counts. submittedEmails is what you sent. totalEmails is what is left after we clean the list: duplicates and rows that are not address-shaped are dropped, and that smaller number is the base your credits are reserved against. Your balance should track distinct usable addresses and never the row count of the file.
Results we already hold for an address can be reused. Set forceFresh to true to skip that cache and verify every address live. Then poll the job, read the per-address rows, or download the whole thing as CSV. Those calls are on the next page.
curl -X POST https://app.zapbounce.com/api/v1/jobs \
-H "X-API-Key: $ZB_API_KEY" \
-H "Content-Type: application/json" \
-H "Idempotency-Key: $(uuidgen)" \
-d '{"emails":["ada@example.com","grace@example.org"],"source":"api"}'{
"id": "0b6f2d3e-6a52-4c58-9d0e-3f6a1c9a7e10",
"source": "api",
"submittedEmails": 2,
"totalEmails": 2,
"validEmails": 0,
"invalidEmails": 0,
"catchAllEmails": 0,
"otherEmails": 0,
"spamEmails": 0,
"status": "pending",
"error": null,
"startedAt": null,
"completedAt": null,
"createdAt": "2026-10-26T10:04:00.000Z"
}Fields
| Field | Type | What it carries |
|---|---|---|
| emails | array | Required. One to 50,000 strings, each up to 320 characters. |
| source | string | Optional: paste, csv or api. Defaults to paste, so set api for programmatic calls. |
| forceFresh | boolean | Optional, default false. True skips reused results and verifies every address live. |
| totalEmails | integer | Addresses after cleaning. This is the number credits are reserved against. |
| otherEmails | integer | The job's count of unknown results. Valid, invalid, catch-all and other add up to totalEmails once the job finishes. |
| spamEmails | integer | Addresses carrying a spam-related flag. A signal count that overlaps the four results and is not added to them. |
An 8,400-row webinar export, start to finish
Say you export 8,400 registrants from a webinar tool. That's far under the 50,000 cap, so one job is enough. You read the file in your own code and send the addresses as the emails array with source set to csv, plus an Idempotency-Key built from your export job's id.
Back comes a job with submittedEmails: 8400 and totalEmails: 8050. So 350 rows were dropped as repeats, mostly people who registered twice. Before you submit, compare your balance from GET /v1/credits/balance with the row count. The row count is the most the job can cost, and a balance below it is rejected with CREDITS_INSUFFICIENT.
When the job completes, give every verdict a home before you open your email tool. Valid goes to the main send. Invalid goes to suppression. Catch-all gets its own segment, and unknown waits in a retry pile. Then apply the flags inside each pile: a role address follows whatever rule you've set, and a disposable one is dropped.
Splitting, deduping and resubmitting: what helps
Don't split one list into many small jobs to make it faster. Pacing happens per receiving domain, so ten jobs that all contain the same slow mail host wait on that host ten times over. You also spend request budget for nothing.
Skip the manual dedupe step if its only purpose is saving money, because repeats are dropped when the list is cleaned and counted once anyway. Trimming spaces and lowercasing is still worth doing. Then Ada@Example.com and ada@example.com arrive as one address, and your own row counts match the totalEmails figure we return.
Resubmitting the unknown pile a day later is cheap, since unknowns are refunded and only a definitive verdict keeps its credit. Greylisting and throttling are temporary, so some of those addresses will resolve. Addresses on hosts that accept everything by policy won't change, however many times you ask.
Questions developers ask
How large can one job be?
50,000 addresses in the JSON body. For a larger list, split it and submit several jobs.
Am I billed for duplicates?
No. Duplicates are dropped when the list is cleaned, and the distinct address is counted once in totalEmails.
Can I cancel a running job?
Yes. POST /v1/jobs/{id}/cancel stops it, and what had finished stays available.
How do I list my jobs?
GET /v1/jobs returns them newest first with page and pageSize parameters. Add archived=true for archived jobs, or q to search by id, status, source or an address inside the job.