Skip to content

API reference/address

Send a finished run's completion webhook again

POST/api/v1/address/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.

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_address_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/address/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 description
jobIdrequiredpathintegerThe id returned at creation.

Example request

curl -X POST https://spaw.co/api/v1/address/bulk/918/webhook/redeliver \
  -H "Authorization: Bearer sk_live_…"
const response = await fetch('https://spaw.co/api/v1/address/bulk/918/webhook/redeliver', {
  method: 'POST',
  headers: {
    'Authorization': 'Bearer sk_live_…',
  },
});
const result = await response.json();
import requests

response = requests.post(
    'https://spaw.co/api/v1/address/bulk/918/webhook/redeliver',
    headers={'Authorization': 'Bearer sk_live_…'},
)
result = response.json()
$ch = curl_init('https://spaw.co/api/v1/address/bulk/918/webhook/redeliver');
curl_setopt_array($ch, [
    CURLOPT_RETURNTRANSFER => true,
    CURLOPT_CUSTOMREQUEST => 'POST',
    CURLOPT_HTTPHEADER => ['Authorization: Bearer sk_live_…'],
]);
$result = json_decode(curl_exec($ch), true);
from spaw import Client

client = Client('sk_live_…')
result = client.redeliver_address_bulk_webhook(918)
import Spaw from 'spaw';

const spaw = new Spaw({ apiKey: 'sk_live_…' });
const result = await spaw.redeliverAddressBulkWebhook(918);
use Spaw\Client;

$spaw = new Client('sk_live_…');
$result = $spaw->redeliverAddressBulkWebhook(918);

This endpoint has no console on its page. It queues a real signed POST at the webhook URL on one of your runs, so a click here would send traffic at a server of yours from a page you are reading. The Bulk runs tab has the button beside the run it belongs to, and the run's `webhook_status` reports what came back.

Responses

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

{
    "success": true,
    "data": {
        "job": {
            "id": 918,
            "status": "completed",
            "total": 20000,
            "duplicate_count": 12,
            "processed": 20000,
            "valid": 18200,
            "invalid": 1800,
            "low": 16400,
            "medium": 3200,
            "high": 400,
            "credits_used": 18200,
            "stopped_reason": null,
            "cancel_requested": false,
            "country": null,
            "deliverability": false,
            "webhook_status": "pending",
            "webhook_detail": null,
            "created_at": "2026-09-05T10:12:44+00:00",
            "finished_at": "2026-09-05T11:02:19+00:00"
        }
    }
}

401The key is missing, malformed, or revoked.

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

404No such record on this account.

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

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

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

429More 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.

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

Error codes

Failures answer { success: false, error: { code, message, request_id } }. Each code has its own page.

markdown version·openapi.json