API reference/bulk

Queue a bulk verification run

POST/api/v1/email/bulk

authentication
Secret API key as a bearer token
billing
Each row bills like a single lookup as it is processed; repeats inside the run and 7-day repeats are free.

Queues up to 100,000 addresses and processes them in the background in chunks, with every chunk's mailbox probes sent concurrently. Creation answers 202 with the job's id and status. Poll GET /api/v1/email/bulk/{jobId} for progress, then download the result CSV from GET /api/v1/email/bulk/{jobId}/results. Jobs and their files are kept for 30 days.

Rows repeating an address already in the list are verified once and counted in duplicate_count. Addresses at a domain already known to be catch-all, addresses on your suppression list, and addresses your account reported delivered recently answer without a probe. A run that exhausts the balance stops with stopped_reason: "insufficient_credits" and keeps everything verified so far.

Completion webhook

With a webhook_url, the finish (completed, failed, or cancelled) is POSTed there as { "event": "bulk_email_job.finished", "job": { "id", "status", "total", "processed", "deliverable", "risky", "undeliverable", "credits_used", "stopped_reason", "finished_at" } }. The X-Spaw-Signature header is the HMAC-SHA256 of the exact raw body keyed with webhook_secret, which this response returns once and never again. Connection errors and 5xx answers are retried three times with a short backoff.

$expected = hash_hmac('sha256', $request->getContent(), $secret);
abort_unless(hash_equals($expected, $request->header('X-Spaw-Signature')), 401);

Request body

field type description
emailsrequiredstring[]1 to 100,000 addresses; each item at most 254 characters.
webhook_urlstring | nullAn https URL to POST when the job finishes. The response includes the signing secret — shown once, never again. At most 2,048 characters.

Example request

curl -X POST https://spaw.co/api/v1/email/bulk \
  -H "Authorization: Bearer sk_live_…" \
  -H "Content-Type: application/json" \
  -d '{
  "emails": [
    "[email protected]",
    "[email protected]"
  ],
  "webhook_url": "https://www.example.com/hooks/spaw"
}'
const response = await fetch('https://spaw.co/api/v1/email/bulk', {
  method: 'POST',
  headers: {
    'Authorization': 'Bearer sk_live_…',
    'Content-Type': 'application/json',
  },
  body: JSON.stringify({
    "emails": [
      "[email protected]",
      "[email protected]"
    ],
    "webhook_url": "https://www.example.com/hooks/spaw"
  }),
});
const result = await response.json();
import requests

response = requests.post(
    'https://spaw.co/api/v1/email/bulk',
    headers={'Authorization': 'Bearer sk_live_…'},
    json={
        'emails': [
            '[email protected]',
            '[email protected]'
        ],
        'webhook_url': 'https://www.example.com/hooks/spaw'
    },
)
result = response.json()
$ch = curl_init('https://spaw.co/api/v1/email/bulk');
curl_setopt_array($ch, [
    CURLOPT_RETURNTRANSFER => true,
    CURLOPT_CUSTOMREQUEST => 'POST',
    CURLOPT_HTTPHEADER => ['Authorization: Bearer sk_live_…', 'Content-Type: application/json'],
    CURLOPT_POSTFIELDS => json_encode([
        'emails' => [
            '[email protected]',
            '[email protected]'
        ],
        'webhook_url' => 'https://www.example.com/hooks/spaw'
    ]),
]);
$result = json_decode(curl_exec($ch), true);

Responses

202The job was queued.

{
    "success": true,
    "data": {
        "job": {
            "id": 512,
            "status": "queued",
            "total": 2,
            "duplicate_count": 0,
            "processed": 0,
            "deliverable": 0,
            "risky": 0,
            "undeliverable": 0,
            "credits_used": 0,
            "stopped_reason": null,
            "cancel_requested": false,
            "webhook_status": null,
            "created_at": "2026-09-03T10:12:44+00:00",
            "finished_at": null,
            "webhook_secret": "8fJ2…40 characters…Qk1"
        }
    }
}

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

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

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

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

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