# Send a finished run's completion webhook again

`POST /api/v1/ip/bulk/{jobId}/webhook/redeliver`

- Authentication: Secret API key as a bearer token
- Billing: Free. A redelivery spends no credits and bills nothing. It is capped at 10 a minute counted per signed-in user — neither per key nor per account, so two keys held by one person share the one budget while two teammates have one each, and the dashboard button and the other products' redeliveries all spend the caller's own.
- Group: IP

POSTs a finished run's completion webhook to its `webhook_url` a second time. This is for the case the webhook exists to cover: the run finished, the delivery went out, your endpoint was down for the few minutes it was tried, and the message is gone. `webhook_status` and `webhook_detail` tell you that happened; this is how you ask for it again, instead of falling back to polling — which is the thing the webhook was set up to avoid.

**The body is the same body.** It is rebuilt from the run's own row rather than replayed from a stored blob, and every field it carries — `status`, `processed`, the row counts, `credits_used`, `stopped_reason`, `finished_at` — is frozen once a run has settled. The bytes are the ones the first delivery carried, so reconciling a redelivery against the original compares equals. (A run that is resumed later settles again and posts a new completion webhook of its own; a redelivery always repeats the most recent one.)

**The signature is new, and that is correct.** Each delivery is signed as it is sent, so `X-Spaw-Signature-V2` carries a later `t` and a different `v2` digest from the first attempt. That is required rather than incidental: a receiver refuses anything whose `t` is more than five minutes from its own clock, so a signature copied from the original would be refused on arrival. Verify a redelivery exactly as you verify any other delivery. The untimestamped `X-Spaw-Signature` is the HMAC of the body alone, so it *is* identical to the first attempt's, until it stops being sent on 1 March 2027.

**Nothing deduplicates this for you.** Each call is one real POST, so a receiver that gets both the original and the redelivery sees the event twice. Both bodies carry `job.id`: treat a `bulk_ip_job.finished` for a job id you have already handled as a repeat and drop it.

**The delivery is queued, and the answer is `202`.** One attempt is a 10-second timeout retried twice, about half a minute against a receiver that is down — which is the receiver you are asking about. So this call does not wait for it: it answers `202` at once with the run, and `webhook_status` on it reads `pending`, a delivery asked for and not yet answered. Poll the run (`GET /api/v1/ip/bulk/{jobId}`) until `webhook_status` is `delivered` or `failed`; `webhook_detail` then carries the HTTP status your endpoint gave, or the transport failure when it gave none. A `pending` run has one delivery in flight and asking again queues a second — wait for the first before you decide it failed.

A run created without a `webhook_url` answers `409 WEBHOOK_NOT_CONFIGURED`, which is checked FIRST because waiting will never fix it; one still queued or running answers `409 JOB_NOT_FINISHED`, because there is no completion to announce yet, and so does a run that stops being finished between the check and the queueing (a resume in another window). A run whose last delivery *succeeded* is not refused — a receiver can lose a message it has already acknowledged, and only you know whether it did.

A URL that resolves onto a private address is refused at delivery here exactly as on the first attempt, every time, because DNS can be repointed after a URL is saved.

## Parameters

| Name | In | Type | Required | Description |
| --- | --- | --- | --- | --- |
| `jobId` | path | integer | yes | The id returned at creation. |

## Example request

```bash
curl -X POST https://spaw.co/api/v1/ip/bulk/512/webhook/redeliver \
  -H "Authorization: Bearer sk_live_…"
```

## Responses

### 202 — The delivery is queued; `webhook_status` reads `pending` until a worker has an answer.

```json
{
    "success": true,
    "data": {
        "job": {
            "id": 512,
            "status": "completed",
            "total": 20000,
            "duplicate_count": 12,
            "processed": 20000,
            "low": 18100,
            "medium": 1600,
            "high": 300,
            "credits_used": 19600,
            "stopped_reason": null,
            "cancel_requested": false,
            "webhook_status": "pending",
            "webhook_detail": null,
            "created_at": "2026-09-04T10:12:44+00:00",
            "finished_at": "2026-09-04T10:31: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"
    }
}
```

### 404 — No such record on this account.

```json
{
    "success": false,
    "error": {
        "code": "NOT_FOUND",
        "message": "No record with that id on this account.",
        "request_id": "req_01m1kgdm4xngzmbmff68g94w0c"
    }
}
```

### 409 — The run has no webhook URL (`WEBHOOK_NOT_CONFIGURED`), or it has not finished yet (`JOB_NOT_FINISHED`).

```json
{
    "success": false,
    "error": {
        "code": "WEBHOOK_NOT_CONFIGURED",
        "message": "This run was created without a webhook URL, so there is no completion webhook to send.",
        "request_id": "req_01m1kgdm4xngzmbmff68g94w0c"
    }
}
```

### 429 — More than ten redeliveries in a minute from this user, across every product and the dashboard button alike. The shared limit named on other endpoints is not the one that fires here.

```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
- `NOT_FOUND` — https://spaw.co/docs/errors/NOT_FOUND
- `WEBHOOK_NOT_CONFIGURED` — https://spaw.co/docs/errors/WEBHOOK_NOT_CONFIGURED
- `JOB_NOT_FINISHED` — https://spaw.co/docs/errors/JOB_NOT_FINISHED
- `RATE_LIMITED` — https://spaw.co/docs/errors/RATE_LIMITED

---

Canonical page: https://spaw.co/docs/api/redeliver-ip-bulk-webhook · OpenAPI document: https://spaw.co/openapi.json · All endpoints: https://spaw.co/docs/api
