Clean an email list in bulk
What you'll build: a submit-and-collect pipeline that verifies a list of addresses as one bulk job, then pulls the graded results — no polling loop of your own to maintain.
| Section | Server-side workflows |
|---|---|
| Actions used | POST /v1/data/workflows/clean-list:estimatePOST /v1/bulk/jobsGET /v1/bulk/jobs/{job_id}/result |
| Credits | 1 per address checked (worst case) |
| Test-key safe | No — needs a live key |
| Time to complete | ~15 min |
| Prerequisites | A qk_live_ key with a balance, and a list of addresses as an array or CSV text. |
Submit the list as one job
Post the action and your addresses to the bulk endpoint as either a queries array or csv_text. You get a job id back immediately rather than holding a connection open for the whole list.
# Exactly one input source: queries[] or csv_text.
curl -X POST https://api.qannasapi.com/v1/bulk/jobs \
-H "Authorization: Bearer $QANNAS_API_KEY" \
-H "Content-Type: application/json" \
-d '{
"action": "verify_email",
"queries": ["jane@acme.example", "sam@acme.example"],
"webhook_url": "https://yourapp.example/hooks/qannas"
}'
# < 200 { "status": "OK", "data": { /* job id */ } }Collect the results
Fetch the job to see its state, then fetch the result once it has finished. Supply a webhook_url on submission and we will call you instead, which is the better shape for anything above a few hundred addresses.
# Poll the job …
curl https://api.qannasapi.com/v1/bulk/jobs/$JOB_ID \
-H "Authorization: Bearer $QANNAS_API_KEY"
# … then pull the graded results once it has finished.
curl https://api.qannasapi.com/v1/bulk/jobs/$JOB_ID/result \
-H "Authorization: Bearer $QANNAS_API_KEY"
# Supplying webhook_url on submission skips this loop entirely.Read the grades, not just the verdicts
Verification returns a grade per address rather than a bare yes or no. Decide your own threshold: mailing an address graded risky is a deliverability choice, not an API one.
Re-verify on a schedule
Address quality decays as people change jobs. Re-running a list quarterly and diffing the grades gives you a decay rate for your own data, which is usually more useful than any single verification pass.
# Re-run quarterly and diff the grades. Your own decay rate is
# more useful than any single verification pass.
previous = load_grades("2026-Q2")
current = load_grades("2026-Q3")
decayed = [e for e in previous
if ok(previous[e]) and not ok(current.get(e))]
print(f"{len(decayed) / len(previous):.1%} decayed this quarter")Summary
| Pattern | How |
|---|---|
| Submit | POST /v1/bulk/jobs with action and queries or csv_text |
| Poll | GET /v1/bulk/jobs/{job_id} |
| Collect | GET /v1/bulk/jobs/{job_id}/result |
| Skip polling | Pass webhook_url on submission |
| Cost shape | Per address resolved, not per address submitted |
Production checklist
- Use a webhook for anything above a few hundred addresses.
- Store the grade, not a boolean — you will want to re-threshold later.
- Quote the run before submitting a large list.
- Re-verify quarterly and track your own decay rate.
Next steps
Run the estimate first. It costs nothing and it is the same arithmetic the meter will apply.