Spaw calls you back in two situations: when a queued bulk run finishes, and when a single lookup that first answered unverified settles later. Both are plain HTTPS POSTs with a JSON body and one signature header, so they work with a few lines of code on your server or with the incoming-webhook trigger of any automation tool.
The bulk completion webhook
- Create the run with
POST https://spaw.co/api/v1/email/bulk, passingemails(up to 100,000) and awebhook_urlthat speaks HTTPS. - Store the
webhook_secretfrom the response. It is returned at creation, and again only if you replay the same request with the sameIdempotency-Keyheader; otherwise losing it means creating a new run to get a new one. - When the job reaches
completed,failedorcancelled, Spaw POSTs to your URL:
{
"event": "bulk_email_job.finished",
"job": {
"id": 512,
"status": "completed",
"total": 2,
"processed": 2,
"deliverable": 1,
"risky": 0,
"undeliverable": 1,
"credits_used": 1,
"stopped_reason": null,
"finished_at": "2026-09-03T10:14:02+00:00"
}
}
- Answer with any 2xx status quickly and do the real work afterwards. A connection error or a 5xx answer is retried three times with a short backoff; the job's
webhook_statusfield then readsdeliveredorfailed. - Fetch the CSV from the results endpoint, optionally filtered with
?variant=deliverable,riskyorundeliverable. Results are kept for 30 days.
The settled-verdict callback
Some mail servers will not say whether a mailbox exists on the first try (greylisting, timeouts). Pass callback_url and a callback_secret of 16 to 128 characters on POST /api/v1/email, and an unverified answer is re-checked after 5 and 20 minutes. The response to your original request then carries meta.settling: true, and the callback body is:
{
"event": "email.settled",
"request_id": "req_01m1kgdm4xngzmbmff68g94w0c",
"settled": true,
"attempt": 1,
"data": { "email": "[email protected]", "deliverable": "deliverable", "smtp_checked": true },
"meta": { "credits_used": 0, "cache_hit": true }
}
data is the full 27-field result. settled is false when the last re-check still could not say. Re-checks are repeats of a lookup you already paid for, so they cost nothing.
Verifying the signature
Both callbacks carry X-Spaw-Signature: the lowercase hex HMAC-SHA256 of the exact raw request body, keyed with the secret you were given. Compute it over the bytes as received, before any JSON parsing, and compare in constant time.
import { createHmac, timingSafeEqual } from 'node:crypto';
export function isFromSpaw(rawBody, header, secret) {
const expected = createHmac('sha256', secret).update(rawBody).digest('hex');
return header.length === expected.length && timingSafeEqual(Buffer.from(header), Buffer.from(expected));
}
$expected = hash_hmac('sha256', $request->getContent(), $secret);
abort_unless(hash_equals($expected, (string) $request->header('X-Spaw-Signature')), 401);
Reject anything that fails the check and log the event and job.id or request_id of what you accept, so a replayed delivery is easy to spot.
Receiving webhooks in an automation tool
Zapier's catch-hook trigger, Make's custom webhook and n8n's Webhook node all accept these POSTs and expose the body's fields to the following steps. Most low-code tools cannot compute an HMAC on the raw body, so keep the receiving URL private and check event plus the job id against the run you created. For a typical flow, the webhook trigger is followed by an HTTP step that downloads the results CSV and a step that writes rows into a sheet or CRM; the n8n page shows the HTTP step.
What to do with the verdict
The completion payload is a summary. Decisions belong to the per-row verdicts in the CSV: deliverable rows go to your mailing tool, risky rows are worth a look at their reason, and undeliverable rows should be removed, or corrected when a did_you_mean column is filled. Undeliverable rows are also added to your account's suppression list automatically, so future batch and bulk runs answer them for free.
Cost
Webhooks and callbacks themselves are free. The bulk run bills each row like a single lookup as it is processed: 1 credit for a fresh deliverable or risky verdict, nothing for undeliverable rows, duplicates, seven-day repeats or suppressed addresses.