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.

JSON
// 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.

JavaScript
// 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.

JavaScript
// 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.