# Send a test webhook

`POST /api/v1/webhooks/test`

- Authentication: Secret API key as a bearer token
- Billing: Free. A test delivery costs no credits and is not a lookup. Limited to 10 requests a minute for the key: it is a debugging tool, not a way to generate traffic.
- Group: Webhooks

Posts one sample payload to a URL you name, signed exactly as a real delivery is, and answers with the secret it signed with, both signatures it sent, and the exact bytes all of them were computed over. It is how a signature check gets written and confirmed before a paid run depends on it: until this endpoint existed, the first signed request you ever saw arrived when a bulk job you had queued and paid for happened to finish.

`event` is one of the names this API documents: `bulk_email_job.finished`, `bulk_phone_job.finished`, `bulk_ip_job.finished`, `bulk_address_job.finished`, `bulk_consistency_job.finished`, `email.settled`, `email_monitor.run`, `phone_monitor.run`, `ip_monitor.run`, `address_monitor.run` and `entity_monitor.run`. The body has the shape that event really carries, with fixed sample values and one addition: `test` is `true`. Where an event wraps a whole lookup result — `email.settled` does — the sample carries a handful of that result's fields rather than all of them, because what a signature check needs to see is the envelope and the exact bytes, and a full result copied into this document would be one more thing to drift. That field is there because a receiver that cannot compute an HMAC — most low-code webhook triggers cannot — still has to be able to tell a test delivery from a real one. Nothing else about the shape differs, and nothing in the body comes from your request.

The delivery runs through the same signer every other webhook on the platform runs through, so it behaves the same way: HTTPS only, both signature headers — the timestamped `X-Spaw-Signature-V2` (`t={unix seconds},v2={lowercase hex}`, the HMAC-SHA256 of `v2:{t}:{raw body}`) and the original `X-Spaw-Signature` over the body alone, which is sent until 1 March 2027 — a ten-second timeout, three attempts on a connection error or a 5xx answer, and no redirect followed — a `3xx` answer is reported as the failure it is, because a redirected POST arrives as a GET with no body. A URL whose host resolves onto a private or reserved address is never posted to at all: `delivered` is false and `detail` reads `Refused: private address`.

`status_code` is what your endpoint answered, or `null` when it answered nothing. What it answered in its own body is never read and never returned. The `secret` is minted for this one call and stored nowhere — it is not, and never will be, a real job's or monitor's signing secret, which is shown once at creation and never again.

## Request body

| Field | Type | Required | Description |
| --- | --- | --- | --- |
| `url` | string | yes | The HTTPS URL to post to. Plain http is refused, as it is everywhere a webhook URL is accepted. At most 2,048 characters. |
| `event` | string | yes | Which documented payload to send. One of: bulk_email_job.finished, bulk_phone_job.finished, bulk_ip_job.finished, bulk_address_job.finished, bulk_consistency_job.finished, email.settled, email_monitor.run, phone_monitor.run, ip_monitor.run, address_monitor.run, entity_monitor.run. |

## Example request

```bash
curl -X POST https://spaw.co/api/v1/webhooks/test \
  -H "Authorization: Bearer sk_live_…" \
  -H "Content-Type: application/json" \
  -d '{
  "url": "https://hooks.example.com/spaw",
  "event": "bulk_email_job.finished"
}'
```

## Responses

### 200 — The delivery was attempted, and this is everything about it.

```json
{
    "success": true,
    "data": {
        "delivered": true,
        "status_code": 200,
        "detail": "HTTP 200",
        "secret": "3nQ8Zk2xVb7Lp1sTfY6wR4mJcH0aD5gU9eKqNvXz",
        "signature": "45f6705121de705be218247b0f31a49d895c46d2e9f9d38d150bec2f08b629b9",
        "timestamped_signature": "t=1757502000,v2=6a0e5f0f6bd0ba9a4bd6c9bbd0c0b5b6b1a1d0f04a2b9a02de3b1a3d0c4b6f21",
        "timestamp": 1757502000,
        "body": "{\"test\":true,\"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\"}}"
    }
}
```

### 401 — The key is missing, malformed, or revoked.

```json
{
    "success": false,
    "error": {
        "code": "UNAUTHENTICATED",
        "message": "Provide a valid API key as a bearer token.",
        "request_id": "req_01m1kgdm4xngzmbmff68g94w0c"
    }
}
```

### 422 — The request body could not be validated; `error.errors` lists the fields.

```json
{
    "success": false,
    "error": {
        "code": "VALIDATION_FAILED",
        "message": "The email field is required.",
        "errors": {
            "email": [
                "The email field is required."
            ]
        },
        "request_id": "req_01m1kgdm4xngzmbmff68g94w0c"
    }
}
```

### 429 — Over 5 requests per second for the key. Retry after the limit resets.

```json
{
    "success": false,
    "error": {
        "code": "RATE_LIMITED",
        "message": "Too many requests. Retry after the limit resets.",
        "request_id": "req_01m1kgdm4xngzmbmff68g94w0c"
    }
}
```

## Error codes

- `UNAUTHENTICATED` — https://spaw.co/docs/errors/UNAUTHENTICATED
- `VALIDATION_FAILED` — https://spaw.co/docs/errors/VALIDATION_FAILED
- `RATE_LIMITED` — https://spaw.co/docs/errors/RATE_LIMITED
- `EMAIL_NOT_VERIFIED` — https://spaw.co/docs/errors/EMAIL_NOT_VERIFIED

---

Canonical page: https://spaw.co/docs/api/send-test-webhook · OpenAPI document: https://spaw.co/openapi.json · All endpoints: https://spaw.co/docs/api
