Email verification in Postman
Postman is where most people meet an API before they write a line against it. Two environments, one collection-level auth setting, and you can exercise every endpoint without touching your codebase.
The setup worth copying is the pair of environments. A second environment holding a separate key keeps experiments apart from production traffic, and a key scoped to jobs.view and credits.view is safe to leave in a scheduled monitor.
Collection-level API Key auth (header X-API-Key) reading {{api_key}}, with environments swapping the key between environments.
Verify one address
You branch on four statuses: valid, invalid, catch_all and unknown. Role and disposable arrive in the flags array beside the status, so test them separately. Unknown is the branch most code forgets, and its credit is refunded.
// Environment: "ZapBounce — staging"
{
"base_url": "https://app.zapbounce.com/api/v1",
"api_key": "zb_live_...", // set type: secret, not default
"sample_email": "ada@example.com"
}
// Environment: "ZapBounce — production"
{
"base_url": "https://app.zapbounce.com/api/v1",
"api_key": "zb_live_...",
"sample_email": "ada@example.com"
}
// Collection → Authorization → API Key → Key: X-API-Key, Value: {{api_key}}, Add to: Header
// Every request inherits it. Nothing hardcodes a key into a saved request,
// which is what leaks when someone exports the collection to a colleague.
// POST {{base_url}}/verify
{
"emails": ["{{sample_email}}"]
}Verify a list
Submit a job rather than looping the single endpoint. We pace probes per receiving mail host, which protects the sending reputation your results depend on.
// Request 1 — POST {{base_url}}/jobs body: { "emails": [...], "source": "api" }
// Tests tab: capture the job id for the next request in the chain.
const body = pm.response.json();
pm.test("job accepted", () => {
pm.expect(pm.response.code).to.be.oneOf([200, 201, 202]);
pm.expect(body.id).to.be.a("string");
});
pm.test("submitted addresses are counted", () => {
pm.expect(body.totalEmails).to.be.at.most(body.submittedEmails);
});
pm.collectionVariables.set("job_id", body.id);
// Request 2 — GET {{base_url}}/jobs/{{job_id}}
// Pre-request: pause between polls without a fixed sleep in the runner.
setTimeout(() => {}, 5000);
// Tests: loop back to this request until the job reaches a terminal status.
const job = pm.response.json();
if (!["completed", "partial", "failed", "cancelled"].includes(job.status)) {
pm.execution.setNextRequest("Get job status"); // poll again
} else {
pm.test("counts reported next to the verdicts", () => {
pm.expect(job).to.have.property("validEmails");
pm.expect(job).to.have.property("otherEmails"); // unknown
});
}
// Request 3 — GET {{base_url}}/jobs/{{job_id}}/emails?page=1&pageSize=100
// (or GET {{base_url}}/jobs/{{job_id}}/export for the CSV)Errors and retries
A 429 and a 5xx are worth retrying. A 422 CREDITS_INSUFFICIENT and a 400 for a malformed address will fail the same way on every attempt, so stop.
// Tests tab on POST {{base_url}}/verify.
const r = pm.response.json().results?.[0];
pm.test("responds in under 10 seconds", () => {
pm.expect(pm.response.responseTime).to.be.below(10000);
});
pm.test("status is one of the four", () => {
pm.expect(r.status).to.be.oneOf(["valid", "invalid", "catch_all", "unknown"]);
});
pm.test("role and disposable are entries in flags, not statuses", () => {
pm.expect(r.flags).to.be.an("array");
pm.expect(r.status).to.not.be.oneOf(["role", "disposable"]);
});
pm.test("every result carries a reason code", () => {
pm.expect(r.reasonCode).to.be.a("string");
});
// Error path: point the collection at an empty key and run this.
pm.test("errors carry a request id", () => {
if (pm.response.code >= 400) {
const err = pm.response.json(); // { statusCode, code, message, requestId }
pm.expect(err).to.have.property("code");
pm.expect(err).to.have.property("requestId"); // quote it in support
}
});
// 429 handling in the runner: there is no Retry-After header, so wait on the window length.
if (pm.response.code === 429) {
const wait = Number(pm.response.headers.get("X-RateLimit-Window-Seconds") ?? 60) * 1000;
setTimeout(() => {}, wait);
pm.execution.setNextRequest(pm.info.requestName);
}Postman: common questions
Is there a published collection?
The requests on this page are the whole surface: verify, jobs, job status, job emails, export, credits. Build them once and the collection is done.
Can I run it as a monitor?
Yes, and keep it to read-only requests such as job status and the credit balance. A monitor that posts verifications spends credits on a schedule forever.
How do I test the unknown branch?
Save an example response with a status of unknown on a Postman mock server and point the collection at it. A real address rarely returns one on demand.
Run this Postman code today
100 free checks a month, no card, credits that never expire, and unknown results refunded.