# Webhooks from Spaw: bulk completion and settled verdicts

The two HTTPS callbacks Spaw sends, how to verify the X-Spaw-Signature HMAC in Node or PHP, how retries work, and how to receive them in any automation tool.

Updated: 2026-09-03

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

1. Create the run with `POST https://spaw.co/api/v1/email/bulk`, passing `emails` (up to 100,000) and a `webhook_url` that speaks HTTPS.
2. Store the `webhook_secret` from the response. It is returned at creation, and again only if you replay the same request with the same `Idempotency-Key` header; otherwise losing it means creating a new run to get a new one.
3. When the job reaches `completed`, `failed` or `cancelled`, Spaw POSTs to your URL:

```json
{
  "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"
  }
}
```

4. 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_status` field then reads `delivered` or `failed`.
5. Fetch the CSV from the [results endpoint](/docs/api/download-bulk-results), optionally filtered with `?variant=deliverable`, `risky` or `undeliverable`. 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:

```json
{
  "event": "email.settled",
  "request_id": "req_01m1kgdm4xngzmbmff68g94w0c",
  "settled": true,
  "attempt": 1,
  "data": { "email": "mia@acme.com", "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.

```js
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));
}
```

```php
$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](/integrations/n8n) 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.

Reference: https://spaw.co/integrations/webhooks
