- authentication
- Secret API key as a bearer token
- billing
- Creation is free. Each run bills every number like a single lookup: 1 credit for a fresh valid answer, plus the live-check credits when the check answered; invalid, unassigned, suppressed and 7-day repeats are free, the base credit is shared with any other lookup of the number in the last 7 days, and a check the account paid for in the last 7 days is free while the network's answer is held (a day).
Saves up to 500 numbers and re-checks them every week or every month. The first run starts right away and only sets the baseline; from the next run on, every number that was valid last time and is now invalid, in a block nobody holds or unreachable counts as decayed and is reported to the account's email; a number you suppressed yourself (an import, your own failure or abuse reports, an opt-out) answers suppressed and is not decay. A number that still answers valid but changed is flagged and reported in a second email: an SMS-receiving website started listing it, three or more accounts reported abuse, its risk rose to high, or its line type changed — and with the live check on, it was ported or another network serves it (see PhoneMonitorFlag; this is not SIM-swap detection). Runs honour the suppression list and the 7-day repeat cache. With live_check true, runs ask for the live carrier check whenever the service has it enabled, so decay also covers handsets that stopped answering. There is no daily cadence: most regulator tables behind a run refresh weekly (NANPA's and the SMS-receiving lists daily), a weekly run already catches a change within days, and a daily live check would pay for the network seven times a week.
Answers 201 with the monitor. The baseline run is queued, not finished: poll the monitor for last_run_at and last_summary.
With a webhook_url, a finished run is POSTed there as { "event": "phone_monitor.run", "monitor": { "id", "name", "cadence", "last_run_at", "next_run_at" }, "summary": { … the same block as last_summary … }, "changes": [ … ], "flags": { … } }, where changes names the numbers the two alert emails name — decayed and newly flagged, in list order, each once — and flags maps each newly flagged number to the flags it raised ({} when none did). The X-Spaw-Signature-V2 header carries t={unix seconds},v2={lowercase hex}, where the hex is the HMAC-SHA256 of v2:{t}:{raw body} keyed with webhook_secret, which this response returns once; a receiver should refuse anything whose t is more than five minutes from its own clock, since a signature over the body alone would let a captured delivery be replayed at it forever. The original X-Spaw-Signature, that HMAC of the body alone, is sent beside it until 1 March 2027. Connection errors and 5xx answers are retried twice with a short backoff, three attempts in all, and the outcome is readable afterwards as webhook_status and webhook_detail. A URL that resolves onto a private network is never posted to.
Request body
| field | type | description |
|---|---|---|
| namerequired | string | A label for the dashboard and the decay alerts. At most 100 characters. |
| numbersrequired | string[] | 1 to 500 numbers in any notation; each item at most 64 characters. Each is saved in E.164, read against `country` where it has no calling code, so two spellings of one number are one entry and the limit counts distinct numbers; an entry that does not parse is saved as typed and answers invalid on every run. |
| cadencerequired | string | How often the list is re-checked. There is no daily option: most regulator tables refresh weekly (NANPA's and the SMS-receiving lists daily), a weekly run already catches a number that stopped being valid or started being listed within days, and a daily live check would pay for the network seven times a week. One of: weekly, monthly. |
| country | string | null | The region for numbers written without a calling code. |
| live_check | boolean | Ask for the live carrier check on every run where the service has it enabled. Default: . |
| webhook_url | string | null | An https URL every reported run is POSTed to. The signing secret is returned once, in this response, and never again. At most 2,048 characters. |
| webhook_events | string | When to post. `changes` posts only when the run found something new — decay, a status change, a newly flagged entry — or stopped early; `every_run` posts after every run, which also says the monitor is still running. One of: changes, every_run. Default: changes. |
Example request
curl -X POST https://spaw.co/api/v1/phone/monitors \
-H "Authorization: Bearer sk_live_…" \
-H "Content-Type: application/json" \
-d '{
"name": "SMS audience",
"numbers": [
"+44 7911 012345",
"020 3013 0018"
],
"cadence": "monthly",
"country": "GB"
}'This endpoint has no console on its page. It writes to your account. A documentation page can show you the request; making the change is for the dashboard or for a call you make yourself.
Responses
201The monitor was saved and its baseline run queued.
{
"success": true,
"data": {
"monitor": {
"id": 7,
"name": "SMS audience",
"cadence": "monthly",
"number_count": 2,
"country": "GB",
"live_check": false,
"next_run_at": "2026-10-05T10:12:44+00:00",
"last_run_at": null,
"last_summary": null,
"created_at": "2026-09-05T10:12:44+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"
}
}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.