- authentication
- Secret API key as a bearer token
- billing
- Each row bills like a single lookup as it is processed; invalid numbers, repeats inside the run and 7-day repeats are free.
Queues up to 100,000 numbers and processes them in the background. Creation answers 202 with the job's id and status. Poll GET /api/v1/phone/bulk/{jobId} for progress, then download the result CSV from GET /api/v1/phone/bulk/{jobId}/results: one row per input number with every response field as a column. Jobs and their files are kept for 30 days.
Rows repeating a number already in the list (as submitted) are looked up once and counted in duplicate_count. A run that exhausts the balance stops with stopped_reason: "insufficient_credits" and keeps everything answered so far. Because nothing about a phone lookup is stored, the input list and the result file are the only copies and both are deleted with the job. country applies to every row; the live carrier-network check is single lookups only.
With a webhook_url, the finish (completed, failed, or cancelled) is POSTed there as { "event": "bulk_phone_job.finished", "job": { … } }, signed in X-Spaw-Signature with the HMAC-SHA256 of the raw body keyed with webhook_secret, exactly like bulk email and IP runs. Send an Idempotency-Key header to make creation safe to retry: a repeat of the same request answers the job the first attempt created, as 200 with Idempotent-Replayed: true; the same key with a different request answers 409 IDEMPOTENCY_KEY_REUSED.
Parameters
| name | in | type | description |
|---|---|---|---|
| Idempotency-Key | header | string | A value unique to this request, so a retry after a timeout answers the same job instead of queueing a second one. 1 to 128 characters. |
Request body
| field | type | description |
|---|---|---|
| phonesrequired | string[] | 1 to 100,000 numbers, in any common notation. Each item is a plain string, or an object carrying its own country. |
| phones[].phonerequired | string | The number, in any common notation. At most 32 characters. |
| phones[].country | string | null | This item's own ISO 3166-1 alpha-2 region code, for a number written without a calling code. Wins over the list-level country. |
| country | string | null | An ISO 3166-1 alpha-2 region code used to parse every number written without a calling code, unless the item names its own. |
| hlr | boolean | Ask for the live carrier check on every number. While the check is not enabled on the service it answers `hlr_checked` false at no extra cost; when it is, each item that answers is charged the premium credits, and the balance is gated per item before any network query. Default: . |
| webhook_url | string | null | An https URL to notify when the run finishes. |
Example request
curl -X POST https://spaw.co/api/v1/phone/bulk \
-H "Authorization: Bearer sk_live_…" \
-H "Content-Type: application/json" \
-d '{
"phones": [
"+1 415 555 0142",
"(415) 555-0143"
],
"country": "US",
"webhook_url": "https://example.com/hooks/spaw"
}'Responses
202The job was queued.
{
"success": true,
"data": {
"job": {
"id": 733,
"status": "queued",
"total": 2,
"duplicate_count": 0,
"processed": 0,
"valid": 0,
"invalid": 0,
"low": 0,
"medium": 0,
"high": 0,
"credits_used": 0,
"stopped_reason": null,
"cancel_requested": false,
"country": "US",
"webhook_status": null,
"created_at": "2026-09-04T10:12:44+00:00",
"finished_at": null,
"webhook_secret": "8fJ2…40 characters…Qk1"
}
}
}200A retry with an `Idempotency-Key` already used for this exact request: the job the first attempt created, with `Idempotent-Replayed: true`.
{
"success": true,
"data": {
"job": {
"id": 733,
"status": "processing",
"total": 2,
"duplicate_count": 0,
"processed": 1,
"valid": 1,
"invalid": 0,
"low": 0,
"medium": 0,
"high": 1,
"credits_used": 1,
"stopped_reason": null,
"cancel_requested": false,
"country": "US",
"webhook_status": null,
"created_at": "2026-09-04T10:12:44+00:00",
"finished_at": null
}
}
}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"
}
}409The `Idempotency-Key` was already used for a different list, country or webhook URL.
{
"success": false,
"error": {
"code": "IDEMPOTENCY_KEY_REUSED",
"message": "This Idempotency-Key was already used for a different request.",
"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.