# Spaw: full documentation
This file concatenates every public Spaw document in Markdown. Spaw (https://spaw.co) is a verification API for the identifiers people type into forms: one API key, one credit balance, typed JSON responses that name their data sources. Six signals are live and bill off the same balance: email verification, phone intelligence, IP intelligence, address verification, business verification and cross-signal consistency, which runs any subset of the first four in one call and names where they agree and where they do not. The short index lives at https://spaw.co/llms.txt; the OpenAPI document at https://spaw.co/openapi.json; every page below also exists on its own at the URL it names, with a .md twin.
---
# Getting started
Get started with the Spaw verification API: create an API key, make your first request, and understand how credits work.
Spaw verifies the identifiers people type into forms, over a plain REST API and a dashboard. Six signals run off one key and one balance: email verification without sending mail, phone intelligence, IP intelligence, postal address verification, business verification against public registers, and a cross-signal consistency check that reports where the others agree and where they do not. Every answer names the datasets behind it, and the four scored signals publish the weights their 0 to 100 risk score is summed from. This guide takes you from a fresh account to your first lookup in a few minutes.
The first request does not wait for any of it. A published sandbox key answers the sample requests in these docs without an account, so you can see a real response before you decide to sign up — skip to "Your first request" below and paste it.
## Create an API key
Past the sandbox key, every API request authenticates with a key created from the dashboard.
1. **Create an account.** Sign up with an email and password at https://spaw.co/register, then confirm the address from the email that arrives: the API refuses every request from an unconfirmed account. Confirming also credits a one-off signup bonus, on top of the free credits every account is granted at the start of each month. No card required.
2. **Open the API keys page.** Once signed in, choose API keys in the header (https://spaw.co/api-keys). Name the key after where it will live — "Production", "Staging" — so your usage history stays readable.
3. **Copy the key.** The secret key is shown once and stored hashed; copy it before leaving the page. Revoking a key stops its requests immediately.
There is a second kind of key: publishable `pk_` keys, made for the browser form widget (https://spaw.co/docs/email-verification#widget). They can run the four browser lookups — email, phone, IP and postal address — only from the domains you allow, and stay visible in the dashboard. Their answers carry no `meta` block, so a page visitor never sees the account's balance. Keep secret keys on servers; ship publishable keys to pages.
**Key controls.** A secret key can be limited when it is created: scopes (email, IP, phone, address, entity, consistency, account) confine it to the product families an integration needs, a daily credit cap stops a runaway script (single and batch calls count what they bill; bulk runs are not counted but cannot be started once the cap is reached), and an allowlist of addresses or CIDR ranges makes a leaked key useless elsewhere. Refused calls answer `KEY_SCOPE_DENIED`, `KEY_SPEND_CAP_REACHED` or `KEY_IP_NOT_ALLOWED` and bill nothing. A batch that reaches the cap part-way is not refused: it stops there, keeps the items already paid for, and answers 200 with `meta.stopped_reason: "key_spend_cap"` — so a single large batch can never spend past the cap. All three controls apply to tool calls over the MCP server (https://spaw.co/docs/mcp) exactly as they apply here, so a key given to an agent is limited by the same three things; a scope or cap refusal reaches the agent as a tool error naming the same code, because an HTTP status is something an MCP client reports as a broken connection. The controls are fixed at creation: create a new key to change them. To replace a key without downtime — a leak, or a scheduled rotation — use Rotate on the API keys page: the replacement carries the same scopes, cap, address allowlist and default country, and the old key keeps working for 24 hours so you can move each integration over, or you can end it immediately with Revoke now.
## Your first request
Send the key as a bearer token, JSON in and out. Verifying an address is one POST — and this one runs as printed, because `sk_sandbox_spaw` is the published sandbox key rather than a placeholder:
```bash
curl https://spaw.co/api/v1/email \
-H "Authorization: Bearer sk_sandbox_spaw" \
-H "Content-Type: application/json" \
-d '{"email": "deliverable@spaw.test"}'
```
```json
{
"success": true,
"data": {
"email": "deliverable@spaw.test",
"deliverable": "deliverable",
"reason": null,
"risk_score": 0,
"mx_provider": "other",
"disposable": false,
"…": "21 more fields, see the API reference"
},
"meta": { "credits_used": 0, "credits_remaining": null, "sandbox": true }
}
```
A phone number is the same call on `/api/v1/phone`, and the sandbox answers the eight test numbers the same way:
```bash
curl https://spaw.co/api/v1/phone \
-H "Authorization: Bearer sk_sandbox_spaw" \
-H "Content-Type: application/json" \
-d '{"phone": "+12025550100"}'
```
```json
{
"success": true,
"data": {
"valid": true,
"e164": "+12025550100",
"line_type": "mobile",
"line_type_source": "block_allocation",
"carrier": "Spaw Test Wireless",
"carrier_source": "block_allocation",
"block_status": "allocated",
"risk_score": 0,
"hlr_checked": false,
"ok_to_send": true,
"blocked_by": null,
"…": "more fields, see the API reference"
},
"meta": { "credits_used": 0, "credits_remaining": null, "sandbox": true }
}
```
The carrier is the company the number's block was allocated to in the regulator's table, not necessarily the network that serves it today, and `ok_to_send` is the published default SMS policy applied to the answer. The live carrier-network check (`hlr: true`) is not enabled on the service: `hlr_checked` answers false and nothing extra is charged.
**What the sandbox key does.** It answers five lookups — `POST /api/v1/email`, `/phone`, `/ip`, `GET /api/v1/ip/{ip}` and `POST /api/v1/address` — and only for the sample values these docs print: the six test addresses on spaw.test, the eight test numbers +1 202 555 0100 to 0106 and 0113, the sample IP 8.8.8.8, the sample postal address in the reference and the five test postal addresses at 1 to 5 Spaw Test Street, Springfield, IL 99999 (each answers `ok_to_ship` and `blocked_by` from the published default shipping policy, and the test numbers answer `ok_to_send` and `blocked_by` from the published default SMS policy, so the decision fields can be exercised without an account). The request body has to carry those fields and nothing else, so a body with an extra option is refused rather than quietly ignored. Everything else answers `SANDBOX_ENDPOINT_NOT_ALLOWED` or `SANDBOX_VALUE_NOT_ALLOWED`, naming what it would have taken. It is limited to 30 requests a minute from one address.
**What it does not do.** The key is printed here, so everybody has it: it is attached to no account, spends no credits, writes nothing that lasts, and cannot read a balance, a bulk run, a monitored list, a suppression list or a request log. That is why `meta.credits_remaining` answers `null` and `meta.sandbox` is true. Swap in your own `sk_live_` key and the same call answers for any address, bills a credit for a fresh usable answer, and reports the balance it left.
Every endpoint answers the same envelope — `{ success, data, meta }` on success, a typed `error.code` on failure — and no verification ever sends mail. The dashboard playground runs the exact same lookups if you want to try addresses before writing code, and the six fixed test email addresses (deliverable@spaw.test, undeliverable@spaw.test, …), the eight test phone numbers and the five test postal addresses the sandbox answers for are free on your own key too, so integration tests never spend credits; see the API reference at https://spaw.co/docs/api.
## Credits & billing
You pay for answers, not attempts, and anything you paid to check once is free to check again for a week.
| Outcome | Credits |
| --- | --- |
| Fresh lookup with a usable answer | 1 credit |
| Five fresh IP lookups with a usable answer | 1 credit |
| Email answer whose mailbox check was uncertain (unverified) | 0 |
| Undeliverable email verdict within the fair-use allowance | 0 |
| Undeliverable email verdict past the fair-use allowance | 1 credit |
| Invalid input or no match | 0 |
| Repeat of a recent lookup, within 7 days | 0 |
| Rejected or failed request | 0 |
IP lookups are billed in blocks of five: the answered IP lookup that opens a block reports `credits_used` 1 and the next four report 0. Undeliverable email verdicts that needed a mailbox check are free within a fair-use allowance; the pricing page (https://spaw.co/pricing) gives its size.
Two credits arrive without being bought: a free grant at the start of every calendar month, and a one-off signup bonus the first time the account confirms its email address. Both land on the same balance, never expire, and are listed on the Usage page like any other movement. The size of the monthly grant is on the pricing page (https://spaw.co/pricing); the bonus is named in the welcome email.
The dashboard's Usage page lists every credit movement with the key or channel that spent it, and `GET /api/v1/account` returns the live balance for pre-flight checks.
Need more than the free grant? Buy a credit pack from the Billing page in the dashboard, or start a plan that deposits the same credits every month for less, billed monthly or yearly. Packs start at $9 for 1,000 credits, plans at $7 a month. Pack credits never expire; plan credits roll over up to three months' allowance and stay usable for 60 days after you cancel, and a plan is cancelled at any time. Pricing details: https://spaw.co/pricing.
## Related
- Email verification guide: https://spaw.co/docs/email-verification — verdicts, lists, bulk runs, and the form widget
- Phone intelligence guide: https://spaw.co/docs/phone-intelligence — every field, the regulators' tables, the SMS policy and the test numbers
- API reference: https://spaw.co/docs/api — every endpoint and field
- MCP server: https://spaw.co/docs/mcp — the same tools for AI agents
- OpenAPI document: https://spaw.co/openapi.json
---
# Email verification
How Spaw email verification works: verdicts and the risk score, list validation, bulk runs with webhooks, monitors, the suppression list, and the form widget.
Every lookup runs RFC syntax parsing, live DNS and MX checks, SPF and DMARC records, open disposable and role lists, and a mailbox-level SMTP handshake — without ever sending mail. This guide covers how to read the answer and every way to run verifications at scale.
## Verdicts and the risk score
Each answer leads with one of three verdicts in `deliverable`, and a machine-readable `reason` when it is not a clean pass:
| Verdict | Meaning |
| --- | --- |
| deliverable | The mailbox accepts mail. Nothing in the checks argues against sending. |
| risky | Mail may arrive, but something argues for caution: a typo-squat of a major provider, a parked domain that still accepts mail, a disposable domain, mail served from a blocklisted network, a web-only domain with no MX record, a full mailbox, a no-reply or role inbox, a mailbox that was dead and exists again, a catch-all server that accepts everything, or a mailbox the handshake could not confirm (reason unverified). |
| undeliverable | Do not send. The syntax is invalid, the username breaks its own provider's rules, the domain has no usable mail server, or the exact mailbox was confirmed missing or disabled. |
Every reason value is explained at https://spaw.co/docs/reasons.
The `risk_score` is not a black box: it is the documented flags added up with published weights, so you can recompute it — or ignore it and branch on the explicit booleans behind it.
| Signal | Fires when | Points |
| --- | --- | --- |
| disposable | disposable domain | +80 |
| parked_domain | parked or for-sale domain that still accepts mail | +60 |
| likely_typo | known typo-squat domain (gamil.com) | +60 |
| mx_blocklisted | mail served from a blocklisted network | +60 |
| implicit_mx | web-only domain, no MX record | +30 |
| role | role address (info@, support@, …) | +30 |
| catch_all | catch-all domain | +30 |
| young_domain | domain registered under 30 days ago | +30 |
| recycled_mailbox | mailbox that was dead and exists again | +30 |
| mailbox_full | mailbox full | +20 |
| gibberish | gibberish local part | +20 |
| generated | machine-made or placeholder local part | +20 |
| no_reply | no-reply address, on top of role | +20 |
| widely_seen | looked up by three or more other accounts in a day | +20 |
| catch_all_gateway | catch-all behind a security gateway (instead of catch-all) | +15 |
| new_domain | domain registered under 180 days ago | +10 |
| no_spf | no SPF record on a business domain | +10 |
| unverified | mailbox not SMTP-verified | +10 |
| expiring_domain | registration ending within 30 days | +10 |
| alias | plus-tag alias | +5 |
| relay | relay or forwarding service (never a burner) | +5 |
An undeliverable verdict is always 100. Scores of 60 and above report `risk_level: "high"`, 30 and above medium, below that low. When mail servers refuse to say whether an inbox exists, the answer degrades honestly — `smtp_checked: false`, null, and a risky verdict with reason `unverified` — never to a guess. An unverified answer is free whatever the cause and leaves no seven-day repeat marker, so a later lookup is a fresh one: the uncertain result is reused for up to 15 minutes, then the mailbox is asked again, and the lookup bills only once the mailbox is confirmed; an undeliverable verdict that needed a mailbox check is free within a fair-use allowance (https://spaw.co/pricing). Where the mailbox could not be settled, catch-all domains above all, `mailbox_confidence` adds a recomputable 0–100 estimate from the signals already in the answer: DMARC, SPF, an established mail provider, an old registration, and a name-shaped address raise it; gibberish, a fresh domain, and a plus-tag lower it. Reported delivery outcomes for the domain outweigh all of that once five or more have been sent in: a bounce rate of 10% or less adds 15, one of 50% or more takes 20 away, and a username that follows the naming convention seen in the domain's confirmed deliveries adds 15 while a name shape that breaks it takes 10 away. A relay alias is the exception: it stays at 50, and only a bounce rate of 50% or more or a plus-tag lowers it, because the domain's records describe the relay operator, and an alias's name, whether the relay generated it or its owner chose it, says nothing about whether it exists.
Three rules keep the sum honest. The two registration-age rows are exclusive, and so are `catch_all` and `catch_all_gateway`: behind Proofpoint, Mimecast or Barracuda a catch-all is the gateway's design rather than a squat, so it weighs 15 instead of 30. A no-reply address is a role address and carries both rows, 50 in all. And a relay address — Firefox Relay, SimpleLogin, Addy.io, DuckDuckGo — is a real person's real mailbox, so it is never disposable: it carries 5 for the withheld identity and nothing else. `risk_signals` lists exactly the rows that fired, each with its weight and the dataset it was read from, so the sum is always in the answer; an undeliverable verdict lists nothing and answers 100.
Beyond the verdict the answer says what the username and the domain look like, without a network call for the first and from data already in hand for the second. `local_part_pattern` names the shape of the username (first.last, flast, a lone first name, digits, a plain word) with the name tokens the bundled dictionary recognised in `first_name` and `last_name`, and `looks_generated` marks a hex string, a UUID, a placeholder word or a name glued to a run of digits. On the domain side `domain_category` sorts relay, disposable, webmail, academic, government and military domains from the rest, `mx_provider_type` says whether the provider hosts mailboxes, is a security gateway, a transactional relay, a web host or the domain's own servers, `mx_blocklisted` and `mx_network_type` run the mail hosts through the threat blocklists and network feeds the IP product already installs, and `domain_expires_at` and `parked` read the registration's end and the parking nameservers. Two signals grow from traffic rather than lists: `previously_invalid` remembers a "no such mailbox" answer for a year so a mailbox that exists again is reported as recycled, and `widely_seen` says whether three or more other accounts checked the address inside a day. Both are counts and dates under a keyed hash, never the address, and both are erased with it. The full 46-field contract, with every enum, is in the endpoint reference: https://spaw.co/docs/api/verify-email.
Repeats inside the seven-day window are free and served from the earlier answer. To ask for a fresh one, pass `refresh: true` (the earlier answer and the shared mailbox answer are set aside, a real handshake runs, and the lookup bills as fresh when answered) or `max_age` in seconds to do so only when the earlier answer is older than that; `meta.refreshed` says whether an earlier answer was discarded. `timeout` (2 to 60 seconds) caps how long the handshake may wait for one call; when it runs out the answer is unverified with `smtp_reason: timeout` instead of holding the request. All three work on the single and batch endpoints.
Every answer ends with the decision your own sending policy makes about it: `ok_to_send`, and `blocked_by`, the rule that blocked it. An undeliverable verdict never passes; the rest is yours to set (see Sending policy below).
Pass `fields`, a comma-separated list of the keys you want, to get only those in `data`, in the answer's own order: unknown names are ignored, a blank list means everything, `sources` is kept only when named, and `meta` is never narrowed. It works on the single and the browser endpoint, not on batch or bulk. `did_you_mean` now draws on the thousand most-linked domains and the domains you verify most, not only the big mailbox providers, and a domain outside that list is suggested only when its own mail servers accept mail.
## Sending policy
The score is arithmetic; the decision is yours. A sending policy turns an answer into one boolean, `ok_to_send`, so a form or a campaign branches once instead of re-implementing the verdict table. It has three rules, all optional: `block_signals`, the weights that block whenever they fire; `max_risk_score`, a ceiling on the score; and `min_mailbox_confidence`, a floor for a catch-all or unverified mailbox. The rules are applied in that order after the verdict, and `blocked_by` names the first one that fired: `undeliverable`, the blocking signal's own name, `risk_score` or `mailbox_confidence`. An undeliverable verdict never passes, whatever the policy says.
Until you set one, the published defaults apply: the `disposable`, `likely_typo`, `parked_domain`, `mx_blocklisted` and `no_reply` signals block, so does any score over 60, and there is no confidence floor. The defaults are what the guest demo and the sandbox key answer with, because neither has an account to keep a policy. The test addresses answer under your account's stored policy, like every other lookup your key makes, so they are a way to try a policy before a customer meets it.
The policy belongs to the account and applies to every channel the account is billed for: the single and batch endpoints, bulk runs (the result file carries `ok_to_send` and `blocked_by` as columns and the summary counts the blocked rows), monitors, the form widget and the dashboard. Read it with `GET /api/v1/email/policy` (https://spaw.co/docs/api/get-email-policy), change it with `PUT /api/v1/email/policy` (https://spaw.co/docs/api/update-email-policy) — send only the keys you are changing, a null clears the ceiling or the floor, a body naming none of the three answers `422` — and restore the defaults with `DELETE /api/v1/email/policy` (https://spaw.co/docs/api/reset-email-policy). All three are free and answer the policy with `is_default` and `updated_at`. The Sending policy tab on the verification page edits the same three rules.
## From the dashboard
Everything the API does is also a tab on the verification page (https://spaw.co/verify/email), under the same billing rules:
| Tab | What it does |
| --- | --- |
| Single address | One lookup with the full field breakdown and raw response — the same call the API serves. |
| Validate a list | Paste or drop up to 500 addresses; results stream in with verdict filters, CSV export, and a "Monitor list" save. |
| Bulk runs | Upload a CSV with any columns or paste up to 100,000 addresses; runs process in the background, keep your columns in the results, count repeated addresses, and can be cancelled mid-run. |
| Monitors | Saved lists re-verified weekly or monthly, with an email alert when previously deliverable addresses go bad. |
| Domains | Saved domain lists re-checked daily, weekly or monthly, with an email alert when a domain goes dead, gets parked, lands on a blocklist, loses a mail policy or changes provider. |
| Suppressions | Your known-bad list: view, import, export, and remove entries. |
| History | Your last verifications with a 30-day quality breakdown, re-verify shortcuts, and CSV export. |
| Sending policy | Which signals block, how high the score may go and how sure a catch-all mailbox must be: the rules behind `ok_to_send` on every answer. |
## Bulk runs & webhooks
A bulk run queues up to 100,000 addresses and processes them in the background — upload or paste from the Bulk runs tab, or automate with `POST /api/v1/email/bulk` (https://spaw.co/docs/api/create-bulk-job). Poll the job for progress, then download the result CSV with every response field per row. The file leads with your own columns, then this verdict block in this order:
```
verified_email, deliverable, reason, risk_score, risk_level, risk_signals, normalized_email, is_alias, is_gibberish, looks_generated, local_part_pattern, first_name, last_name, disposable, is_relay, relay_service, role, role_type, free_provider, domain_category, mx_found, mx_implicit, mx_provider, mx_provider_type, mx_blocklisted, mx_network_type, has_spf, dmarc_policy, domain_registered_at, domain_age_days, domain_expires_at, parked, smtp_checked, smtp_checked_at, mailbox_exists, previously_invalid, previously_invalid_at, catch_all, smtp_reason, mailbox_confidence, widely_seen, did_you_mean, ok_to_send, blocked_by, credits_used, cache_hit, suppressed
```
`risk_signals` is flattened rather than dropped, as `signal:weight` pairs joined with semicolons. A finished run also carries a `summary` built from the same rows the file holds, so the two can never disagree: rows and duplicates, the verdict split, every reason seen with its row count, the risk split, the ten most frequent providers and domains, the domain categories, and how many rows carried each of the disposable, relay, role, catch-all, parked, blocklisted, previously invalid, widely seen and generated signals. The webhook body is unchanged.
Cancelling stops a queued job on the spot and a running one at its next checkpoint; rows already verified stay billed and downloadable. A run that failed part-way keeps the rows it had already written, and they stay downloadable too. If a webhook URL was set at creation, the finish — completed, failed, or cancelled — is POSTed to it, signed with the secret creation returned once: `X-Spaw-Signature-V2` carries `t={unix seconds},v2={hex}`, the HMAC-SHA256 of `v2:{t}:{raw body}`, and a receiver refuses anything whose `t` is more than five minutes from its own clock so a captured delivery cannot be replayed later. The original `X-Spaw-Signature`, over the body alone, is sent beside it until 1 March 2027. Transient delivery failures are retried. Verify it, and the exact request shape, in the API reference: https://spaw.co/docs/api/create-bulk-job.
A run that stopped because the credit balance ran out is resumed rather than uploaded again: top up, then press Resume on the run in the dashboard or call `POST /api/v1/email/bulk/{jobId}/resume` (https://spaw.co/docs/api/resume-bulk-job). The whole list runs again from the top, and the rows it already verified are repeats inside the seven-day window, so only the rows it never reached are charged. The job keeps its id, its webhook and its cumulative `credits_used`, while `processed` and the verdict counts restart at zero and are rebuilt as the rows come back. A run that stopped for any other reason, or that is already going again, answers `409 JOB_NOT_RESUMABLE`; one whose list has passed out of the 30-day retention window answers `409 JOB_INPUT_EXPIRED`, and an empty balance answers `402 INSUFFICIENT_CREDITS` rather than queueing a run that would stop again at once.
If a delivery fails — your endpoint was down for the half-minute the three attempts cover — ask for it again rather than falling back to polling: press **Send webhook again** on the run in the dashboard, or call `POST /api/v1/email/bulk/{jobId}/webhook/redeliver` (https://spaw.co/docs/api/redeliver-bulk-webhook). The body is byte-for-byte the body the first delivery carried, rebuilt from the run's own frozen row, so the two reconcile. The signature is new, and has to be: a receiver refuses anything whose `t` is more than five minutes from its own clock, so verify a redelivery exactly as you verify any other delivery. Nothing deduplicates it for you — a receiver that gets both the original and the redelivery sees `bulk_email_job.finished` twice, and `job.id` is the key to deduplicate on. A run created without a `webhook_url` answers `409 WEBHOOK_NOT_CONFIGURED` and one that has not settled answers `409 JOB_NOT_FINISHED`; a run whose last delivery succeeded is not refused, because a receiver can lose a message it has already acknowledged. The delivery is queued rather than made while you wait — one attempt takes half a minute against an endpoint that is down, which is the endpoint you are asking about — so the call answers `202` and the run reads `webhook_status: "pending"` until a worker has an answer, then `delivered` or `failed` with what your endpoint said in `webhook_detail`. The dashboard shows the same three states beside the run. It is free and capped at ten 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.
Creation is safe to retry: send an `Idempotency-Key` header and a repeat with the same key replays the job (status 200, header `Idempotent-Replayed: true`, the same webhook secret), while the same key with a different request answers `409 IDEMPOTENCY_KEY_REUSED`.
## Monitors
Lists decay: mailboxes are abandoned, domains lapse. Saving a finished list run with "Monitor list" re-verifies it weekly or monthly. The first run only sets the baseline; after that, any address that was deliverable and no longer is triggers an email naming the decayed addresses. Repeats of unchanged answers ride the 7-day cache and the suppression list, so a scheduled run costs far less than the first one. "Run now" re-verifies on demand.
Monitors are also available over the API: `GET`, `POST` and `DELETE /api/v1/email/monitors`, `GET /api/v1/email/monitors/{id}` for the last run's verdict per address, and `POST /api/v1/email/monitors/{id}/run` to re-verify now (https://spaw.co/docs/api/list-monitors). Same 500-address limit, weekly or monthly cadence and decay alerts as the dashboard.
The History tab's rows are available as `GET /api/v1/email/history` (https://spaw.co/docs/api/list-history), newest first, 100 per page, with filters for verdict, address, date range (`since`/`until`) and channel (`via`), for the retention window.
A monitor can post its runs as well as email about them. Set `webhook_url` (https) when you create it, or later with a `PATCH`, and the signing secret comes back once in that response and is never readable again. `webhook_events` decides which runs are posted: `changes`, the default, posts only when the run found something new — an address that has decayed — or stopped early, and `every_run` posts after every run, which also tells you the monitor is still running. The body is `{ "event": "email_monitor.run", "monitor": {…}, "summary": {…}, "changes": [ … ] }`, where `changes` names the addresses the alert email names, and the delivery is signed with `X-Spaw-Signature-V2`: `t={unix seconds},v2={hex}`, where the hex is the HMAC-SHA256 of `v2:{t}:{raw body}` keyed with the secret. Refuse anything whose `t` is more than five minutes from your own clock — that window is what stops a captured delivery being replayed at you later. The original `X-Spaw-Signature`, the HMAC of the body alone with no timestamp, is sent beside it until 1 March 2027 and still verifies; both recipes are at https://spaw.co/integrations/webhooks. Connection errors and 5xx answers are retried twice, three attempts in all; `webhook_status` and `webhook_detail` on the monitor say how the last delivery went. A URL that resolves onto a private network is refused at delivery, every time, because DNS can be repointed after a URL is saved.
A monitor is changed rather than replaced: `PATCH /api/v1/email/monitors/{id}` (https://spaw.co/docs/api/update-monitor) takes `name`, `emails`, `cadence` and the webhook fields, and applies only the ones you send. That matters because what the last run recorded per entry is the baseline every alert is compared against: deleting a monitor and creating it again throws that away, and the first run of the new one only sets a baseline — a full cycle of silence, which is the one thing a monitor is bought to prevent. Replacing the list keeps what was recorded about the addresses that stayed, deletes what was recorded about the ones you removed, and gives a new entry no baseline. Changing the cadence neither skips a run nor starts a second one: a run already scheduled sooner than the new interval still happens, and a longer interval applies from the one after it. A body naming no field it could change answers `422`.
A monitor that stops being run is deleted, and the identifiers and results it stored go with it. `prunes_after_days` on every monitor payload says how long that takes, and `meta.retention_days` on the list endpoint says the same. A monitor in active use is never pruned: each run advances `last_run_at`, so the window only closes on a list that has genuinely stopped being re-checked — at which point holding the details somebody gave you serves no purpose. Deleting a monitor yourself removes everything it held at once.
## Monitored domains
A domain's mail setup drifts too: a provider is switched, a DMARC policy is loosened, a registration lapses into a parking page, a mail host lands on a blocklist. A monitored domain list re-checks up to 500 domains daily, weekly or monthly through the same domain lookup the API sells (https://spaw.co/docs/api/domain-intelligence), and bills it the same way: one credit when the domain accepts mail, nothing when it is dead or invalid, and repeats inside the seven-day window free — which is why a daily cadence is allowed here and not on address lists. It is DNS and a registry record, no handshake, and it costs at most one credit per domain a week.
Every domain answers one of four statuses: `ok`, `flagged` (a flag fired), `dead` (no usable mail servers) or `invalid` (not a hostname). The flags, every one that fires: `parked`, `blocklisted`, `expiring` (the registration ends within 30 days), `no_spf`, `no_dmarc`, and five that compare with the previous run — `provider_changed`, `spf_changed`, `dmarc_changed`, `dkim_lost` and `mta_sts_lost`. Beside the flags each entry keeps the facts they were read from: the provider and its type, SPF and its all-qualifier, the DMARC policy, the DKIM selectors found, MTA-STS, parked, blocklisted and the expiry date.
The first run only sets the baseline. After that, an alert email goes out for a domain that was ok and no longer is, or that gained a flag it did not have, and the run's summary counts `total`, `ok`, `flagged`, `dead`, `invalid`, `changed`, `newly_flagged` and `credits_used`, with a `stopped_reason` when the run could not finish.
Over the API: `GET` and `POST /api/v1/email/domain-monitors` (https://spaw.co/docs/api/list-domain-monitors), `GET`, `PATCH` and `DELETE /api/v1/email/domain-monitors/{id}`, and `POST /api/v1/email/domain-monitors/{id}/run` to re-check now. The detail endpoint carries `results`, the last run per domain. Creation takes `name`, `domains`, `cadence` and the webhook fields; a webhook posts `{ "event": "domain_monitor.run", "monitor": {…}, "summary": {…}, "changes": [ … ] }` under the same `webhook_events` rule, signature and retry contract as the address monitors above, and the `PATCH` keeps the baseline of every domain that stays, exactly as it does for addresses. An unused list prunes itself after `prunes_after_days`. The Domains tab on the verification page manages the same lists.
## Suppression list
Every undeliverable verdict adds the address — in normalized form, so tagged and dotted variants count as one mailbox — to your account's suppression list. List, bulk, and monitor runs answer suppressed addresses from the stored verdict, free and marked `meta.suppressed: true`, instead of re-checking them.
Two escape hatches keep the list honest: a single-address re-verify always runs fresh, and a recovered verdict removes an entry a verdict added; and an entry a verdict or a bounce report added is re-verified instead of trusted once it is untouched for 90 days; an import or a complaint never goes stale. You can also import your email provider's bounce export — up to 10,000 addresses per request, from the Suppressions tab or `POST /api/v1/email/suppressions` (https://spaw.co/docs/api/add-suppressions) — so your known-bads are honored from day one without spending a credit. What you import, and what your own bounce and complaint reports add, is your decision rather than a verdict: no lookup clears it, not even one typed into your public form, so it leaves the list only when you remove it or, for a bounce your reports added, when you later report a delivery to that address. A complaint is the recipient's objection rather than a fact about the mailbox: no delivery, later bounce or passing of time takes it off. A complaint about an address you imported is recorded on your import, which stays an import. An address you import that a verdict already suppressed keeps that verdict and becomes yours the same way; the verdict is served for 90 days after the entry was last renewed, then the import answers `suppressed`, because nothing verifies an import again.
Finding one address again does not mean paging the list. The Suppressions tab has a search box, and `GET /api/v1/email/suppressions` (https://spaw.co/docs/api/list-suppressions) takes `value`, `source`, `reason`, `since`, `until` and `per_page`. A searched address is normalized before it is matched, so any spelling of a suppressed mailbox finds it. To drop addresses without looking up their ids, `DELETE /api/v1/email/suppressions` (https://spaw.co/docs/api/remove-suppressions) takes up to 1,000 of them in `values` and answers which were on the list and which were not.
## Form widget
The form helper verifies addresses as visitors type them into your signup form. Create a publishable key on the API keys page — name it and list the domains it may run from — then include the script and mark your input:
```html
```
Phone fields work the same way. Mark the input `data-spaw-phone` and tell the helper the country, either fixed with `data-spaw-country="GB"` or read from a country select on the same form with `data-spaw-country-field`; without either, the number must carry its calling code:
```html
```
On blur it renders "Did you mean…?" corrections and can't-receive-mail notices for emails, and for phones the international format of a valid number, a note when the number is fictional, shared on SMS-receiving sites or virtual, and the reason when it is not valid (an unassigned block reads "not in service"). It never blocks a submit on its own. Every verdict also fires a `spaw:result` event whose detail is the full response plus `type` (email, phone or address) and the checked `input`, so your own policy is one listener away:
```js
addEventListener('spaw:result', (event) => {
if (event.detail.disposable || event.detail.is_disposable) {
// e.g. disable the submit button
}
});
```
Lookups bill your account and responses carry no meta block, so visitors never see your balance. Prefer to keep even the publishable key off the page? Point `data-endpoint` at a small server-side proxy that forwards to the API with your secret key instead; phone inputs use `data-phone-endpoint` on the script tag, and that proxy receives `{ phone, country }` to forward to the phone endpoint.
Bots that script your form can spend your credits. To keep them out, create a Cloudflare Turnstile widget, store its site key and secret on the publishable key when you create it, and add the site key to the script tag:
```html
```
The helper loads Turnstile once, renders it next to the field only when Cloudflare asks for an interaction, and obtains a fresh token before every lookup. The browser endpoint refuses lookups without a confirmed token with `403 TURNSTILE_FAILED` before any credit is spent. If Turnstile cannot load, the helper skips the lookup rather than blocking the form. The attribute also works on the input itself, and a proxy endpoint simply receives the token as `turnstile_token`.
## Related
- API reference: https://spaw.co/docs/api — every endpoint, field, and error code
- Verify an email address: https://spaw.co/docs/api/verify-email
- Browser endpoint for publishable keys: https://spaw.co/docs/api/verify-email-public
- Browser endpoint for phone numbers: https://spaw.co/docs/api/validate-phone-public
- Verdict reasons: https://spaw.co/docs/reasons · Error codes: https://spaw.co/docs/errors
- Getting started: https://spaw.co/docs
---
# IP intelligence
How Spaw IP intelligence works: every field and flag, the recomputable risk score, self and browser lookups, batch and bulk runs, your own allow and deny lists, and what is stored.
One lookup places an IPv4 or IPv6 address on the map, names the network and the operator, and flags the traffic that is not a person on a home connection: cloud and datacenter ranges, Tor exits, named VPN exits, iCloud Private Relay, corporate gateway egress, and addresses on public threat blocklists, and names the verified search and AI crawlers. Every signal comes from a local database or a compiled feed, so an answer takes milliseconds and the address never leaves the server unless you ask for the registry's abuse contact.
```bash
curl https://spaw.co/api/v1/ip \
-H "Authorization: Bearer sk_live_…" \
-H "Content-Type: application/json" \
-d '{"ip": "8.8.8.8"}'
```
## Fields and flags
Every response carries the same fields in the same order. The threat flags are tri-state: `false` is a checked negative and `null` means the signal was not evaluated, because no feed for it covers the address family. Every feed group, Tor included, carries IPv6 today.
| Field | Type | Meaning |
| --- | --- | --- |
| ip | string | The canonical form of the address; IPv4-mapped IPv6 input collapses to dotted-quad. |
| version | 4 or 6 | The address family. |
| reason | string or null | `reserved_range` for private, loopback, CGNAT, documentation and multicast space. Every other field is then null and the call is free. See [CGNAT](/glossary/cgnat). |
| network | string or null | The CIDR prefix the location answer covers, such as 8.8.8.0/24. |
| continent | string or null | Two-letter continent code: AF, AN, AS, EU, NA, OC, SA. |
| country | string or null | ISO 3166-1 alpha-2 code. |
| country_name | string or null | The English name of the country. |
| is_eu | boolean or null | Whether the country is a member of the European Union. Null when the country is unknown. |
| is_eea | boolean or null | Whether the country is in the European Economic Area: the EU plus Iceland, Liechtenstein and Norway, where the GDPR applies directly. Null when the country is unknown. |
| privacy_regime | string or null | The comprehensive privacy law where the address is, as a lowercase code: the region first where a state or province has its own law (California `ccpa`, Quebec `law25`), otherwise the country (`gdpr` across the EU and EEA, `uk_gdpr`, `fadp`, `lgpd`, `pipeda`, `appi`, `pipl`, `popia`, …). Null means no comprehensive law is known for the place, never that there is none; read the code with the country, since `pdpa` names different laws in Singapore, Thailand, Malaysia and Taiwan. See [the country pages](/tools/ip-lookup/countries). |
| sanctions | array or null | The sanctions programmes that name the country, in order: `ofac_comprehensive` (a comprehensive US programme: Cuba, Iran, North Korea) and `ofac_programme` (any country-related OFAC programme). An empty list when the country is known and no programme names it; null when the country is unknown. A fact about a place, never a verdict about a person. See [the country pages](/tools/ip-lookup/countries). |
| fatf_list | string or null | `call_for_action` when the FATF lists the country as high-risk (Iran, North Korea, Myanmar), `increased_monitoring` when it is under increased monitoring (22 jurisdictions after the June 2026 plenary). Null means on neither list or unknown. See [the country pages](/tools/ip-lookup/countries). |
| region | string or null | State or province name. Null when an operator-declared location disagrees with the database. |
| city | string or null | City name. Approximate from the database; exact where the operator publishes it. |
| latitude, longitude | number or null | Coordinates of the located area. Null when an operator-declared location disagrees with the database, rather than a contradicting point. |
| location_source | database, operator or null | `operator` when the network operator publishes the location itself (an RFC 8805 geofeed, Apple's relay list, a VPN provider's server list); `database` for the DB-IP Lite answer. See [geofeed](/glossary/geofeed). |
| location_confidence | high, medium, low or null | `low` whenever `is_anycast`, `is_mobile` or `is_satellite` is true, whoever placed the address: the place is then an anycast instance, a carrier gateway or a ground station, not the user's. Otherwise `high` when the operator declared the location itself, and graded from the weekly accuracy report's city agreement for the country: 75 % and above high, 50 % medium, below that low. Null with fewer than 20 compared samples for the country or no report yet. |
| timezone | string or null | IANA zone such as Europe/Berlin: the curated map's answer when the country or a region known to lie in one zone settles it, else the zone of the nearest listed place inside the located country within 250 km. Null when neither settles it. |
| timezone_source | region, country, nearest_city or null | What settled the zone: `region` when the map knows the region lies in one zone, `country` when the country keeps one clock, `nearest_city` when the nearest GeoNames place inside the country did, the case for split countries and regions such as Texas, Canada, Australia, Brazil, Russia and Indonesia. |
| timezone_matches | boolean or null | Only when the lookup carries `browser_timezone`: true when the browser's zone is the located zone or keeps the same clock right now (Paris and Berlin), false when the clocks differ, null when there is nothing to compare or the zone is unknown. See [time-zone mismatch](/glossary/timezone-mismatch). |
| currency | string or null | ISO 4217 currency of the country, from ICU region data. |
| calling_code | number or null | The country's international calling code, such as 49. |
| asn, org | number, string or null | The autonomous system number and the organisation operating it. See [ASN](/glossary/asn) and [the autonomous system pages](/tools/ip-lookup/asn). |
| asn_type | string or null | `hosting`, `isp`, `education` or `government`. `hosting` whenever is_datacenter is true or the operator name says so; null when it cannot be told. |
| connection_type | string or null | What the address is attached to, from positive evidence only and in this precedence: `corporate_proxy`, `hosting` (a datacenter range or a verified crawler), `satellite`, `mobile`, `residential` (a dynamic reverse-DNS name, so only with hostname: true). Null when nothing said so; it never means residential by default. |
| is_mobile | boolean or null | Whether the autonomous system is a listed mobile carrier, from a curated positive-only list (T-Mobile USA, Verizon Wireless, AT&T Mobility, EE, Three, Free Mobile, NTT Docomo, SK Telecom, Reliance Jio, Airtel, Vodafone Idea, the China Mobile provinces, Telkomsel, Smart, Telcel, Turkcell, MTN, Vodacom, MegaFon, …). False means not listed, never a guess; null when the ASN is unknown. Such addresses are shared behind carrier NAT and move with the handset. |
| is_satellite | boolean or null | Whether the autonomous system is a listed satellite ISP (Starlink, Viasat, HughesNet), whose addresses land at a ground station rather than near the subscriber. Same semantics as is_mobile. |
| registry | string or null | The regional registry that allocated the space: `arin`, `ripencc`, `apnic`, `lacnic` or `afrinic`, from the RIRs' daily delegated statistics. See [RIR allocation](/glossary/rir-allocation) and [the autonomous system pages](/tools/ip-lookup/asn). |
| registry_country | string or null | The country the registry recorded for the allocation. When it disagrees with the geolocation, look twice. |
| allocated_at | string or null | The allocation date the registry recorded (YYYY-MM-DD). A block allocated or transferred weeks ago is a classic fraud tell; a fact for you to weigh, not scored. |
| abuse_contact | string or null | The network's abuse mailbox from its RDAP record, only when asked for with `abuse_contact: true` (a registry lookup, cached per allocation). Null otherwise or when the registry lists none. |
| network_details | object or null | Only with `network_details: true`: the RDAP record's `name`, `organisation`, `range`, `registered_at` and `updated_at`, from the same cached registry lookup as abuse_contact. Null otherwise or when the registry could not be read. |
| hostname | string or null | Only with `hostname: true`: the reverse DNS (PTR) name of the address, a live DNS query with a two-second budget. Null otherwise or when there is none. |
| hostname_verified | boolean or null | Whether the PTR name resolves back to the address (forward-confirmed reverse DNS). A verified name under googlebot.com, google.com, search.msn.com, applebot.apple.com, yandex.* or crawl.baidu.* also proves the crawler. Null unless hostname was requested. See [forward-confirmed reverse DNS](/glossary/forward-confirmed-reverse-dns). |
| hostname_kind | string or null | What the naming convention says the address is: `dynamic` (a pool, DHCP, PPP or dial-up name) or `static` (a fixed, dedicated or hosting name). Null when the name says neither, or unless hostname was requested. |
| hostname_domain | string or null | The registrable domain of the PTR name (comcast.net, amazonaws.com), so the operator behind a reverse name is one field. Null unless hostname was requested. |
| is_datacenter | boolean or null | Inside known hosting or cloud ranges: the providers' own feeds, GitHub's hosted runner ranges, plus the open X4BNet list. See [datacenter IP](/glossary/datacenter-ip). |
| hosting_provider | string or null | Amazon Web Services, Google Cloud, Microsoft Azure, Oracle Cloud, DigitalOcean, Linode, Vultr, Cloudflare, Fastly or GitHub Actions when the address is inside that provider's own published ranges. |
| is_corporate_proxy | boolean or null | The egress of a corporate secure web gateway, from the cloud enforcement node ranges Zscaler publishes for its eight clouds. An employee behind one reaches the Internet from the gateway's address, so the location is the gateway's and the traffic is a workplace, not a home connection. No weight, and the datacenter weight is skipped for it. See [corporate proxy](/glossary/corporate-proxy). |
| corporate_proxy | string or null | The gateway operator: Zscaler. Null unless is_corporate_proxy is true. |
| is_crawler | boolean or null | Inside the ranges a crawler operator publishes for verification (Googlebot and Google's special crawlers and fetchers, Bingbot, GPTBot, ChatGPT-User, OAI-SearchBot, PerplexityBot, Perplexity-User, DuckDuckBot, AhrefsBot), or proved by a verified hostname. Not a risk: no weight, and the datacenter weight is skipped for it. See [the crawler pages](/tools/ip-lookup/crawlers). |
| crawler | string or null | The crawler that owns the range or the verified name: Googlebot, Bingbot, GPTBot, ChatGPT-User, OAI-SearchBot, PerplexityBot, Perplexity-User, DuckDuckBot, AhrefsBot, Applebot, YandexBot, Baiduspider, Google special crawlers or Google user-triggered fetchers. Null unless is_crawler is true. See [the crawler pages](/tools/ip-lookup/crawlers). |
| crawler_kind | string or null | What the crawler does: `search`, `ai_training`, `ai_assistant`, `seo` or `fetcher` (Google's special crawlers and user-triggered fetchers). |
| is_anycast | boolean or null | A published anycast range: CDN edges and the public resolvers. Geolocation means little for such an address. See [anycast](/glossary/anycast). |
| is_unallocated | boolean or null | No regional Internet registry has allocated or assigned the space (the five RIRs' daily statistics). Such an address should not be on the public Internet; it scores 100. See [bogon](/glossary/bogon). |
| is_blocklisted | boolean or null | A public threat blocklist names the address's netblock or its operator: Spamhaus DROP (netblocks hijacked or leased by spam and cybercrime operations), Spamhaus ASN-DROP (the operators behind them, matched by ASN) and abuse.ch's Feodo Tracker list of active botnet C2 servers. Scores 80. |
| blocklist | string or null | The list that names the address: Spamhaus DROP, Spamhaus ASN-DROP or abuse.ch Feodo Tracker, the first in that order when several do. |
| is_attack_source | boolean or null | An attack-source list names the address: single hosts seen attacking honeypots and sensors in the last days, from Emerging Threats' compromised-hosts list, the CINS Army list and blocklist.de, refreshed hourly. Apart from `is_blocklisted` because these name hosts rather than netblocks or operators and churn daily. Scores 50. Null where no list covers the family (the three are IPv4 today). See [attack-source list](/glossary/attack-source-list). |
| attack_source | string or null | The list that names the address: Emerging Threats compromised hosts, CINS Army or blocklist.de, the first in that order when several do. |
| is_tor | boolean or null | A listed Tor exit, from the Tor Project's bulk exit list and its Onionoo relay data, so IPv6 exits are covered too. Both lists are synced hourly. See [Tor exit node](/glossary/tor-exit-node). |
| tor_last_seen | string or null | When the address was last seen exiting Tor (YYYY-MM-DD): the current list's date while it is on it, otherwise the date the thirty-day exit history last saw it. Null when the history never saw it. Exits rotate daily, and an address that left the list yesterday is still the same machine. |
| is_vpn | boolean or null | A known VPN exit: the Mullvad, NordVPN, Private Internet Access, IVPN, AirVPN and VPN Gate server lists, the open X4BNet list, and Cloudflare WARP by inference. False on iCloud Private Relay egress unless a named list has the address. |
| is_relay | boolean or null | An iCloud Private Relay egress address, from Apple's published list. See [iCloud Private Relay](/glossary/icloud-private-relay). |
| privacy_service | string or null | Mullvad, NordVPN, Private Internet Access, IVPN, AirVPN, VPN Gate or iCloud Private Relay when a provider's own list names the address, and `Cloudflare WARP (inferred)` when Cloudflare's autonomous system carries the address outside the CDN ranges it publishes and outside the public resolvers: Cloudflare publishes no WARP egress list, so the suffix says the answer is an inference. |
| is_anonymous | boolean or null | True when any of is_tor, is_vpn or is_relay is true; false when at least one was checked and none matched; null when none was evaluated. |
| account_list | object or null | The account's own allow or deny list that names the address: `id`, `name`, `kind` (`allow` or `deny`) and the `entry` (address or prefix) that matched. Null when no list names it. |
| risk_score | number or null | Denylisted 100 + unallocated 100 + blocklist 80 + Tor 70 + reported_abuse 60 + attack_source 50 + datacenter 40 + reported_abuse_widely 40 + recently_tor 40 + VPN 30 + relay 10 over the signals checked, capped at 100; 0 whatever else fired when an allow list names the address. Null only when no feed evaluated the address and nobody reported or listed it. |
| risk_level | string or null | `high` from 60, `medium` from 30, `low` below that. |
| risk_signals | array | Each contributing signal with the weight applied and the dataset behind it; the weights sum to risk_score. An allow-list hit is one `allowlisted` entry of weight 0 and nothing beside it. |
| seen_by_you | object or null | How often this account looked the address up inside the last seven days before this call: `count`, `first_seen_at` and `window_days`, kept under a keyed hash of the address. Null under `privacy: true` and for reserved addresses. |
| sources | array | The datasets consulted, each with the date it was last synced. Only groups that were evaluated appear. |
Where the network operator publishes the location itself, that declaration wins over the database and `location_source` says `operator`: [RFC 8805 geofeeds](/tools/ip-lookup/sources/geofeeds) from DigitalOcean, Linode and Vultr, Apple's relay egress list, and the server lists of Mullvad, NordVPN, IVPN and AirVPN name the city, Private Internet Access and VPN Gate the country. When the operator disagrees with the database on country or city, the region and the coordinates answer null instead of a point in the wrong place.
Country facts come from data the service already ships: `currency` from ICU region data, `calling_code` from libphonenumber, and `timezone` from the IANA zone list when the country or a region known to lie in a single zone settles it. A visitor in California gets America/Los_Angeles from the map. A visitor in Texas, which straddles two zones, gets the zone of the nearest place in the GeoNames list of populated places (CC BY 4.0, synced monthly) inside the United States, and `timezone_source` says `nearest_city`; a coordinate more than 250 km from any listed place, or whose nearest place lies across a border, answers null rather than the nearest mainland zone. Every country's registries, regime, sanctions status, clock and measured accuracy are on the [country pages](/tools/ip-lookup/countries).
`sanctions` and `fatf_list` read the curated compliance file that ships with the service: the country-related programmes on the OFAC sanctions programme list (checked on 2026-09-12; comprehensive for Cuba, Iran and North Korea, and the programmes for the occupied regions of Ukraine are region-level, which a country code cannot answer) and the FATF's two lists as of the 19 June 2026 plenary (Iran, North Korea and Myanmar under a call for action; 22 jurisdictions under increased monitoring). They are facts about a place, never a verdict about a person: a visitor located in a listed country is somebody whose screening your compliance policy decides, not a fraud signal, and neither field carries a weight.
The threat blocklists are [Spamhaus DROP and ASN-DROP](/tools/ip-lookup/sources/spamhaus-drop), published by The Spamhaus Project and used with the credit its terms ask for, and abuse.ch's [Feodo Tracker](/tools/ip-lookup/sources/feodo-tracker) (CC0). A hit names the list in `blocklist`. ASN-DROP can only answer once the ASN database resolves the address, so when neither a range list nor the ASN could settle it the flag answers null rather than false.
The attack-source lists are a different kind of thing and are kept apart: [Emerging Threats' compromised-hosts list](/tools/ip-lookup/sources/emerging-threats-compromised) (from its open ruleset, BSD-licensed, credited here as its terms ask), the [CINS Army list](/tools/ip-lookup/sources/cins-army) ("provided as a simple text file, with which you can parse and use in any way you see fit", in CINS's words) and [blocklist.de](/tools/ip-lookup/sources/blocklist-de)'s export (a free service that publishes no licence text and offers the files "as they are"). They name single hosts seen attacking honeypots and sensors in the last days, so an entry says a machine was misbehaving this week, not that a netblock belongs to a criminal operation; that is why a hit weighs 50 rather than 80, why the lists are refreshed hourly, and why `attack_source` names the list so you can weigh the publisher yourself.
The [crawler ranges](/tools/ip-lookup/crawlers) are the files Google, Microsoft, OpenAI, Perplexity, DuckDuckGo and Ahrefs publish so sites can verify their bots, synced daily, and the corporate gateway ranges are the [cloud enforcement node lists](/tools/ip-lookup/sources/zscaler) Zscaler publishes so its customers can allow-list them. City answers improve over time from the RFC 8805 geofeeds discovered across the five registries every week, and the registry facts come from the same daily delegated statistics that answer `is_unallocated`. The [mobile carrier](/tools/ip-lookup/sources/mobile-carriers) and [satellite operator](/tools/ip-lookup/sources/satellite-operators) lists are curated files that ship with the service and name each operator's autonomous system; an operator not in them answers false, never a guess. Every source, its terms and its size today are on the [data sources pages](/tools/ip-lookup/sources).
## Risk score
The score is arithmetic over the flags in the same response, capped at 100. Recompute it, or ignore it and branch on the booleans. Three rules apply: the datacenter weight is skipped when `is_relay`, `is_crawler` or `is_corporate_proxy` is true, because relay egress, verified crawlers and corporate gateways sit on infrastructure by design and their operator is known; `is_vpn` answers false on relay egress unless a VPN provider's own list names the address, because Apple's first-party declaration outranks the aggregate list that also carries those ranges; and an address on one of your own allow lists scores 0 whatever else fired, with a single `allowlisted` signal of weight 0 saying why.
| Signal | Points |
| --- | --- |
| On your own deny list | +100 |
| Unallocated space (no registry has assigned it) | +100 |
| Threat blocklist (Spamhaus DROP or ASN-DROP, abuse.ch Feodo Tracker) | +80 |
| Tor exit | +70 |
| Reported by you as abuse, fraud, a bot or a chargeback inside the window | +60 |
| Seen attacking honeypots and sensors in the last days (Emerging Threats, CINS Army, blocklist.de) | +50 |
| datacenter or cloud range | +40 |
| Reported by three or more accounts | +40 |
| Seen exiting Tor in the last seven days, no longer listed | +40 |
| VPN exit | +30 |
| iCloud Private Relay egress | +10 |
Worked examples, as the API answers them:
| Situation | Signals | Score |
| --- | --- | --- |
| Home broadband, nothing matched | none | 0, low |
| Google's public resolver 8.8.8.8, in the aggregate datacenter list | datacenter 40 | 40, medium |
| A Mullvad exit hosted in a datacenter | datacenter 40, VPN 30 | 70, high |
| A Tor exit on a hosting provider | Tor 70, datacenter 40 | 100, high (capped) |
| A netblock on Spamhaus DROP, nothing else matched | blocklist 80 | 80, high |
| A host CINS saw attacking yesterday, on a home connection | attack_source 50 | 50, medium |
| A host CINS saw attacking yesterday, on a hosting range | attack_source 50, datacenter 40 | 90, high |
| iCloud Private Relay egress on a CDN | datacenter 0 (skipped for relay), relay 10 | 10, low |
| Googlebot on Google's own published ranges | datacenter 0 (skipped for a crawler); is_crawler true | 0, low |
| Zscaler egress on a datacenter range | datacenter 0 (skipped for a corporate gateway); is_corporate_proxy true | 0, low |
| An address that left the Tor exit list two days ago | recently_tor 40 | 40, medium |
| Home broadband you reported as fraud last week | reported_abuse 60 | 60, high |
| Home broadband on your deny list | denylisted 100 | 100, high |
| A Tor exit on your allow list | allowlisted 0 | 0, low |
| IPv6 address, Tor list not evaluated, nothing else matched | none; is_tor null | 0, low |
| Private address 192.168.1.1 | reserved_range | null, free |
`risk_signals` spells the sum out so nobody re-implements it. For the relay example it reads:
```json
"risk_signals": [
{ "signal": "datacenter", "weight": 0, "dataset": "datacenter-ranges" },
{ "signal": "relay", "weight": 10, "dataset": "relay-ranges" }
],
"risk_score": 10,
"risk_level": "low"
```
[Residential proxies](/glossary/residential-proxy) are not detectable with open data and are not claimed. When that signal ships it will be a separately labelled field, never folded into these weights. Nor is `connection_type` a score: it names what positive evidence says the address is attached to, and null never means residential.
## Self and browser lookups
The same lookup is available three ways from a server. `POST /api/v1/ip` takes the address in the body, `GET /api/v1/ip/{ip}` takes it in the path, and `GET /api/v1/ip/me` answers for the address the call came from, as seen through the proxy in front of the service. Options ride in the body or the query string.
```bash
curl https://spaw.co/api/v1/ip/me \
-H "Authorization: Bearer sk_live_…"
curl "https://spaw.co/api/v1/ip/2001:4860:4860::8888?privacy=1" \
-H "Authorization: Bearer sk_live_…"
curl "https://spaw.co/api/v1/ip/72.21.1.1?browser_timezone=America/Chicago&fields=country,timezone,timezone_matches,is_anonymous" \
-H "Authorization: Bearer sk_live_…"
```
Two options shape the answer rather than the lookup. `browser_timezone` is the IANA zone your page read from the visitor's browser (`Intl.DateTimeFormat().resolvedOptions().timeZone`); the response compares it with the located zone in `timezone_matches`, the cheapest strong signal that a connection is not where its owner is. A zone PHP does not know answers 422. `fields` is a comma-separated list of the field names to keep in `data`: unknown names are ignored, an empty list means everything, and `sources` is only sent when named, so a page that wants a country and a currency receives two keys rather than sixty. Both work on the three server endpoints and on the browser endpoint below. The two GET endpoints answer with `Cache-Control: private, max-age=300`, since nothing about an address changes within five minutes.
From a page, use a publishable key against `POST /api/v1/ip/public` (https://spaw.co/docs/api/lookup-ip-public). Create the key on the API keys page and list the domains it may run from; the endpoint checks the browser's `Origin` header against that list. Leave `ip` out and the visitor's own address is looked up, which is the usual case for choosing a language, a currency, or a consent banner on first paint:
```js
const response = await fetch('https://spaw.co/api/v1/ip/public', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({
key: 'pk_live_…',
browser_timezone: Intl.DateTimeFormat().resolvedOptions().timeZone,
fields: 'country,is_eu,currency,timezone_matches,is_anonymous',
}),
});
const { data } = await response.json();
if (data.is_eu) showConsentBanner();
if (data.currency) setDefaultCurrency(data.currency);
if (data.is_anonymous || data.timezone_matches === false) requireEmailConfirmation();
```
Lookups bill the key's owner and the response carries no meta block, so visitors never see the balance. Because the key sits in page source, give it a daily credit cap; once spent, the endpoint answers `429 KEY_SPEND_CAP_REACHED` until the next day, and free answers never count toward it. A key that carries a Cloudflare Turnstile pair requires a confirmed `turnstile_token` with every lookup. The endpoint is throttled at 20 requests a minute per visitor.
## Batch and bulk
`POST /api/v1/ip/batch` (https://spaw.co/docs/api/lookup-ip-batch) looks up as many as 1,000 addresses in one call, in input order, under the single-lookup billing rules: one credit buys five answered addresses, the lookup that opens a block reports `credits_used` 1 and the next four 0, and each item's `meta.ip_lookups_prepaid` says how many are left. Reserved and private ranges, unallocated space, addresses nothing knows and repeats are free and use none. Repeats inside the list are looked up once. If the balance runs out mid-list, the paid partial results come back with `meta.stopped_reason: "insufficient_credits"`; only a batch whose first lookup is refused answers the typed 402. A key with a daily credit cap stops the list the same way once the cap is spent, reporting `meta.stopped_reason: "key_spend_cap"`, so one large batch can never spend past the cap; a call that starts with the cap already spent answers `429 KEY_SPEND_CAP_REACHED` and runs nothing. A batch counts as one request against the rate limit.
```bash
curl https://spaw.co/api/v1/ip/batch \
-H "Authorization: Bearer sk_live_…" \
-H "Content-Type: application/json" \
-d '{"ips": ["8.8.8.8", "103.124.165.2", "192.168.1.1"]}'
```
For log files and exports, a bulk run queues up to 100,000 addresses with `POST /api/v1/ip/bulk` (https://spaw.co/docs/api/create-ip-bulk-job) or from the IP intelligence page in the dashboard (https://spaw.co/verify/ip). Poll `GET /api/v1/ip/bulk/{jobId}` for progress and the running low, medium and high counts, then download the CSV from `GET /api/v1/ip/bulk/{jobId}/results`: one row per input address, in input order, with these columns:
```
ip, version, reason, network, continent, country, country_name, is_eu, is_eea, privacy_regime, sanctions, fatf_list, region, city, latitude, longitude, location_source, location_confidence, timezone, timezone_source, currency, calling_code, asn, org, asn_type, connection_type, is_mobile, is_satellite, registry, registry_country, allocated_at, is_datacenter, hosting_provider, is_corporate_proxy, corporate_proxy, is_crawler, crawler, crawler_kind, is_blocklisted, blocklist, is_attack_source, attack_source, is_tor, tor_last_seen, is_vpn, is_relay, privacy_service, is_anonymous, account_list, risk_score, risk_level, credits_used
```
`sanctions` is written as its codes joined with `;` and `account_list` as `kind:name`. The fields that only mean something for one live request stay out of the file: `timezone_matches` needs a browser, `seen_by_you` would count the run's own rows, and the opt-in `abuse_contact`, `network_details` and hostname fields are never fetched by a run.
Once the run has settled, its payload carries a `summary` added up from the same cells the CSV holds, so it can never disagree with the download: `rows` and `reserved`, the top ten `countries` (`country`, `rows`) and `networks` (`asn`, `org`, `rows`), how many rows carried each signal in `signals` (`datacenter`, `corporate_proxy`, `crawler`, `tor`, `vpn`, `relay`, `anonymous`, `blocklisted`, `attack_source`, `mobile`, `satellite`), how many hit one of your lists in `listed` (`allow`, `deny`), and the `risk` split (`low`, `medium`, `high`). It is null until then. That is the shape of a log file: which countries, which networks, how much of the traffic was infrastructure.
Cancelling stops a queued job on the spot and a running one at its next checkpoint; rows already answered stay billed and downloadable. A run that failed part-way keeps the rows it had already written, and they stay downloadable too. With a `webhook_url`, the finish is POSTed there as `bulk_ip_job.finished`, signed with the secret creation returns once: `X-Spaw-Signature-V2` carries `t={unix seconds},v2={hex}`, the HMAC-SHA256 of `v2:{t}:{raw body}`, and a receiver refuses a `t` more than five minutes from its own clock so a captured delivery cannot be replayed later. The original `X-Spaw-Signature`, over the body alone, is sent beside it until 1 March 2027. Send an `Idempotency-Key` header to make creation safe to retry: the same key with the same request replays the job with `Idempotent-Replayed: true`, the same key with a different request answers `409 IDEMPOTENCY_KEY_REUSED`.
A run that stopped because the credit balance ran out is resumed rather than uploaded again: top up, then press Resume on the run in the dashboard or call `POST /api/v1/ip/bulk/{jobId}/resume` (https://spaw.co/docs/api/resume-ip-bulk-job). The whole list runs again from the top, and the rows it already answered are repeats inside the seven-day window, so only the rows it never reached are charged. The job keeps its id, its webhook and its cumulative `credits_used`, while `processed`, the verdict counts and the summary restart and are rebuilt as the rows come back. A run that stopped for any other reason, or that is already going again, answers `409 JOB_NOT_RESUMABLE`; one whose list has passed out of the 30-day retention window answers `409 JOB_INPUT_EXPIRED`, and an empty balance answers `402 INSUFFICIENT_CREDITS` rather than queueing a run that would stop again at once.
If a delivery fails — your endpoint was down for the half-minute the three attempts cover — ask for it again rather than falling back to polling: press **Send webhook again** on the run in the dashboard, or call `POST /api/v1/ip/bulk/{jobId}/webhook/redeliver` (https://spaw.co/docs/api/redeliver-ip-bulk-webhook). The body is byte-for-byte the body the first delivery carried, rebuilt from the run's own frozen row, so the two reconcile. The signature is new, and has to be: a receiver refuses anything whose `t` is more than five minutes from its own clock, so verify a redelivery exactly as you verify any other delivery. Nothing deduplicates it for you — a receiver that gets both the original and the redelivery sees `bulk_ip_job.finished` twice, and `job.id` is the key to deduplicate on. A run created without a `webhook_url` answers `409 WEBHOOK_NOT_CONFIGURED` and one that has not settled answers `409 JOB_NOT_FINISHED`; a run whose last delivery succeeded is not refused, because a receiver can lose a message it has already acknowledged. The delivery is queued rather than made while you wait — one attempt takes half a minute against an endpoint that is down, which is the endpoint you are asking about — so the call answers `202` and the run reads `webhook_status: "pending"` until a worker has an answer, then `delivered` or `failed` with what your endpoint said in `webhook_detail`. The dashboard shows the same three states beside the run. It is free and capped at ten 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.
IP endpoints have their own rate limit of 50 requests per second per key, separate from the 5 per second that applies to the email endpoints.
## Outcome feedback
A lookup can only say what the feeds know. What you learn afterwards, a confirmed fraudulent order, a scraper, a bot that got through, a chargeback, can be sent back so your own later lookups of that address carry it: `POST /api/v1/ip/feedback` (https://spaw.co/docs/api/report-ip-feedback) takes up to 1,000 items of an address and an outcome, `abuse`, `fraud`, `bot` or `chargeback`, with an optional `occurred_at`. It is free and never logged as a lookup.
```bash
curl https://spaw.co/api/v1/ip/feedback \
-H "Authorization: Bearer sk_live_…" \
-H "Content-Type: application/json" \
-d '{"items": [{"ip": "203.0.113.9", "outcome": "fraud"}]}'
```
Any report you made about an address inside the last 30 days adds `reported_abuse` (+60, dataset `abuse-feedback`) to your own lookups of it. Once three or more accounts report the same address inside 30 days, everyone's lookups of it carry `reported_abuse_widely` (+40); only the number of reporting accounts is ever read, never who reported or what. Neither signal turns a null score into a number: an address no feed evaluated stays unscored.
Reports are the one thing the IP product stores on purpose, and even they never hold the address: each is a salted SHA-256 hash of the canonical address with the outcome and a timestamp, which can confirm a later lookup of the same address but cannot be read back into one. Malformed and reserved addresses are skipped and counted, not rejected. Reports are kept for 90 days, and `GET /api/v1/ip/feedback/summary` (https://spaw.co/docs/api/ip-feedback-summary) answers that window: totals per outcome, distinct addresses, and the IPv4 and IPv6 split.
## Monitored lists
Your own addresses deserve the same check as your visitors'. A monitored list (`POST /api/v1/ip/monitors`, https://spaw.co/docs/api/create-ip-monitor) keeps up to 500 of them, egress addresses, mail servers and office connections, under a daily, weekly or monthly re-check. The first run sets the baseline; from the next run on, every address that newly lands on a threat blocklist or an attack-source list, becomes a Tor exit or scores high arrives by email with the reason, so you learn that a mail server sits on Spamhaus DROP before your recipients' inboxes do.
An entry can be a prefix as well as an address: a CIDR of at most 256 addresses (`/24` to `/32`, `/120` to `/128`) is expanded into its addresses when the list is saved, and the whole list still fits under the 500 cap. A wider prefix is refused with a 422 that says to split it, because a `/16` is 65,536 lookups a run and would be a bill nobody asked for.
Each run bills every address like a single lookup, five answered addresses to the credit, and the seven-day repeat marker applies, so a daily list costs at most one credit per five addresses a week; reserved and unallocated addresses are free. `GET /api/v1/ip/monitors/{id}` (https://spaw.co/docs/api/get-ip-monitor) answers the last run per address: its status (clean, flagged or reserved), the flags that fired (blocklisted, attack_source, tor, high_risk), the list that names it, and whether it changed. `POST /api/v1/ip/monitors/{id}/run` re-checks the list ahead of schedule, and `DELETE /api/v1/ip/monitors/{id}` stops it.
A monitor can post its runs as well as email about them. Set `webhook_url` (https) when you create it, or later with a `PATCH`, and the signing secret comes back once in that response and is never readable again. `webhook_events` decides which runs are posted: `changes`, the default, posts only when the run found something new — an address that is newly flagged, or one whose status changed — or stopped early, and `every_run` posts after every run, which also tells you the monitor is still running. The body is `{ "event": "ip_monitor.run", "monitor": {…}, "summary": {…}, "changes": [ … ] }`, where `changes` names the addresses the alert email names, and the delivery is signed with `X-Spaw-Signature-V2`: `t={unix seconds},v2={hex}`, where the hex is the HMAC-SHA256 of `v2:{t}:{raw body}` keyed with the secret. Refuse anything whose `t` is more than five minutes from your own clock — that window is what stops a captured delivery being replayed at you later. The original `X-Spaw-Signature`, the HMAC of the body alone with no timestamp, is sent beside it until 1 March 2027 and still verifies; both recipes are at https://spaw.co/integrations/webhooks. Connection errors and 5xx answers are retried twice, three attempts in all; `webhook_status` and `webhook_detail` on the monitor say how the last delivery went. A URL that resolves onto a private network is refused at delivery, every time, because DNS can be repointed after a URL is saved.
A monitor is changed rather than replaced: `PATCH /api/v1/ip/monitors/{id}` (https://spaw.co/docs/api/update-ip-monitor) takes `name`, `ips`, `cadence` and the webhook fields, and applies only the ones you send. That matters because what the last run recorded per entry is the baseline every alert is compared against: deleting a monitor and creating it again throws that away, and the first run of the new one only sets a baseline — a full cycle of silence, which is the one thing a monitor is bought to prevent. Replacing the list keeps what was recorded about the addresses that stayed, deletes what was recorded about the ones you removed, and gives a new entry no baseline. Changing the cadence neither skips a run nor starts a second one: a run already scheduled sooner than the new interval still happens, and a longer interval applies from the one after it. A body naming no field it could change answers `422`.
A monitor that stops being run is deleted, and the identifiers and results it stored go with it. `prunes_after_days` on every monitor payload says how long that takes, and `meta.retention_days` on the list endpoint says the same. A monitor in active use is never pruned: each run advances `last_run_at`, so the window only closes on a list that has genuinely stopped being re-checked — at which point holding the details somebody gave you serves no purpose. Deleting a monitor yourself removes everything it held at once.
## Your own lists
The feeds know the Internet; only you know your office egress, your partners' ranges and the addresses that already burned you. An account keeps up to 20 lists of up to 5,000 entries each, every entry an address or a CIDR prefix (IPv4 or IPv6), of one of two kinds. An `allow` list zeroes the score of every address it names: `risk_score` 0, `risk_level` low, and `risk_signals` carrying a single `allowlisted` entry of weight 0 so the reader sees why. A `deny` list tops it: `denylisted` +100, which is `high` on its own, whether or not any feed had anything to say. Whichever list matched is named in `account_list` with the entry that matched, and `sources` gains `account-lists` with the date the list last changed. Deny lists are checked before allow lists, so an address on both fails closed.
```bash
curl https://spaw.co/api/v1/ip/lists \
-H "Authorization: Bearer sk_live_…" \
-H "Content-Type: application/json" \
-d '{"name": "Office egress", "kind": "allow", "entries": ["203.0.113.0/24", "2001:db8:10::/48"]}'
```
`GET /api/v1/ip/lists` names the account's lists with their entry counts and the limits in `meta`; `POST` creates one (201); `GET /api/v1/ip/lists/{listId}` returns it with its entries; `PATCH` renames it, changes its kind or replaces its entries, applying only the fields you send, and a body naming none is a 422; `DELETE` removes it. Entries are canonicalised and deduplicated, an entry that is neither an address nor a prefix is a 422, and creating a twenty-first list answers `409 IP_LIST_LIMIT_REACHED`. The dashboard's IP page has a Lists tab for the same thing by hand.
Lists belong to the account, not to the member who created them: any member manages any list, and they apply to every lookup the account is billed for — the API, the dashboard, batches, bulk runs, monitors and the browser widget alike. They are the one signal in the answer that is yours rather than the Internet's, and the bulk summary counts how many rows hit one. Nothing about an entry leaves the account, and a list is deleted with it.
## Networks and ranges
Two lookups answer about a network rather than an address. `GET /api/v1/ip/asn/{asn}` (https://spaw.co/docs/api/get-ip-asn) turns the ASN database inside out: every prefix the database assigns to the autonomous system, in both families, with the operator's name, the `asn_type` the name settles, the registry facts the RIR statistics record for the system (`registry`, `registry_country`, `allocated_at`), and the operator-level flags the feeds answer without an address: `is_blocklisted` with the `blocklist` that names it (Spamhaus ASN-DROP), `is_satellite` and `is_mobile`. `prefix_count` is never capped; `prefixes` carries at most 500 per family in index order and `truncated` says when a family was cut. A system the index does not know answers `404 ASN_NOT_FOUND`, free; a server whose index has not been built yet answers `501 ASN_INDEX_UNAVAILABLE`, free. An answer costs 1 credit, and the seven-day repeat rule applies per number.
```bash
curl https://spaw.co/api/v1/ip/asn/14593 \
-H "Authorization: Bearer sk_live_…"
```
`GET /api/v1/ip/range?prefix=203.0.113.0/24` (https://spaw.co/docs/api/lookup-ip-range) answers about a prefix (a bare address is a /32 or /128): the canonical `prefix`, `first` and `last` address, `addresses` (null when 2^bits does not fit an integer), the `network` of the first address with `network_agrees` (whether the last address resolves to the same system), the `country` of the first address with `country_agrees`, the registry facts of the first address, and `signals`: for each of datacenter, vpn, relay, tor, anycast, allocated, blocklist, attack_source, crawler and corporate_proxy, whether any feed record `overlaps` the prefix (tri-state like the flags, null when no installed feed of that signal covers the family), how many `ranges` intersect it (capped at 10,000 per feed, `truncated` says so) and the `providers` named. It is the quick way to learn that a /24 sits on Spamhaus DROP or that a partner's range is really a cloud's, before adding it to a list or a monitor. A prefix inside reserved space answers `reason: reserved_range` with everything else null, free; an answer costs 1 credit with the repeat rule keyed by the canonical prefix.
```bash
curl "https://spaw.co/api/v1/ip/range?prefix=8.8.8.0/24" \
-H "Authorization: Bearer sk_live_…"
```
The ASN index is rebuilt from the monthly DB-IP ASN database by `lookup:index-asns` and reported on /status as `asn-prefixes`; both endpoints sit under the `ip` key scope and the IP rate limit. The same facts are published without a key for the largest systems on the [autonomous system pages](/tools/ip-lookup/asn), and every feed a range answer reads has its own [data source page](/tools/ip-lookup/sources).
## Privacy
An IP address is personal data, and the service treats it that way. Nothing about an IP lookup is stored: no history row, no log line with the address. The only traces are a keyed seven-day marker that makes a repeat of the same address free, and, under the same keyed hash, the count behind `seen_by_you`: how many times your account asked about the address inside the window and when it first did, which is a velocity signal (fifty signups from one address in an hour) that no feed could give you. Neither holds the address. Send `privacy: true` in the body, or `?privacy=1` on a GET, to skip both; every such call is then a fresh, billable lookup and `seen_by_you` answers null.
IP lookups never leave the server. Every signal is a local database or a compiled feed, so no third-party processor sees the address, with one opt-in exception: `abuse_contact: true` sends the address to the Internet registry's public RDAP service (via rdap.org) to read the network's abuse mailbox, cached per allocation. The `sources` block names each dataset consulted and the date it was synced. Bulk input lists and result files are the only copies of what you sent, and both are deleted with the job after 30 days.
The security page (https://spaw.co/security) states the same facts alongside the rest of the data handling, and a data processing agreement is available on request from support@spaw.co.
Next: the endpoint reference for every field, example, and error code: https://spaw.co/docs/api/lookup-ip.
---
# Phone intelligence
How Spaw phone intelligence works: every field, where each fact comes from, the recomputable risk score, the signals only your own traffic can raise, the reasons, batch and bulk runs, browser lookups, test numbers, and what is stored.
One lookup turns a phone number in any notation into its canonical forms, names the country and its facts, reads the line type and the block holder off the numbering plan and the regulators' own allocation tables, and flags the numbers that are not a person's handset: fictional ranges, blocks held by virtual-number wholesalers, numbers published by SMS-receiving websites. Every answer comes from local data, so it takes milliseconds and the number never leaves the server unless you ask for the live carrier-network check. The live carrier-network check (`hlr: true`) is not enabled on the service: `hlr_checked` answers false and nothing extra is charged. This guide covers every field.
```bash
curl https://spaw.co/api/v1/phone \
-H "Authorization: Bearer sk_live_…" \
-H "Content-Type: application/json" \
-d '{"phone": "020 7946 0018", "country": "GB"}'
```
## Fields and flags
Every response carries the same 49 fields in the same order, then `sources`. The flags are tri-state: `false` is a checked negative and `null` means the signal was not evaluated, because no regulator table covers the region or the list behind it is not installed. Nothing is guessed to fill a null.
| Field | Type | Meaning |
| --- | --- | --- |
| valid | boolean | Assignable under the numbering plan and, where a regulator table is installed, inside an allocated block. A number in a range reserved for fiction (`is_fictional`) answers valid even where the regulator lists its block as protected, with `block_status` unassigned. |
| reason | string or null | Why it is not valid: `missing_country`, `invalid_country_code`, `too_short`, `too_long`, `not_a_number`, `invalid_length`, `invalid_number`, `unassigned_block`, `emergency_number` or `short_number`. `unassigned_block` keeps the parsed number; every other reason answers null fields. Invalid answers are free. Glossary: [Short number](https://spaw.co/glossary/short-number). |
| e164 | string or null | The canonical form to store and dedupe by, such as +14155550142. Glossary: [E.164](https://spaw.co/glossary/e164). |
| national, international | string or null | Display formats: (415) 555-0142 and +1 415-555-0142. |
| rfc3966 | string or null | The tel: URI, tel:+1-415-555-0142, ready for a click-to-call link. |
| country | string or null | ISO 3166-1 alpha-2. Null for non-geographic numbers such as international toll-free +800. Glossary: [International network number](https://spaw.co/glossary/international-network-number). |
| country_name, calling_code | string, number or null | The English name of the country and its calling code, such as 44. |
| continent, is_eu, currency | string, boolean, string or null | Facts about the number's country, from the same data the IP lookup uses: continent code, EU membership, ISO 4217 currency. |
| line_type | string or null | `mobile`, `fixed_line`, `fixed_line_or_mobile`, `voip`, `toll_free`, `premium_rate`, `shared_cost`, `personal_number`, `pager`, `uan`, `voicemail` or `unknown`. A UK 084 or 087 number answers `shared_cost`, not `premium_rate`: Ofcom's numbering plan classes them as service numbers with a capped charge, and keeps premium rate for 09. Glossary: [Line type](https://spaw.co/glossary/line-type). |
| line_type_source | numbering_plan, block_allocation or null | `numbering_plan` when the numbering plan decided the type: libphonenumber's metadata, or the regulator's plan where it types a range differently (the drama ranges the metadata leaves out, such as Ofcom's 07700 900xxx and ARCEP's +262 639 98, and UK 084 and 087, which the plan classes as shared cost). `block_allocation` when a North American `fixed_line_or_mobile` was settled by the block holder: a wireless carrier's block is mobile, a virtual-number wholesaler's is voip. |
| carrier | string or null | The block holder as the regulator lists it, else the carrier the metadata knows for the prefix. The original allocation, never a later port. Glossary: [Number portability](https://spaw.co/glossary/number-portability). |
| carrier_source | numbering_plan, block_allocation or null | Where the carrier came from. |
| block_status | allocated, unassigned, reserved or null | What the regulator's table says about the block: allocated to a holder, unassigned (free, protected or quarantined, so no number in it can be in service), or reserved for special use. Null when no table covers the region or the table says nothing about the number. Glossary: [Unassigned block](https://spaw.co/glossary/unassigned-block). |
| region | string or null | A geographic description such as "San Francisco, CA". |
| is_geographic | boolean or null | Tied to a place (fixed lines and geographic mobiles) rather than a service such as toll-free. |
| timezones | string[] | Every IANA zone the number can belong to: one for most numbers, several for country-wide ranges, none for non-geographic numbers. |
| extension | string or null | Parsed from input like "ext 123" and kept out of e164. |
| is_fictional | boolean or null | The number sits in a range reserved for fiction and drama: the NANP 555-01XX block, or a range or number Ofcom, ARCEP, ACMA or ComReg sets aside for drama. Still valid, never in service. Glossary: [Fictional number](https://spaw.co/glossary/fictional-number). |
| is_virtual | boolean or null | The block is held by a virtual-number wholesaler or VoIP provider. Null when no regulator table covers the number. Glossary: [Virtual number](https://spaw.co/glossary/virtual-number). |
| is_disposable | boolean or null | Published by an SMS-receiving website, so anyone can read a code sent to it. Null until the list is installed. Glossary: [SMS-receiving number](https://spaw.co/glossary/sms-receiving-number). |
| disposable_seen_at | string or null | When that list last saw the number (YYYY-MM-DD), where the list says so. A number the lists still carry scores `disposable` until this date is a year old; a number every list has dropped, only until it is 90 days old. After that it scores `disposable_stale` instead. |
| risk_score, risk_level | number, string or null | The sum of the weights below, capped at 100; `high` from 60, `medium` from 30. Null for invalid numbers. |
| risk_signals | array | Each contributing signal with the weight applied and the dataset behind it; the weights sum to risk_score. |
| hlr_checked | boolean | Whether a live carrier-network check answered; false whenever the lookup did not ask for one. The live carrier-network check (`hlr: true`) is not enabled on the service: `hlr_checked` answers false and nothing extra is charged. Glossary: [Live carrier check](https://spaw.co/glossary/live-carrier-check). |
| reachable, ported, current_carrier, roaming | boolean, boolean, string, boolean or null | What the live check learned about the handset. Null until it runs. |
| carrier_code | string or null | The regulator's code for the block holder (the NANPA or CNAC operating company number, the ARCEP operator code, the UKE operator ID). Like `carrier`, it names the original allocation, not a later port; in a pooled US code it is the holder of the number's own thousands-block where NANPA lists that block. Null when no table row names one, including when `carrier` comes from the plan's prefix metadata. Glossary: [Operating company number (OCN)](https://spaw.co/glossary/operating-company-number). |
| carrier_brand | string or null | The network brand behind `carrier` ("AT&T" for NEW CINGULAR WIRELESS PCS, "Lumen" for QWEST CORPORATION), from a bundled, versioned map (`carrier-brands` in `sources`) matched by the holder's OCN first and its name second, country by country. Null when the name is not mapped. |
| block_pooled | boolean or null | North American numbers only: the central-office code is pooled into thousands-blocks. In the United States `carrier`, `carrier_code` and `line_type` then describe the holder of the number's own thousands-block, from NANPA's block report; in Canada, and for a US block the report does not settle, they describe the code holder, not necessarily this number's block. Null outside the NANPA and CNAC tables. Glossary: [Number pooling](https://spaw.co/glossary/number-pooling), [NPA/NXX](https://spaw.co/glossary/npa-nxx). |
| block_assigned_at | string or null | When the regulator assigned the code or range (YYYY-MM-DD), where its table dates it (NANPA, CNAC, ARCEP); in a pooled US code, when NANPA assigned the number's own thousands-block. Null for a block its holder retained when the code was pooled, which NANPA leaves undated; for a pooled code the code holder answers for (in Canada, or a block NANPA's report does not settle), because the code's date says nothing about the number's block; and for most US codes assigned before 2001, which NANPA leaves undated too: a null does not mean new. |
| region_level | locality, area, country or null | How specific `region` is: `locality` for a North American rate centre or exchange area, or a place named with its state ("San Francisco, CA"); `area` for the area a code covers, usually named after its main city or, in North America, its state ("London", "California"); `country` when the plan only places the number in its country, as for most mobiles ("France"). Read from where the description came from and its shape, not measured. Null when `region` is null. Glossary: [Rate centre](https://spaw.co/glossary/rate-centre), [Area-code overlay](https://spaw.co/glossary/area-code-overlay). |
| local_time | string or null | The local time at the number now (ISO 8601, "2026-09-24T09:30:00-04:00"), only when every zone in `timezones` shares the current UTC offset. Worked out on every request, never stored. |
| utc_offsets | string[] | The distinct current UTC offsets across `timezones` ("-05:00"), sorted west to east. More than one means the number's zones disagree right now. Empty when there are no zones. |
| in_call_window | boolean or null | Only when the request sent `call_window`: true when the local time in every zone in `timezones` is inside it, false when any is outside. Null without a window, without zones, and on an answer served from the suppression list. Glossary: [Calling window](https://spaw.co/glossary/calling-window). |
| did_you_mean | string or null | Invalid answers only, when you sent a country: the E.164 form of the one valid reading of what was typed, the digits with a missing + (447400123456 sent with US) or with a foreign international prefix left in (011 outside North America). A suggestion, never applied: the answer stays invalid and free. Null when no reading or more than one is valid, when the reading lands in a block nobody holds (unless it sits in a range reserved for fiction, which answers valid), or when the input could be a national number with one digit too many or too few. Glossary: [International (exit) prefix](https://spaw.co/glossary/international-prefix). |
| range_seen_by_you | object or null | `{distinct_numbers, window_minutes}`: distinct numbers your account looked up in the same 1,000-number range (the number without its last three digits) inside the window, this one included. Counted for valid single and browser lookups only; null on batch, bulk and monitor rows, test numbers, invalid answers and publishable-key answers. Glossary: [SMS pumping](https://spaw.co/glossary/sms-pumping). |
| client_velocity | object or null | `{distinct_numbers, window_minutes}`: distinct numbers your account looked up from the same client address inside the window, this one included: the `client_ip` you sent, or the browser visitor's address; an IPv6 address counts by its /64. Null without a client address, on every answer `range_seen_by_you` does not count, and on publishable-key answers. |
| destination_new_for_you | boolean or null | True when your account has not looked up a valid number in this country in the last 30 days. Null until a week of the account's own lookups has been counted, on every answer `range_seen_by_you` does not count, and on publishable-key answers. No weight. |
| complaints_90d, complaints_last_at | number, string or null | North American numbers only: consumer complaints naming the number as the caller in the FTC's reported-calls data over the last 90 days, and the date of the latest; 0 when nobody complained. Null outside the NANP or before the first sync. A count, not a verdict: the reports are unverified and caller ID is often spoofed. Glossary: [Unwanted-call complaints](https://spaw.co/glossary/unwanted-call-complaints). |
| ok_to_send | boolean | Your account's SMS policy applied to this answer (see The SMS policy below). A fraud and deliverability decision, not consent and not a do-not-call scrub. Glossary: [SMS policy](https://spaw.co/glossary/sms-policy). |
| blocked_by | string or null | The first rule that decided a false, in this order: `invalid`, `opted_out`, `country`, `line_type`, a risk-signal name, `risk_score`, then `unreachable` or `unconfirmed`; `suppressed` on batch, bulk and monitored answers served from your suppression list. `opted_out` answers a single or browser lookup of a number your suppression list holds as opted out, while a batch, bulk or monitored run answers `suppressed` for it. Null when `ok_to_send` is true. |
| sources | array | The datasets consulted, each with its version or sync date: the numbering metadata always, the regulator table when it answered, the carrier brand map when it named a brand, the fictional ranges, the listed numbers, the complaint data for a North American number (dated by its newest file), your own outcome reports when one fired a signal, `account-velocity` (today) when `range_burst` or `numbers_per_client` fired, the live check when it ran. |
Two optional request fields feed the answer. `client_ip` is the address of the person behind the lookup, your visitor rather than your server: it is counted for `client_velocity`, never looked up and never billed. `call_window`, such as `"09:00-20:00"` in the number's local time, answers `in_call_window`; the batch takes it too. The window runs from its start up to, not including, its end, and may run past midnight (`"21:00-08:00"`). A number whose zones disagree is inside only when every one of them is: an Australian mobile spans six, on five clocks.
The zone is where the number was issued, not where the person is: a mobile travels with its owner and a VoIP number can be answered anywhere. `in_call_window` is clock arithmetic, not compliance advice. Calling-hour rules apply at the called person's actual location, some states and countries set shorter hours or ban whole days, and no holiday calendar or do-not-call register is checked. You choose the window.
Two fields carry a source next to them because two datasets can answer them. The numbering plan decides `line_type` everywhere except in the North American plan, where mobiles and landlines share the same blocks and the plan can only say `fixed_line_or_mobile`. There the block holder settles it when it can: a block held by a wireless carrier is mobile, one held by a virtual-number wholesaler is voip, and `line_type_source` says `block_allocation`. A block held by a company whose name settles nothing keeps the plan's answer. The `carrier` prefers the regulator's block holder over the plan's prefix metadata, because the table is more granular and refreshed weekly (NANPA's daily).
Neither source is port-aware. A number ported to another carrier keeps its original block, so both fields describe who the numbers were allocated to, not who serves them today. The live carrier-network check behind `hlr: true` is the only thing that can answer that, and it is reported in its own fields so the two are never confused.
Most North American codes are pooled: 72% of the assigned US codes are split into thousands-blocks that carriers other than the code holder can hold. In the United States NANPA also publishes who holds each of those blocks, and the lookup reads it: `carrier`, `carrier_code`, `carrier_brand`, a `line_type` settled by the holder and `is_virtual` describe the holder of the number's own block of a thousand. A block NANPA lists as available answers `unassigned_block`, like a code nobody holds, while the copy is fresh. A block still partly in use by its former holder, or one available only from a future date, keeps the code holder's answer. In Canada the table is the code holder's alone. `block_pooled` says the code is pooled, and `block_assigned_at` is the date NANPA assigned the number's own block; it stays null for a block its holder retained when the code was pooled, which NANPA leaves undated, and wherever the code holder answers, because the code's assignment date says nothing about the number's block.
## Where each fact comes from
Every response names the datasets it consulted, with the version or sync date each one carried, so an answer can be checked against the data that produced it. [Phone facts by country](https://spaw.co/tools/phone-number-lookup/countries) lists every numbering region with its calling code, its number formats and, where there is one, the table below that covers it. [Phone data sources](https://spaw.co/tools/phone-number-lookup/sources) gives each publisher a page: its files and their format, the terms as it states them, and what the copy installed here holds.
| Dataset | Settles | Refreshed |
| --- | --- | --- |
| libphonenumber | Validity, formats, line type, the carrier prefixes it knows, geographic descriptions, time zones, emergency numbers and short codes. | every release, version named in each response |
| nanpa-co-codes | For the United States and its territories: which central office codes are assigned, to whom, and therefore which geographic blocks nobody holds; and, from NANPA's thousands-block report, who holds each block of a thousand inside a pooled code and which of those blocks are free. See [phone facts for the United States](https://spaw.co/tools/phone-number-lookup/countries/us). | daily, from NANPA's daily reports |
| cnac-co-codes | The same for Canada, from the Canadian Numbering Administrator's code status table. See [phone facts for Canada](https://spaw.co/tools/phone-number-lookup/countries/ca). | weekly |
| ofcom-numbering | For the United Kingdom and the Crown dependencies: allocated, free, protected and quarantined blocks and their communications providers. See [phone facts for the United Kingdom](https://spaw.co/tools/phone-number-lookup/countries/gb). | weekly |
| arcep-numbering | For France and its overseas regions: attributed and frozen ranges with the operator behind each, so a geographic number in a range nobody holds is unassigned. See [phone facts for France](https://spaw.co/tools/phone-number-lookup/countries/fr). | weekly |
| acma-numbering | For Australia: allocated and spare ten-digit ranges with their current holder, from the ACMA numbering register. See [phone facts for Australia](https://spaw.co/tools/phone-number-lookup/countries/au). | weekly |
| acm-nummerregister | For the Netherlands: assigned, cooling-off and blocked ranges with their holder, from the ACM number register. See [phone facts for the Netherlands](https://spaw.co/tools/phone-number-lookup/countries/nl). | weekly |
| nad-number-register | For New Zealand: assigned, spare and reserved codes with their holder, from the Number Administration Deed register. See [phone facts for New Zealand](https://spaw.co/tools/phone-number-lookup/countries/nz). | weekly |
| ift-numeracion | For Mexico: every assigned range with its operator and modality from the national numbering plan the CRT (formerly the IFT) publishes, so a number in a range nobody holds is unassigned. See [phone facts for Mexico](https://spaw.co/tools/phone-number-lookup/countries/mx). | weekly |
| uke-numeracja | For Poland: fixed and mobile scopes with the operator behind each, and the space UKE holds in reserve, so a number in a reserve is unassigned. See [phone facts for Poland](https://spaw.co/tools/phone-number-lookup/countries/pl). | weekly |
| mimit-numerazione | For Italy: assigned mobile, nomadic (55) and geographic ranges with the operator each was assigned to, from the MIMIT numbering workbook. See [phone facts for Italy](https://spaw.co/tools/phone-number-lookup/countries/it). | weekly |
| fictional-ranges | The NANP 555-01XX block and the drama ranges and numbers of Ofcom, ARCEP, ACMA and ComReg, each with the regulator's document and the day it was read. | bundled and versioned |
| carrier-brands | The network brand behind a carrier name (`carrier_brand`): the legal entities and former names of the main operators of the regulator-table countries, matched by OCN first and name second. | bundled and versioned |
| disposable-phone-numbers | Numbers published by SMS-receiving websites, with the date each was last seen. A number that leaves the lists is kept at that date for two years, so it ages rather than vanishing: it scores `disposable` for 90 days after it was last seen, then `disposable_stale`. | daily |
| us-complaint-reports | Complaints consumers filed with the FTC naming a North American number as the caller, from its Do Not Call reported-calls data: counts and dates over 90 days. The consumer's location is dropped when the file is read. | daily, from the FTC's weekday files |
| test-numbers | The eight documented test numbers below. | bundled |
What the lookup deliberately does not claim: it does not know who serves a ported number; it does not split US mobiles from landlines beyond what the block holder proves; a valid number is assignable, not necessarily in service; and there is no caller name, no reverse lookup and no do-not-call scrub, because those sit on regulated personal data that open sources do not carry. The complaint counts are not one either: they are what consumers told the FTC about the numbers that called them, not the Do Not Call Registry, and they say nothing about whether you may call or text a number. Nor are they proof of wrongdoing: the FTC verifies none of it and caller ID is often spoofed, so an innocent number can collect complaints, which is why the weight is small.
### Test numbers
Eight fixed numbers in the Washington 555-01XX fiction block always answer the same result, cost no credits and leave no repeat marker, on every endpoint including batch, bulk and the browser endpoint. National notation works with country US.
| Number | Answer |
| --- | --- |
| +1 202 555 0100 | valid, mobile from the block holder, carrier Spaw Test Wireless, risk 0 |
| +1 202 555 0101 | valid, fixed_line, carrier Spaw Test Telecom, risk 0 |
| +1 202 555 0102 | valid, voip, is_virtual true, risk 40 |
| +1 202 555 0103 | valid, is_disposable true, seen today, risk 70 |
| +1 202 555 0104 | invalid, reason unassigned_block, the parsed number kept |
| +1 202 555 0105 | valid, is_fictional true, risk 80 |
| +1 202 555 0106 | invalid, reason invalid_number, every other field null |
| +1 202 555 0113 | valid, not listed but in a range crowded with SMS-receiving numbers, risk 30 |
## Risk score
The score is arithmetic over the flags in the same response, capped at 100. Recompute it, or ignore it and branch on the booleans. It measures how unlikely a number is to reach a person's own handset, not fraud, and an invalid number has no score at all.
| Signal | What fires it | Points |
| --- | --- | --- |
| [`fictional`](https://spaw.co/docs/signals/phone/fictional) | Range reserved for fiction (555-01XX; Ofcom, ARCEP, ACMA and ComReg drama ranges) | +80 |
| [`disposable`](https://spaw.co/docs/signals/phone/disposable) | Published by an SMS-receiving website: still listed and seen within the last year, or dropped from the lists and seen within 90 days | +70 |
| [`premium_rate`](https://spaw.co/docs/signals/phone/premium_rate) | Premium-rate number (in the UK, the 09 range) | +60 |
| [`reported_abuse`](https://spaw.co/docs/signals/phone/reported_abuse) | Reported as abused by your own account within 90 days | +60 |
| [`reported_undelivered`](https://spaw.co/docs/signals/phone/reported_undelivered) | Reported as undelivered by your own account within 90 days, with no later delivery or verification | +50 |
| [`reported_abuse_widely`](https://spaw.co/docs/signals/phone/reported_abuse_widely) | Reported as abused by three or more accounts within 30 days | +40 |
| [`reserved_block`](https://spaw.co/docs/signals/phone/reserved_block) | Block reserved for special use (555, test codes, designated ranges) | +40 |
| [`virtual`](https://spaw.co/docs/signals/phone/virtual) | Block held by a virtual-number wholesaler | +40 |
| [`disposable_stale`](https://spaw.co/docs/signals/phone/disposable_stale) | Published by an SMS-receiving website, but not recently: still listed and last seen more than a year ago, or dropped from the lists and last seen more than 90 days ago | +40 |
| [`range_burst`](https://spaw.co/docs/signals/phone/range_burst) | Your account looked up several numbers in this 1,000-number range within the window | +40 |
| [`voip`](https://spaw.co/docs/signals/phone/voip) | VoIP number by the numbering plan | +30 |
| [`pager`](https://spaw.co/docs/signals/phone/pager) | Pager number | +30 |
| [`digit_pattern`](https://spaw.co/docs/signals/phone/digit_pattern) | Repeated or sequential subscriber digits (555-5555, 123-4567) | +30 |
| [`numbers_per_client`](https://spaw.co/docs/signals/phone/numbers_per_client) | One client IP tried several different numbers within the window | +30 |
| [`disposable_range`](https://spaw.co/docs/signals/phone/disposable_range) | Not listed itself, but in a 1,000-number range holding ten or more listings that still score `disposable` | +30 |
| [`international_network`](https://spaw.co/docs/signals/phone/international_network) | International network or satellite number (+870, +881, +882, +883) | +30 |
| [`shared_cost`](https://spaw.co/docs/signals/phone/shared_cost) | Shared-cost number, including the UK 084 and 087 ranges | +20 |
| [`reported_unwanted_calls`](https://spaw.co/docs/signals/phone/reported_unwanted_calls) | Consumers reported unwanted calls from this North American number three or more times, on at least two days, within 90 days | +20 |
Worked examples, as the API answers them:
| Situation | Signals | Score |
| --- | --- | --- |
| A UK mobile in a block Ofcom lists as allocated to its carrier | none | 0, low |
| A US number in a block held by a virtual-number wholesaler | virtual 40 | 40, medium |
| A number an SMS-receiving site still lists but last showed fourteen months ago | disposable_stale 40 | 40, medium |
| A number every list dropped, last seen four months ago | disposable_stale 40 | 40, medium |
| An unlisted mobile beside a run of recently listed SMS-receiving numbers | disposable_range 30 | 30, medium |
| +1 415 555 5555: the reserved 555 code, one digit repeated | reserved_block 40, digit_pattern 30 | 70, high |
| The docs example +1 415 555 0142: fictional, in the reserved 555 code | fictional 80, reserved_block 40 | 100, high (capped) |
| A +882 international-network number, which the plan also types as VoIP | voip 30, international_network 30 | 60, high |
| A geographic US number whose block nobody holds | unassigned_block | null, invalid, free |
`risk_signals` spells the sum out so nobody re-implements it. For the wholesaler example it reads:
```json
"risk_signals": [
{ "signal": "virtual", "weight": 40, "dataset": "nanpa-co-codes" }
],
"risk_score": 40,
"risk_level": "medium"
```
`disposable_range` is for the neighbours of listed numbers. SMS-receiving websites rent numbers in runs and the public lists lag the sites, so every daily sync counts, per 1,000-number range (the number without its last three digits, a run of digits rather than a regulator's block), the listings that still score `disposable`: numbers the lists carry and dated within the last year, and numbers they dropped that were seen within the last 90 days. A number that is not listed itself, in a range holding ten or more of them, scores 30: `medium` on its own, never `high`. A listed number scores its own listing instead, so the two never add up. `GET /api/v1/phone/block/{country}/{prefix}` reports the same count under `disposable`: the fresh listings under the prefix, the newest date among them, and how many of the ranges it overlaps the signal flags. Spaw measures the signal weekly against the list itself: how many listed numbers it would catch if the list missed them, and how many of the week's new listings already sat in a flagged range.
## The SMS policy
Every answer ends with a decision, so an OTP or signup flow branches on one boolean instead of re-implementing the weights table. `ok_to_send` and `blocked_by` come from your account's SMS policy, seven rules with published defaults:
| Rule | Default | What it does |
| --- | --- | --- |
| `allowed_countries` | null (any) | Send only to numbers of these regions. The country is the number's own, read from its calling code and prefix, so +1 876 is Jamaica; a number with no country is outside every allow-list. |
| `blocked_countries` | null (none) | Never send to numbers of these regions. |
| `blocked_line_types` | none | Block these line types (`mobile`, `fixed_line`, `voip`, `toll_free`, `premium_rate`, `shared_cost`, `personal_number`, `pager`, `uan`, `voicemail`, `unknown`). `fixed_line_or_mobile` can never be blocked. UK 084 and 087 numbers are `shared_cost`, so blocking `premium_rate` alone does not catch them. |
| `block_signals` | `fictional`, `disposable`, `reported_abuse`, `reported_abuse_widely`, `premium_rate` | Block whenever one of these signals fires, whatever the score. Any weight name may be listed. |
| `max_risk_score` | 60 | Block when `risk_score` is above this; null ignores the score. |
| `require_reachable` | false | Block unless a live check reached the handset: `unreachable` when the network could not reach it, `unconfirmed` when no live check answered. |
| `block_opted_out` | true | Block a single or browser lookup of a number your suppression list holds as opted out (`meta.opted_out: true`), as `opted_out`. Off, the number is answered and decided like any other and `meta.opted_out` still says so. |
The rules apply in a fixed order, and `blocked_by` names the one that decided: `invalid` for a number that is not valid — blocked whatever the policy says — then `opted_out`, `country`, `line_type`, the first blocked signal in the weights' order, `risk_score`, and last `unreachable` or `unconfirmed`; a batch, bulk or monitored run answers `suppressed` for a number served from your suppression list. Null means sending is fine. Under the defaults the docs example +1 415 555 0142 answers `blocked_by: "fictional"`, and so does the fictional test number +1 202 555 0105.
`ok_to_send` is a fraud and deliverability decision computed from the answer and your rules. It is not consent to message anyone and not a do-not-call scrub: neither is part of the answer, and both stay your obligation. A country allow-list is the first control against SMS pumping to destinations you never serve; it is a business choice about where you send, not a verdict that a country's numbers are fraudulent. The live-check rule needs a lookup made with `hlr: true`; while the live check is not enabled on the service, every lookup answers `unconfirmed` under it.
The opt-out rule reads your own suppression list: an `opted_out` report, or your SMS provider's opt-out code, puts the number there. It is on by default, so a number that replied STOP is not sent your next message by mistake. An OTP or transactional sender may want it off: an opt-out from marketing does not always forbid a verification code, and the person typing the number into your form may be its owner asking for a code again. Either way the rule is not a consent record, and honouring an opt-out stays your obligation. Batch, bulk and monitored runs serve an opted-out number from the list as `suppressed` whichever way the rule is set, and the test numbers never read the list.
The policy is account configuration, not money: any member may change it. `GET /api/v1/phone/policy` reads it, `PUT` changes any subset of the seven keys (signals come back in the weights' order, countries upper-case and sorted; an empty allow-list and a body naming none of the keys are a 422), `DELETE` restores the defaults, and the dashboard has an "SMS policy" tab. All three are free and unlogged, and every lookup — single, batch, bulk rows, monitors, the browser endpoint, the test numbers — applies the stored policy at once. The score is not the decision: the policy, not the weights, answers "may I send".
The glossary defines the [SMS policy](https://spaw.co/glossary/sms-policy) in one page, and the guide to [gating SMS verification codes with it](https://spaw.co/guides/gate-sms-verification-codes-with-the-sms-policy) walks through a signup flow.
## Your own traffic
Three fields and two signals come from nothing but your own account's recent phone lookups. They are counted for single lookups (`POST /api/v1/phone`, the dashboard, the MCP verify-phone tool, the consistency check) and for browser lookups with a publishable key, and for nothing else: batch, bulk and monitor runs hold lists, and a list naturally holds neighbouring numbers, so they are never counted and answer these fields null. Neither are the test numbers or the guest demo. An invalid answer counts only toward its destination below. No other account's lookups are ever read.
**range_seen_by_you and range_burst.** The distinct numbers your account looked up in the same 1,000-number range in the last 60 minutes, this one included, so a first lookup answers `{"distinct_numbers": 1, "window_minutes": 60}`. The range is the E.164 number without its last three digits (+14152010142 sits in `+14152010xxx`). From five distinct numbers the answer carries `range_burst`, +40 from the dataset `account-velocity`. SMS pumping walks consecutive numbers in a range where someone earns a share of every message sent there, and each of those numbers looks clean on its own. The range is a heuristic, since not every regulator allocates in thousands, and in North America, where pumping is rare, a burst more likely means someone is testing numbers against your form or opening accounts in bulk.
**client_velocity and numbers_per_client.** Send `client_ip`, the address of the person behind the lookup (your visitor, not your server), and the answer counts the distinct numbers tried from that address on your account in the last 60 minutes, this one included; an IPv6 address counts by its /64. A browser lookup counts the visitor's own address without being asked. From five distinct numbers the answer carries `numbers_per_client`, +30 from `account-velocity`, unless Spaw's own IP data places the address on a mobile carrier or a corporate proxy, where many people share one address, or the address is private or reserved, which makes it your proxy rather than your visitor; those still answer the count. The address is never looked up and never billed. The signal catches one script cycling numbers from one source, not an attack rotated through residential proxies. Without a client address the field is null.
**destination_new_for_you.** True when your account has not looked up a valid number in the number's country in the last 30 days, false when it has. It is null until a week of the account's own lookups has been counted, when there is no baseline yet, and on invalid answers, and it carries no weight: read it beside the score.
A browser lookup always answers the three fields null, because the answer reaches your visitor's browser and would tell anyone which countries you serve and how busy your form is. The two signals still score there, and `range_burst` on a browser answer counts only the browser lookups, so it cannot reveal what your backend looked up; your own single lookups count both.
**Alerts.** Three emails go to the account owner, all worked out from these counts:
| Alert | When | How often |
| --- | --- | --- |
| Range burst | A range reaches five distinct numbers inside the window. | Once per range per window, at most five a day. |
| Destination spike | An hour with at least 20 lookups to one country and ten times that country's hourly average over the previous 14 days, or over the days counted so far when fewer. Not sent until a week of the account's lookups has been counted. | At most once per country a day. |
| Enumeration | An hour with at least 50 lookups to one country, half or more of them invalid or in blocks nobody holds: a script cycling digits through a form. Input that names no country counts as one destination of its own. | At most once per country a day. |
The destination alerts are checked every five minutes. A list checked one number at a time trips them too; send lists to `POST /api/v1/phone/batch`, which is never counted. The Usage page (https://spaw.co/usage) shows the last seven days of the destination counts: lookups per country per day, and how many were invalid.
What is kept: each number as a truncated keyed hash under your account's own key, filed under a keyed hash of its range and of the client address, for the 60-minute window; and per country and hour, how many lookups there were and how many were invalid, as counts, for 30 days. Erasing a number takes it out of every range count.
## Reasons
An invalid number always says why, in one of ten values, each with its own reference page at https://spaw.co/docs/reasons/{value}. Every one of them is free.
| Reason | Meaning |
| --- | --- |
| missing_country | A national-format number came without a country to parse it in. |
| invalid_country_code | The calling code after the plus belongs to no plan. |
| too_short | Fewer digits than the region ever assigns. |
| too_long | More digits than the region ever assigns. |
| invalid_length | A length the region uses for no type of number. |
| not_a_number | Nothing in the input parses as a number. |
| invalid_number | The right length, but no range in the plan admits it. |
| unassigned_block | The plan allows it, but the regulator's table shows nobody holds the block. |
| emergency_number | An emergency number for the given country, such as 911 or 999. |
| short_number | A valid short code for the given country, not a subscriber number. |
## Batch and bulk
`POST /api/v1/phone/batch` (https://spaw.co/docs/api/lookup-phone-batch) looks up as many as 1,000 numbers in one call, in input order, under the single-lookup billing rules. Each item is a string or an object with its own `country`; a list-level country applies to the items that name none. If the balance runs out mid-list, the paid partial results come back with `meta.stopped_reason: "insufficient_credits"`; only a batch whose first lookup is refused answers the typed 402. A key with a daily credit cap stops the list the same way once the cap is spent, reporting `meta.stopped_reason: "key_spend_cap"`, so one large batch can never spend past the cap; a call that starts with the cap already spent answers `429 KEY_SPEND_CAP_REACHED` and runs nothing. A batch counts as one request against the rate limit.
```bash
curl https://spaw.co/api/v1/phone/batch \
-H "Authorization: Bearer sk_live_…" \
-H "Content-Type: application/json" \
-d '{"phones": ["+1 415 555 0142", {"phone": "020 7946 0018", "country": "GB"}], "country": "US"}'
```
For a CRM export, a bulk run queues up to 100,000 numbers with `POST /api/v1/phone/bulk` (https://spaw.co/docs/api/create-phone-bulk-job) or from the phone validation page in the dashboard (https://spaw.co/verify/phone), where an uploaded file keeps every column and a country column gives each row its own region. Poll `GET /api/v1/phone/bulk/{jobId}` for progress and the running valid, invalid, low, medium and high counts, then download the CSV from `GET /api/v1/phone/bulk/{jobId}/results`, whole or as one variant (valid, invalid, low, medium, high, deduped, mobile, ok, blocked): the input columns first, then these:
```
phone, valid, reason, e164, national, international, rfc3966, country, country_name, calling_code, continent, is_eu, currency, line_type, line_type_source, carrier, carrier_source, block_status, region, is_geographic, timezones, extension, is_fictional, is_virtual, is_disposable, disposable_seen_at, risk_score, risk_level, risk_signals, hlr_checked, reachable, ported, current_carrier, roaming, carrier_code, carrier_brand, block_pooled, block_assigned_at, region_level, local_time, utc_offsets, did_you_mean, complaints_90d, complaints_last_at, ok_to_send, blocked_by, sources, credits_used, suppressed, is_duplicate, duplicate_of_row
```
`local_time` and `utc_offsets` are the clock when the row was processed. Four fields of the single answer are not columns: a run is not counted in your own traffic, so `range_seen_by_you`, `client_velocity` and `destination_new_for_you` would always be empty, and it takes no call window, so neither would `in_call_window`.
A row that repeats a number an earlier row already held, in any spelling, is looked up once and is free; it says so in `is_duplicate`, and `duplicate_of_row` names the earlier row's position in your list (1 for the first row under the header). The `deduped` variant keeps the valid rows that are the first to hold their number; `mobile` keeps the valid rows whose line type is mobile — most North American numbers answer `fixed_line_or_mobile`, because mobiles and landlines share blocks there, and are not in it; `ok` and `blocked` split the rows by your phone policy's `ok_to_send`, with `blocked_by` saying why. Once a run has settled, the job also carries a `summary` added up from the same cells: rows, repeats, verdicts and reasons, the risk split, valid rows per line type and what settled it, the ten commonest countries and carriers, rows per risk signal, fictional, virtual and disposable counts, rows your suppression list answered, what the live check found when the run asked for it, and the rows your policy held back, by reason. The completion webhook does not carry it.
Cancelling stops a queued job on the spot and a running one at its next checkpoint; rows already answered stay billed and downloadable. A run that failed part-way keeps the rows it had already written, and they stay downloadable too. With a `webhook_url`, the finish is POSTed there as `bulk_phone_job.finished`, signed with the secret creation returns once: `X-Spaw-Signature-V2` carries `t={unix seconds},v2={hex}`, the HMAC-SHA256 of `v2:{t}:{raw body}`, and a receiver refuses a `t` more than five minutes from its own clock so a captured delivery cannot be replayed later. The original `X-Spaw-Signature`, over the body alone, is sent beside it until 1 March 2027. An `Idempotency-Key` header makes creation safe to retry. Phone endpoints have their own rate limit of 50 requests per second per key, like the IP endpoints.
A run that stopped because the credit balance ran out is resumed rather than uploaded again: top up, then press Resume on the run in the dashboard or call `POST /api/v1/phone/bulk/{jobId}/resume` (https://spaw.co/docs/api/resume-phone-bulk-job). The whole list runs again from the top, and the rows it already answered are repeats inside the seven-day window, so only the rows it never reached are charged. On a run with the live check, a number whose network answer is more than a day old is asked again and charged the premium, even inside the seven days, so resume within a day to keep those rows free. The job keeps its id, its webhook and its cumulative `credits_used`, while `processed` and the verdict counts restart at zero and are rebuilt as the rows come back. A run that stopped for any other reason, or that is already going again, answers `409 JOB_NOT_RESUMABLE`; one whose list has passed out of the 30-day retention window answers `409 JOB_INPUT_EXPIRED`, and an empty balance answers `402 INSUFFICIENT_CREDITS` rather than queueing a run that would stop again at once.
If a delivery fails — your endpoint was down for the half-minute the three attempts cover — ask for it again rather than falling back to polling: press **Send webhook again** on the run in the dashboard, or call `POST /api/v1/phone/bulk/{jobId}/webhook/redeliver` (https://spaw.co/docs/api/redeliver-phone-bulk-webhook). The body is byte-for-byte the body the first delivery carried, rebuilt from the run's own frozen row, so the two reconcile. The signature is new, and has to be: a receiver refuses anything whose `t` is more than five minutes from its own clock, so verify a redelivery exactly as you verify any other delivery. Nothing deduplicates it for you — a receiver that gets both the original and the redelivery sees `bulk_phone_job.finished` twice, and `job.id` is the key to deduplicate on. A run created without a `webhook_url` answers `409 WEBHOOK_NOT_CONFIGURED` and one that has not settled answers `409 JOB_NOT_FINISHED`; a run whose last delivery succeeded is not refused, because a receiver can lose a message it has already acknowledged. The delivery is queued rather than made while you wait — one attempt takes half a minute against an endpoint that is down, which is the endpoint you are asking about — so the call answers `202` and the run reads `webhook_status: "pending"` until a worker has an answer, then `delivered` or `failed` with what your endpoint said in `webhook_detail`. The dashboard shows the same three states beside the run. It is free and capped at ten 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.
Batch, bulk and monitor runs honour your phone suppression list: a number in a block the regulator lists as unassigned lands there on its own, and so does a number your own SMS reports called undelivered twice, failed for good once, abused, or opted out; such numbers answer free, marked `meta.suppressed: true` per item and in the CSV's trailing `suppressed` column, while a single lookup always looks the number up afresh. A valid answer — from any lookup, the public widget included — removes only an entry an unassigned block put there: one your own failure reports added leaves when you report a delivery or a `verified` outcome (the recipient entered the code), and an abuse report, an opt-out or one you imported only when you remove it, so nobody typing a number into your form can take it off your list. An abuse or opt-out entry you removed comes back only with a new abuse report or opt-out, never from one you had already made. Imports, abuse reports and opt-outs never go stale: batch, bulk and monitor runs serve them for as long as they are listed. An entry a lookup or your own failure reports added is looked up afresh in those runs once it is untouched for 90 days: one an unassigned block put there leaves if the fresh answer is valid, and one your failure reports added stays listed, renewed by a new failure report. Importing a number that is already listed makes it one you imported, keeping its reason, so it stops going stale; importing it again is harmless and never needed. An import that kept the stored answer of a block nobody held serves that answer for 90 days after the entry was last renewed, then drops it with the `unassigned_block` reason and answers `suppressed`, because nothing looks an import up again. An abuse report or opt-out about a number you imported is recorded on that entry, which stays an import. Only failures about the number count toward it: a failure your SMS provider's error code (or your own report's `failure_class`) calls sender-side or not SMS-capable is kept in your feedback summary and never scored. An `opted_out` report, or a provider's opt-out code such as Twilio 21610, is never scored either: it suppresses the number with reason `opted_out` and never goes stale. A single lookup still answers and scores the number in full with `meta.opted_out: true`, and under the default SMS policy it answers `ok_to_send: false` with `blocked_by: "opted_out"`; switch `block_opted_out` off to decide it like any other number. Honouring an opt-out stays your obligation; the list is your own record, not a consent system. Both endpoints take `hlr: true` to run the live carrier-network check on every item where the service has it enabled: the balance is gated for the premium up front, it is charged only where the network answered, and a bulk job records the choice as `live_check`. Manage the list from the Suppressions tab or with `GET`, `POST` and `DELETE /api/v1/phone/suppressions` (https://spaw.co/docs/api/list-phone-suppressions). The list endpoint takes `value` — with a `country` for a number written without a calling code — plus `source`, `reason`, `since`, `until` and `per_page`, and a searched number is read to E.164 before it is matched, so any notation of a suppressed number finds it. The `DELETE` on the collection (https://spaw.co/docs/api/remove-phone-suppressions) removes up to 1,000 numbers named in `values` and answers which were on the list and which were not, so nothing has to be paged for an id.
A monitored list (`POST /api/v1/phone/monitors`, https://spaw.co/docs/api/create-phone-monitor, or the Monitors tab of the dashboard page) keeps up to 500 numbers under a weekly or monthly re-check against the same tables, your suppression list and, when switched on, the live check. The first run sets the baseline; from the next run on, numbers that stop being valid or reachable count as decayed and arrive by email, and each run bills every number like a single lookup. A number you took out yourself (an import, your own failure or abuse reports, an opt-out) answers `suppressed` and is never decay. Numbers are saved in E.164, read against the monitor's `country` where they have no calling code, so two spellings of one number are one entry and the 500 counts distinct numbers; an entry that does not parse is saved as typed and answers invalid.
A number that still answers valid but changed since the last valid answer is flagged, counted in the summary's `newly_flagged` and sent in a second email: `became_disposable` (an SMS-receiving website started listing it), `reported_abuse_widely` (three or more accounts reported abuse), `risk_high` (its risk rose from low or medium to high) and `line_type_changed` — a North American number going from `fixed_line_or_mobile` to mobile or landline is a sharper answer, not a change, and a type now read from another block holder, or from a holder where the plan decided it before (as when NANPA's thousands-block report first names the holder of a pooled number's own block), sets a new baseline instead, because neither says the number changed. With the live check on, `ported` and `carrier_changed` say the network reports a port or that another network serves the number; that is not SIM-swap detection, because the check sees the network, not the SIM in the handset. Each flag needs the earlier value to have been known, so a first sighting only records it. `GET /api/v1/phone/monitors/{id}` answers `flags` beside `results`, per number. There is deliberately 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 with the live check a daily run would pay for the network check seven times a week.
A monitor can post its runs as well as email about them. Set `webhook_url` (https) when you create it, or later with a `PATCH`, and the signing secret comes back once in that response and is never readable again. `webhook_events` decides which runs are posted: `changes`, the default, posts only when the run found something new — a number that has stopped being valid or reachable, or a newly flagged one — or stopped early, and `every_run` posts after every run, which also tells you the monitor is still running. The body is `{ "event": "phone_monitor.run", "monitor": {…}, "summary": {…}, "changes": [ … ], "flags": {…} }`, where `changes` names the numbers the alert emails name and `flags` maps each newly flagged one to what it raised, and the delivery is signed with `X-Spaw-Signature-V2`: `t={unix seconds},v2={hex}`, where the hex is the HMAC-SHA256 of `v2:{t}:{raw body}` keyed with the secret. Refuse anything whose `t` is more than five minutes from your own clock — that window is what stops a captured delivery being replayed at you later. The original `X-Spaw-Signature`, the HMAC of the body alone with no timestamp, is sent beside it until 1 March 2027 and still verifies; both recipes are at https://spaw.co/integrations/webhooks. Connection errors and 5xx answers are retried twice, three attempts in all; `webhook_status` and `webhook_detail` on the monitor say how the last delivery went. A URL that resolves onto a private network is refused at delivery, every time, because DNS can be repointed after a URL is saved.
A monitor is changed rather than replaced: `PATCH /api/v1/phone/monitors/{id}` (https://spaw.co/docs/api/update-phone-monitor) takes `name`, `numbers`, `cadence`, `country`, `live_check` and the webhook fields, and applies only the ones you send. That matters because what the last run recorded per entry is the baseline every alert is compared against: deleting a monitor and creating it again throws that away, and the first run of the new one only sets a baseline — a full cycle of silence, which is the one thing a monitor is bought to prevent. Replacing the list keeps what was recorded about the numbers that stayed, deletes what was recorded about the ones you removed, and gives a new entry no baseline. Changing the cadence neither skips a run nor starts a second one: a run already scheduled sooner than the new interval still happens, and a longer interval applies from the one after it. A body naming no field it could change answers `422`.
A monitor that stops being run is deleted, and the identifiers and results it stored go with it. `prunes_after_days` on every monitor payload says how long that takes, and `meta.retention_days` on the list endpoint says the same. A monitor in active use is never pruned: each run advances `last_run_at`, so the window only closes on a list that has genuinely stopped being re-checked — at which point holding the details somebody gave you serves no purpose. Deleting a monitor yourself removes everything it held at once.
## Browser lookups
The form helper that checks email fields checks phone fields too. Mark an input with `data-spaw-phone` and it is validated on blur through the publishable-key endpoint; the country comes from `data-spaw-country` on the input, else from the form field named by `data-spaw-country-field`, else it is left out and international notation is expected. Valid numbers show their international format, flagged ones a note, invalid ones the reason in plain words. The helper never blocks a submit, and every result is dispatched as a `spaw:result` event whose detail carries `type: "phone"` and the answer.
```html
```
Or call `POST /api/v1/phone/public` (https://spaw.co/docs/api/validate-phone-public) yourself with the publishable key, the number and an optional country. Create the key on the API keys page and list the domains it may run from; the endpoint checks the browser's `Origin` header against that list, bills the key's owner, and answers without a meta block so visitors never see the balance.
```js
const response = await fetch('https://spaw.co/api/v1/phone/public', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ key: 'pk_live_…', phone: form.phone.value, country: form.country.value }),
});
const { data } = await response.json();
if (!data.valid) showHint(data.reason);
if (data.is_disposable || data.is_fictional) askForAnotherNumber();
if (data.e164) form.phone.value = data.e164;
```
Because the key sits in page source, give it a daily credit cap; once spent, the endpoint answers `429 KEY_SPEND_CAP_REACHED` until the next day, and free answers never count toward it. A key that carries a Cloudflare Turnstile pair requires a confirmed `turnstile_token` with every lookup. The endpoint is throttled at 20 requests a minute per visitor and never runs the live check.
## Billing and privacy
A fresh lookup that answers `valid: true` costs one credit. Invalid numbers cost nothing, including a number in a block nobody holds, and a repeat of the same number within seven days is free. That base credit is charged once per number every seven days whether or not the lookup asked for the live check: a check after a plain lookup of the same number costs only the check, and a plain lookup after a check costs nothing. The live carrier-network check adds four credits only when it actually answers, five in all with the base credit. The live carrier-network check (`hlr: true`) is not enabled on the service: `hlr_checked` answers false and nothing extra is charged. Asking for an enabled check reserves the four credits up front, so a request that cannot afford them is refused cleanly. Once your account has paid for a check of a number, a repeated check within the next seven days is free for a day, while the network's answer is still held; after that a repeat asks the network again and costs the four credits, reserved the same way. A check your account has not paid for in the last seven days costs the four credits whenever it answers, even when the network's answer is already held. Paying for the check alone does not restart the base credit's seven days. A number in a block nobody holds or in a range reserved for fiction is never checked, and never charged for the check. The test numbers are always free.
A phone number is personal data, and the service treats it the way it treats an IP address: nothing about a phone lookup is stored. No history row, no log line with the number. The only traces are the keyed seven-day marker that makes a repeat free (and a second one when you paid for the live check), the short-lived keyed hashes and per-country counts behind your own traffic signals (above), plus, when you ask for the live check, the network's answer, held for a day under a hash of the number so a repeat does not ask again. Every other signal is a local table or a bundled file. The live carrier-network check is the one call that leaves the server: it sends that number, and only that number, to a carrier-network partner. The `sources` block names each dataset consulted and its version. Bulk input lists and result files are the only copies of what you sent, and both are deleted with the job after 30 days.
The privacy policy (https://spaw.co/privacy) states the same facts alongside the rest of the data handling.
Next: the endpoint reference for every field, example, and error code: https://spaw.co/docs/api/validate-phone.
---
# Address verification
How Spaw address verification works: every field, the nine reasons, the address types, the recomputable risk score, what each part of the address was confirmed by and what was changed, the postcode directories, registers, streets and number ranges behind each answer with their licences, batch, bulk and browser lookups, the postcode and streets endpoints, the test addresses, and what is licensed and therefore not answered.
One lookup reads a postal address against its own country's format, splits it into the parts that country writes, names the delivery point behind it — a street, a post-office box, a private mailbox at a mail-receiving agency, general delivery, a rural route, military mail, a parcel locker, a business-volume arrangement — checks the postcode against the national directory, follows the building into the country's own address register, and counts the companies registered there. Every answer comes from open data the app carries or syncs, so it takes milliseconds and the address never leaves the server.
The product is about places, not people. Nothing in the response says who lives or works at an address, and no field ever will.
```bash
curl https://spaw.co/api/v1/address \
-H "Authorization: Bearer sk_live_…" \
-H "Content-Type: application/json" \
-d '{"address": "71-75 Shelton Street, Covent Garden, London WC2H 9JQ", "country": "GB"}'
```
## Sending an address
An address arrives either as one written line in `address`, the way it would be on an envelope, or as the separated fields `address_line1`, `address_line2`, `organization`, `building`, `dependent_locality`, `locality`, `administrative_area`, `postal_code` and `po_box`. One of the two is required, and so is `country`: an ISO 3166-1 alpha-2 code the format table covers, because without a format there is nothing to measure the address against. A country it does not know is refused with a validation error rather than answered.
Fields sent beside a written address win over what the parser read out of it — the caller knows more than the parser does. The optional `deliverability` flag asks the licensed partner whether post actually arrives; see Billing below.
Nothing else about the address is a validation failure. A missing town, a malformed postcode, a postcode that does not exist: those are answers, with a typed `reason`, and they are free.
A building named instead of numbered — Rose Cottage, Kemp House — goes in `building` when you send separated fields, and is separated by the parser when a written address puts it on the line above the street. The parser also sets aside a last line that only repeats the country and a c/o or attention line that names the addressee rather than the place, and folds a state written in full onto its code. Every such move is reported in `changes`, so a normalisation is never silent; the values are listed under "What was confirmed and what was changed" below.
A floor, flat or suite written on a line of its own, or before or after the street on its line, is read as the unit, and every unit an address carries is kept, joined with `, ` in the order written. A lone line with no number before the postcode is read by what the installed data says: a place the postcode directory lists for the code or around it makes it the town, and a street a register's listing files there makes it the street, so with Open Names installed `Strand, WC2R 0RL` is a street address, and with the US directory `Mountain View, CA 94043` answers `missing_street`; where the data cannot tell, the shape of the line decides. Neither reading is reported in `changes`.
### The fields option
`fields` — a comma-separated list of the answer's keys — keeps only the ones you name, in the answer's own order, on the single lookup and the browser endpoint. It is applied after billing, so a narrowed answer costs what a full one does; `meta` is never narrowed, unknown names are ignored, and `sources` is dropped unless you name it. Batch, bulk and monitor runs always carry every field.
## Fields and flags
Every response carries the same 61 fields in the same order, then `sources`. An invalid address answers the same shape with the fields nothing could be established for set to null. The signals are tri-state: `false` is a checked negative and `null` means the dataset behind it is not installed for that country, so nothing was evaluated. Nothing is guessed to fill a null.
| Field | Type | Meaning |
| --- | --- | --- |
| valid | boolean | The address stands as written for its own country: every part the format requires is there, the postcode matches the country's pattern, and where a national directory is installed, that postcode exists. |
| reason | string or null | Why it does not, in one of nine values, listed below. Null when valid is true. Invalid answers are free. |
| country | string or null | The ISO 3166-1 alpha-2 code you sent, uppercased. Required on every request: without a country there is no format to measure the address against. |
| country_name, continent | string or null | The English name of the country and its continent code. |
| is_eu, is_eea, privacy_regime | boolean, boolean, string or null | EU and EEA membership, and the comprehensive privacy law that applies where the address is — `gdpr`, `uk_gdpr`, `ccpa`, `law25`, `fadp` and the rest. The same country facts the phone and IP lookups answer. |
| formatted | string or null | The address written the way that country's postal operator writes it: its line order, its uppercasing, its position for the house number, with the parts that are missing left out rather than blanked. Null for an invalid address. |
| formatted_lines | string[] or null | The same rendering as formatted, one line per array entry, for a label printer or a form with one field per line. Null when formatted is. |
| formatted_local | string or null | The address in the country's own script and line order where the format table carries one (Japan, China, Korea, Taiwan) and the input was written in that script. Null otherwise. See [local-script rendering](https://spaw.co/glossary/local-script-rendering). |
| organization | string or null | The company line, as sent or as recognised in a written address: a line above a numbered street that carries no number of its own is read as the organisation, and `changes` says so. |
| building | string or null | A building named instead of numbered — Rose Cottage, The Old Rectory, Kemp House — as sent in the `building` field, or separated from the street line of a written address when the line above the street carries a building word and no number. |
| street, house_number | string or null | The street name and its number, split apart. The number goes on the side of the name that country writes it — leading in the United States and the United Kingdom, trailing in Germany and most of continental Europe. |
| unit | string or null | The flat, suite, floor or mailbox line. A floor written on a line of its own, before or after the street on its line, or between the house number and the street, is read as the unit rather than the street, and a private-mailbox number written on the street line is lifted out into this field. Several units written on one address are kept, joined with `, ` in the order written: `5th Floor, Suite 500`. |
| po_box | string or null | The box line in the operator's own words (PO Box 1234, Postfach 10 01 01, BP 12). A box written where the street goes moves here and the street is cleared, because the box replaces it; a box on a line of its own beside a street keeps both. See [PO box](https://spaw.co/glossary/po-box). |
| dependent_locality | string or null | The district or dependent locality, where that country writes one — Covent Garden under London, a barangay in the Philippines. |
| locality | string or null | The town, city or post town. |
| administrative_area | string or null | The state, province, region or county, folded to its code where that country writes one, so "California" and "CA" answer the same. |
| postal_code | string or null | The postcode as sent, or as read out of a written address. See [postcode](https://spaw.co/glossary/postcode). |
| postal_code_formatted | string or null | The postcode written the way its operator prints it — the British space, the Dutch space, the Polish hyphen, the ZIP+4 hyphen — from the value you sent. Null when the postcode is missing or of the wrong shape. |
| postal_code_valid | boolean or null | Whether it matches that country's postcode pattern. Null in the countries that use no postcodes at all. |
| postal_code_type | string or null | What the country calls it: `zip`, `eircode`, `pin` or `postal_code`. Null where there are none. |
| postcode_status | live, terminated or null | What the national postcode directory says about it. A terminated postcode is a real address whose postcode has been retired, not a typo, which is why it is kept and scored rather than refused. Null whenever the directory has no status to give: when no directory is installed for the country; when the code is not listed, which a complete directory also answers with the reason `postcode_not_found` and a partial one never treats as a denial; and for a code outside the installed directory's scope — in Great Britain a Northern Irish BT postcode, a BFPO number, a BF1 or a BX code — which the directory is not asked about and never calls missing. A ZIP+4 is looked up by its ZIP, the first five digits, while `postal_code_formatted` keeps all nine. See [postcode directory](https://spaw.co/glossary/postcode-directory). |
| locality_matches_postcode | boolean or null | Whether the town on the address is one of the places the directory publishes for that postcode, compared on folded names and under the other names GeoNames lists for the same place (Munich for München). Null when either side is unknown or the directory lists no place for the code. A false is only scored when the town is listed nowhere in the postcode's wider area either, and only by a directory that lists every place a code is written with; `confirmation.locality` says which case you have. See [postcode area](https://spaw.co/glossary/postcode-area). |
| administrative_area_matches_postcode | boolean or null | Whether the state, province or region on the address is the one the directory publishes for that postcode, compared as a code or as a name. Null when either side is unknown, and when the value written is a place inside the published region that the directory cannot place the postcode in or out of — an Irish county, where an Eircode routing key is filed only under its province, or a county that a key crossing into another province reaches. A ZIP written with the wrong state is a common data-entry error, and this is the field that catches it. |
| address_type | string | What kind of delivery point the address names: `street`, `po_box`, `private_mailbox`, `general_delivery`, `rural_route`, `military`, `parcel_locker`, `bulk_mail` or `unknown`. `unknown` means no box naming is carried for that country, which is not the same answer as `street`. See [delivery point](https://spaw.co/glossary/delivery-point). |
| address_type_source | string or null | What settled the type. `address_rules` when the operator's own naming did. |
| is_po_box | boolean or null | A shortcut for `address_type: po_box`. Null in a country whose box naming is not carried, because there "not a box" was never established. |
| is_private_mailbox | boolean or null | True when a PMB number follows a real street address — one of the two spellings the postal service allows for a box at a mail-receiving agency — or when the licensed check reported the delivery point as an agency. The other spelling, a bare # number, is read as a unit, because flats and suites are written the same way. See [private mailbox](https://spaw.co/glossary/private-mailbox). |
| exists | boolean or null | True only when the building itself was found in the country's official address register. False only when a register whose coverage is complete does not carry it. Null in every other case: no register for the country, a box with no street behind it, a register whose coverage is partial, or a register of streets alone, which can never answer about a building. See [address register](https://spaw.co/glossary/address-register). |
| match_level | premise, range, street, postcode, none or null | How far the address could be followed into that register: to the building, into a published house-number range on the street, to the street, to the postcode, or not at all. Null when no register was consulted. See [address range](https://spaw.co/glossary/address-range) and [street register](https://spaw.co/glossary/street-register). |
| register | string or null | Which register answered, by dataset name, so the answer can be checked against the file it came from. |
| register_coverage | complete, partial or null | How complete that register's own publisher says it is, except where Spaw marks a register partial because its copy cannot vouch for an address it lacks; the address data sources page says which and why. Always read `exists` against this field. See [address data sources](https://spaw.co/tools/address-checker/sources). |
| premise_key | string or null | A sixteen-character hex digest of the building — the country, the postcode or town, the folded street and the house number — and the key your outcome reports and your suppression list are filed under. Two spellings of one address share it, so it is what to join and deduplicate on. It is a key of a place, never of a person. Null when the address names no building. See [premise key](https://spaw.co/glossary/premise-key). |
| building_use | string or null | What the register that found the building says it is used for — `residential`, `commercial`, `mixed`, `industrial`, `institutional`, `agricultural` or `other` — where that register publishes a use. Null where it does not, and on anything short of a building match. Never inferred. See [building use](https://spaw.co/glossary/building-use). |
| is_multi_unit | boolean or null | Whether the register lists several flats or suites at the building. True from two or more unit rows; false only when a register that declares its units lists none there; null otherwise — including the US National Address Database, whose contributors list units unevenly, so its silence means nothing. |
| unit_check | string or null | What the unit you wrote says against the units the register lists at the building: `ok`, `missing` (several units listed, none written) or `unexpected` (a flat or suite written at a single residence). Null when the register did not find the building, does not say how many units it has, or lists one unit at a non-residential building, where a suite is ordinary. A private-mailbox number never counts as a unit. See [unit check](https://spaw.co/glossary/unit-check). |
| street_suggestion | string or null | The street the register most likely meant, in its own spelling, when the typed street is not in the postcode but one close to it is — Banhofstrasse for Bahnhofstrasse. Null when the street was found, when nothing is close, or where no street listing is installed. See [street suggestion](https://spaw.co/glossary/street-suggestion). |
| latitude, longitude | number or null | Where the building stands, when the register that found it publishes a position — about a metre of precision — and otherwise the centroid the postcode directory publishes for the postcode. Never derived from the street or the house number. |
| location_source | register, postcode_centroid or null | What the coordinates describe, so nothing about their precision has to be guessed: the building, or the postcode's centroid. Null when there are none. |
| companies_registered | number or null | How many companies still on an open company register name this address. Null where no register covers the country. |
| is_mail_drop | boolean or null | Whether mail is received at this address on other people's behalf. True from any of the three sources below; false when an operator's branch list or a company register covers the country and none of the three matched; null when neither covers it. See [mail drop](https://spaw.co/glossary/mail-drop). |
| mail_drop_provider | string or null | The operator that publishes this address as one of its branches, or the registered-agent or formation service the address belongs to. Null when the flag came from the company count instead. |
| mail_drop_source | operator_list, company_cluster, registered_agent or null | Which of the three answered: the operator's own published branch list, more companies registered at the address than the published threshold, or an address a registered-agent or formation service publishes, or that a public listing or filing naming the service carries — a place where a company being registered is ordinary and a person claiming to live is not. See [registered agent address](https://spaw.co/glossary/registered-agent-address). |
| deliverability_checked | boolean | Whether the licensed deliverability check ran. False whenever the request did not ask for it with `deliverability: true`, and until a partner is configured. |
| is_deliverable, is_cmra, is_vacant, is_residential | boolean or null | What that check said: whether post is actually delivered there, and the postal service's own agency, vacant and residential flags for the delivery point. Null until it runs. |
| confirmation | object or null | What stood behind each part — `postal_code`, `street`, `house_number`, `locality`, `administrative_area` — named by the dataset that confirmed it: `postcode_directory`, `postcode_area`, `register`, `range`, `company_register`, `mail_drop_list` or `pattern`; `unconfirmed` when a dataset that could have confirmed the part did not, and null when nothing could check it. Null as a whole for an invalid address. See [address confirmation](https://spaw.co/glossary/address-confirmation). |
| changes | string[] | What the parser and the classifier moved or set aside on the way from your input to these parts, so a normalisation is never silent: `country_line_removed`, `care_of_removed`, `building_separated`, `organization_separated`, `administrative_area_folded`, `box_line_moved`, `private_mailbox_moved_to_unit`. Empty when nothing was changed. |
| looks_placeholder | boolean or null | Whether the street or the town is what somebody types to get past a form rather than a place: a test word, a keyboard run, a refusal, a name with no vowel. Whole names only — a real street that merely contains one of the words is never flagged. Null when there is no street and no town to judge. See [placeholder address](https://spaw.co/glossary/placeholder-address). |
| seen_by_you | object or null | How many times your own account looked this address up inside the window before this call (`count`), when the first of those was (`first_seen_at`) and the window in days — kept under the premise digest, never the address, and never anybody else's lookups. Null on the guest demo and for test addresses. See [address velocity](https://spaw.co/glossary/address-velocity). |
| risk_score, risk_level | number, string or null | The sum of the weights below, capped at 100; `high` from 60, `medium` from 30. Null for an invalid address. See [risk score](https://spaw.co/glossary/risk-score). |
| risk_signals | array | Each contributing signal with the weight applied and the dataset behind it; the weights sum to risk_score. |
| ok_to_ship | boolean | Your account's shipping policy applied to this answer: false when a blocked signal fired, the score is above your ceiling, the register did not confirm a building you require confirmed, or the address cannot stand as written. See [shipping policy](https://spaw.co/glossary/shipping-policy). |
| blocked_by | string or null | What decided a false ok_to_ship: `invalid`, the first blocked signal in the weights' order, `risk_score`, or `unconfirmed` — and `suppressed` on a batch, bulk or monitored answer served from your suppression list. Null when shipping is fine. |
| sources | array | The datasets consulted, each with the version installed when the answer was given: the format table and the box rules always, then the register, the postcode directory, the company register, the mail-drop list and the deliverability partner, each only when it answered. |
### Address types
`address_type` takes nine values, read from the words the postal operator uses itself: `street`, `po_box`, `private_mailbox`, `general_delivery`, `rural_route`, `military`, `parcel_locker`, `bulk_mail` and `unknown`. Detection is deliberately conservative: some spellings only count at the start of a line and with a number after them, because a Swedish "Box", a Finnish "PL" and a Spanish "Apartado" are ordinary words elsewhere, and words that look like a box but name a flat — the Belgian "bus" — never classify anything.
A box written where the street goes replaces it: the line clears `street` and `house_number` and lands in `po_box` under the operator's name (Canada's `GD` reads `GENERAL DELIVERY`, and `P O Box` reads `PO BOX`); what follows the box on its line stays with it, and Canada Post's station too (`GD STN A` reads `GENERAL DELIVERY STN A`). A numbered box written after the street on the same line is read the same way, in whatever spelling the operator uses: `123 Main St P.O. Box 5` answers `po_box` `PO BOX 5` and no street. A box on a line of its own beside a street keeps both, and `formatted` writes the box under the street, nearest the town. A CEDEX after the town is business-mail routing, not a box: `address_type` reads `bulk_mail` and `po_box` stays null. A private mailbox is the exception: it stands at a real street address, so the street stays and the mailbox number moves into `unit`.
| address_type | What it means |
| --- | --- |
| street | An ordinary street delivery point: a place, where somebody can be visited or served. |
| po_box | A post-office box, in that operator's own words — PO Box, Postfach, Boîte Postale, Casella Postale, Skr. poczt., 私書箱. See [PO box](https://spaw.co/glossary/po-box). |
| private_mailbox | A box rented at a mail-receiving agency, written with a PMB number after a real street address. See [private mailbox](https://spaw.co/glossary/private-mailbox). |
| general_delivery | Held at the counter for collection: General Delivery, Poste Restante, Postlagernd. See [general delivery](https://spaw.co/glossary/general-delivery). |
| rural_route | A rural route or highway contract route rather than a numbered street address: RR, HC, RFD. See [rural route](https://spaw.co/glossary/rural-route). |
| military | Military and diplomatic mail: APO, FPO, DPO, PSC, CMR, BFPO, CFPO. The address is a routing instruction, not a place. See [military mail](https://spaw.co/glossary/military-mail). |
| parcel_locker | An automated parcel machine or collection point: Deutsche Post's Packstation, Australia Post's Parcel Locker and Parcel Collect. See [parcel locker](https://spaw.co/glossary/parcel-locker). |
| bulk_mail | A business-volume arrangement: the French CEDEX, TSA and CS, the Dutch and Belgian Antwoordnummer, the Swedish Svarspost. A CS or TSA line replaces the street, as a box does; CEDEX is written after the town, so the street stays. See [CEDEX](https://spaw.co/glossary/cedex). |
| unknown | No box naming is carried for that country, so nothing was classified. Not the same answer as `street`. |
### Registers, coverage, and what exists means
Thirteen countries have an official register of buildings behind `exists` — the United States, France, Switzerland, Austria, Czechia, Norway, Belgium, Luxembourg, Australia, Latvia, Denmark, the Netherlands and Brazil, with New Zealand on servers that hold a LINZ key, and three German Länder — Berlin, North Rhine-Westphalia and Rhineland-Palatinate — carry a partial register each; Great Britain has a register of streets, and each publisher says how complete its own file is, except where Spaw marks a register partial because its copy cannot vouch for an address it lacks: the [address data sources page](https://spaw.co/tools/address-checker/sources) says which and why. A register is matched through the same street folding the parser applies to the input — suffixes, directionals and, since 2026-09-17, Portuguese street types and titles — so a register compiled before a change to that table has to be recompiled, or its round trip drops; the weekly coverage report shows it. `exists` is true only on a `premise` match — the building itself. It is false only when the register that answered says `register_coverage: "complete"` and does not carry the address. Everywhere else it is null: no register for the country, a box with no street behind it, or a register whose coverage is `partial`.
That distinction matters, so it is also built into the score: the `not_in_register` signal fires only when `exists` is false, so a partial register never contributes a point. The United States National Address Database and the French Base Adresse Nationale are partial — states and communes contribute voluntarily and coverage is uneven — and so is Ordnance Survey's Open Names for Great Britain, which lists streets and never buildings. Denmark's register is partial too: its answers come from a copy taken on 26 September 2026, because the interface it was read through closed on 1 October 2026, and it stays partial until the register is read from its new distribution. Absence from a partial register is not evidence that an address does not exist, and it is never treated as such. Switzerland, Austria, Czechia, Norway, Belgium, Luxembourg, Australia, Latvia and the Netherlands have registers whose coverage is complete, and there absence is an answer. The registers section below says what each one carries beyond existence.
## Reasons
An address that cannot stand as written always says why, in one of nine values, in the order a person would fix them: what is missing first, then what is wrong, then what does not exist. Each has its own reference page at https://spaw.co/docs/reasons/{value}. Every one of them is free, and the parts that were recognised are still reported.
| Reason | Meaning |
| --- | --- |
| [empty_address](https://spaw.co/docs/reasons/empty_address) | Nothing in the input was recognisable as an address part. |
| [missing_country](https://spaw.co/docs/reasons/missing_country) | No country was given, so there is no format to measure the address against. |
| [unknown_country](https://spaw.co/docs/reasons/unknown_country) | The country code is not one the format table covers. |
| [missing_street](https://spaw.co/docs/reasons/missing_street) | The country's format requires a street line and none was given — and no box, route or general-delivery line stands in for it. |
| [missing_locality](https://spaw.co/docs/reasons/missing_locality) | The format requires a town or city, and the address names neither a town nor a postcode that would identify one. A street and a postcode stand on their own. |
| [missing_administrative_area](https://spaw.co/docs/reasons/missing_administrative_area) | The format requires a state or province, and the country has no postcodes to carry the geography instead. |
| [missing_postal_code](https://spaw.co/docs/reasons/missing_postal_code) | The format requires a postcode and none was given. |
| [invalid_postal_code](https://spaw.co/docs/reasons/invalid_postal_code) | The postcode does not match the country's pattern — the wrong length, the wrong shape, the wrong alphabet. |
| [postcode_not_found](https://spaw.co/docs/reasons/postcode_not_found) | The postcode has the right shape but the national directory does not list it. Only a country whose whole directory is installed can say this. |
A missing state or province only breaks an address in a country with no postcodes, where it is the only geographic anchor left. Everywhere else the postcode already carries the geography and post arrives without the province written out, so it is not demanded.
## Risk score
The score is arithmetic over the flags in the same response, capped at 100; `high` from 60, `medium` from 30. Recompute it, or ignore it and branch on the flags. It measures how unlikely an address is to be somebody's own front door, not fraud, and an invalid address has no score at all.
| Signal | Fires when | Points |
| --- | --- | --- |
| undeliverable | The deliverability partner says post is not delivered there | +80 |
| private_mailbox | A private mailbox at a mail-receiving agency | +60 |
| placeholder | A street or town typed to get past the form: a test word, a keyboard run, a refusal | +60 |
| mail_drop | A mail drop: an operator's published branch, or more companies registered than the threshold | +50 |
| reported_returned | Your own most recent outcome report for the address was returned, moved or refused | +50 |
| vacant | The deliverability partner records the delivery point as vacant | +40 |
| general_delivery | General delivery, held at the counter for collection | +40 |
| po_box | A post-office box, where a box is not the ordinary way to receive mail | +30 |
| unit_unexpected | A flat or suite written at a building the register lists as a single residence | +30 |
| terminated_postcode | A postcode the national directory has terminated | +30 |
| locality_mismatch | The town does not match the one the directory publishes for the postcode, nor any place it lists in the postcode's area | +30 |
| region_mismatch | The state, province or region does not match the one the directory publishes for the postcode | +30 |
| not_in_register | Not in a register whose coverage is complete, so `exists` is false | +20 |
| unit_missing | No flat or suite written at a building the register lists several units at | +20 |
| street_not_found | The street is not in a register of streets whose coverage is complete | +20 |
| number_out_of_range | The street is known, but the house number falls outside every range published for it | +20 |
| bulk_mail | A business-volume arrangement such as a CEDEX or an Antwoordnummer | +20 |
Worked examples, as the API answers them:
| Situation | Signals | Score |
| --- | --- | --- |
| An ordinary street address with a live postcode | none | 0, low |
| A United States address carrying a PMB number at a mail-receiving agency | private_mailbox 60 | 60, high |
| A London address with thousands of companies registered at it | mail_drop 50 | 50, medium |
| A French CEDEX address in a register that lists it | bulk_mail 20 | 20, low |
| A post-office box in a country where a box is not the usual way to get post, written with a town the postcode directory disagrees with | po_box 30, locality_mismatch 30 | 60, high |
| A post-office box in the United Arab Emirates | none — a box is how post normally arrives there | 0, low |
| A postcode the directory does not list | postcode_not_found | null, invalid, free |
`risk_signals` spells the sum out so nobody re-implements it. For the London example it reads:
```json
"risk_signals": [
{ "signal": "mail_drop", "weight": 50, "dataset": "uk-companies-house" }
],
"risk_score": 50,
"risk_level": "medium"
```
Two weights are deliberately conditional. A post-office box scores nothing in the countries where a box is the ordinary way to receive post — the United Arab Emirates, Oman, Qatar, Kuwait, Bahrain, Saudi Arabia, Jordan, Lebanon and Hong Kong — because there it says nothing about the recipient. And `not_in_register` needs a register whose coverage is complete, so absence from a partial one costs nothing.
## Where each fact comes from
Every dataset behind an answer is public and licensed for commercial use, and every response names the ones it consulted with the version installed at the time, so an answer can be checked against the file that produced it. The licences that ask for attribution get it in the footer of every page.
| Dataset | Settles | Licence |
| --- | --- | --- |
| address-formats | What an address needs in 207 countries and territories: the line layout, the required fields, the postcode pattern, the names that country uses for its postcode, locality and administrative area, and which fields it uppercases. | CC BY 4.0 (Google Address Data Service, © Google LLC); initial import via commerceguys/addressing (MIT) |
| address-rules | How 52 postal operators name a delivery point that is not a street, plus the words that look like a box but name a flat, which side of the street name the house number goes on, the state and province codes of the countries that write one, and the countries where a post-office box is the ordinary way to receive post. | compiled by Spaw from the operators' own addressing guides and the UPU country pages |
| us-national-address-database | `exists` and `match_level` in the United States, the building's position and, where the contributing authority filled it in, its use. Coverage is partial: states and local authorities contribute voluntarily, so an address that is not in it may simply be in a part that was never shared, and units are listed too unevenly for `is_multi_unit` to be answered from it. | public domain, U.S. Department of Transportation (not a mailing list) |
| us-tiger-address-ranges | `match_level` range in the United States: whether the house number sits inside a range the Census Bureau publishes for the street in that ZIP, and `number_out_of_range` when the street is known and the number sits outside every range. Coverage is partial — not every road edge carries a range, and a street on a ZIP boundary can carry only its neighbour's ZIP — so a street it does not know is never denied. | public domain, U.S. Census Bureau TIGER/Line |
| fr-base-adresse-nationale | `exists`, `match_level` and the building's position in France and its overseas regions. Coverage is partial. | Licence Ouverte 2.0 (IGN/DINUM) |
| os-open-names | `match_level` street in Great Britain, keyed by postcode district, and the places every British postcode is written with. It lists streets and never buildings, so `exists` stays null; Open Names files a road under one district while a long road runs through several, so its coverage is declared partial and a street it does not carry is never denied. | Open Government Licence v3.0 — contains OS data © Crown copyright and database right |
| ch-gebaeudeadressverzeichnis | `exists` and `match_level` in Switzerland, from the official directory of building addresses, with the building's position and its category as a use. Coverage is complete. | © swisstopo |
| at-adressregister | `exists` and `match_level` in Austria, from the address register of the Bundesamt für Eich- und Vermessungswesen, with the building's position, its use and its dwellings, which is what answers `is_multi_unit`. Coverage is complete. | CC BY 4.0, credited as "© Österreichisches Adressregister, Stichtagsdaten vom DD.MM.YYYY" — the reference date of the installed snapshot, which is the version `sources` cites for this dataset |
| cz-ruian-adresni-mista | `exists`, `match_level` and the building's position in Czechia, from the RÚIAN address points of the Czech Office for Surveying, Mapping and Cadastre. Coverage is complete. | CC BY 4.0 |
| no-matrikkelen-adresse | `exists`, `match_level` and the building's position in Norway, from Matrikkelen, the national property register kept by Kartverket. Coverage is complete. | CC BY 4.0 |
| be-best-address-flanders, be-best-address-wallonia, be-best-address-brussels | `exists`, `match_level`, the building's position and its box numbers in Belgium, from the three regional registers the federal BeST service republishes daily; every language spelling of a street is indexed. Coverage is complete. | CC BY 4.0 (Digitaal Vlaanderen, Service public de Wallonie, CIRB via FPS BOSA) |
| lu-bd-adresses | `exists`, `match_level` and the building's position in Luxembourg, from the cadastre's national address file. Coverage is complete. | CC0 (Administration du cadastre et de la topographie) |
| au-gnaf | `exists`, `match_level`, the building's position and its flats in Australia, from the Geocoded National Address File. Coverage is complete. The licence forbids using the file to build a mailing list, and nothing here does. | CC BY 4.0 under the Open G-NAF End User Licence Agreement (© Geoscape Australia, licensed by the Commonwealth of Australia) |
| lv-valsts-adresu-registrs | `exists`, `match_level` and the building's position in Latvia, from the State Address Register. Coverage is complete. | CC BY 4.0 (Valsts zemes dienests) |
| dk-danmarks-adresser | `exists`, `match_level`, the building's position and its floors and doors in Denmark, from a copy of Danmarks Adresseregister (DAR) taken on 26 September 2026. Coverage is partial: the interface the copy was read through closed on 1 October 2026, so an address created since is not in it, and until the register is read from its new distribution an address that is not found answers `exists` null, never `not_in_register`. | CC BY 4.0 (Klimadatastyrelsen) |
| nz-linz-addresses | `exists`, `match_level` and the building's position in New Zealand, from the LINZ street address dataset — on servers configured with a LINZ Data Service key. Coverage is complete. | CC BY 4.0 (Land Information New Zealand) |
| ons-postcode-directory | `postcode_status`, the introduction and termination dates, the centroid and the local authority in Great Britain, and lets `postcode_not_found` be said there. Northern Ireland postcodes are excluded: reusing them needs a separate Land & Property Services licence. So the directory is not consulted for them: a BT postcode answers `postcode_status` null — not evaluated, never `postcode_not_found` — with no town or county comparison, no centroid and no signal from the directory. The same goes for a BFPO number and the BF1 and BX codes, which the release does not carry. | Open Government Licence v3.0 — contains ONS, OS and Royal Mail data © Crown copyright and database right |
| geonames-postal-codes | `postcode_status`, the places and the region behind `locality_matches_postcode` and `administrative_area_matches_postcode`, and the centroid, in about a hundred countries outside Great Britain; `postcode_not_found` where the copy is complete. Which countries are complete, partial or prefix-only is set out under Postcode directories below. | CC BY 4.0 (GeoNames) |
| uk-companies-house | `companies_registered` in the United Kingdom, from the registered office of every company in the free company data product — a copy of the live register, which a dissolved company has already left. | Open Government Licence v3.0 |
| fr-sirene-establishments | `companies_registered` in France and its overseas territories, from the address of every active establishment in INSEE's Sirene base — which is where a French domiciliation address shows itself; an establishment whose owner has opposed publication has no address in the file and is not counted. | Licence Ouverte 2.0 (INSEE) |
| no-enhetsregisteret | `companies_registered` in Norway, from the business address of every entity in the Brønnøysund register: a deleted entity has left the file, while one in bankruptcy or winding up is still registered and still counted. | NLOD 2.0 (Brønnøysundregistrene) |
| company-registers (US) | `companies_registered` in the United States, from the Colorado, New York, Oregon, Connecticut and Hawaii business registries — principal and registered-agent addresses of the entities each registry still lists as existing; a dissolved, withdrawn, revoked or merged entity is not counted. | public domain, the states' own open data portals |
| mail-drop-locations | `is_mail_drop` and `mail_drop_provider` from the branch addresses mailbox and mail-forwarding operators publish themselves: The UPS Store, PostNet, PostScan Mail, Opus Virtual Offices and Davinci Virtual Offices in North America, Mail Boxes Etc in the United Kingdom and Ireland. | the operators' own published location lists |
| registered-agents | `mail_drop_source` registered_agent: the addresses of registered-agent and formation services in the United States and the United Kingdom, where a cluster of companies is the agent's and not a mailbox shop's. Each entry is an address the agent publishes as its own, or one that a public listing or filing naming the service carries, and the file records which. | compiled by Spaw from the agents' own sites and the state listings and filings, and Companies House filings, that name them |
| nl-bag-adressen | `exists` and `match_level` in the Netherlands, from the Basisregistratie Adressen en Gebouwen (BAG) kept by Kadaster: every nummeraanduiding with its street and town, one monthly extract. Coverage is complete. | CC0 1.0 (Kadaster) |
| br-cnefe-2022 | `exists`, `match_level` and the building's position in Brazil, from IBGE's Cadastro Nacional de Endereços para Fins Estatísticos, the address list of the 2022 census, one file per state. Coverage is partial: a census list is a snapshot, not a live register, so absence never becomes `not_in_register`. | public data (IBGE) |
| de-berlin-adressen | `exists` and `match_level` in Berlin, from the Geoportal Berlin address register kept by the Amt für Statistik Berlin-Brandenburg, with the postcode on every row. Coverage is partial (one Land), so absence never becomes `not_in_register`. | Datenlizenz Deutschland – Zero 2.0 |
| de-nrw-gebaeudereferenzen | `exists` and `match_level` in North Rhine-Westphalia, from the Gebäudereferenzen published by Geobasis NRW. The open product carries no postcode, so buildings are filed by municipality and matched through the town written. Coverage is partial. | Datenlizenz Deutschland – Zero 2.0 |
| de-rlp-hauskoordinaten | `exists` and `match_level` in Rhineland-Palatinate, from the Hauskoordinaten of the Landesamt für Vermessung und Geobasisinformation (LVermGeoRP), without postal enrichment, so buildings are filed by municipality. Coverage is partial. | Datenlizenz Deutschland – Namensnennung 2.0 (© GeoBasis-DE / LVermGeoRP) |
| register-derived postcodes (`-postcodes`) | The postcodes a complete register of buildings lists, added to the country's directory where the directory lacks them, with the register's places and centroids beside the directory's own. They augment and never replace: box-only and large-recipient codes a register of buildings never lists keep answering, which is also why no country becomes complete by augmentation alone — that takes the operator's published total in `postcodes_complete_at`. | the register's own licence |
| geonames-cities | The other names a town is written under — Munich for München, Moscow for Москва — behind `locality_matches_postcode`, and the other names of a one-city region written as the town or as the region, behind `administrative_area_matches_postcode` too: places of 15,000 people or more, compared inside one country, a town only with the postcode's own places and only within 30 km of the place. A name the directory also lists as a town of its own more than 30 km from the place counts only as the place's own name cut short (Biberach for Biberach an der Riß), so Perlin, a village near Schwerin that GeoNames lists among Berlin's names, does not count for a Berlin code. The same file gives the time zone of the nearest place in IP answers. An answer that matched a town or a region through it names `geonames-cities` in `sources`, dated by the day the cities file was installed; one whose town and region matched as the directory writes them does not. | CC BY 4.0 (GeoNames) |
| ie-property-price-register | Which counties each of the fourteen Eircode routing keys that reach a county of another province, or that GeoNames files under the wrong one (P14), covers, and that D11 and D15 reach into Meath, behind `administrative_area_matches_postcode` in Ireland: a county counts for a key once it holds at least ten of that key's sales, among the 250,671 of the register's residential sales that carry an Eircode. Only that derived table ships with the code, so `sources` does not name it; no sale, address or price is held. | © Property Services Regulatory Authority (PSRA re-use terms; read 24 September 2026) |
| census-zcta-places (inside geonames-postal-codes) | The places whose land a US ZIP Code Tabulation Area covers, from the Census Bureau's ZCTA-to-place relationship file, added to each ZIP's places beside the default city so that `locality_matches_postcode` reads true for far more legitimate inputs. Not the postal service's list of acceptable names, so a US mismatch stays unweighted. | public domain (U.S. Census Bureau) |
| address-placeholders | `looks_placeholder`: the words and shapes people type instead of an address — test words, refusals, keyboard runs — matched as whole names only. | compiled by Spaw, bundled and versioned |
What the lookup deliberately does not claim: it does not know whether post is actually delivered to an address; it cannot list the buildings at a UK postcode, only the streets; a partial register cannot say an address does not exist; a postcode centroid is not a rooftop, and the answer says which of the two its coordinates are; a building's use and its units are what a register declares and never an inference; and nothing anywhere says who lives or works there. Each of those is a null or a typed refusal, never a guess.
## What was confirmed and what was changed
`match_level` says how far the register got, but not which parts of the address it stood behind, and a normalised address does not say what was normalised. Two fields answer that. `confirmation` names, for the postcode, the street, the house number, the town and the region, the dataset that confirmed it — or `unconfirmed` when a dataset that could have did not, or null when nothing could check it. It is derived from data the lookup already consulted and costs nothing extra.
| Part | Values | What it means |
| --- | --- | --- |
| postal_code | `postcode_directory`, `register`, `pattern`, `unconfirmed` | The directory lists the code, or a register knows it, or only the country's pattern was met. `unconfirmed` when a directory or register covers the country and neither knew the code. A code outside the directory's scope — BT, BFPO, BF1 and BX in Great Britain — is never put to the directory, so it reads `pattern`, never `postcode_directory` or `unconfirmed`: Open Names covers Great Britain only and is not asked about BT, BF or BX codes either. |
| street | `register`, `company_register`, `mail_drop_list`, `unconfirmed` | A register lists the street (or the building, or a number range on it), a company register or a mailbox operator's list names the building, or nothing that could have did. |
| house_number | `register`, `range`, `company_register`, `mail_drop_list`, `unconfirmed` | The building itself is in a register, the number sits inside a published range on the street, a company register or an operator's list names it, or nothing did. |
| locality | `postcode_directory`, `postcode_area`, `unconfirmed` | The town is one the directory publishes for the postcode — as written, or under another name GeoNames lists for the same place — or one it lists elsewhere in the postcode's area (a settlement written where the operator writes the district, or the other way round), or, where the postcode's region is a single city, that region's own name (Distrito Federal or DF for 06000, Tokyo for a code in one of its wards). The last two are reported, never scored. Otherwise none of these, and `unconfirmed`: any other region's name, such as Jalisco for a Guadalajara code, is not its postcode's area. |
| administrative_area | `postcode_directory`, `unconfirmed` | The region is the one the directory publishes for the postcode, under any of the names and codes listed for it below, or it is not. An Irish county inside the province an Eircode routing key is filed under answers null, because the directory can neither confirm nor refuse it, and so does every county a key that crosses into another province reaches. P14 is the exception: GeoNames files it under Leinster though none of its sales is there, so a Leinster county is refused for it. County Dublin for a Dublin district key other than D11 and D15 is confirmed. |
`changes` lists what the parser and the classifier moved or set aside on the way from your input to the parts, in the order it happened. An empty list means the address was read exactly as sent.
| Change | What happened |
| --- | --- |
| country_line_removed | The last line only repeated the country — United Kingdom, USA, Deutschland, an alpha-3 code — and was set aside instead of being read as the town. |
| care_of_removed | A c/o, attn or FAO line named the addressee rather than the place. The product is about places, so it was dropped and never carried into the answer. |
| building_separated | A named building above the street — Rose Cottage, High Street — became `building` rather than being mistaken for the street. |
| organization_separated | A line without a number above a numbered street became `organization`, because the street below it already carried the number. |
| administrative_area_folded | A state or province written in full was folded onto the code its country writes: California to CA. |
| box_line_moved | A line that opens with a box, written where the street goes, moved to `po_box` under the operator's name — `P O Box 12` reads `PO BOX 12` — and the street was cleared, because the box replaces it; the same goes for a numbered box written after the street on the same line (`123 Main St P.O. Box 5`). A box on its own line beside a street moves nothing. |
| private_mailbox_moved_to_unit | A PMB number on the street line moved into `unit`, leaving the real street it stands at. |
## Postcode directories
A postcode directory answers `postcode_status`, the places and the region a postcode is written with, and a centroid. Two things decide how much a directory may claim. Its coverage: only a copy that reaches the operator's published total may say a postcode does not exist, and everything else confirms what it holds and denies nothing. And its places: only a directory that lists every place a code is written with may score a town it does not list.
| Scope | Source | What it answers |
| --- | --- | --- |
| Great Britain | ONS Postcode Directory (`ons-postcode-directory`) | Every postcode with its status, the dates it was introduced and terminated, its centroid and its local authority, so `postcode_not_found` can be said. The places a postcode is written with come from Ordnance Survey's Open Names — the populated place, the borough, the county and the region — so a town is checked against all four. Northern Ireland is excluded: reusing BT postcodes needs a Land & Property Services licence, so a BT postcode is not looked up and answers `postcode_status` null rather than `postcode_not_found`. BFPO numbers and the BF1 and BX codes are not in the release either, and are answered the same way. |
| Complete elsewhere | GeoNames postal codes (`geonames-postal-codes`), CC BY 4.0 | In the United States, Germany, France, Spain, Italy, Austria, Switzerland, Belgium, Denmark, Norway, Finland, Slovakia, Japan, Mexico and India the copy reaches the operator's published total, so a postcode it does not list answers `postcode_not_found`. A ZIP+4 is looked up by its first five digits, the ZIP, and `postal_code_formatted` keeps all nine; the registers, the number ranges, the company counts and the mail-drop and registered-agent lists match it on its ZIP too, whichever of the two spellings the address or the dataset carries. Every place listed under a code counts for the town check, and the region is compared by name or code. |
| Partial elsewhere | GeoNames postal codes, the other ninety-odd countries | The postcode is confirmed when listed and never denied when it is not: an absent code answers `postcode_status` null, not `postcode_not_found`. Three directories hold only the leading part of a postcode and are matched on that part — Canada's forward sortation area (M5V of M5V 3L9), the Netherlands' four digits, Ireland's routing key — and they are always partial. See [forward sortation area](https://spaw.co/glossary/forward-sortation-area). |
| Default place only | GeoNames for the United States, the prefix directories | The US file carries only the postal service's default city per ZIP, not every name it accepts, and a prefix directory names an area rather than its towns. A town such a directory does not list answers `locality_matches_postcode` false with `confirmation.locality` unconfirmed and no weight, because the directory cannot tell an acceptable alternate name from a wrong one. |
The town check reads the postcode's own places first, then the places the directory lists anywhere in the postcode's area — the letters of a British postcode, the first three digits of a ZIP, the first two characters elsewhere — because an envelope carries the settlement where the operator publishes the district, or the other way round. Where the postcode's region is a single city — Mexico City, Delhi, Jakarta, Bangkok, Bogotá, Moscow, Saint Petersburg, Bucharest, Seoul, Korea's six metropolitan cities and Sejong, and Tokyo for a code filed under one of its wards — the region's own name counts as the postcode's area too, so Distrito Federal for 06000 and Tokyo for 100-0005 read `postcode_area`, while the name of any other state — Jalisco for a Guadalajara code, or Tokyo for a Hachiōji code in the Tama cities — does not. A town written under another name of the same place counts as that place — Munich for München, Moscow for Москва — from the GeoNames list of places of 15,000 people or more, inside the same country only, and only against the postcode's own places or the name of a one-city region, never against its neighbours', and only where the postcode lies within 30 km of the point GeoNames gives the place. Bristol written for Briston's NR24 2AD is therefore refused, and a name the directory also lists as a town of its own more than 30 km away counts only as the place's own name cut short — Biberach for Biberach an der Riß, Yarmouth for Great Yarmouth — so Perlin, a village near Schwerin that GeoNames lists among Berlin's names, is refused for a Berlin code. A Danish postal town counts without the letter or name of its district: København for København V, Aarhus for Aarhus C, Nykøbing for Nykøbing F. `locality_mismatch` fires only when the town is nowhere in that area and the directory lists every place; `region_mismatch` fires when the state, province or region is not the one published for the postcode, compared as a code or a name. A postcode GeoNames lists under two regions or more is published for each, so 3644 answers to Victoria as well as New South Wales. Where the directory files a region under a number, an older name or a wider area, the names people write for it count too: a Mexican state by its INEGI number, ISO code, abbreviation or name (CMX, CDMX, DF or Ciudad de México for Mexico City), an Irish province by its name, letter or Irish name, an Indian state by its ISO code, old or new, an Indonesian province by its statistics-office number, ISO code, Indonesian or English name or short form (Jawa Barat, West Java, Jabar), a Korean province or city by its romanised, English or Hangul name, Bogotá by its capital-district names, and a Spanish province that has two official names by either (Bizkaia or Vizcaya).
The directory files an Eircode routing key only under its province, so a county inside that province is neither confirmed nor refused — `administrative_area_matches_postcode` answers null — while a county in another province is refused. Fourteen keys cross a county line into another province, or are filed under the wrong one — Athlone's N37 reaches Roscommon, Waterford's X91 Kilkenny — and for those every county the key reaches, and that county's province, answers null too; the province GeoNames files the key under answers null rather than true, and a county the key does not reach is still refused unless it lies in that province — and for P14, which GeoNames files under Leinster though none of its sales is there, even then. Which counties a key reaches is read from the Property Services Regulatory Authority's Residential Property Price Register: a county counts once it holds at least ten of the key's sales. A Dublin district key (D01–D24, D6W) also confirms County Dublin (Dublin, Co. Dublin, D) and its own district (Dublin 2 for D02), except D11 and D15, which reach into Meath (17 of D11's 3,132 sales, 149 of D15's 7,075) and confirm only their own district (Dublin 11, Dublin 15) and its province.
## Registers: buildings, streets and number ranges
A national address register is compiled here as three indexes — buildings, streets, postcodes — plus what the register publishes about each building beyond its existence.
### Where a building stands
Every register of buildings publishes a position, so a `premise` match answers `latitude` and `longitude` with `location_source: register` — the building, to about a metre, converted from each publisher's own grid — and anything short of one falls back to the postcode centroid. Three registers publish what a building is used for, which is what `building_use` carries: the Swiss directory's building category, the Austrian register's use, and the US National Address Database where the contributing authority filled the field in. Five declare the flats or suites at each building — Austria's dwelling counts, the Belgian box numbers, the Australian flat numbers, the Danish floors and doors — and there `is_multi_unit` is true from two or more and false from none. The US database lists units so unevenly that its silence means nothing, so it answers null there. None of this is inferred: a register that publishes no use answers null, never a guess from the street.
### A register of streets: Great Britain
No open dataset lists Great Britain's buildings, but Ordnance Survey's Open Names lists its streets, keyed by [postcode district](https://spaw.co/glossary/postcode-district). A British address whose street is there answers `match_level: street` with `confirmation.street: register`, and `exists` stays null, because a register of streets can never answer about a building. Open Names files a road under one district while a long road runs through several, so a street it does not carry answers `match_level: none` and nothing more: the register is declared partial, and `street_not_found` is reserved for a register of streets whose coverage is complete; no such register is installed today. The same file gives every British postcode its places, so `locality_matches_postcode` is answered in Great Britain against the populated place, the borough, the county and the region at once.
### Number ranges: the United States
The National Address Database covers the states that share their data. For the rest, the Census Bureau's TIGER/Line files publish, for every road edge, the range of house numbers along it and the ZIP it sits in. When no building register carries an address, a number inside a published range on its street answers `match_level: range` with `confirmation.house_number: range`, and a number outside every range published for a street the files know answers `match_level: street` and scores `number_out_of_range`. Ranges are potential, not actual — a range says the number could stand on that edge, not that a building does — so a range match never sets `exists`, and because not every edge carries a range and a street on a ZIP boundary can carry only its neighbour's ZIP, the files are declared partial and a street they do not know is never denied.
## Street suggestions and the streets endpoint
Every register with streets keeps a listing of them by postcode, in the register's own spelling. Two things read it. When a typed street is not in the postcode but one close to it is — Banhofstrasse for Bahnhofstrasse, Limatquai for Limmatquai — a single lookup answers `street_suggestion` with the register's spelling; a street that was found, or one nothing is close to, answers null.
And `GET /api/v1/address/streets?country=CH&postal_code=8001` (https://spaw.co/docs/api/list-address-streets) answers every street the register lists inside one postcode, sorted, for a street picker under a postcode field: one call returns the whole postcode, so a form filters locally as the visitor types rather than calling per keystroke. The answer is `{ country, postal_code, register, streets, count }` with the register named as it is in a lookup's `sources`. A ZIP+4 answers every street in its five-digit ZIP, and the answer names that ZIP, because no register files streets by the four-digit add-on. It costs one credit per answered postcode and is free for the next seven days for the same postcode, every spelling of one ZIP counting as that ZIP. It costs nothing at all when no register with a listing covers the country, when the postcode lists no street, or when the listing does not cover the postcode — all three answer `404 ADDRESS_STREETS_NOT_FOUND`, with a message that says which. It carries no house numbers and nothing about who is there.
```bash
curl "https://spaw.co/api/v1/address/streets?country=CH&postal_code=8001" \
-H "Authorization: Bearer sk_live_…"
```
```json
{
"success": true,
"data": {
"country": "CH",
"postal_code": "8001",
"register": "ch-gebaeudeadressverzeichnis",
"streets": ["Bahnhofstrasse", "Limmatquai", "Paradeplatz", "…"],
"count": 200
},
"meta": { "credits_used": 1, "cache_hit": false, "…": "…" }
}
```
The listing exists wherever a register with streets is installed: Switzerland, Austria, Czechia, Norway, Belgium, Luxembourg, Latvia, Australia and the Netherlands in full, Denmark as its register stood on 26 September 2026, the parts of the United States, France and Brazil their registers carry, Berlin, whose register is filed by postcode (the North Rhine-Westphalia and Rhineland-Palatinate registers are filed by town, so they list no street at a postcode), and Great Britain by postcode district from Open Names. Where several registers share a country region by region, as Belgium's three do, the postcode is read from the register that lists streets there, and `register` names it. Open data only: the registers publish their streets and Spaw lists them back.
A miss reads "No street is listed at 9999 in CH." only where the registers' coverage is complete and none of them holds an address at the postcode. Every other miss says why instead of reading as a postcode with no street: a Northern Irish BT, forces BFPO or BF, or non-geographic BX code is outside Ordnance Survey's Great Britain data; a German postcode outside Berlin, or a postcode in a Belgian region whose register is not installed, is in a part of the country no installed register covers; a British district Open Names files no street under is one the register cannot vouch for, since it files each road under one district; and in many Czech villages the register holds addresses but none on a named street, so there is no street to list. The streets endpoint answers these with `404 ADDRESS_STREETS_NOT_FOUND` and the postcode list with `501 ADDRESS_LIST_UNAVAILABLE`, both free and neither leaving a repeat marker.
## Mail drops and registered agents
`is_mail_drop` is a fact about a place, and three sources say it. `operator_list`: a mailbox or mail-forwarding operator publishes the address as one of its branches — The UPS Store, PostNet, PostScan Mail, Opus Virtual Offices and Davinci Virtual Offices in North America, Mail Boxes Etc in the United Kingdom and Ireland — and `mail_drop_provider` names it. `company_cluster`: more companies are registered at the address than the published threshold, from the open company registers of the United Kingdom, France, Norway and five US states. Only companies each register still lists are counted. A business park named after its road — Ferry Road Office Park — is counted under the park and under the road inside the same full British postcode, so 7 Ferry Road and 7 Ferry Road Office Park read the same companies; a park that numbers its units can share a count with the same number on its road.
`registered_agent` is the third, and it exists to keep the second honest. A registered-agent or formation service publishes its own address so that companies can name it as their registered office: 251 Little Falls Drive in Wilmington, 71-75 Shelton Street in London. Tens of thousands of companies stand there, and every one of them is ordinary — a company registered at its agent is what the law expects. A person claiming to live there is not. So the cluster is named for what it is, `mail_drop_provider` carries the agent, and the `mail_drop` weight applies as it does to any address that receives post on other people's behalf. It remains a statement about the place, never about anyone who uses it.
## Test addresses
Five fixed [test addresses](https://spaw.co/glossary/test-address) on a street that does not exist, under a ZIP the postal service has never assigned, answer the same canonical result every time, cost no credits, leave no repeat marker, read no outcome report and touch no suppression list — the way payment providers publish test card numbers. They answer on every endpoint, including batch, bulk, monitors, the browser endpoint and the MCP tools, and the published sandbox key answers them without an account. Their `sources` name the `test-postal-addresses` dataset. Any spelling of the street and the ZIP is recognised, a ZIP+4 of 99999 included; the country must be US.
| Address | Answer |
| --- | --- |
| 1 Spaw Test Street, Springfield, IL 99999 | valid · exists true from the test register, located, risk_score 0 |
| 2 Spaw Test Street, Springfield, IL 99999 | valid · is_mail_drop true (operator_list, Spaw Test Mailboxes), companies_registered 120, risk_score 50 (mail_drop) |
| 3 Spaw Test Street, Springfield, IL 99999 | valid · exists false, match_level street, risk_score 20 (not_in_register) |
| 4 Spaw Test Street, Springfield, IL 99999 | valid · postcode_status terminated, risk_score 30 (terminated_postcode) |
| 5 Spaw Test Street PMB 5, Springfield, IL 99999 | valid · address_type private_mailbox, unit PMB 5, risk_score 60 (private_mailbox) |
## The shipping policy
Every answer ends with a decision, so a checkout branches on one boolean instead of re-implementing the weights table. `ok_to_ship` and `blocked_by` come from your account's shipping policy, three rules with published defaults:
| Rule | Default | What it does |
| --- | --- | --- |
| `block_signals` | `undeliverable`, `placeholder`, `vacant` | Block whenever one of these signals fires, whatever the score. Any weight name may be listed. |
| `max_risk_score` | 60 | Block when `risk_score` is above this; null ignores the score. |
| `require_exists` | false | Block unless a register confirmed the building (`exists` true). Only useful where a register covers the country. |
The rules apply in a fixed order, and `blocked_by` names the one that decided: `invalid` for an address that cannot stand as written — blocked whatever the policy says — then the first blocked signal in the weights' order, then `risk_score`, then `unconfirmed`; a batch, bulk or monitored run answers `suppressed` for an address on your suppression list. Null means shipping is fine. The policy is account configuration, not money: any member may change it. `GET /api/v1/address/policy` reads it, `PUT` changes any subset of the three keys (block signals come back in the weights' order; a body naming none of them is a 422), `DELETE` restores the defaults, and the dashboard has a "Shipping policy" tab. All three are free and unlogged, and every lookup — single, batch, bulk rows, monitors, the browser endpoint, the test addresses — applies the stored policy at once. The score is not the decision: the policy, not the weights, answers "may I ship".
## Placeholders and units
`looks_placeholder` is true when the whole street name or the whole town is what somebody types to get past a form rather than a place: a test word (`test`, `asdf`, `qwerty`), a refusal (`n/a`, `none`, `no address`, `same as above`), a shape nobody would name a place with — one character repeated, digits alone, words with no vowel among them and one of four letters or more, a run along a keyboard row. The shapes are judged on the street without the street type and directional at its end, so initials such as `MLK Blvd` and `MG Road` and a numbered street such as `23. Straße` pass. Whole names only: the list is compared with the street's suffix folded (`Fake Street` and `Fake St` are one name), and a real street that merely contains one of the words — Test Valley Road, Sample Road — is never flagged. Null when there is no street and no town to judge. The `placeholder` weight is 60, its dataset is the bundled `address-placeholders` list, and it sits in the default block list.
`unit_check` reads the unit written on the address against the units the register lists at the building, so it only answers on a `premise` match at a register that declares its units: `missing` (several units listed, none written; weight 20 as `unit_missing`), `unexpected` (a flat or suite written at a building the register lists as a single residence; weight 30 as `unit_unexpected`), or `ok`. It stays null when the register lists one unit at a building whose use is not residential or unknown — offices have suites — and a private-mailbox number never counts as a unit. Use and units are never inferred: a register that publishes no units answers null, not false.
## Your own lookups
`seen_by_you` counts how many times your own account looked the same address up inside the last seven days before this call, and when the first of those was. It is kept under the premise digest (or a hash of the folded input when the address names no building), never the address, and it reads nobody else's lookups: a postal address is where somebody lives, and a pooled record of who is checking it would be a database about residences. It answers null on the guest demo and for the test addresses, and it carries no weight.
## Local rendering and the postcode as printed
`formatted_lines` is `formatted` as an array, one line per entry, for a label printer or a form with one field per line. `postal_code_formatted` is the postcode written the way its operator prints it — the British space (`SW1A 2AA`), the Dutch space (`1012 NX`), the Polish hyphen (`00-950`), the ZIP+4 hyphen — from the value sent, and null when the postcode is missing or of the wrong shape. `formatted_local` is the address in the country's own script and line order — largest part first, no uppercase rules — for Japan, China, Taiwan, Hong Kong, Macau and Korea, and only when the parts were written in that script; Latin-script input answers null even for Japan, because transliterating would be inventing. A Japanese address written on one line does not yet split into parts, so the local rendering is most reliable from the separated fields.
## Postcodes from registers
A complete register of buildings lists every postcode its buildings carry, and that list feeds the country's postcode directory: codes the directory lacks are added under `-postcodes`, and the register's [places and centroids](https://spaw.co/glossary/derived-postcode-places) join the directory's own. They augment and never replace, because a register of buildings never lists the box-only and large-recipient codes a postal operator issues — Swiss and Austrian Postfach codes, Australian GPO boxes — and replacing the directory would have turned real box addresses into `postcode_not_found`. For the same reason no country becomes complete by augmentation alone; that takes the operator's published total. In the United States the ZIP directory also carries the places whose land each ZIP Code Tabulation Area covers, from the Census Bureau's relationship file, so `locality_matches_postcode` reads true for a neighbouring place; that file is not the postal service's list of acceptable names, so a US mismatch stays unweighted. For Great Britain, every postcode district has a page of its own at /tools/address-checker/gb/districts: the places Open Names lists, live and terminated postcode counts, the streets filed under the district and the datasets behind them.
## Batch, browser and postcode lookups
`POST /api/v1/address/batch` looks up as many as 100 addresses in one call, in input order, under the single-lookup billing rules. Each item is a written address or an object with the same fields as a single lookup, and may carry its own `country`; a list-level country applies to the items that name none. If the balance runs out mid-list, the paid partial results come back with `meta.stopped_reason: "insufficient_credits"`; a key that reaches its daily credit cap stops the list the same way, with `meta.stopped_reason: "key_spend_cap"`. Only a batch whose first lookup is refused answers the typed 402, and a call that starts with the cap already spent answers `429 KEY_SPEND_CAP_REACHED` without running anything. A batch counts as one request against the rate limit.
```bash
curl https://spaw.co/api/v1/address/batch \
-H "Authorization: Bearer sk_live_…" \
-H "Content-Type: application/json" \
-d '{"addresses": ["10 Downing Street, London SW1A 2AA", {"address_line1": "Postfach 10 01 01", "postal_code": "50441", "locality": "Köln", "country": "DE"}], "country": "GB"}'
```
`POST /api/v1/address/public` is the browser endpoint: a domain-locked publishable key instead of a secret one, checked against the browser's `Origin` header, billed to the key's owner and answered without a meta block so visitors never see the balance. Give such a key a daily credit cap, because it sits in page source.
The form helper that checks email and phone fields checks address fields too. Mark the one input that holds the whole written address with `data-spaw-address` and give it a country: `data-spaw-country="GB"` on the input, or a country select on the same form named by `data-spaw-country-field`. An address means nothing measured against no country, so until one is there nothing is sent — and the helper watches that select, so an address typed before the country is picked, the ordinary checkout order, is checked as soon as a country is chosen. The hint shows the address as the country writes it, a note when the place is a box, a locker or a counter rather than a front door, and the reason in plain words when it cannot stand as written. It never blocks a submit, and every answer is dispatched as a `spaw:result` event whose detail carries `type: "address"` and the same fields the API gives a server, minus the billing block. To keep even the publishable key off the page, point `data-address-endpoint` on the script tag at a route you host: it receives `{ "address": "…", "country": "GB" }` and forwards it to `POST /api/v1/address`.
```html
```
`GET /api/v1/address/postcode/{postcode}?country=GB` lists what stands at a postcode. Wherever a national register with a street listing is installed it answers the streets that register lists inside the postcode, in its own spelling and with no house numbers, for one credit. For Great Britain that listing is Ordnance Survey's Open Names, filed by postcode district, so the answer is the streets of the district rather than of the postcode, at most 100 per answer; in the United States a ZIP+4 lists the streets of its five-digit ZIP. The buildings are what no open dataset lists: every full UK address list derives from the Royal Mail Postcode Address File, so the delivery-point list waits for a licensed partner, and a country with neither source answers `501 ADDRESS_LIST_UNAVAILABLE` and is not charged. So does a postcode the street listing cannot speak for — a BT, BFPO, BF or BX code, a German postcode outside Berlin, a British district Open Names files no street under, a Czech village whose houses carry no street name — with a message that says which, because an empty list would read as "no addresses here"; an empty list is answered, free, only where the registers' coverage is complete and none of them holds an address at the postcode. The streets themselves are the streets endpoint described above.
Agents reach the same lookup through the MCP server (https://spaw.co/docs/mcp) as the `verify-address` tool, and you can run one by hand on the address verification page in the dashboard (https://spaw.co/verify/address), which shows every field with "not evaluated" where a dataset is missing. Without an account, the free postal address checker (https://spaw.co/tools/address-checker) runs the same pipeline against a throttled guest endpoint and publishes the datasets this server currently holds.
Where it runs: Google Sheets with `=SPAW_ADDRESS`, working today (https://spaw.co/integrations/google-sheets); Zapier, Make and n8n through their own HTTP steps today, with Spaw apps built but not yet listed (https://spaw.co/integrations/zapier, https://spaw.co/integrations/make, https://spaw.co/integrations/n8n); the Node.js, Python and PHP SDKs, built and awaiting publication (https://spaw.co/integrations/node-sdk, https://spaw.co/integrations/python-sdk, https://spaw.co/integrations/php-sdk); and a Shopify recipe that checks each order's shipping address after checkout through Zapier or Make (https://spaw.co/integrations/shopify).
## Bulk address jobs
For a customer list or a CRM export, `POST /api/v1/address/bulk` (https://spaw.co/docs/api/create-address-bulk-job) queues up to 100,000 addresses and answers `202` straight away with the job. Each item is a written address or an object of the separated fields, and may carry its own `country`; the list-level `country` applies to the rest, and a row left with no country is answered `missing_country`, free, rather than refusing the whole run. `deliverability: true` asks the licensed partner about every row, and `webhook_url` (https only) is where the finish is announced.
```bash
curl https://spaw.co/api/v1/address/bulk \
-H "Authorization: Bearer sk_live_…" \
-H "Content-Type: application/json" \
-H "Idempotency-Key: addresses-2026-09-05" \
-d '{"addresses": ["10 Downing Street, London SW1A 2AA", {"address_line1": "Postfach 10 01 01", "postal_code": "50441", "locality": "Köln", "country": "DE"}], "country": "GB", "webhook_url": "https://www.example.com/hooks/spaw"}'
```
The `webhook_secret` in that answer is returned once and never again. Keep it: it signs the completion webhook.
An `Idempotency-Key` header (1 to 128 printable ASCII characters) makes creation safe to retry. A retry carrying the same key and the same request answers `200` with the same body — the same job, the same `webhook_secret` — and the header `Idempotent-Replayed: true`, so a timed-out POST can simply be sent again without queueing the list twice. The same key with a different body answers `409 IDEMPOTENCY_KEY_REUSED` rather than the wrong job. The request is fingerprinted as the lookup will read it — the list-level country already folded into each item — so a retry that spells the same request differently still replays.
Poll `GET /api/v1/address/bulk/{jobId}` (https://spaw.co/docs/api/get-address-bulk-job) for the status, `processed`, `duplicate_count`, the running `valid` and `invalid` counts, the `low`, `medium` and `high` risk counts and `credits_used`. Polling is free. `POST /api/v1/address/bulk/{jobId}/cancel` stops a queued run on the spot and a running one at its next checkpoint; the rows already answered stay billed and downloadable, and a job that has already finished answers `409 JOB_ALREADY_FINISHED`.
A run that stopped because the credit balance ran out is resumed rather than uploaded again: top up, then press Resume on the run in the dashboard or call `POST /api/v1/address/bulk/{jobId}/resume` (https://spaw.co/docs/api/resume-address-bulk-job). The whole list runs again from the top, and the rows it already answered are repeats inside the seven-day window, so only the rows it never reached are charged. The job keeps its id, its webhook and its cumulative `credits_used`, while `processed` and the verdict counts restart at zero and are rebuilt as the rows come back. A run that stopped for any other reason, or that is already going again, answers `409 JOB_NOT_RESUMABLE`; one whose list has passed out of the 30-day retention window answers `409 JOB_INPUT_EXPIRED`, and an empty balance answers `402 INSUFFICIENT_CREDITS` rather than queueing a run that would stop again at once.
If a delivery fails — your endpoint was down for the half-minute the three attempts cover — ask for it again rather than falling back to polling: press **Send webhook again** on the run in the dashboard, or call `POST /api/v1/address/bulk/{jobId}/webhook/redeliver` (https://spaw.co/docs/api/redeliver-address-bulk-webhook). The body is byte-for-byte the body the first delivery carried, rebuilt from the run's own frozen row, so the two reconcile. The signature is new, and has to be: a receiver refuses anything whose `t` is more than five minutes from its own clock, so verify a redelivery exactly as you verify any other delivery. Nothing deduplicates it for you — a receiver that gets both the original and the redelivery sees `bulk_address_job.finished` twice, and `job.id` is the key to deduplicate on. A run created without a `webhook_url` answers `409 WEBHOOK_NOT_CONFIGURED` and one that has not settled answers `409 JOB_NOT_FINISHED`; a run whose last delivery succeeded is not refused, because a receiver can lose a message it has already acknowledged. The delivery is queued rather than made while you wait — one attempt takes half a minute against an endpoint that is down, which is the endpoint you are asking about — so the call answers `202` and the run reads `webhook_status: "pending"` until a worker has an answer, then `delivered` or `failed` with what your endpoint said in `webhook_detail`. The dashboard shows the same three states beside the run. It is free and capped at ten 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.
`GET /api/v1/address/bulk/{jobId}/results` streams the CSV. `?variant=` narrows the rows to the ones that stand as written or the ones that do not (`valid`, `invalid`), or to one risk level (`low`, `medium`, `high`); `full` is the default. A job that is still running answers `409 RESULTS_NOT_READY`, one cancelled before any row was processed answers `409 JOB_CANCELLED`, and one that failed before writing a row answers `409 JOB_FAILED`; neither of those last two billed anything. A run cancelled or failed part-way keeps the rows it did write, and they stay downloadable.
The CSV leads with the uploaded file's own columns, unchanged and in their original order, so the results line up with the file you sent. Then `address` — the address exactly as it was submitted, so a pasted list has something to join on — then every response field in the order the API answers them, then `sources`, `credits_used` and `suppressed` — the last of those `true` on a row answered from your suppression list rather than read again:
```
address, valid, reason, country, country_name, continent, is_eu, is_eea, privacy_regime, formatted, formatted_lines, formatted_local, organization, building, street, house_number, unit, po_box, dependent_locality, locality, administrative_area, postal_code, postal_code_formatted, postal_code_valid, postal_code_type, postcode_status, locality_matches_postcode, administrative_area_matches_postcode, address_type, address_type_source, is_po_box, is_private_mailbox, exists, match_level, register, register_coverage, premise_key, building_use, is_multi_unit, unit_check, street_suggestion, latitude, longitude, location_source, companies_registered, is_mail_drop, mail_drop_provider, mail_drop_source, deliverability_checked, is_deliverable, is_cmra, is_vacant, is_residential, confirmation, changes, looks_placeholder, risk_score, risk_level, risk_signals, ok_to_ship, blocked_by, sources, credits_used, suppressed
```
The array fields are flattened rather than dropped: `sources` as `dataset@version` and `risk_signals` as `signal:weight`, both joined with `;`. The multi-line `formatted` address is written on one line, its lines joined with a comma and a space, and any cell containing a space or a comma is quoted. An empty cell is a null, and a null means the field was not evaluated — never "no".
When the run ends, whatever the outcome, the finish is POSTed to `webhook_url` as `bulk_address_job.finished`:
```json
{
"event": "bulk_address_job.finished",
"job": {
"id": 91,
"status": "completed",
"total": 2,
"processed": 2,
"valid": 1,
"invalid": 1,
"low": 1,
"medium": 0,
"high": 0,
"credits_used": 1,
"stopped_reason": null,
"finished_at": "2026-09-05T12:04:11+00:00"
}
}
```
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 the `webhook_secret` creation returned once. Verify it against the exact bytes you received — not against a re-encoded copy of the parsed JSON — compare with a constant-time function, and refuse anything whose `t` is more than five minutes from your own clock: without that check a captured delivery can be replayed at you forever. The original `X-Spaw-Signature`, the HMAC of the body alone, is sent beside it until 1 March 2027 and still verifies. A cancelled run fires the same webhook with `status: "cancelled"`, and a delivery that fails is retried twice with a backoff, three attempts in all.
Billing is per row and identical to a single lookup: a row that answers `valid: true` costs one credit, a row that cannot stand costs nothing whatever the reason, and a row repeating an address you looked up in the last seven days is free. Rows repeating an address already in the same list are answered once, cost nothing, and are counted in the job's `duplicate_count`. Deliverability adds its premium only to the rows a partner actually answered. If the balance runs out part-way, the run stops with `stopped_reason: "insufficient_credits"` and the rows already paid for stay downloadable.
The address verification page in the dashboard (https://spaw.co/verify/address) runs the same jobs from a Bulk runs tab: upload a CSV, or paste one written address per line, pick the country for the rows that do not name their own, and follow the run's progress there. Its valid, invalid and risk counts fill in when it finishes. An uploaded file keeps every column, and its headers are mapped to the address fields — `address`, `street`, `city`, `postcode`, `country` and the rest — so a spreadsheet of separated fields works as well as a column of written lines.
## Monitored address lists
An address is not a fact that stays true. A postal operator terminates a postcode, a building leaves the national register, a suite quietly becomes a branch on a mailbox operator's published list. A monitored list (`POST /api/v1/address/monitors`, https://spaw.co/docs/api/create-address-monitor) keeps up to 500 addresses — your own sites, a supplier's registered offices, the delivery addresses behind an order book — and re-reads all of them on a schedule.
`cadence` is `weekly` or `monthly`. There is deliberately no daily option: every dataset an address is measured against — the national address registers, the postcode directories, the open company registers — is republished weekly at best, so a daily run would charge you for reading the same files again.
```bash
curl https://spaw.co/api/v1/address/monitors \
-H "Authorization: Bearer sk_live_…" \
-H "Content-Type: application/json" \
-d '{"name": "Supplier sites", "cadence": "monthly", "country": "GB", "addresses": ["10 Downing Street, London SW1A 2AA", {"address_line1": "Postfach 10 01 01", "postal_code": "50441", "locality": "Köln", "country": "DE"}]}'
```
Each item is a written address or an object of the separated fields and may carry its own `country`; the list-level `country` applies to every item that does not. `deliverability: true` asks the licensed partner on every run. Creation answers `201` and queues the baseline run straight away.
### What a run says about each address
`GET /api/v1/address/monitors/{monitorId}` (https://spaw.co/docs/api/get-address-monitor) answers `results`: the last run's verdict per address, keyed by the address as you saved it, with the `status` below, the `flags` that fired, and whether it `changed` since the run before. One address can match several conditions — a terminated postcode at an address the register also dropped — so `flags` lists them all and `status` is the first of them.
| status | what it means |
| --- | --- |
| `ok` | The address still stands as written, and nothing was flagged. |
| `invalid` | It no longer stands as written; the row's `reason` says which part failed. |
| `not_in_register` | A register whose coverage is complete no longer carries the address. |
| `terminated_postcode` | The postal operator retired the postcode. |
| `mail_drop` | The address now looks like a mail drop. |
| `high_risk` | The published weights now add up to a high `risk_score`. |
| `unverifiable` | The address stands, but nothing installed here could check it. |
`unverifiable` is the honest answer for a country whose datasets this server does not hold. The address parsed and formatted correctly against its own country's rules, and then there was nothing to compare it with: no address register, no postcode directory and no company register covering it. It is counted apart from `ok` so you can see how much of a list nobody here can check, it says something about the data installed on this server and never about the address, and it never raises an alert. A list of addresses in a country with no installed dataset reports `unverifiable` for every one of them rather than pretending to have looked.
`not_in_register` fires only where `exists` is false, and `exists` is false only when the register that answered has complete coverage and does not carry the address. A register whose coverage is partial — the US National Address Database and the French Base Adresse Nationale among them — answers `exists: null` for an address it does not carry, and null means the address was not evaluated, never that it does not exist. So a partial register can never move an address into `not_in_register`, and neither can a country with no register at all.
`mail_drop` is a heuristic, not an official flag: it fires when a mailbox operator's own published branch list names the address, when a registered-agent or formation service publishes the address as its own or a public listing or filing naming the service carries it, or when so many companies are registered at it that it is one in practice, and `mail_drop_source` says which of the three. It is never a postal service's own agency flag, and it is a statement about a place, not about anyone who receives post there.
### Who gets told, and when
An email goes out only when an address that was already known and not flagged becomes flagged. A first run sets the baseline and mails nothing, however the list reads; an address you add to an existing list is baselined the same way on its own first run; and an address that stays flagged run after run never alerts again. The mail names each newly flagged address and why: the reason it stopped standing, that the register no longer carries it, the day the postcode was terminated where the directory records one, the operator whose branch list names it or the number of companies registered there, or the risk score.
`last_summary` counts every status — `total`, `ok`, `invalid`, `not_in_register`, `terminated_postcode`, `mail_drop`, `high_risk` and `unverifiable` — alongside `changed`, `newly_flagged` (the addresses that email named), `credits_used` and `stopped_reason`.
### Billing, and the rest of the endpoints
Each run bills every address exactly like a single lookup: one credit for an address that answers `valid: true`, nothing for one that cannot stand whatever the reason, and nothing for a repeat of the same address within seven days. Runs are at least a week apart, so that marker has normally expired and each scheduled run pays for a fresh answer; a run you trigger by hand right after another is free for the same reason. `deliverability: true` adds its premium only to the addresses a partner actually answered, and adds nothing while no partner is configured. If the balance runs out part-way the run stops with `stopped_reason: "insufficient_credits"`, and the addresses already re-read keep their new status.
`GET /api/v1/address/monitors` lists the account's monitors, newest first, 100 per page, each with the summary of its last run. `POST /api/v1/address/monitors/{monitorId}/run` re-checks a list ahead of schedule and answers `202`. `DELETE /api/v1/address/monitors/{monitorId}` stops it and removes its stored results. A monitor that belongs to another account answers `404`.
A monitor is the one place an address is kept: the list you saved and the last run's status per address stay in your account until you delete the monitor, because there is no other way to tell you what changed. Nothing else about those lookups is stored.
The address verification page in the dashboard (https://spaw.co/verify/address) runs the same lists from a Monitors tab: paste up to 500 written addresses, one per line, name the list, choose weekly or monthly and the country they are read against, and each row then shows the address count, the last run with anything flagged in red and the unverifiable count beside it, the next run, and buttons to run it now or remove it.
A monitor can post its runs as well as email about them. Set `webhook_url` (https) when you create it, or later with a `PATCH`, and the signing secret comes back once in that response and is never readable again. `webhook_events` decides which runs are posted: `changes`, the default, posts only when the run found something new — an address that is newly flagged, or one whose status changed — or stopped early, and `every_run` posts after every run, which also tells you the monitor is still running. The body is `{ "event": "address_monitor.run", "monitor": {…}, "summary": {…}, "changes": [ … ] }`, where `changes` names the addresses the alert email names, and the delivery is signed with `X-Spaw-Signature-V2`: `t={unix seconds},v2={hex}`, where the hex is the HMAC-SHA256 of `v2:{t}:{raw body}` keyed with the secret. Refuse anything whose `t` is more than five minutes from your own clock — that window is what stops a captured delivery being replayed at you later. The original `X-Spaw-Signature`, the HMAC of the body alone with no timestamp, is sent beside it until 1 March 2027 and still verifies; both recipes are at https://spaw.co/integrations/webhooks. Connection errors and 5xx answers are retried twice, three attempts in all; `webhook_status` and `webhook_detail` on the monitor say how the last delivery went. A URL that resolves onto a private network is refused at delivery, every time, because DNS can be repointed after a URL is saved.
A monitor is changed rather than replaced: `PATCH /api/v1/address/monitors/{id}` (https://spaw.co/docs/api/update-address-monitor) takes `name`, `addresses`, `cadence`, `country`, `deliverability` and the webhook fields, and applies only the ones you send. That matters because what the last run recorded per entry is the baseline every alert is compared against: deleting a monitor and creating it again throws that away, and the first run of the new one only sets a baseline — a full cycle of silence, which is the one thing a monitor is bought to prevent. Replacing the list keeps what was recorded about the addresses that stayed, deletes what was recorded about the ones you removed, and gives a new entry no baseline. Changing the cadence neither skips a run nor starts a second one: a run already scheduled sooner than the new interval still happens, and a longer interval applies from the one after it. A body naming no field it could change answers `422`.
A monitor that stops being run is deleted, and the identifiers and results it stored go with it. `prunes_after_days` on every monitor payload says how long that takes, and `meta.retention_days` on the list endpoint says the same. A monitor in active use is never pruned: each run advances `last_run_at`, so the window only closes on a list that has genuinely stopped being re-checked — at which point holding the details somebody gave you serves no purpose. Deleting a monitor yourself removes everything it held at once.
## Outcome reporting and the suppression list
A lookup can only say what open data knows. What the post office tells you afterwards is better evidence than any register, and it can be sent back: `POST /api/v1/address/feedback` (https://spaw.co/docs/api/report-address-feedback) takes up to 1,000 items, each an address — written on one line as `address`, or as the separated fields, with an optional own `country` — an `outcome`, and an optional `occurred_at`. The list-level `country` applies to the items that do not name their own. It answers `202` with `{"recorded", "skipped"}`, costs nothing, and is never logged as a lookup.
```bash
curl https://spaw.co/api/v1/address/feedback \
-H "Authorization: Bearer sk_live_…" \
-H "Content-Type: application/json" \
-d '{"country": "GB", "items": [
{"address": "10 Downing Street, London SW1A 2AA", "outcome": "returned", "occurred_at": "2026-09-01"},
{"address_line1": "Postfach 10 01 01", "country": "DE", "outcome": "moved"}
]}'
```
There are four outcomes, and they describe what happened to post you actually sent:
| Outcome | Means | Effect on the suppression list |
| --- | --- | --- |
| `delivered` | The post arrived. | Clears an entry the feedback put there. An entry you imported is left alone. |
| `returned` | The item came back undelivered. | Suppresses on the second such report that is not followed by a later `delivered`. |
| `moved` | The occupant has moved on. | Suppresses at once. |
| `refused` | Delivery was refused at the door. | Suppresses at once. |
`GET /api/v1/address/feedback/summary` (https://spaw.co/docs/api/address-feedback-summary) answers the account's own counts by outcome over the reporting window.
### Your reports are never shared with another account
This is a deliberate decision and not an oversight. The IP and phone products both have a cross-account signal: enough separate accounts reporting one IP address raises everyone's risk on it, and the phone product reads the regulator's own tables. Address feedback has no equivalent and never will. An IP address is a network endpoint and a phone number is a handset, but a postal address is where somebody lives, and a shared "post fails here" database would be a database about residences, assembled from other people's post. Your reports raise the risk of your own later lookups of an address and nobody else's, and nobody else's reports ever reach yours.
That is what `reported_returned` (+50, dataset `address-feedback`) is: it fires when the calling account's own most recent report for the address was `returned`, `moved` or `refused` rather than `delivered`. A later `delivered` report cancels it. The same address looked up by a different account carries no such signal, because that account has reported nothing about it.
### The suppression list
An address that earns its way onto the list is answered from the list instead of being read again. `GET /api/v1/address/suppressions` (https://spaw.co/docs/api/list-address-suppressions) lists the account's entries newest first, 100 per page, each with the address as it was submitted, the reason (`returned`, `moved`, `refused`, or `imported` for a list the owner uploaded), the source and when it was added. `POST /api/v1/address/suppressions` (https://spaw.co/docs/api/add-address-suppressions) imports up to 10,000 addresses in one call, free — a written line or the separated fields per item, with a list-level `country` for the ones that do not name their own. `DELETE /api/v1/address/suppressions/{suppressionId}` removes one entry. An entry you imported is never overwritten or cleared by your reports or a lookup: it is your do-not-send list, not ours, and it leaves only when you remove it. Importing an address that is already listed makes it one you imported, keeping its reason.
Finding one address again does not mean paging the list. The Suppressions tab has a search box, and the list endpoint takes `value` with its `country`, plus `source`, `reason`, `since`, `until` and `per_page`. A searched address is folded against that country's postal format before it is matched, so any spelling of a suppressed address finds it. To drop addresses without looking up their ids, `DELETE /api/v1/address/suppressions` (https://spaw.co/docs/api/remove-address-suppressions) takes up to 1,000 of them in `values` with a `country`, and answers which were on the list and which were not.
Four rules keep the list honest:
- **Batch, bulk and monitor runs honour it.** A suppressed address is answered from the list without being read again, free, with `meta.suppressed: true` on the item.
- **A single lookup never honours it.** `POST /api/v1/address` always reads the address fresh and bills it normally, because a new occupant is exactly the case a cached "post fails here" would get wrong. It never takes off an entry your reports still stand behind, or an import: an entry your reports added leaves when you report a delivery, or at the next single lookup once those reports have aged out of the 90-day window, and an import only when you remove it.
- **An entry your reports added is re-verified rather than served once it is older than 90 days.** Nothing your reports put on the list is trusted forever; people move in as well as out. A stale entry is kept while the reports still stand behind it, and a new moved, refused or returned report renews it. An import never goes stale: it is your own list, served for as long as it stays on it.
- **Reporting and importing are free.** Neither endpoint costs a credit, and neither is logged as a lookup.
The address verification page in the dashboard (https://spaw.co/verify/address) has a Suppressions tab over the same list: paste addresses one per line to import them, name the country they are all read against, export the whole list as CSV, and remove an entry with one button. Each row shows the address, the reason in words — returned twice, occupant moved, delivery refused, or imported — the source and when it was added.
## Billing and privacy
A fresh lookup that answers `valid: true` costs one credit. An address that cannot stand as written costs nothing, whatever the reason. A repeat within seven days is free, and the repeat marker is keyed on the address as the pipeline folds it — case, punctuation, abbreviations and spacing already folded away — so two spellings of one address are one lookup and one charge.
Asking for `deliverability: true` reserves the premium credits up front, so a request that cannot afford them is refused cleanly, and charges them only when the partner actually answered. While no partner is configured the check answers `deliverability_checked: false` and adds nothing to the bill. Its answers are cached apart from the plain lookup, like any premium answer.
A postal address is personal data, and nothing about an address lookup is stored. No history row, no log line with the address. The traces are the keyed seven-day marker that makes a repeat free and the count behind `seen_by_you`, kept under the building's digest; both are hashes, never the address. Every signal is a local table or a bundled file, so no third party sees the address — unless you ask for the deliverability check, which is the one call that leaves the server, and only for the address you asked about.
A bulk run is the one exception, and only for as long as it needs to be: the list you submitted and the result CSV are written to disk because there is no other way to process a job in the background and hand the answers back. They are the only copies that exist, and both are deleted along with the job after 30 days. A monitored list is the other: the addresses you saved and the last run's status for each of them stay in your account until you delete the monitor.
Outcome reports are the third: an address you report on is kept with the outcome and its date, and an address on your suppression list is kept until you remove it or, for one your reports added, until they no longer stand behind it, because a list you cannot see is not a list you can trust. Both live inside your account, are readable and deletable there, and are never read by another account.
The privacy policy (https://spaw.co/privacy) states the same facts alongside the rest of the data handling.
Next: the API reference for the envelope, the error codes and every endpoint: https://spaw.co/docs/api.
---
# Business verification
Read a business identifier against the registers this server holds: a Legal Entity Identifier against the GLEIF Level 1 golden copy, or a UK company number against the Companies House free company data product. Both are bulk files synced locally, so no third party sits in the request path, nothing about a lookup leaves the server, and an answer takes milliseconds.
Where a LEI names its entry in a national register Spaw also holds, the two records are compared and both sides are quoted, so a disagreement is something you can read rather than take on trust.
```
curl https://spaw.co/api/v1/entity \
-H "Authorization: Bearer sk_live_…" \
-H "Content-Type: application/json" \
-d '{"identifier": "213800QILIUD4ROSUO03", "type": "lei"}'
```
## Sending an identifier
`identifier` and `type` are required. `type` is `lei` or `company_number`.
A company number needs the `country` whose register it belongs to, because company numbers are only unique inside their own register; `GB` is the register installed today. A LEI needs no country — the identifier is global by construction. A UK company number is zero-padded to eight characters before it is looked up, so `1234567` and `01234567` are the same company.
An identifier the server holds no register for answers `unsupported_register` rather than guessing, and it is free.
## What comes back
| field | type | meaning |
| --- | --- | --- |
| identifier | `string` | The identifier as the register writes it: a LEI uppercased, a company number padded to eight. |
| identifier_type | `string` | `lei` or `company_number`. |
| found | `boolean` | Whether a register carried it. `false` is free. |
| reason | `string \| null` | Why not: `unknown_identifier`, `malformed_identifier`, `unsupported_register` or `register_unavailable`. |
| name | `string \| null` | The legal name the register carries. |
| status | `string \| null` | `active`, `inactive`, `dissolved` or `unknown`. |
| status_detail | `string \| null` | The register's own wording, which distinguishes a strike-off from a liquidation. |
| country / jurisdiction | `string \| null` | The country of registration, and the jurisdiction of formation, which can be a subdivision (`US-DE`). |
| category / legal_form | `string \| null` | The register's entity category, and the ISO 20275 legal form code where one is carried. |
| registered_at | `date \| null` | When the entity was formed or incorporated. |
| record_updated_at | `date \| null` | When the register last updated this record. |
| checksum_valid | `boolean \| null` | LEI only: whether the check digit is right. Null for identifiers that carry none. |
| registration_status | `string \| null` | LEI only: `ISSUED`, `LAPSED`, `RETIRED`, `ANNULLED`, `MERGED` or `DUPLICATE`. |
| next_renewal_at | `date \| null` | LEI only: when the registration was last due for renewal. |
| other_register / other_identifier | `string \| null` | The national register this record names, and its identifier there. |
| flags | `string[]` | What the registers disagree about, or what one of them warns. |
| agreements | `object[]` | Each cross-register comparison, with both sides quoted. |
| sources | `object[]` | Every register behind the answer, with the date of the copy. |
## The check digit is a typing check
A LEI carries a two-digit check digit computed with ISO 7064 MOD 97-10, the same arithmetic an IBAN uses. `checksum_valid` reports it, and it catches a mistyped or transposed character — nothing more. A perfectly formed LEI can never have been issued, which is what the register lookup answers.
Measured against the published register on 8 September 2026: of 45,515 consecutive real LEIs, 45,512 satisfy the checksum, and the three that do not are all `ANNULLED` registrations — LEIs issued in error and withdrawn. So a record can exist with a failing checksum, which is why the two facts are reported side by side rather than one standing in for the other.
## An entity can outlive its LEI
`status` is about the entity: is this company still a company. `registration_status` is about the identifier: has anyone renewed it. A LEI must be reconfirmed each year against the entity's own national register, and a great many are not, so `LAPSED` is common and means the reference data has not been rechecked recently — not that the company has gone.
When the registration is not current, `lei_registration_not_current` is flagged. If a counterparty needs a current LEI for a regulatory reason, that flag is the one that says they do not have one.
## Comparing two registers
When a LEI record names a Companies House number and both registers are installed, the two are compared:
- **name** — on their letters and digits, with the company-form words (limited, ltd, plc, llp, inc) removed, so `ACME HOLDINGS LTD.` and `Acme Holdings Limited` agree. A real difference raises `name_differs_across_registers`, and both names are in `agreements`.
- **status** — a disagreement raises `status_differs_across_registers`. The national register is the authority on whether a company exists; the LEI record reflects what was last reported. A disagreement usually means the LEI record is stale.
- Where the national register records the company as dissolved, `dissolved_in_national_register` is flagged. That is the strongest signal the product carries: a dissolved company no longer exists as a legal person and cannot enter a contract.
Where there is no counterpart record, `agreements` is empty. An absent register is not a disagreement.
## No addresses, on purpose
Nothing in the response is a registered address, and nothing will be.
A registered address is very often somebody's home — that is why Companies House operates a protected-address regime and why several registers mask an entrepreneur's address by law. An identifier on its own does not say whether a limited company or a sole trader stands behind it, and a sole trader's registered details are personal data in a way a company's are not.
So the product carries country and jurisdiction, which are enough to answer "is this a real, live entity", and holds no register of where people live. If you need to check an address, that is what the address product does, and it is about places rather than people for the same reason.
## Where the data comes from
The Legal Entity Identifier reference data is published by the Global Legal Entity Identifier Foundation under a CC0 1.0 public-domain dedication, as a golden copy refreshed daily. Spaw syncs that file and answers from its own copy. Spaw is not affiliated with, endorsed by or connected to that foundation in any way, and an answer here is Spaw's, not theirs.
UK company data is the Companies House free company data product, used under the Open Government Licence v3.0. It is republished monthly, within five working days of the previous month end, so an answer can be up to five weeks behind the live register. Every answer carries the date of the copy in `sources`, and `/status` lists both registers with the date each was last synced. Where the age of a record matters, read it there.
## Monitored lists
Save up to 500 identifiers and have them re-checked weekly or monthly. There is no daily cadence: the UK company file is republished monthly, so a daily run would bill for data that cannot have changed.
`POST /api/v1/entity/monitors` takes a `name`, a list of `identifiers` and a `cadence`. Each entry is an object of `identifier`, `type` and, for a company number, `country`; a bare string is read as a LEI, because that is the only identifier that needs no country.
The first run happens immediately and only sets the baseline — so saving a list never emails you about what was already true when you saved it. From the next run on, an email goes out for an entry that was fine last time and is not any more:
| status | what happened |
| --- | --- |
| `left_register` | A register carried the identifier before and does not now. For the UK this is what a dissolution looks like — see below. |
| `dissolved` | The register records the company as dissolved. Rare on the UK register, for the reason below. |
| `inactive` | The register no longer records the entity as active. |
| `registration_lapsed` | The LEI registration is no longer current. |
| `registers_disagree` | The two registers have stopped agreeing about the name or the status. |
| `unverifiable` | No register on this server could answer for it. Never alerted on: that is a limit of the data here, not a fact about the company. |
| `ok` | Nothing to report. |
Each run bills each identifier exactly as a single lookup, and the seven-day repeat marker applies — so a weekly cadence over a monthly register costs almost nothing after the first run. A run that exhausts the balance stops where it got to and reports `stopped_reason`.
### How a UK dissolution actually shows up
The Companies House free company data product is a snapshot of the **live** register. A company that is struck off is **removed from the file** rather than marked dissolved — so the statuses that file carries are `Active`, `Active - Proposal to Strike off`, `Liquidation`, `In Administration` and similar, and never `Dissolved`.
That means a dissolution reaches you as the identifier disappearing between runs, which the monitor reports as `left_register` and alerts on. Without that status it would read as `unverifiable` and pass silently, which would be the single worst thing this product could get wrong.
`Active - Proposal to Strike off` is the earlier warning, and it is the most common non-plain status on the register by a wide margin. It folds to `status` `active` — because it is still on the register — with the register's own wording in `status_detail`, so read that field if you want to act before the company goes.
## How long a monitor is kept
A monitor can post its runs as well as email about them. Set `webhook_url` (https) when you create it, or later with a `PATCH`, and the signing secret comes back once in that response and is never readable again. `webhook_events` decides which runs are posted: `changes`, the default, posts only when the run found something new — an identifier that is newly flagged, or one whose status changed — or stopped early, and `every_run` posts after every run, which also tells you the monitor is still running. The body is `{ "event": "entity_monitor.run", "monitor": {…}, "summary": {…}, "changes": [ … ] }`, where `changes` names the identifiers the alert email names, and the delivery is signed with `X-Spaw-Signature-V2`: `t={unix seconds},v2={hex}`, where the hex is the HMAC-SHA256 of `v2:{t}:{raw body}` keyed with the secret. Refuse anything whose `t` is more than five minutes from your own clock — that window is what stops a captured delivery being replayed at you later. The original `X-Spaw-Signature`, the HMAC of the body alone with no timestamp, is sent beside it until 1 March 2027 and still verifies; both recipes are at https://spaw.co/integrations/webhooks. Connection errors and 5xx answers are retried twice, three attempts in all; `webhook_status` and `webhook_detail` on the monitor say how the last delivery went. A URL that resolves onto a private network is refused at delivery, every time, because DNS can be repointed after a URL is saved.
A monitor is changed rather than replaced: `PATCH /api/v1/entity/monitors/{id}` (https://spaw.co/docs/api/update-entity-monitor) takes `name`, `identifiers`, `cadence` and the webhook fields, and applies only the ones you send. That matters because what the last run recorded per entry is the baseline every alert is compared against: deleting a monitor and creating it again throws that away, and the first run of the new one only sets a baseline — a full cycle of silence, which is the one thing a monitor is bought to prevent. Replacing the list keeps what was recorded about the identifiers that stayed, deletes what was recorded about the ones you removed, and gives a new entry no baseline. Changing the cadence neither skips a run nor starts a second one: a run already scheduled sooner than the new interval still happens, and a longer interval applies from the one after it. A body naming no field it could change answers `422`.
A monitor that stops being run is deleted, and the identifiers and results it stored go with it. `prunes_after_days` on every monitor payload says how long that takes.
A monitor in active use is never pruned: each run advances `last_run_at`, so the window only closes on one that has genuinely stopped — the account ran out of credits and never came back, or the list was created and never ran. That is the point at which holding somebody else's identifiers stops serving the purpose they were given for.
This is the only monitor in the product line that prunes itself, and it does so because it has to. An identifier is often a company number, but it can equally be a sole trader's, and nothing in the identifier says which. Deleting your own monitor removes everything it held immediately.
## Billing and privacy
1 credit when a register answers. Free when it does not: a malformed identifier, one no register carries, an unsupported register and a register that is not installed all cost nothing, because none of them told you anything you could not have worked out. A repeat of the same identifier on the same account inside seven days is free.
A register that has not been synced answers `register_unavailable` and is never cached, so the next lookup answers as soon as the file lands. Absence of data is never reported as absence of the company.
Nothing about a single lookup is stored beyond the credit ledger entry that bills it. A monitor stores the identifiers you saved and the status of each from its last run, and nothing else. [POST /v1/entity/batch](https://spaw.co/docs/api/lookup-entity-batch) takes up to 500 identifiers in one call and bills the same way a single lookup does — only the ones that were found. There is no browser endpoint and no bulk upload for this product: the browser endpoints exist for a form a visitor fills in, and nobody types a company number into one.
---
# Cross-signal consistency
One call takes any subset of an email address, a phone number, an IP address and a postal address, runs each through the product that owns it, and reports where the answers agree and where they do not. A phone allocated in one country beside an address in another, a burner mailbox arriving from a datacenter, a mail-drop address beside a free consumer provider: none of those is visible to a single lookup, and all of them are visible here.
It is built for signup, onboarding and marketplace vetting — the moment before there is an order, a card or a payment, when the contact details are all you have. No new dataset stands behind it: every comparison is arithmetic over fields the four pipelines already computed, which is why the analysis is free.
```
curl https://spaw.co/api/v1/consistency \
-H "Authorization: Bearer sk_live_…" \
-H "Content-Type: application/json" \
-d '{"email": "ada@example.com", "phone": "+442079460018",
"ip": "81.2.69.142",
"address": {"address": "10 Downing Street, London, SW1A 2AA", "country": "GB"}}'
```
## Sending the signals
Send any one of `email`, `phone`, `ip` and `address`, or any combination of them; a body with none of the four is a validation error. Each field takes exactly what its own single-lookup endpoint takes, so `address` is an object with either a written `address` or the separated fields, always with its own `country`. The top-level `country` is the region hint for the phone number only.
The signals run in a fixed order — `ip`, `phone`, `address`, `email` — because a JSON object has no input order, and because email is the only leg that does DNS and a mailbox handshake, so a call that runs out of credits stops at the slowest leg last.
The per-product extras are deliberately not accepted here: there is no `hlr`, no `deliverability`, no `abuse_contact` and no freshness controls. Call the product endpoint directly when you want those; this one is for the comparison.
## The per-signal blocks
`data.signals` carries one entry per product, and all four keys are always present. Each `data` is exactly what that product's own endpoint returns, field for field — the 46 email fields, the 49 phone fields, the 62 IP fields, the 61 address fields — so nothing has to be learned twice and nothing is summarised away. Beside it, `meta` says what that one signal cost and whether it was a repeat.
A key is `null` when you did not ask for that signal, and also when the call ran out of credits before reaching it. The difference is in `meta.requested` against `meta.processed`, and in `meta.stopped_reason`.
## Comparisons and flags
Nine comparisons, each emitted only when both of its signals answered. A signal nobody asked about produces silence, not a row saying nothing could be compared.
| comparison | fields | what it says |
| --- | --- | --- |
| phone_address_country | `phone.country` ↔ `address.country` | The country the number was allocated under, against the country the address stands in. Both come from registers, neither is a guess. |
| ip_address_country | `ip.country` ↔ `address.country` | Where the request came from, against where the post would go. Withheld when the IP is a VPN exit, a relay or a datacenter egress. |
| ip_phone_country | `ip.country` ↔ `phone.country` | The same comparison against the numbering plan, withheld under the same conditions. |
| ip_address_distance | `ip.latitude/longitude` ↔ `address.latitude/longitude` | How far apart the two places are, in kilometres, as evidence only. Measured just once the countries already agree and the IP location is graded high confidence. It never raises a flag. |
| email_ip_disposable_datacenter | `email.disposable` ↔ `ip.is_datacenter` | A burner address arriving from hosting rather than from a consumer network. |
| email_ip_disposable_anonymous | `email.disposable` ↔ `ip.is_anonymous` | A burner address arriving over a VPN, a proxy or Tor. |
| email_address_free_provider_mail_drop | `email.free_provider` ↔ `address.is_mail_drop` | A free consumer mailbox beside a commercial mail-receiving agency. |
| email_phone_disposable | `email.disposable` ↔ `phone.is_disposable` | Both contact details on disposable lists: neither can receive anything for long. |
| phone_address_region | `phone.region` ↔ `address.administrative_area` | The state or province a North American number was issued in, against the region on the address, as evidence only. People keep a number when they move, so a difference is reported and never flagged; withheld where a number is placed in a town rather than a region, and when the two countries differ. |
Every entry quotes both sides — `left` and `right`, each naming the signal, the field and the value it held — so nothing is asserted that you cannot see the working for. The `result` is `agree`, `differ` or `not_compared`, and each flag has its own page under https://spaw.co/docs/reasons#consistency.
## When a comparison is withheld
Two rules keep the analysis from manufacturing evidence, and both report themselves in `withheld` rather than settling the comparison quietly.
`missing_value` — one of the two fields was null. Across every Spaw product a null means "not evaluated" and never "no": an invalid phone number carries no country, a reserved IP carries no location, an address outside every installed mail-drop list carries no mail-drop answer. Comparing against a null would turn a gap in the data into a finding about a person.
`low_confidence` — the IP is a VPN exit, a relay or a datacenter egress, so its country belongs to the operator rather than to the person. Every country comparison involving that IP is withheld. Reporting a difference there would flag everyone who uses a VPN, which is a great many ordinary people. Read `is_anonymous`, `is_relay` and `is_datacenter` on the IP block if the network itself is what you want to act on.
## The consistency block
| field | type | meaning |
| --- | --- | --- |
| signals_compared | `string[]` | Which of email, phone, ip and address answered and were available to compare. |
| country_agreement | `'agree' \| 'differ' \| null` | The roll-up over the three country comparisons only. Differ if any of them differed, agree if at least one agreed and none differed, null when none of them could be made — never silently agree. |
| flags | `string[]` | Every flag raised, in comparison order. Each has its own reference page. |
| comparisons | `object[]` | Every comparison that was possible, including the ones withheld, each with both sides quoted. |
## Bulk runs & webhooks
The same check over a whole list. A bulk consistency run takes up to 100,000 rows and works through them in the background. Upload a CSV from the Bulk runs tab on the dashboard (there is no paste box, because a pasted line cannot say which signal it is), or call `POST /api/v1/consistency/bulk` (https://spaw.co/docs/api/create-consistency-bulk-job). Each row carries any subset of `email`, `phone`, `ip` and `address` and at least one of them, exactly as the single call does, and a row's own `country` wins over the list-level one. Poll the job for progress, then download the result CSV: your own columns first, then each signal as it was read, then what agreed, what did not, and what could not honestly be settled. `?variant=flagged`, `unflagged` or `uncompared` narrows the download to the rows you are actually going to work through.
Billing is the single call's billing, row by row: each leg costs what its own product costs, a repeat inside seven days is free, and the comparison itself is free. A run that exhausts the balance stops rather than half-charging, keeps every row it paid for downloadable, and is resumed from the top with `POST /api/v1/consistency/bulk/{jobId}/resume` (https://spaw.co/docs/api/resume-consistency-bulk-job) once you have topped up — the rows it already answered come back as free repeats. Cancelling stops a queued run on the spot and a running one at its next checkpoint, and the partial file stays downloadable. Runs, their input and their results are kept for 30 days and then deleted together.
Pass a `webhook_url` at creation and the finish — completed, failed, or cancelled — is POSTed there as `{"event": "bulk_consistency_job.finished", "job": {…}}`, signed with the secret creation returns once: `X-Spaw-Signature-V2` carries `t={unix seconds},v2={hex}`, the HMAC-SHA256 of `v2:{t}:{raw body}`, and a receiver should refuse anything whose `t` is more than five minutes from its own clock so a captured delivery cannot be replayed later. The original `X-Spaw-Signature`, over the body alone, is sent beside it until 1 March 2027. Transient delivery failures are retried twice.
If a delivery fails — your endpoint was down for the half-minute the three attempts cover — ask for it again rather than falling back to polling: press **Send webhook again** on the run in the dashboard, or call `POST /api/v1/consistency/bulk/{jobId}/webhook/redeliver` (https://spaw.co/docs/api/redeliver-consistency-bulk-webhook). The body is byte-for-byte the body the first delivery carried, rebuilt from the run's own frozen row, so the two reconcile. The signature is new, and has to be: a receiver refuses anything whose `t` is more than five minutes from its own clock, so verify a redelivery exactly as you verify any other delivery. Nothing deduplicates it for you — a receiver that gets both the original and the redelivery sees `bulk_consistency_job.finished` twice, and `job.id` is the key to deduplicate on. A run created without a `webhook_url` answers `409 WEBHOOK_NOT_CONFIGURED` and one that has not settled answers `409 JOB_NOT_FINISHED`; a run whose last delivery succeeded is not refused, because a receiver can lose a message it has already acknowledged. The delivery is queued rather than made while you wait — one attempt takes half a minute against an endpoint that is down, which is the endpoint you are asking about — so the call answers `202` and the run reads `webhook_status: "pending"` until a worker has an answer, then `delivered` or `failed` with what your endpoint said in `webhook_detail`. The dashboard shows the same three states beside the run. It is free and capped at ten 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.
## What this endpoint will not do
There is no score, no risk level, no verdict and no recommendation, and there will not be one. The flags are the whole output. A difference between two fields is a fact about the fields: people travel, move house, keep a foreign number after emigrating, and browse through a VPN because they would rather not be tracked. Rolling that into a single number would hide which fact drove it, and a number that decides about a person is a number that has to be explained to that person.
So the endpoint hands you the evidence and stops. Weigh it against what you know about your own customers, and write your own rule from named flags you can quote back — that rule will be better than a generic one, and you will be able to defend it.
## Billing and privacy
Each signal bills exactly as its own single-lookup endpoint: 1 credit for a definitive answer, free when the answer is no, and free for a seven-day repeat of the same input on the same account. The IP signal is one of five IP lookups to the credit, so it usually costs 0, and an email answer whose mailbox check was uncertain is free. `meta.credits_used` is the sum of the per-signal costs, and the consistency analysis itself is free — it reads answers you already paid for.
A call that runs out of credits part-way keeps the signals it paid for, answers 200, and reports `meta.stopped_reason`; only a call refused on its very first signal answers 402. Nothing billed is ever discarded.
Suppression lists are not applied here: a consistency check is a fresh vetting decision, so every signal is looked up afresh. A fresh answer only ever clears a suppression entry a lookup put there, or a postal entry your own reports added once they no longer stand behind it (a delivery reported since, or reports aged out of the 90-day window) — never one you imported, and never one your reports still stand behind. The phone answer inside still carries your SMS policy's decision, so a number your list holds as opted out answers `blocked_by: "opted_out"` there under the default policy; the comparisons never read it. Nothing about the call is stored beyond what each product already logs for its own lookup, and the comparisons themselves are computed and discarded with the response. The endpoint keeps its own rate-limit bucket, because one call fans out to as many as five lookups.
---
# MCP server
Give AI agents Spaw's verification tools over the Model Context Protocol: one endpoint, your existing API key, the same billing.
Spaw speaks the Model Context Protocol (https://modelcontextprotocol.io), so an AI agent in Claude Code, Cursor, Visual Studio Code, Codex or any other MCP client that can send an Authorization header can verify email addresses, inspect domains, validate phone numbers, look up IP addresses, check postal addresses, read business identifiers against public registers and compare several signals against each other as tools, one at a time or a whole list in one call. Same API keys, same billing, same answers; agent usage shows up on your Usage page as `mcp · key name`.
## Connect a client
The server lives at `https://spaw.co/mcp` over streamable HTTP, authenticated with a secret API key as a bearer token. Create a secret key on the API keys page and export it as `SPAW_KEY` in the environment your agent starts from: the commands and the Cursor link below read the key from that variable, and VS Code asks for it instead. Getting started explains how keys work (https://spaw.co/docs#api-keys).
Use a secret key, never a publishable one — the agent runs server-side on your behalf. Requests share the API's rate limit of 5 per second per account.
### Claude Code
```bash
claude mcp add --transport http spaw https://spaw.co/mcp \
--header "Authorization: Bearer $SPAW_KEY"
```
The shell expands `$SPAW_KEY` when the command runs (in bash or zsh; in PowerShell write `$env:SPAW_KEY`, or use the single-quoted form below), so export it first; Claude Code then stores the key in its own configuration. Single-quote the header instead, `'Authorization: Bearer ${SPAW_KEY}'`, and Claude Code stores only the reference, reading the variable from its environment each time it connects. Keep `--header` after the name and the URL: it takes several values, so a name placed after it is read as another header. Claude Code adds the server to the current project only; add `--scope user` to use it in every project.
### Cursor
[Add to Cursor](cursor://anysphere.cursor-deeplink/mcp/install?name=spaw&config=eyJ1cmwiOiJodHRwczovL3NwYXcuY28vbWNwIiwiaGVhZGVycyI6eyJBdXRob3JpemF0aW9uIjoiQmVhcmVyICR7ZW52OlNQQVdfS0VZfSJ9fQ%3D%3D) opens Cursor with the server filled in. Cursor fills in `${env:SPAW_KEY}` from its own environment when it connects (some earlier releases sent it literally, so update Cursor if the server answers 401); export `SPAW_KEY` in your shell profile and restart Cursor after changing it. If nothing happens, Cursor may not be installed or the browser blocked the link; add the server by hand, in `~/.cursor/mcp.json` for every project or `.cursor/mcp.json` for one:
```json
{
"mcpServers": {
"spaw": {
"url": "https://spaw.co/mcp",
"headers": { "Authorization": "Bearer ${env:SPAW_KEY}" }
}
}
}
```
### VS Code
[Install in VS Code](vscode:mcp/install?%7B%22name%22%3A%22spaw%22%2C%22type%22%3A%22http%22%2C%22url%22%3A%22https%3A%2F%2Fspaw.co%2Fmcp%22%2C%22headers%22%3A%7B%22Authorization%22%3A%22Bearer%20%24%7Binput%3Aspaw-key%7D%22%7D%2C%22inputs%22%3A%5B%7B%22type%22%3A%22promptString%22%2C%22id%22%3A%22spaw-key%22%2C%22description%22%3A%22Spaw%20secret%20API%20key%22%2C%22password%22%3Atrue%7D%5D%7D) opens VS Code with the server filled in. VS Code asks for the key the first time the server starts and keeps it in its secret storage, so nothing needs exporting. If nothing opens, VS Code may not be installed (VS Code Insiders takes `vscode-insiders:` links) or the browser blocked the link; add the server by hand, in your user `mcp.json` (run MCP: Open User Configuration from the Command Palette) or in `.vscode/mcp.json` for one workspace. VS Code's file lists servers under `servers`, not `mcpServers`, and the header takes the key from the input declared under `inputs`:
```json
{
"servers": {
"spaw": {
"type": "http",
"url": "https://spaw.co/mcp",
"headers": { "Authorization": "Bearer ${input:spaw-key}" }
}
},
"inputs": [
{
"type": "promptString",
"id": "spaw-key",
"description": "Spaw secret API key",
"password": true
}
]
}
```
### Codex
OpenAI's Codex adds the server with one command:
```bash
codex mcp add spaw --url https://spaw.co/mcp \
--bearer-token-env-var SPAW_KEY
```
It writes this to `~/.codex/config.toml`, which the Codex CLI, the Codex IDE extension and the ChatGPT desktop app share:
```toml
[mcp_servers.spaw]
url = "https://spaw.co/mcp"
bearer_token_env_var = "SPAW_KEY"
```
Codex stores the variable's name, not the key, and reads it each time it connects. If `SPAW_KEY` is unset or empty, Codex refuses to start the server and names the variable. A desktop app started from the Dock or the Start menu sees `SPAW_KEY` only if it is set in the environment the app launches with.
### Other clients
Any client that connects to remote MCP servers over streamable HTTP and can send a custom header works: send `Authorization: Bearer` followed by your secret key to `https://spaw.co/mcp`. Where that goes differs by client (VS Code's file uses `servers` where Cursor's uses `mcpServers`), so follow your client's own documentation.
ChatGPT on the web adds MCP servers as apps that sign in with OAuth or use no authentication; this server offers no OAuth, so it cannot be added there, while the ChatGPT desktop app reads the Codex configuration above. claude.ai's custom connectors expect OAuth too, except in Anthropic's request-headers beta for a limited set of organizations: choose No sign-in and add an Authorization request header whose value is Bearer followed by a secret key, which everyone in the organization then shares.
A missing or wrong key answers 401. Some clients, Cursor and VS Code among them, then try an OAuth sign-in and report an OAuth or registration error rather than a bad key, so check the key, or the variable that holds it, first.
## Key controls
The scopes, daily credit cap and address allowlist a secret key was created with (https://spaw.co/docs#api-keys) apply to tool calls exactly as they apply to REST requests. Give an agent its own key, scoped to what it is there to do.
- **Scopes.** A tool needs the scope of the product family it belongs to, the same one its REST endpoint needs: the three email tools need `email`, the IP tools `ip`, the phone tools `phone`, the address tools `address`, verify-entity needs `entity`, check-consistency needs `consistency`, and account-balance needs `account`. A tool the key is not scoped for answers a tool error naming `KEY_SCOPE_DENIED` — something the agent can read and act on, rather than a failed connection it can only report.
- **Daily credit cap.** What each tool call bills is added to the key's counter, and once the cap is spent, further calls answer `KEY_SPEND_CAP_REACHED` and run nothing. A batch tool that reaches the cap part-way down its list stops there, keeps the rows already paid for and reports `meta.stopped_reason: "key_spend_cap"`.
- **Address allowlist.** A key with an allowlist is refused from any other address before anything runs — `403 KEY_IP_NOT_ALLOWED`, not even the tool listing. A leaked agent key is as useless here as it is on the REST API.
## Tools
Twelve tools, mirroring the REST endpoints. Four of them take a list, so an agent checks two hundred numbers in one call rather than two hundred calls. An agent that runs out of credits gets a clear tool error, not a half answer.
| Tool | Arguments | Behavior |
| --- | --- | --- |
| verify-email | email | One full verification — verdict, risk score, and every documented field. 1 credit for a deliverable or risky verdict; unverified answers and 7-day repeats free, undeliverable free within fair use. |
| verify-email-batch | emails (≤50) | Compact per-address rows under the same billing. Suppressed addresses answer free; running dry mid-batch returns the paid partial results. |
| domain-intelligence | domain | MX, provider, SPF/DMARC, and disposable/free-provider lists for a whole domain. 1 credit when it accepts mail; dead or invalid domains free. |
| lookup-ip | ip, privacy? | Locate and risk-score an IPv4 or IPv6 address: location, network and operator type, tri-state datacenter/Tor/VPN/relay flags with the provider named, is_anonymous, and the published risk score. Pass "me" for the calling address. One credit covers five fresh answered lookups; reserved and unallocated ranges and 7-day repeats free. |
| lookup-ip-batch | ips (≤1,000), privacy? | The same answer for a whole list, in input order, each row carrying its position and the address as submitted. Every item must be an address — "me" is the single tool only. Five answered addresses cost one credit, and reserved and unallocated ranges, unknown addresses and 7-day repeats are free, so a thousand addresses cost at most two hundred credits; running dry mid-list returns the paid rows with stopped_reason of insufficient_credits or key_spend_cap. |
| verify-phone | phone, country?, hlr?, client_ip?, call_window? | Validate and enrich a phone number: E.164 and display forms, country facts, line type and carrier with the source that settled them, the regulator's block status where a table is installed, the fictional, virtual and disposable flags, and the published risk score. 1 credit for a valid number; invalid numbers, unassigned blocks and 7-day repeats free. hlr asks for the live carrier-network check. The live carrier-network check (`hlr: true`) is not enabled on the service: `hlr_checked` answers false and nothing extra is charged. |
| verify-phone-batch | phones (≤1,000), country?, hlr?, call_window? | The same answer for a whole list, in input order, each row carrying its position and the number as submitted. An item is a number or an object { phone, country }, and its own country wins over the list-level one. Suppressed numbers answer free; running dry mid-list returns the paid rows with stopped_reason. |
| verify-address | address \| fields, country, deliverability? | Read one postal address against its own country's format: valid with a typed reason, the address split into its parts and re-rendered the way that country writes it, the postcode checked against the national directory, the delivery point named (street, po_box, private_mailbox, general_delivery, rural_route, military, parcel_locker, bulk_mail), the national register's answer with its published coverage, how many companies are registered there, and the published risk score. Send one written address or the separated fields, always with country. 1 credit for a valid answer; addresses that cannot stand and 7-day repeats free. Signals whose dataset is not installed answer null, never false, and nothing says who lives or works there. |
| verify-address-batch | addresses (≤100), country?, deliverability? | The same answer for a whole list, in input order, each row carrying its position and the parts that were given. An item is a written address or an object of the separated fields, and its own country wins over the list-level one; an item left with no country is answered missing_country, free. Suppressed addresses answer free; running dry mid-list returns the paid rows with stopped_reason. |
| verify-entity | identifier, type, country? | Check a business identifier against locally synced registers: a Legal Entity Identifier, or a UK company number with its country. Answers the legal name, whether the entity is still live, the country and jurisdiction of formation, the legal form and the incorporation date; a LEI adds its check digit, registration status and renewal date, because a registration can lapse while the entity behind it is alive. Where both registers carry the same entity they are compared field by field. 1 credit when a register answers; a malformed identifier, one no register carries, and a register that is not installed are free. No registered address is stored or returned. |
| check-consistency | email?, phone?, country?, ip?, address?, address_country? | Run any subset of an email address, a phone number, an IP and a postal address in one call and name where they agree and where they do not. Each signal answers exactly what its own tool answers and bills the same; the analysis on top is free: country agreement plus named flags, each quoting both sides. A comparison is emitted only when both of its signals answered, and the country comparisons are withheld for a VPN, relay or datacenter IP, whose country belongs to the network operator. No score, no verdict — a difference between two fields is a fact about the fields, not a judgement about a person. |
| account-balance | — | The current credit balance and monthly grant. Free — useful before a batch. |
Field meanings and billing details are the same as the REST API — see the API reference (https://spaw.co/docs/api) and the email verification guide (https://spaw.co/docs/email-verification).
## Related
- API reference: https://spaw.co/docs/api
- Verify an email address over REST: https://spaw.co/docs/api/verify-email
- Validate a phone number over REST: https://spaw.co/docs/api/validate-phone
- Look up an IP address over REST: https://spaw.co/docs/api/lookup-ip
- Validate a postal address over REST: https://spaw.co/docs/api/validate-address
- Address verification guide: https://spaw.co/docs/address-verification
- Email verification guide: https://spaw.co/docs/email-verification
- Getting started: https://spaw.co/docs
---
# Email verification statistics
Measured on 2026-10-11 from 0 lookups across 0 accounts (published months only). Each month is published only when it holds at least 500 lookups from at least 5 accounts and no account holds more than 50% of its rows.
These are shares of the addresses customers chose to verify — signup forms, lead lists, old databases — pooled across every account as counts. They are not shares of email in general, and nothing here describes an address or a customer.
## October 2026 (in progress)
Not published yet: the month is still in progress.
## September 2026
Not published: under the floors.
## August 2026
Not published: under the floors.
## July 2026
Not published: under the floors.
## June 2026
Not published: under the floors.
## May 2026
Not published: under the floors.
## April 2026
Not published: under the floors.
## March 2026
Not published: under the floors.
## February 2026
Not published: under the floors.
## January 2026
Not published: under the floors.
## December 2025
Not published: under the floors.
## November 2025
Not published: under the floors.
## October 2025
Not published: under the floors.
Licence: CC BY 4.0, attribution "Spaw (spaw.co)".
---
# Phone number statistics
61.5% of the 503,140 US-area-code numbers consumers reported to the FTC as the caller ID of unwanted calls between July 13 and October 8, 2026 sit in number blocks held by VoIP providers, against 19.7% of all allocated US numbers (NANPA tables of October 10, 2026; VoIP holders on Spaw’s classification list, so both shares are lower bounds).
Measured on October 11, 2026. Counts only: no number, no holder and no carrier is named, and nothing is cut below the area code. A count under 20 is not published.
## Where reported caller IDs sit in the US numbering plan
What the FTC figures are, and are not:
- These are complaints consumers filed with the FTC about calls they received, naming the number shown to them as the caller ID. They are not the Do Not Call Registry, which lists consumers’ own numbers.
- They say nothing about whether you may call or text a number.
- They are unverified, and caller ID is often spoofed, so a number can appear here without its holder having placed any call. A block records the holder NANPA assigned it to, never the network that carried a call or who serves the number today.
701,278 complaints filed between July 13 and October 8, 2026 named 597,385 valid North American numbers as the caller ID; 73.6% of the 655,704 complaints that answered the question were marked "recorded message or robocall". 9,438 numbers were reported at least 3 times on at least 2 different days. FTC reported-calls data to October 9, 2026.
| Caller ID | Reported numbers | Share | Reported 3+ times on 2+ days | Share |
| --- | ---: | ---: | ---: | ---: |
| US area code | 503,140 | 84.2% | 4,700 | 49.8% |
| Toll-free code (800, 833, 844…) | 72,521 | 12.1% | 4,714 | 49.9% |
| Canadian area code | 21,387 | 3.6% | 23 | 0.2% |
| Other North American code (Caribbean, US territories, non-geographic) | 337 | 0.1% | under 20 | — |
Numbers with a US area code, by the block NANPA’s tables of October 10, 2026 place them in:
| Block held by | Reported numbers | Share | Reported 3+ times on 2+ days | Share | Share of allocated US numbers |
| --- | ---: | ---: | ---: | ---: | ---: |
| A VoIP provider | 309,529 | 61.5% | 3,415 | 72.7% | 19.7% |
| A wireless carrier | 23,750 | 4.7% | 273 | 5.8% | 35.0% |
| A holder not on the classification list | 168,349 | 33.5% | 1,011 | 21.5% | 45.3% |
| Nobody (free pool block, reserved or vacant code) | 1,512 | 0.3% | under 20 | — | n/a |
## FTC complaints by month
| Month | Complaints | Caller numbers | Recorded message or robocall | Toll-free share of numbers | US-area-code numbers in VoIP-held blocks |
| --- | ---: | ---: | ---: | ---: | ---: |
| September 2026 | 246,603 | 213,318 | 74.7% | 11.8% | 62.0% |
| August 2026 | 237,002 | 205,884 | 73.0% | 12.8% | 61.1% |
| July 2026 | 234,331 | 202,522 | 73.0% | 13.2% | 59.7% |
## US numbering by holder kind
NANPA’s tables of October 10, 2026 allocate 1,884,647,000 US numbers: 197,271 codes of ten thousand numbers, 71.9% of them pooled, and 1,305,547 thousands-blocks inside the pooled codes, with 88,089 more blocks free in the pool. Holders on Spaw’s classification list cover 54.7% of those numbers.
| Block held by | Allocated US numbers | Share |
| --- | ---: | ---: |
| A VoIP provider | 371,258,000 | 19.7% |
| A wireless carrier | 660,181,000 | 35.0% |
| A holder not on the classification list | 853,208,000 | 45.3% |
## Codes and blocks assigned recently
Rows the regulators’ tables date inside each window, counting back from the table’s own date. A row returned since is not in the table.
| Table | Unit | Table date | 7 days | 30 days | 365 days |
| --- | --- | --- | ---: | ---: | ---: |
| NANPA | codes | 2026-10-10 | 69 | 252 | 2,822 |
| NANPA | thousands-blocks | 2026-10-10 | 1,257 | 4,142 | 37,745 |
| CNAC | codes | 2026-10-04 | 35 | 66 | 445 |
| Arcep | ranges | 2026-10-05 | 0 | 120 | 1,267 |
## US codes and thousands-blocks still held, by year assigned
What NANPA’s tables of October 10, 2026 still list, by the date they give. Blocks and codes returned since are gone from the table, so earlier years are undercounted; retained blocks and codes assigned before NANPA dated them carry no date (42.9% of codes and 75.7% of thousands-blocks are dated).
| Assigned | Codes | Thousands-blocks | VoIP-held blocks | Wireless-held blocks | Unclassified blocks |
| --- | ---: | ---: | ---: | ---: | ---: |
| before 2016 | 43,206 | 461,288 | 96,191 | 219,005 | 146,092 |
| 2016 | 2,891 | 44,954 | 20,830 | 16,562 | 7,562 |
| 2017 | 2,342 | 29,868 | 10,339 | 12,048 | 7,481 |
| 2018 | 2,540 | 34,390 | 14,990 | 10,750 | 8,650 |
| 2019 | 3,207 | 41,874 | 22,706 | 10,750 | 8,418 |
| 2020 | 4,594 | 57,877 | 35,644 | 16,851 | 5,382 |
| 2021 | 6,618 | 84,039 | 53,108 | 24,243 | 6,688 |
| 2022 | 5,183 | 52,550 | 23,737 | 21,571 | 7,242 |
| 2023 | 4,721 | 56,988 | 29,913 | 21,400 | 5,675 |
| 2024 | 3,912 | 51,710 | 22,529 | 24,749 | 4,432 |
| 2025 | 3,303 | 41,737 | 9,550 | 24,099 | 8,088 |
| 2026 | 2,147 | 30,377 | 13,286 | 13,734 | 3,357 |
| undated | 112,607 | 317,895 | 6,305 | 99,655 | 211,935 |
## SMS-receiving numbers by calling code
The two open SMS-receiving lists (iP.1 Networks and Castle, both MIT) synced on October 11, 2026 hold 129,884 numbers; 20,397 (15.7%) are fresh enough to score disposable rather than disposable_stale.
| Calling code | Listed numbers | Share | Fresh |
| --- | ---: | ---: | ---: |
| +1 United States and 24 other regions | 36,294 | 27.9% | 5,780 |
| +44 United Kingdom and 3 other regions | 16,214 | 12.5% | 3,383 |
| +86 China | 13,144 | 10.1% | 633 |
| +33 France | 10,427 | 8.0% | 305 |
| +31 Netherlands | 9,480 | 7.3% | 2,104 |
| +358 Finland and 1 other region | 7,925 | 6.1% | 848 |
| +46 Sweden | 6,592 | 5.1% | 1,316 |
| +45 Denmark | 5,327 | 4.1% | 92 |
| +7 Russia and 1 other region | 4,380 | 3.4% | 236 |
| +62 Indonesia | 4,361 | 3.4% | 135 |
| +852 Hong Kong SAR China | 2,975 | 2.3% | 275 |
| +32 Belgium | 1,331 | 1.0% | 774 |
| +371 Latvia | 900 | 0.7% | 66 |
| +49 Germany | 880 | 0.7% | 197 |
| +48 Poland | 836 | 0.6% | 378 |
| +254 Kenya | 721 | 0.6% | 7 |
| +63 Philippines | 515 | 0.4% | 244 |
| +34 Spain | 507 | 0.4% | 196 |
| +386 Slovenia | 489 | 0.4% | 345 |
| +380 Ukraine | 452 | 0.3% | 86 |
| +41 Switzerland | 424 | 0.3% | 402 |
| +61 Australia and 2 other regions | 407 | 0.3% | 146 |
| +372 Estonia | 382 | 0.3% | 58 |
| +84 Vietnam | 325 | 0.3% | 239 |
| +370 Lithuania | 279 | 0.2% | 63 |
| +91 India | 274 | 0.2% | 125 |
| +60 Malaysia | 254 | 0.2% | 127 |
| +66 Thailand | 253 | 0.2% | 129 |
| +43 Austria | 209 | 0.2% | 66 |
| +82 South Korea | 204 | 0.2% | 172 |
| +40 Romania | 194 | 0.1% | 53 |
| +351 Portugal | 183 | 0.1% | 84 |
| +81 Japan | 182 | 0.1% | 75 |
| +995 Georgia | 181 | 0.1% | 32 |
| +972 Israel | 177 | 0.1% | 108 |
| +27 South Africa | 167 | 0.1% | 146 |
| +385 Croatia | 153 | 0.1% | 78 |
| +998 Uzbekistan | 145 | 0.1% | 89 |
| +52 Mexico | 137 | 0.1% | 114 |
| +670 Timor-Leste | 129 | 0.1% | 2 |
| +39 Italy and 1 other region | 117 | 0.1% | 75 |
| +95 Myanmar (Burma) | 106 | 0.1% | 45 |
| +420 Czechia | 102 | 0.1% | 52 |
| +55 Brazil | 98 | 0.1% | 52 |
| +212 Morocco and 1 other region | 83 | 0.1% | 10 |
| +234 Nigeria | 83 | 0.1% | 75 |
| +353 Ireland | 79 | 0.1% | 23 |
| +359 Bulgaria | 76 | 0.1% | 16 |
| +64 New Zealand | 73 | 0.1% | 27 |
| +57 Colombia | 66 | 0.1% | 34 |
| +30 Greece | 48 | 0.0% | 13 |
| +853 Macao SAR China | 47 | 0.0% | 37 |
| +357 Cyprus | 36 | 0.0% | 11 |
| +886 Taiwan | 33 | 0.0% | 6 |
| +421 Slovakia | 29 | 0.0% | 17 |
| +47 Norway and 1 other region | 28 | 0.0% | 14 |
| +880 Bangladesh | 28 | 0.0% | 12 |
| +36 Hungary | 27 | 0.0% | 19 |
| +373 Moldova | 26 | 0.0% | 10 |
| +54 Argentina | 25 | 0.0% | 24 |
| +56 Chile | 20 | 0.0% | 1 |
| +356 Malta | 20 | 0.0% | 3 |
| 41 calling codes with fewer than 20 listed numbers each | 225 | 0.2% | 113 |
Listed numbers with a US area code (25,828), by the block they sit in:
| Block held by | Listed numbers | Share | Share of allocated US numbers |
| --- | ---: | ---: | ---: |
| A VoIP provider | 16,738 | 64.8% | 19.7% |
| A wireless carrier | 3,133 | 12.1% | 35.0% |
| A holder not on the classification list | 4,086 | 15.8% | 45.3% |
| Nobody (free pool block, reserved or vacant code) | 1,871 | 7.2% | n/a |
## Method
- A number is placed by its area code in NANPA’s NPA report, then, for a US area code, by the longest prefix NANPA’s tables list for it: the thousands-block first, else the code. A free pool block, a reserved code and a code NANPA does not list at all count as not assigned.
- VoIP and wireless come from the holder’s FCC filing type and its own description, matched on the regulator’s holder name. Holders not on that list are counted as unclassified, so both the VoIP and the wireless shares are lower bounds.
- The US baseline counts numbers, not rows: each allocated thousands-block is 1,000 numbers with its own holder, and the rest of each allocated code is the code holder’s.
- A share is a share of the regulator’s table or of the reported numbers, never of traffic. Nothing here says who is calling, or who serves a number today: ported numbers keep their block.
- The FTC counts cover the 90 days of reports Spaw keeps; monthly totals are carried forward as counts once a month is complete, never as numbers.
Sources: Federal Trade Commission, Do Not Call reported-calls data (https://www.ftc.gov/policy-notices/open-government/data-sets/do-not-call-data); North American Numbering Plan Administrator (NANPA) (https://www.nanpa.com/); Canadian Numbering Administrator (CNAC) (https://www.cnac.ca/); Arcep, ressources en numérotation téléphonique (Licence Ouverte / Open Licence) (https://www.data.gouv.fr/fr/datasets/ressources-en-numerotation-telephonique/); iP.1 Networks and Castle SMS-receiving number lists (MIT) (https://github.com/iP1SMS/disposable-phone-numbers).
Licence: Spaw’s derived counts are CC BY 4.0, attribution "Spaw (spaw.co)"; the underlying data belongs to its publishers. The same counts as CSV: https://spaw.co/statistics/phone.csv
---
# Address data sources
This server holds 20 address registers in 15 countries, and postcode directories for 109 countries. Each row names its publisher, licence and declared coverage, with the version installed (newest sync 2026-10-10).
## Address registers
| Country | Register | Publisher | Answers | Coverage | Records | Version | Publisher releases | Refreshed here | Licence |
| --- | --- | --- | --- | --- | ---: | --- | --- | --- | --- |
| Australia | Geocoded National Address File (G-NAF) (`au-gnaf`) | [Geoscape Australia, licensed by the Commonwealth of Australia](https://data.gov.au/data/dataset/geocoded-national-address-file-g-naf) | buildings, streets, postcodes | complete | 11,692,513 addresses | 2026-08 | quarterly | monthly | [CC BY 4.0 under the Open G-NAF End User Licence Agreement](https://creativecommons.org/licenses/by/4.0/) |
| Austria | Adressregister (`at-adressregister`) | [Bundesamt für Eich- und Vermessungswesen (BEV)](https://www.bev.gv.at/) | buildings, streets, postcodes | complete | 2,517,812 addresses | 2026-04-01 | — | weekly | [CC BY 4.0](https://creativecommons.org/licenses/by/4.0/) |
| Belgium — Brussels | BeST Address — Brussels (`be-best-address-brussels`) | [CIRB (paradigm.brussels), via FPS BOSA](https://opendata.bosa.be/) | buildings, streets, postcodes | complete | 451,289 addresses | 2026-10-10 | daily | weekly | [CC BY 4.0](https://creativecommons.org/licenses/by/4.0/) |
| Belgium — Flanders | BeST Address — Flanders (`be-best-address-flanders`) | [Digitaal Vlaanderen, via FPS BOSA](https://opendata.bosa.be/) | buildings, streets, postcodes | complete | 2,802,870 addresses | 2026-10-10 | daily | weekly | [CC BY 4.0](https://creativecommons.org/licenses/by/4.0/) |
| Belgium — Wallonia | BeST Address — Wallonia (`be-best-address-wallonia`) | [Service public de Wallonie, via FPS BOSA](https://opendata.bosa.be/) | buildings, streets, postcodes | complete | 1,666,292 addresses | 2026-10-10 | daily | weekly | [CC BY 4.0](https://creativecommons.org/licenses/by/4.0/) |
| Brazil | Cadastro Nacional de Endereços para Fins Estatísticos (CNEFE) (`br-cnefe-2022`) | [IBGE](https://ftp.ibge.gov.br/Cadastro_Nacional_de_Enderecos_para_Fins_Estatisticos/) | buildings, streets, postcodes | partial (marked here) | 60,567,115 addresses | 2024-05-17 | the 2022 census list, one file per state | monthly | Public data, source credited |
| Czechia | RÚIAN address points (`cz-ruian-adresni-mista`) | [Český úřad zeměměřický a katastrální (ČÚZK)](https://cuzk.gov.cz/ruian/RUIAN.aspx) | buildings, streets, postcodes | complete | 2,626,306 addresses | 2026-09-30 | monthly, on the first of the month | weekly | [CC BY 4.0](https://creativecommons.org/licenses/by/4.0/) |
| Denmark | Danmarks Adresseregister (DAR) (`dk-danmarks-adresser`) | [Klimadatastyrelsen](https://www.klimadatastyrelsen.dk/data/danmarks-adresseregister) | buildings, streets, postcodes | partial (marked here) | 2,616,807 addresses | 2026-09-26 | only as one file per entity on Datafordeleren, behind a free API key | not refreshed | [CC BY 4.0](https://creativecommons.org/licenses/by/4.0/) |
| France (with Guadeloupe, Martinique, French Guiana, Réunion, Mayotte, St. Pierre & Miquelon, St. Barthélemy, St. Martin) | Base Adresse Nationale (`fr-base-adresse-nationale`) | [IGN and DINUM](https://adresse.data.gouv.fr/) | buildings, streets, postcodes | partial | 25,579,839 addresses | 2026-10-10 | — | weekly | [Licence Ouverte 2.0](https://www.data.gouv.fr/pages/legal/licences/etalab-2.0) |
| Germany — Berlin | Adressen Berlin (`de-berlin-adressen`) | [Amt für Statistik Berlin-Brandenburg (Geoportal Berlin)](https://gdi.berlin.de/) | buildings, streets, postcodes | partial (marked here) | 402,523 addresses | 2026-10-01 | — | monthly | [Datenlizenz Deutschland – Zero – Version 2.0](https://www.govdata.de/dl-de/zero-2-0) |
| Germany — North Rhine-Westphalia | Gebäudereferenzen NRW (`de-nrw-gebaeudereferenzen`) | [Geobasis NRW (Bezirksregierung Köln)](https://www.opengeodata.nrw.de/produkte/geobasis/lk/akt/gebref_txt/) | buildings, streets, postcodes | partial (marked here) | 4,503,290 addresses | 2026-10-01 | about quarterly | monthly | [Datenlizenz Deutschland – Zero – Version 2.0](https://www.govdata.de/dl-de/zero-2-0) |
| Germany — Rhineland-Palatinate | Hauskoordinaten Rheinland-Pfalz (`de-rlp-hauskoordinaten`) | [Landesamt für Vermessung und Geobasisinformation Rheinland-Pfalz (LVermGeoRP)](https://lvermgeo.rlp.de/geodaten-geoshop/open-data/) | buildings, streets, postcodes | partial (marked here) | 1,404,472 addresses | 2026-03-30 | — | monthly | [Datenlizenz Deutschland – Namensnennung – Version 2.0](https://www.govdata.de/dl-de/by-2-0) |
| Great Britain | OS Open Names (`os-open-names`) | [Ordnance Survey](https://www.ordnancesurvey.co.uk/products/os-open-names) | streets | partial (marked here) | 886,152 streets | 2026-10-01 | quarterly | monthly | [Open Government Licence v3.0](https://www.nationalarchives.gov.uk/doc/open-government-licence/version/3/) |
| Latvia | Valsts adrešu reģistrs (`lv-valsts-adresu-registrs`) | Valsts zemes dienests (State Land Service of Latvia) | buildings, streets, postcodes | complete | 322,437 addresses | 2026-10-10 | — | weekly | [CC BY 4.0](https://creativecommons.org/licenses/by/4.0/) |
| Luxembourg | BD-Adresses (`lu-bd-adresses`) | [Administration du cadastre et de la topographie](https://data.public.lu/fr/datasets/adresses-georeferencees-bd-adresses/) | buildings, streets, postcodes | complete | 358,722 addresses | 2026-10-10 | daily | weekly | [CC0 1.0](https://creativecommons.org/publicdomain/zero/1.0/) |
| Netherlands | Basisregistratie Adressen en Gebouwen (BAG) (`nl-bag-adressen`) | [Kadaster](https://www.kadaster.nl/zakelijk/registraties/basisregistraties/bag) | buildings, streets, postcodes | complete | 10,078,006 addresses | 2026-09-08 | monthly, on the 8th | monthly | [CC0 1.0](https://creativecommons.org/publicdomain/zero/1.0/) |
| Norway | Matrikkelen – Adresse (`no-matrikkelen-adresse`) | [Kartverket](https://www.kartverket.no/) | buildings, streets, postcodes | complete | 2,572,075 addresses | 2026-10-03 | — | weekly | [CC BY 4.0](https://creativecommons.org/licenses/by/4.0/) |
| Switzerland | Official directory of building addresses (`ch-gebaeudeadressverzeichnis`) | [Federal Office of Topography swisstopo](https://www.swisstopo.admin.ch/en/official-directory-of-building-addresses) | buildings, streets, postcodes | complete | 3,194,003 addresses | 2026-10-10 | — | weekly | [swisstopo terms of use for free geodata](https://www.swisstopo.admin.ch/en/terms-of-use-free-geodata-and-geoservices) |
| United States | National Address Database (`us-national-address-database`) | [U.S. Department of Transportation](https://data.transportation.gov/dataset/National-Address-Database-NAD-Text-File/fc2s-wawr) | buildings, streets, postcodes | partial | 84,692,955 addresses | 2026-09-06 | quarterly | monthly | Public domain |
| United States | TIGER/Line address ranges (`us-tiger-address-ranges`) | [U.S. Census Bureau](https://www.census.gov/geographies/mapping-files/time-series/geo/tiger-line-file.html) | number ranges, streets | partial (marked here) | 53,135,059 ranges | 2025-09-22 | a new vintage each year | monthly | Public domain |
What each declared coverage means:
- Geocoded National Address File (G-NAF) (Australia, complete): The end user licence forbids using the file to build a mailing list or send unsolicited mail; an address is checked against it, never listed from it.
- BeST Address — Brussels (Belgium — Brussels, complete): Complete for its region; the three regional files together are the country.
- BeST Address — Flanders (Belgium — Flanders, complete): Complete for its region; the three regional files together are the country.
- BeST Address — Wallonia (Belgium — Wallonia, complete): Complete for its region; the three regional files together are the country.
- Cadastro Nacional de Endereços para Fins Estatísticos (CNEFE) (Brazil, partial): Marked partial here because it is the address list of the 2022 census, a snapshot rather than a maintained register. A building finished since the census is absent, so absence is never read as 'not there'. 27 of its 27 files are installed.
- Danmarks Adresseregister (DAR) (Denmark, partial): Marked partial here because its copy is no longer refreshed. The copy was taken on 26 September 2026 through DAWA, the address web API Klimadatastyrelsen closed on 1 October 2026, so an address created since then is not in it, and until the register is read from its new distribution an address it lacks is never answered as absent. Denmark only: Greenland and the Faroe Islands keep registers of their own.
- Base Adresse Nationale (France (with Guadeloupe, Martinique, French Guiana, Réunion, Mayotte, St. Pierre & Miquelon, St. Barthélemy, St. Martin), partial): Built from the address lists communes publish themselves: on 22 September 2026 the publisher counted 28,632 of France's 35,011 communes with a list of their own, the source of 23.03 million of the 26.1 million addresses it holds, so an address it lacks may sit in a commune whose list is incomplete.
- Adressen Berlin (Germany — Berlin, partial): Marked partial here because it covers one Land, and Germany has no national address register. An address it does not hold, in Berlin or anywhere else in Germany, is never answered as absent.
- Gebäudereferenzen NRW (Germany — North Rhine-Westphalia, partial): Marked partial here because it covers one Land, and Germany has no national address register. The open product carries no postcode, so a building is matched through the town written.
- Hauskoordinaten Rheinland-Pfalz (Germany — Rhineland-Palatinate, partial): Marked partial here because it covers one Land, and Germany has no national address register. The open product carries no postcode, so a building is matched through the town written.
- OS Open Names (Great Britain, partial): Marked partial here because it files each road under one postcode district, though a long road runs through several. Every named and numbered road in Great Britain is in it, as a street: it lists no buildings. A street missing from a district may be filed under a neighbouring one, so it is never denied.
- Official directory of building addresses (Switzerland, complete): The 26 cantons. The file holds no Liechtenstein row, so Liechtenstein is not covered by it.
- National Address Database (United States, partial): States and local governments contribute their address points voluntarily, so an address it lacks may sit in a part that was never shared.
- TIGER/Line address ranges (United States, partial): Marked partial here because not every road edge carries a range, and a street on a ZIP boundary can carry only its neighbour's ZIP. A street it does not know is never denied.
Credits the licences ask for:
- Incorporates or developed using G-NAF © Geoscape Australia, licensed by the Commonwealth of Australia under the Open Geo-coded National Address File (G-NAF) End User Licence Agreement (CC BY 4.0; not for mailing lists)
- © Österreichisches Adressregister, Stichtagsdaten vom 01.04.2026
- BeST Address — Brussels (CIRB/paradigm.brussels via FPS BOSA), CC BY 4.0
- BeST Address — Flanders (Digitaal Vlaanderen via FPS BOSA), CC BY 4.0
- BeST Address — Wallonia (Service public de Wallonie via FPS BOSA), CC BY 4.0
- IBGE, Cadastro Nacional de Endereços para Fins Estatísticos (CNEFE), Censo Demográfico 2022 — dados públicos, fonte citada
- Český úřad zeměměřický a katastrální (ČÚZK), CC BY 4.0
- Danmarks Adresseregister (DAR), Klimadatastyrelsen, CC BY 4.0
- Base Adresse Nationale (IGN/DINUM), Licence Ouverte 2.0
- Geoportal Berlin / Adressen Berlin, Amt für Statistik Berlin-Brandenburg, Datenlizenz Deutschland – Zero – Version 2.0 (dl-de/zero-2-0)
- Gebäudereferenzen NRW, Geobasis NRW (Bezirksregierung Köln), Datenlizenz Deutschland – Zero – Version 2.0 (dl-de/zero-2-0)
- © GeoBasis-DE / LVermGeoRP, Landesamt für Vermessung und Geobasisinformation Rheinland-Pfalz, Datenlizenz Deutschland – Namensnennung – Version 2.0 (dl-de/by-2-0)
- Contains OS data © Crown copyright and database right 2026, Open Government Licence v3.0.
- Valsts adrešu reģistrs, Valsts zemes dienests (Latvia), CC BY 4.0
- BD-Adresses, Administration du cadastre et de la topographie (Luxembourg), CC0
- Basisregistratie Adressen en Gebouwen (BAG), Kadaster, CC0 1.0
- Matrikkelen – Adresse, Kartverket, CC BY 4.0
- Official directory of building addresses, © swisstopo
- U.S. Department of Transportation, National Address Database (public domain; not a mailing list)
- U.S. Census Bureau TIGER/Line (public domain)
## Postcode directories
| Directory | Countries | Complete | Postcodes | Version | Licence |
| --- | --- | ---: | ---: | --- | --- |
| [ONS Postcode Directory](https://geoportal.statistics.gov.uk/) (`ons-postcode-directory`) | Great Britain | 1 | 2,665,797 | 2026-08 | [Open Government Licence v3.0](https://www.nationalarchives.gov.uk/doc/open-government-licence/version/3/) |
| [GeoNames postal codes](https://download.geonames.org/export/zip/) (`geonames-postal-codes`) | 108 countries | 15 | 1,824,518 | 2026-10-10 | [CC BY 4.0](https://creativecommons.org/licenses/by/4.0/) |
- ONS Postcode Directory: Every postcode issued in Great Britain, the Channel Islands and the Isle of Man, live and terminated, with the month each was introduced and terminated. Northern Ireland's BT postcodes are excluded: reusing them needs a Land & Property Services licence.
- GeoNames postal codes: The national postal services' current codes for about a hundred countries in one file. GeoNames lists current codes only, so it never says a code was retired.
A complete directory lists every code its postal operator publishes, so a code it lacks can be answered as not existing; every other directory confirms what it holds and denies nothing. Great Britain's postcodes in numbers: https://spaw.co/tools/address-checker/gb/postcodes
## Reference data behind the town and region checks
Beside the registers and directories, a lookup reads these to decide whether a town or a region written on an address is the postcode's. What is used from them names places, never an address.
| Dataset | Publisher | What it decides | Version | Licence |
| --- | --- | --- | --- | --- |
| GeoNames cities (`geonames-cities`) | [GeoNames](https://download.geonames.org/export/dump/) | The other names a town is written under, in locality_matches_postcode (Munich for München, Moscow for Москва), and the other names of a one-city region, written as the town or as the region, in administrative_area_matches_postcode too (Moscow for Москва); an answer confirmed through one names geonames-cities in its sources, and one whose town and region matched as the directory writes them does not. The same file gives the time zone of the nearest place in IP answers. | 2026-10-03 | [CC BY 4.0](https://creativecommons.org/licenses/by/4.0/) |
| Residential Property Price Register (`ie-property-price-register`) | [Property Services Regulatory Authority (PSRA)](https://www.propertypriceregister.ie/) | Which counties each of the fourteen Eircode routing keys that reach a county of another province, or that GeoNames files under the wrong one (P14), covers, and that D11 and D15 reach into Meath, in administrative_area_matches_postcode for Ireland. | read 2026-09-24 | [PSRA re-use terms](https://www.psr.ie/re-use-of-public-sector-information/) |
- GeoNames cities: Places of 15,000 people or more. A name is compared inside one country and only with the postcode's own places or the name of a one-city region, never with a neighbouring postcode's, and a town's only where the postcode lies within 30 km of the place. A name the directory also lists as a town of its own more than 30 km from the place counts only as the place's own name cut short (Biberach for Biberach an der Riß), so Perlin, a village near Schwerin that the file lists among Berlin's names, does not count for a Berlin code. A name that another listed place of the country also carries is left out, and so are airport codes.
- Residential Property Price Register: The register as last updated on 23 September 2026, read on 24 September 2026: 250,671 of its 807,724 residential sales carry an Eircode, and a county counts for a routing key once it holds at least ten of that key's sales. Only that derived table ships with the code; no sale, address or price is held here. The national file is re-used under the PSRA's terms for the re-use of public sector information (last updated 29 July 2022), which ask for the source and the PSRA's copyright to be acknowledged; data.gov.ie lists only the register's Dublin extract, under CC BY 4.0. The register's download page links those terms as its Re-Use of Information. Source: https://www.propertypriceregister.ie/website/npsra/pprweb.nsf/PPRDownloads?OpenForm
## Company registers and mailbox operators
- 9,519,363 addresses compiled from 8 open company registers (United Kingdom, United States, France, Guadeloupe, Martinique, French Guiana, Réunion, St. Pierre & Miquelon, Mayotte, St. Barthélemy, St. Martin, Wallis & Futuna, French Polynesia, New Caledonia, Norway), version 2026-10-01: they answer companies_registered, the number of companies giving an address as their registered office.
- 9,752 branch addresses published by 6 mailbox operators themselves (United States, Canada, United Kingdom, Ireland): they back is_mail_drop, and an answer names the operator that matched.
## Licensed, and not held
- Royal Mail Postcode Address File (PAF) (United Kingdom): Every full list of British addresses derives from it and reusing it needs a licence, so a British address never answers exists from open data, and the addresses at a postcode are not listed.
- Northern Ireland postcodes (BT) (United Kingdom): Left out of the ONS directory installed here, because reusing them needs a Land & Property Services licence, so the directory holds no BT postcode and an address carrying one is answered as though no directory were installed: postcode_status null, never postcode_not_found.
- The postal service's delivery point data (United States): Whether post is delivered, and the postal service's own agency, vacant and residential flags, are licensed; those fields answer not checked rather than a guess.
## Published elsewhere, not installed here
Address lists these publishers make available, as the publishers describe them. None is installed on this server, so an address there is checked against its format and, where one is installed, a postcode directory. Their terms have not been assessed here.
- Canada: National Address Register, Statistics Canada (https://www150.statcan.gc.ca/n1/en/catalogue/46260002) — a list of commercial and residential addresses in Canada that are extracted from Statistics Canada's Building Register and deemed non-confidential, released semi-annually. Checked on 2026-09-22.
- Estonia: Aadressiandmete süsteem (ADS), Maa- ja Ruumiamet (https://geoportaal.maaamet.ee/est/Ruumiandmed/Aadressiandmed-p112.html) — the central address data system, offered as downloadable extracts and as map services. Checked on 2026-09-22.
- Spain: CartoCiudad, Ministerio de Transportes y Movilidad Sostenible (https://www.cartociudad.es/web/portal) — postal addresses and postcodes among its data, downloadable through the National Geographic Centre. Checked on 2026-09-22.
Nothing on this page says who lives or works at an address: the registers list places, and a lookup against them stores nothing.
---
# Great Britain postcodes in numbers
The ONS Postcode Directory of August 2026 lists 1,749,110 live postcodes in Great Britain and 903,514 terminated; Northern Ireland's BT codes are excluded because reusing them needs a Land & Property Services licence.
The same release lists 11,106 live and 2,067 terminated postcodes in the Channel Islands and the Isle of Man (GY, IM, JE). They are not part of Great Britain and are counted apart from every figure here.
## Introduced and terminated, month by month
The 24 complete months before July 2026, the newest month the release dates anything to, which is left out because it may be only partly recorded.
| Month | Introduced | Terminated |
| --- | ---: | ---: |
| 2024-07 | 1,130 | 843 |
| 2024-08 | 1,100 | 1,016 |
| 2024-09 | 1,606 | 804 |
| 2024-10 | 1,199 | 656 |
| 2024-11 | 1,224 | 639 |
| 2024-12 | 1,297 | 0 |
| 2025-01 | 1,243 | 1,415 |
| 2025-02 | 1,339 | 899 |
| 2025-03 | 1,191 | 704 |
| 2025-04 | 704 | 712 |
| 2025-05 | 1,224 | 852 |
| 2025-06 | 1,103 | 699 |
| 2025-07 | 1,344 | 700 |
| 2025-08 | 1,283 | 695 |
| 2025-09 | 1,416 | 973 |
| 2025-10 | 1,189 | 745 |
| 2025-11 | 1,564 | 764 |
| 2025-12 | 1,084 | 1,083 |
| 2026-01 | 1,467 | 1,313 |
| 2026-02 | 1,446 | 962 |
| 2026-03 | 1,315 | 759 |
| 2026-04 | 1,169 | 684 |
| 2026-05 | 1,206 | 784 |
| 2026-06 | 1,365 | 846 |
The release dates no termination to December 2024: the zero is the directory's own, not a gap in this count.
## Postcode areas
| Area | Name | Districts | Live | Terminated | Streets |
| --- | --- | ---: | ---: | ---: | ---: |
| AB | Aberdeen | 40 | 17,413 | 21,969 | 7,731 |
| AL | St Albans | 10 | 7,792 | 3,717 | 3,234 |
| B | Birmingham | 79 | 41,844 | 21,065 | 19,059 |
| BA | Bath | 19 | 15,524 | 5,110 | 7,538 |
| BB | Blackburn | 15 | 13,608 | 5,832 | 8,707 |
| BD | Bradford | 27 | 17,325 | 6,689 | 10,395 |
| BH | Bournemouth | 26 | 15,253 | 7,522 | 7,081 |
| BL | Bolton | 12 | 10,408 | 3,783 | 7,501 |
| BN | Brighton | 38 | 22,567 | 13,548 | 9,758 |
| BR | Bromley | 9 | 6,781 | 4,024 | 2,997 |
| BS | Bristol | 47 | 27,448 | 19,321 | 13,337 |
| CA | Carlisle | 30 | 12,890 | 3,759 | 4,870 |
| CB | Cambridge | 16 | 11,252 | 8,578 | 6,605 |
| CF | Cardiff | 46 | 23,435 | 19,768 | 13,400 |
| CH | Chester | 38 | 19,090 | 5,770 | 10,728 |
| CM | Chelmsford | 28 | 18,674 | 9,908 | 9,122 |
| CO | Colchester | 16 | 13,771 | 5,020 | 6,937 |
| CR | Croydon | 11 | 8,009 | 7,161 | 3,408 |
| CT | Canterbury | 22 | 15,378 | 5,208 | 7,240 |
| CV | Coventry | 24 | 21,267 | 8,176 | 11,595 |
| CW | Crewe | 13 | 9,862 | 4,696 | 6,039 |
| DA | Dartford | 18 | 9,221 | 4,157 | 4,248 |
| DD | Dundee | 11 | 9,123 | 2,455 | 4,403 |
| DE | Derby | 25 | 17,984 | 12,993 | 11,883 |
| DG | Dumfries | 15 | 6,912 | 951 | 2,694 |
| DH | Durham | 12 | 9,174 | 2,643 | 4,310 |
| DL | Darlington | 18 | 13,555 | 3,588 | 6,379 |
| DN | Doncaster | 33 | 22,343 | 6,390 | 12,721 |
| DT | Dorchester | 11 | 7,945 | 2,207 | 4,319 |
| DY | Dudley | 14 | 11,102 | 3,680 | 5,604 |
| E | East London | 23 | 17,022 | 12,818 | 6,150 |
| EC | East Central London | 30 | 3,550 | 9,105 | 903 |
| EH | Edinburgh | 58 | 25,448 | 8,405 | 11,140 |
| EN | Enfield | 12 | 8,536 | 4,877 | 3,315 |
| EX | Exeter | 33 | 22,913 | 6,703 | 10,856 |
| FK | Falkirk | 21 | 7,789 | 2,037 | 4,274 |
| FY | Blackpool (Fylde) | 9 | 7,390 | 2,973 | 4,261 |
| G | Glasgow | 57 | 32,454 | 15,402 | 15,463 |
| GL | Gloucester | 27 | 21,671 | 8,054 | 9,881 |
| GU | Guildford | 40 | 22,616 | 14,154 | 11,201 |
| HA | Harrow | 10 | 10,255 | 7,717 | 3,286 |
| HD | Huddersfield | 9 | 7,202 | 3,721 | 4,532 |
| HG | Harrogate | 5 | 4,528 | 1,971 | 2,618 |
| HP | Hemel Hempstead | 24 | 14,101 | 8,388 | 7,505 |
| HR | Hereford | 9 | 6,561 | 1,606 | 2,828 |
| HS | Outer Hebrides | 9 | 981 | 138 | 301 |
| HU | Hull | 21 | 11,369 | 4,417 | 6,821 |
| HX | Halifax | 7 | 5,715 | 1,847 | 3,494 |
| IG | Ilford | 11 | 6,106 | 4,266 | 2,324 |
| IP | Ipswich | 34 | 21,951 | 6,072 | 10,807 |
| IV | Inverness | 53 | 7,576 | 3,509 | 3,924 |
| KA | Kilmarnock | 30 | 11,797 | 2,712 | 6,148 |
| KT | Kingston upon Thames | 24 | 14,273 | 8,700 | 6,042 |
| KW | Kirkwall | 16 | 1,853 | 279 | 1,056 |
| KY | Kirkcaldy | 17 | 10,329 | 3,184 | 5,722 |
| L | Liverpool | 66 | 20,862 | 23,321 | 12,118 |
| LA | Lancaster | 23 | 11,945 | 3,072 | 6,016 |
| LD | Llandrindod Wells | 8 | 2,216 | 488 | 761 |
| LE | Leicester | 28 | 23,346 | 12,530 | 14,376 |
| LL | Llandudno | 67 | 20,324 | 4,453 | 10,008 |
| LN | Lincoln | 13 | 9,575 | 2,831 | 5,733 |
| LS | Leeds | 33 | 22,198 | 10,790 | 14,050 |
| LU | Luton | 8 | 6,464 | 4,386 | 3,461 |
| M | Manchester | 48 | 32,420 | 27,778 | 18,162 |
| ME | Medway | 21 | 17,231 | 6,088 | 8,251 |
| MK | Milton Keynes | 29 | 16,215 | 7,643 | 8,455 |
| ML | Motherwell | 12 | 9,884 | 2,228 | 6,634 |
| N | North London | 25 | 17,672 | 12,848 | 5,035 |
| NE | Newcastle upon Tyne | 67 | 33,857 | 12,591 | 16,693 |
| NG | Nottingham | 32 | 29,864 | 8,953 | 17,609 |
| NN | Northampton | 20 | 17,156 | 9,017 | 10,584 |
| NP | Newport | 25 | 13,893 | 14,614 | 7,685 |
| NR | Norwich | 36 | 24,304 | 4,991 | 13,269 |
| NW | North West London | 13 | 14,255 | 12,326 | 3,214 |
| OL | Oldham | 17 | 13,212 | 4,828 | 8,594 |
| OX | Oxford | 28 | 20,531 | 15,686 | 9,711 |
| PA | Paisley | 78 | 9,504 | 3,715 | 4,837 |
| PE | Peterborough | 39 | 27,768 | 12,814 | 16,124 |
| PH | Perth | 43 | 6,235 | 1,394 | 2,994 |
| PL | Plymouth | 36 | 17,884 | 5,385 | 8,272 |
| PO | Portsmouth | 35 | 24,513 | 9,348 | 11,838 |
| PR | Preston | 13 | 13,736 | 7,121 | 8,815 |
| RG | Reading | 35 | 23,816 | 22,915 | 11,317 |
| RH | Redhill | 21 | 16,366 | 8,706 | 8,015 |
| RM | Romford | 21 | 10,007 | 5,781 | 4,534 |
| S | Sheffield | 56 | 34,522 | 16,274 | 20,705 |
| SA | Swansea | 53 | 23,295 | 6,549 | 10,905 |
| SE | South East London | 30 | 20,971 | 14,880 | 6,658 |
| SG | Stevenage | 19 | 12,179 | 5,354 | 5,800 |
| SK | Stockport | 19 | 16,696 | 8,977 | 10,235 |
| SL | Slough | 12 | 10,672 | 6,317 | 4,288 |
| SM | Sutton | 7 | 4,326 | 2,525 | 1,742 |
| SN | Swindon | 24 | 14,710 | 7,454 | 7,511 |
| SO | Southampton | 31 | 18,624 | 22,281 | 9,240 |
| SP | Salisbury | 11 | 8,753 | 2,904 | 4,252 |
| SR | Sunderland | 11 | 6,881 | 2,230 | 3,781 |
| SS | Southend-on-Sea | 19 | 11,987 | 5,668 | 5,991 |
| ST | Stoke-on-Trent | 22 | 17,871 | 5,232 | 10,998 |
| SW | South West London | 29 | 20,109 | 17,448 | 5,309 |
| SY | Shrewsbury | 26 | 14,641 | 3,033 | 5,354 |
| TA | Taunton | 24 | 12,113 | 3,192 | 7,049 |
| TD | Galashiels | 15 | 4,538 | 816 | 2,064 |
| TF | Telford | 13 | 7,234 | 2,377 | 3,604 |
| TN | Tonbridge | 40 | 23,179 | 10,529 | 10,029 |
| TQ | Torquay | 14 | 10,115 | 3,390 | 4,833 |
| TR | Truro | 28 | 12,559 | 3,426 | 5,095 |
| TS | Cleveland | 30 | 17,884 | 5,581 | 10,001 |
| TW | Twickenham | 21 | 11,122 | 9,575 | 4,398 |
| UB | Southall | 12 | 7,216 | 4,611 | 2,800 |
| W | West London | 35 | 18,480 | 22,187 | 3,422 |
| WA | Warrington | 18 | 18,345 | 7,944 | 9,971 |
| WC | West Central London | 15 | 2,537 | 5,000 | 466 |
| WD | Watford | 14 | 7,405 | 8,805 | 2,835 |
| WF | Wakefield | 18 | 14,745 | 4,106 | 8,878 |
| WN | Wigan | 8 | 7,289 | 2,136 | 5,143 |
| WR | Worcester | 17 | 9,020 | 3,960 | 4,900 |
| WS | Walsall | 15 | 10,307 | 4,923 | 6,163 |
| WV | Wolverhampton | 18 | 10,650 | 3,881 | 5,098 |
| YO | York | 37 | 19,287 | 16,045 | 11,047 |
| ZE | Lerwick | 3 | 658 | 103 | 405 |
2,418 postcodes whose outward code is not in the modern shape (GIR, NPT) count in the totals above but in no area row.
Districts are the outward codes the directory lists, live or not; streets are the roads OS Open Names files under the area's districts, each road under one district. A directory says whether a code is in use, never who is there.
Contains ONS, OS and Royal Mail data © Crown copyright and database right 2026, Open Government Licence v3.0. Northern Ireland postcodes are excluded: their reuse needs a Land & Property Services licence.
Licence: Open Government Licence v3.0 (https://www.nationalarchives.gov.uk/doc/open-government-licence/version/3/).
---
# The data behind a phone answer
A Spaw phone answer is built from the number allocation tables of 10 regulators (NANPA, CNAC, Ofcom, ARCEP, ACMA, ACM, NAD, the CRT, UKE and MIMIT), Google’s libphonenumber, two SMS-receiving number lists and the FTC’s reported-calls files. 13 of these 13 sources are installed on this server, the newest dated 2026-10-15. Each page names the publisher, the files, the terms as the publisher states them and the counts installed, never a row.
## Regulators’ number allocation tables
| Publisher | Regions | A number outside the table | Installed here | Terms | Version |
| --- | --- | --- | --- | --- | --- |
| [NANPA numbering reports](https://spaw.co/tools/phone-number-lookup/sources/nanpa) | United States, Puerto Rico, U.S. Virgin Islands, Guam, American Samoa, Northern Mariana Islands | unassigned when geographic and outside the table | 1,605,635 | no terms of use published (NANPA) | file dated 2026-10-14 |
| [CNAC central office code status](https://spaw.co/tools/phone-number-lookup/sources/cnac) | Canada | unassigned when geographic and outside the table | 46,400 | no licence published (CNAC) | file dated 2026-10-11 |
| [Ofcom numbering data](https://spaw.co/tools/phone-number-lookup/sources/ofcom) | United Kingdom, Guernsey, Jersey, Isle of Man | not evaluated outside the table | 343,436 | Ofcom copyright, reuse with acknowledgement | file dated 2026-10-15 |
| [ARCEP numbering resources](https://spaw.co/tools/phone-number-lookup/sources/arcep) | France, Guadeloupe, St. Barthélemy, St. Martin, Martinique, French Guiana, Réunion, Mayotte, St. Pierre & Miquelon | unassigned when geographic and outside the table | 22,605 | Licence Ouverte / Open Licence (Etalab), ARCEP | retrieved on 2026-10-12 |
| [ACMA numbering register](https://spaw.co/tools/phone-number-lookup/sources/acma) | Australia | not evaluated outside the table | 99,624 | terms not confirmed (ACMA) | retrieved on 2026-10-12 |
| [ACM number register](https://spaw.co/tools/phone-number-lookup/sources/acm) | Netherlands | unassigned when geographic and outside the table | 84,420 | CC0 1.0, ACM (data.overheid.nl) | retrieved on 2026-10-12 |
| [NAD number register](https://spaw.co/tools/phone-number-lookup/sources/nad) | New Zealand | not evaluated outside the table | 9,718 | no reuse licence published (NAD) | retrieved on 2026-10-12 |
| [Mexico’s national numbering plan](https://spaw.co/tools/phone-number-lookup/sources/crt) | Mexico | unassigned when geographic and outside the table | 640,280 | terms not confirmed (CRT) | retrieved on 2026-10-12 |
| [UKE numbering tables](https://spaw.co/tools/phone-number-lookup/sources/uke) | Poland | not evaluated outside the table | 47,175 | terms not confirmed for the portal; credited to UKE and marked as processed by Spaw | retrieved on 2026-10-12 |
| [MIMIT assigned numbering](https://spaw.co/tools/phone-number-lookup/sources/mimit) | Italy | not evaluated outside the table | 59,587 | CC BY 3.0 for the site’s texts; not confirmed for the workbook (MIMIT) | retrieved on 2026-10-12 |
## The numbering plan, the lists and the complaints
- [libphonenumber](https://spaw.co/tools/phone-number-lookup/sources/libphonenumber): libphonenumber 9.0.41 is installed here: the numbering plans of 245 regions, and the version every phone answer names in its sources. Terms: Apache 2.0, Google.
- [SMS-receiving number lists](https://spaw.co/tools/phone-number-lookup/sources/sms-receiving-lists): The SMS-receiving number lists installed here, synced on 2026-10-15, hold 130,297 numbers: 129,422 on the lists at that sync, and 875 kept at the date they were last seen after the lists dropped them. Terms: MIT, iP.1 Networks and Castle.
- [FTC Do Not Call reported calls](https://spaw.co/tools/phone-number-lookup/sources/ftc-reported-calls): The FTC reported-calls files installed here, the newest dated 2026-10-14, hold 672,303 complaints filed over the last 90 days, naming 572,607 numbers. They are not the Do Not Call Registry, they say nothing about whether you may call or text a number, and none is verified: caller ID is often spoofed. Terms: public domain, US Government work (FTC).
## Compiled by Spaw
- Fictional ranges (`fictional-ranges`, version 2026-09-27, 78 ranges and single numbers): The number ranges regulators reserve for films, television and examples: the NANP 555-01XX block and the drama ranges and numbers of Ofcom, ARCEP, ACMA and ComReg, compiled by Spaw from the regulators’ own notices. A number in one answers is_fictional.
- Carrier brands (`carrier-brands`, version 2026-09-24, 67 brands): The network brand behind a holder’s legal name (carrier_brand), compiled by Spaw from the regulators’ tables: the main operators’ legal entities, former names and operating company numbers, country by country. The map itself is not published.
- Test numbers (`test-numbers`, version bundled, 8 numbers): The documented test numbers in the Washington 555-01XX fiction block, each answering the same result every time, free, on every endpoint.
## Regulators not synced, and why
- Germany, [Bundesnetzagentur](https://www.bundesnetzagentur.de/DE/Fachthemen/Telekommunikation/Nummerierung/start.html): Its numbering pages offer the numbering plan and the allocation rules as PDFs; we found no downloadable table of allocated number blocks, and questions about allocations go to its numbering office. (checked 2026-09-26)
- Spain, [CNMC](https://numeracionyoperadores.cnmc.es/numeracion): Its numbering register is a search application behind a bot challenge, and the CNMC’s open-data catalogue lists no numbering dataset; we found no downloadable table. (checked 2026-09-26)
- Brazil, [Anatel](https://dados.gov.br/): Its datasets are published through Brazil’s federal open-data portal, whose API answered only with an account key; we found no plain download of the assigned ranges. (checked 2026-09-26)
- Ireland, [ComReg](https://www.comreg.ie/industry/licensing/numbering/number-assignments-availability/): Its “Number Assignments & Availability” page is a JavaScript application with no downloadable file. (checked 2026-09-26)
- Denmark, [Digitaliseringsstyrelsen](https://digst.dk/tele/telefoni-og-internet/numre/nummerregistret/): Its number register (Nummerregistret) is a search application, and the page describing it links no downloadable file. (checked 2026-09-26)
- Switzerland, [OFCOM (BAKOM)](https://www.bakom.admin.ch/): Allocated number blocks can be looked up in its eOfcom application one search at a time; we found no downloadable table. (checked 2026-09-25)
## What none of these tables can say
- A regulator’s table records the original allocation of each block, never who serves a number today: a number ported to another carrier keeps its block.
- An allocated block says a number in it can be in service, not that it is.
- The counts are rows of the regulator’s own table, not numbers or traffic: a busy block and an empty one count once each.
## NANPA numbering reports
NANPA numbering reports, as installed here (file dated 2026-10-14): 205,132 codes (198,628 allocated to 2,762 holders and 6,504 reserved) and 1,400,503 thousands-blocks (1,311,623 allocated to 1,987 holders and 88,880 unassigned). Terms: no terms of use published (NANPA).
The administrator the FCC appoints for the North American plan publishes, every day, which central office codes (NPA-NXX, ten thousand numbers each) are assigned and to which carrier, and, inside the pooled codes, which blocks of a thousand are assigned, retained or free and to whom. Its rate centre report spells out the abbreviations those files use for places, and its NPA report describes every area code: where it serves, when it went into service and which codes overlay it.
- Publisher: [North American Numbering Plan Administrator (NANPA)](https://www.nanpa.com/)
- Regions: United States, Puerto Rico, U.S. Virgin Islands, Guam, American Samoa, Northern Mariana Islands
- Dataset names: `nanpa-co-codes`, `nanpa-rate-centres`, `nanpa-npa-report`
- Published: The code and thousands-block reports are rebuilt daily and re-dated each time; the NPA report is re-dated daily.
- Refreshed here: The codes, the thousands-blocks and the NPA report daily; the rate centre report weekly.
- File [CoCodeAssignment_Utilized_AllStates_Public.zip](https://reports.nanpa.com/public/CoCodeAssignment_Utilized_AllStates_Public.zip): ZIP of one tab-separated text file, headed by a “File Updated” date. Every assigned (AS) and unavailable (UA) central office code with its holder, OCN, rate centre, assignment date and pooled and in-service flags.
- File [ThousandsBlockAssignment_All.zip](https://reports.nanpa.com/public/ThousandsBlockAssignment_All.zip): ZIP (about 12 MB) of one comma-separated file (about 112 MB). Every block of a thousand in a pooled code, with its status (assigned, retained, available), the carrier it is assigned to and its OCN.
- File [NPARateCenterAllStatesReport.XLSX](https://reports.nanpa.com/public/NPARateCenterAllStatesReport.XLSX): Excel workbook, dated by its creation time. The full name of each rate centre abbreviation, state by state.
- File [npa_report.csv](https://reports.nanpa.com/public/npa_report.csv): CSV headed by a “File Date”. One row per area code: where it serves, whether it is assigned and in service and since when, its overlay complex, NANPA’s time-zone letters and the dialling plan.
- How rows read: An assigned code (AS) reads as allocated to its holder; an unavailable code (UA), such as N11 or 555, as reserved. An assigned or retained thousands-block reads as allocated to its own carrier; an available block as unassigned. A contaminated available block, and one available only from a future date, is left to its code.
- Complete for geographic numbers: while the copy installed here is under 45 days old, a geographic number outside every listed block answers unassigned_block.
- Terms (checked 2026-09-26): nanpa.com publishes no terms of use and no licence for its reports: the only policy the site links is a privacy policy. The reports are posted openly by the administrator the FCC appoints. Spaw shows facts and counts read from them, credits NANPA with the file date, and does not republish the files.
- Cannot say: Mobiles and landlines share the same blocks in the North American plan, so the file cannot say which a number is; the holder’s name settles it only where it names a wireless carrier or a virtual-number wholesaler.
- Cannot say: Toll-free (8XX) and the other non-geographic codes are not in the files, so only geographic numbers outside them are answered as unassigned.
- Cannot say: A block its holder retained when the code was pooled carries no date.
## CNAC central office code status
CNAC central office code status, as installed here (file dated 2026-10-11): 46,400 codes (28,769 allocated to 100 holders, 16,834 unassigned and 797 reserved). Terms: no licence published (CNAC).
The Canadian Numbering Administrator publishes the status of every central office code in the Canadian part of the North American plan: in service, assigned but not yet live, set aside for special use or plant testing, or available, with the carrier holding each and the exchange area it serves.
- Publisher: [Canadian Numbering Administrator (CNAC)](https://www.cnac.ca/)
- Regions: Canada
- Dataset names: `cnac-co-codes`
- Published: Updated by CNAC as codes change; the file carries its own date.
- Refreshed here: Weekly.
- File [COCodeStatus_ALL.zip](https://www.cnac.ca/data/COCodeStatus_ALL.zip): ZIP of one CSV file; its second row carries only the file date. Every Canadian central office code with its status, holder, exchange area, pooled flag and assignment date.
- How rows read: In Service and Assigned read as allocated; For Special Use and Plant Test as reserved; every other status as unassigned.
- Complete for geographic numbers: while the copy installed here is under 45 days old, a geographic number outside every listed block answers unassigned_block.
- Terms (checked 2026-09-26): CNAC’s website terms of use (version 1, 20 October 2020) grant no licence and set no condition on the code status file, which carries no licence text either.
- Cannot say: The table names the holder of a whole code: blocks of a thousand inside a pooled Canadian code are not read, so the code holder answers for every number in it.
- Cannot say: Mobiles and landlines share the same codes, as everywhere in the North American plan.
## Ofcom numbering data
Ofcom numbering data, as installed here (file dated 2026-10-15): 343,436 blocks (124,626 allocated to 404 holders and 218,810 unassigned). Terms: Ofcom copyright, reuse with acknowledgement.
Ofcom publishes the National Telephone Numbering Plan’s blocks, range by range: which are allocated and to which communications provider, which are free, and which are protected or quarantined. Numbers in Guernsey, Jersey and the Isle of Man sit in the UK +44 plan and are read from the same tables.
- Publisher: [Ofcom](https://www.ofcom.org.uk/phones-and-broadband/phone-numbers/numbering-data)
- Regions: United Kingdom, Guernsey, Jersey, Isle of Man
- Dataset names: `ofcom-numbering`
- Published: Ofcom says it aims to update the files every Wednesday.
- Refreshed here: Weekly.
- File [s1.csv, s3.csv, s5.csv, s7.csv, s8.csv, s9.csv](https://www.ofcom.org.uk/phones-and-broadband/phone-numbers/numbering-data): Six CSV files (Ofcom also offers them as XLSX), one per leading digit; the files carry no date, so the copy here is dated by the day Ofcom published them (their Last-Modified). Geographic (01 and 02), 03, 05, 07, 08 and 09 number blocks with their status and provider.
- How rows read: Allocated blocks read as allocated to their provider; designated blocks as reserved; free, protected, quarantined and reserved blocks as unassigned.
- Ofcom’s table is not treated as complete: a number outside every listed block is not evaluated, and its block_status answers null.
- Terms (checked 2026-09-26): The numbering data page states no licence; its footer reads “© Ofcom 2026”. Ofcom’s copyright page says its material “may be reproduced free of charge in any format or medium, provided that it is reproduced accurately and not used in a misleading context”, and that it “must be acknowledged as Ofcom copyright”. Its open data page adds: “Where we can make the data available on an open basis, we use the Open Government Licence”.
- Cannot say: Ofcom lists free, protected and quarantined blocks row by row, so an unassigned answer always comes from Ofcom’s own row and never from a block missing from the files.
- Cannot say: Guernsey, Jersey and the Isle of Man have their own regulators; their numbers are read from this table because they sit in the UK plan.
## ARCEP numbering resources
ARCEP numbering resources, as installed here (retrieved on 2026-10-12): 22,605 blocks (21,110 allocated to 420 holders and 1,495 unassigned). Terms: Licence Ouverte / Open Licence (Etalab), ARCEP.
France’s regulator publishes its numbering resources on data.gouv.fr as “Ressources en numérotation téléphonique”: the ranges attributed to each operator, the ranges frozen before they can be attributed again, and the list of operator identifiers, for metropolitan France and the overseas regions.
- Publisher: [Autorité de régulation des communications électroniques, des postes et de la distribution de la presse (ARCEP)](https://www.data.gouv.fr/fr/datasets/ressources-en-numerotation-telephonique/)
- Regions: France, Guadeloupe, St. Barthélemy, St. Martin, Martinique, French Guiana, Réunion, Mayotte, St. Pierre & Miquelon
- Dataset names: `arcep-numbering`
- Published: data.gouv.fr lists the dataset as updated weekly.
- Refreshed here: Weekly.
- File [identifiants_CE.csv](https://extranet.arcep.fr/uploads/identifiants_CE.csv): Semicolon-separated CSV in ISO-8859-1. Each operator code and the operator’s name.
- File [MAJNUM.csv](https://extranet.arcep.fr/uploads/MAJNUM.csv): Semicolon-separated CSV in ISO-8859-1. Every attributed range with its operator and attribution date.
- File [GELNUM.csv](https://extranet.arcep.fr/uploads/GELNUM.csv): Semicolon-separated CSV in ISO-8859-1. The frozen ranges.
- How rows read: An attributed range reads as allocated to its operator; a frozen range as unassigned.
- Complete for geographic numbers: while the copy installed here is under 45 days old, a geographic number outside every listed block answers unassigned_block.
- Mobile and VoIP ranges are listed too, but a mobile or VoIP number outside them is left unanswered rather than called unassigned.
- Terms (checked 2026-09-26): data.gouv.fr lists the dataset under the Licence Ouverte / Open Licence (licence id fr-lo), which allows reuse, including commercial reuse, with credit to the source.
- Cannot say: The attribution date is the day ARCEP attributed the range to its operator, not the day a number in it came into service.
## ACMA numbering register
ACMA numbering register, as installed here (retrieved on 2026-10-12): 99,624 blocks (90,794 allocated to 55 holders and 8,830 unassigned). Terms: terms not confirmed (ACMA).
Australia’s regulator publishes its numbering register through the Numbering System: every range of the ten-digit plan with its status and current holder, and the freephone and local-rate numbers one by one.
- Publisher: [Australian Communications and Media Authority (ACMA)](https://www.thenumberingsystem.com.au/)
- Regions: Australia
- Dataset names: `acma-numbering`
- Published: The register is live; the download reflects it when fetched.
- Refreshed here: Weekly.
- File [EnhancedFullDownload.zip](https://www.thenumberingsystem.com.au/download/EnhancedFullDownload.zip): ZIP of one CSV file (about 91 MB); the file carries no date. Every range and individually allocated number with its service type, status and holder.
- How rows read: Allocated ranges read as allocated to their holder; spare ranges as unassigned. Only ten-digit local, mobile, satellite, IoT, data and VPN ranges are read.
- ACMA’s table is not treated as complete: a number outside every listed block is not evaluated, and its block_status answers null.
- Terms (checked 2026-09-26): The Numbering System’s terms and conditions page is a JavaScript application that could not be read, so the terms of the register download are not confirmed.
- Cannot say: Freephone and local-rate numbers (1800, 1300 and 13), which the register lists one by one, are not read; the numbering plan types them.
- Cannot say: The table is not treated as complete, so an Australian number outside it is not evaluated.
## ACM number register
ACM number register, as installed here (retrieved on 2026-10-12): 84,420 blocks (82,485 allocated to 12,734 holders, 1,912 unassigned and 23 reserved). Terms: CC0 1.0, ACM (data.overheid.nl).
The Dutch regulator publishes every number range it has assigned, every range in its cooling-off period and every blocked range, with the holder of each. Holders include companies, sole traders and private individuals.
- Publisher: [Autoriteit Consument & Markt (ACM)](https://www.acm.nl/nl/onderwerpen/telecommunicatie/telefoonnummers/)
- Regions: Netherlands
- Dataset names: `acm-nummerregister`
- Published: Updated by ACM as assignments change.
- Refreshed here: Weekly.
- File [nummers_csv.zip](https://www.acm.nl/sites/default/files/registers/nummers_csv.zip): ZIP of one semicolon-separated CSV file; the file carries no date. Every assigned, cooling-off and blocked range with its holder and service.
- How rows read: Toegekend (assigned) reads as allocated; Afkoelen (cooling off) as unassigned; Geblokkeerd (blocked) as reserved. Signalling codes, network codes and short numbers (rows under eight digits) are not read.
- Complete for geographic numbers: while the copy installed here is under 45 days old, a geographic number outside every listed block answers unassigned_block.
- Mobile and VoIP ranges are listed too, but a mobile or VoIP number outside them is left unanswered rather than called unassigned.
- Terms (checked 2026-09-26): The register is listed on data.overheid.nl, the Dutch government’s open data portal, as “Register van toegekende telefoonnummers” under CC0 1.0.
- Cannot say: Many holders are sole traders or private individuals, so these pages count holders and never name them.
## NAD number register
NAD number register, as installed here (retrieved on 2026-10-12): 9,718 blocks (5,101 allocated to 14 holders, 4,581 unassigned and 36 reserved). Terms: no reuse licence published (NAD).
New Zealand’s numbering is administered by the industry’s Number Administration Deed, which publishes its number register: every code of every service category with its status and holder.
- Publisher: [Number Administration Deed (NAD), New Zealand](https://www.nad.org.nz/number-register)
- Regions: New Zealand
- Dataset names: `nad-number-register`
- Published: Updated by the NAD as codes change.
- Refreshed here: Weekly.
- File [Number register export](https://www.nad.org.nz/number-register/export): CSV (“Export Number register as a CSV”); the file carries no date. Every code with its service category, status and holder.
- How rows read: Assigned codes read as allocated; spare and protected codes as unassigned; reserved and unallocatable codes as reserved.
- NAD’s table is not treated as complete: a number outside every listed block is not evaluated, and its block_status answers null.
- Terms (checked 2026-09-26): The register’s site is marked “© 2020 Number Administration Deed. All rights reserved”, and no reuse licence is published.
- Cannot say: Service codes (01XY, 011XNT, 1XYZ and 50XY) are not read.
- Cannot say: The table is not treated as complete, so a New Zealand number outside it is not evaluated.
## Mexico’s national numbering plan
Mexico’s national numbering plan, as installed here (retrieved on 2026-10-12): 640,280 blocks (640,280 allocated to 139 holders). Terms: terms not confirmed (CRT).
Mexico’s public numbering plan lists every assigned range of the ten-digit plan with its operator and modality. The Comisión Reguladora de Telecomunicaciones took over the functions of the IFT, which was dissolved on 17 October 2025; the plan is still served from the former IFT’s numbering portal, which carries the IFT’s name.
- Publisher: [Comisión Reguladora de Telecomunicaciones (CRT), formerly the Instituto Federal de Telecomunicaciones (IFT)](https://sns.ift.org.mx/sns-frontend/planes-numeracion/descarga-publica.xhtml)
- Regions: Mexico
- Dataset names: `ift-numeracion`
- Published: The portal regenerates the plan and states the date it was generated.
- Refreshed here: Weekly.
- File [Plan nacional de numeración (public download)](https://sns.ift.org.mx/sns-frontend/planes-numeracion/descarga-publica.xhtml): A file served only in answer to a form on the portal page, never at a plain link; the file carries no date. Every assigned range (about 178,000) with its zone, operator, modality (CPP, MPP or FIJO) and assignment date.
- How rows read: Every listed range reads as allocated to its operator; a CPP or MPP modality marks the range mobile, and a fixed range keeps the plan’s answer.
- Complete for geographic numbers: while the copy installed here is under 45 days old, a geographic number outside every listed block answers unassigned_block.
- Terms (checked 2026-09-26): The portal states no terms of reuse and no licence; its footer reads “Copyright © Sistema de Numeración y Señalización”. The terms have not been confirmed with the CRT.
- Cannot say: Mexican mobiles and landlines share the plan’s geographic numbering; only a CPP or MPP modality marks a range mobile.
## UKE numbering tables
UKE numbering tables, as installed here (retrieved on 2026-10-12): 47,175 blocks (24,296 allocated to 567 holders, 22,376 unassigned and 503 reserved). Terms: terms not confirmed for the portal; credited to UKE and marked as processed by Spaw.
Poland’s regulator publishes its numbering management tables: the fixed-network table by zone and the mobile table, each scope with the operator it was assigned to, and the space held in reserve by UKE’s President, for regulation or for technical use.
- Publisher: [Urząd Komunikacji Elektronicznej (UKE), Poland](https://numeracja.uke.gov.pl/)
- Regions: Poland
- Dataset names: `uke-numeracja`
- Published: The tables are exported live from UKE’s numbering portal.
- Refreshed here: Weekly.
- File [pstn_tables/export.csv](https://numeracja.uke.gov.pl/en/pstn_tables/export.csv): CSV written in Windows-1250 (though served as UTF-8); the export carries no date. The fixed-network scopes by zone with their operator.
- File [plmn_tables/export.csv](https://numeracja.uke.gov.pl/en/plmn_tables/export.csv): CSV written in Windows-1250 (though served as UTF-8); the export carries no date. The mobile scopes with their operator.
- How rows read: A scope assigned to an operator reads as allocated; the President’s and the regulatory reserves as unassigned; the technical reserve as reserved.
- UKE’s table is not treated as complete: a number outside every listed block is not evaluated, and its block_status answers null.
- Terms (checked 2026-09-26): numeracja.uke.gov.pl shows only “© 2019 UKE”. UKE’s page on the reuse of public information sets these conditions for what uke.gov.pl publishes: “obowiązek poinformowania o źródle, czasie wytworzenia i pozyskania informacji od UKE” and “obowiązek poinformowania o przetworzeniu informacji ponownie wykorzystywanej” (state the source, when the information was created and obtained from UKE, and that it was processed). Whether they cover the numbering portal is not confirmed. Spaw follows them: the source is UKE, the date below is the day the tables were retrieved (the export carries no creation date), and the data was processed by Spaw.
- Cannot say: Every zone’s AB0 range and the whole of zones 26 and 47 are missing from the tables, so the table is partial: a Polish number outside it is not evaluated.
- Cannot say: The export carries no creation date: the version shown is the day it was retrieved.
## MIMIT assigned numbering
MIMIT assigned numbering, as installed here (retrieved on 2026-10-12): 59,587 blocks (59,587 allocated to 275 holders). Terms: CC BY 3.0 for the site’s texts; not confirmed for the workbook (MIMIT).
Italy’s ministry publishes, on its numbering resources page, a workbook of the numbering it has assigned: the mobile (3) and nomadic (55) arcs and the geographic blocks by district, each with the operator it was assigned to.
- Publisher: [Ministero delle Imprese e del Made in Italy (MIMIT)](https://www.mimit.gov.it/it/comunicazioni/telefonia/risorse-di-numerazione)
- Regions: Italy
- Dataset names: `mimit-numerazione`
- Published: Updated by MIMIT as assignments change.
- Refreshed here: Weekly.
- File [File_assegnati_2.xlsx](https://www.mimit.gov.it/images/stories/digitale/numerazione/File_assegnati_2.xlsx): Excel workbook with one tab per numbering class; the workbook carries no date. The 3XYZ mobile arcs, the 55 nomadic arcs and the geographic blocks, with their operator. Its premium, M2M and fictitious-number tabs are not read.
- How rows read: Every listed arc and block reads as allocated to its operator.
- MIMIT’s table is not treated as complete: a number outside every listed block is not evaluated, and its block_status answers null.
- Terms (checked 2026-09-26): MIMIT’s legal notes say “i testi pubblicati sul presente sito sono messi a disposizione con licenza CC-BY 3.0”, that texts may be reused “a patto che sia sempre citata la fonte e – ove possibile – riportato l’indirizzo web della pagina originale”, and that “il download e qualunque utilizzo dei dati pubblicati sul sito comporta accettazione delle presenti note legali”. The licence names texts; whether it covers the workbook is not confirmed. Spaw credits MIMIT and links the original page.
- Cannot say: The workbook lists assignments only, so it is partial: an Italian number outside it is not evaluated.
- Cannot say: The workbook carries no date: the version shown is the day it was retrieved.
## libphonenumber
libphonenumber 9.0.41 is installed here: the numbering plans of 245 regions, and the version every phone answer names in its sources.
Google’s libphonenumber carries the numbering plan of every region: which numbers are valid, how they are written, which line type each range is, the original network of many mobile prefixes, the places geographic ranges serve, their time zones, and emergency and short numbers. It ships with the application; every answer names its version.
- Publisher: [Google (libphonenumber), through its PHP port giggsey/libphonenumber-for-php](https://github.com/google/libphonenumber)
- Dataset names: `libphonenumber`
- Published: Google releases new metadata every few weeks.
- Refreshed here: Installed with the application; a weekly check reports a newer release, and every answer names the version installed.
- File [giggsey/libphonenumber-for-php](https://github.com/giggsey/libphonenumber-for-php): A PHP package of Google’s metadata, installed with the application. The numbering-plan metadata of every supported region.
- How rows read: Not a table of blocks: it answers whether a number is valid for its plan and which line type its range is.
- Terms (checked 2026-09-26): libphonenumber and its PHP port are published under the Apache License 2.0.
- Cannot say: A valid number is one the plan allows, not one in service.
- Cannot say: Its carrier names come from prefix data and name the range’s original network, never the one serving a ported number.
## SMS-receiving number lists
The SMS-receiving number lists installed here, synced on 2026-10-15, hold 130,297 numbers: 129,422 on the lists at that sync, and 875 kept at the date they were last seen after the lists dropped them.
Two open lists record the numbers that public SMS-receiving websites publish, the ones anyone can use to receive a verification code: iP.1 Networks’ list, with the date each number was last seen, and Castle’s daily list of the numbers most active on those sites. A number that leaves both lists is kept at its last-seen date and ages, rather than vanishing.
- Publisher: [iP.1 Networks AB and Castle](https://github.com/iP1SMS/disposable-phone-numbers)
- Dataset names: `disposable-phone-numbers`
- Published: Both lists are updated daily.
- Refreshed here: Daily.
- File [number-list.json](https://github.com/iP1SMS/disposable-phone-numbers): JSON map of numbers to the date last seen. Numbers seen on SMS-receiving websites, with dates.
- File [disposable-phone-numbers.txt](https://github.com/castle/disposable-phone-numbers): Text, one E.164 number per line, undated. The day’s 1,000 most active numbers on those sites, counted as seen on the day they are read.
- How rows read: Not a table of blocks: a listed number scores disposable for a while after it was last seen, then disposable_stale.
- Terms (checked 2026-09-26): Both lists are published under the MIT licence: © 2018 iP.1 Networks AB and © 2026 Castle.
- Cannot say: A listing says a website published the number on a date. It says nothing about who holds the number today: carriers recycle numbers the sites give up.
## FTC Do Not Call reported calls
The FTC reported-calls files installed here, the newest dated 2026-10-14, hold 672,303 complaints filed over the last 90 days, naming 572,607 numbers. They are not the Do Not Call Registry, they say nothing about whether you may call or text a number, and none is verified: caller ID is often spoofed.
The FTC publishes, each weekday, the complaints consumers filed about unwanted calls: the number that called them, the date and time, the subject and whether it was a recorded message. Spaw counts the complaints naming each North American number over the last 90 days and drops the consumer’s location when the file is read.
- Publisher: [Federal Trade Commission (FTC)](https://www.ftc.gov/policy-notices/open-government/data-sets/do-not-call-data)
- Dataset names: `us-complaint-reports`
- Published: The FTC says the data set “is typically updated each weekday by about noon Eastern time”.
- Refreshed here: Daily; each night installs the newest file and ages the oldest day out of the 90 days.
- File [DNC_Complaint_Numbers_{date}.csv](https://www.ftc.gov/policy-notices/open-government/data-sets/do-not-call-data): One CSV file per weekday; weekend and holiday reports ride in the next file. Each complaint: the number that called, the date and time, the subject, and whether it was a recorded message.
- How rows read: Not a table of blocks: a count of complaints and the days they fell on, per number, over 90 days.
- Terms (checked 2026-09-26): The data is a work of the US federal government and so in the public domain in the United States; the FTC’s page states no licence of its own.
- Cannot say: These counts are not the Do Not Call Registry.
- Cannot say: They say nothing about whether you may call or text a number.
- Cannot say: The FTC verifies none of the reports, and caller ID is often spoofed, so an innocent number can collect complaints.
---
# New and upcoming North American area codes
NANPA’s NPA report of 14 October 2026 lists 13 North American area codes put in service in the 24 months before it, the newest 465 (New York) on 18 June 2026. Another 6 are assigned and not yet in service, and relief planning is under way for 16. Each code is joined to the central office code tables for how many of its codes are assigned and in service.
## Put in service in the 24 months to 14 October 2026
| Area code | Where | Overlay of | In service (NANPA) | Codes assigned | Codes in service | First dated code | libphonenumber 9.0.41 accepts |
| --- | --- | --- | --- | --- | --- | --- | --- |
| [465](https://spaw.co/tools/phone-number-lookup/us/465) | New York | 718 | 18 June 2026 | 0 | 0 | — | not yet |
| [483](https://spaw.co/tools/phone-number-lookup/us/483) | Alabama | 334 | 23 February 2026 | 14 | 5 | 2026-08-07 | not yet |
| [471](https://spaw.co/tools/phone-number-lookup/us/471) | Mississippi | 662 | 30 January 2026 | 0 | 0 | — | not yet |
| [679](https://spaw.co/tools/phone-number-lookup/us/679) | Michigan | 313 | 7 November 2025 | 0 | 0 | — | not yet |
| [457](https://spaw.co/tools/phone-number-lookup/us/457) | Louisiana | 318 | 25 September 2025 | 0 | 0 | — | not yet |
| [729](https://spaw.co/tools/phone-number-lookup/us/729) | Tennessee | 423 | 5 September 2025 | 0 | 0 | — | not yet |
| [748](https://spaw.co/tools/phone-number-lookup/us/748) | Colorado | 970 | 7 July 2025 | 0 | 0 | — | yes |
| [257](https://spaw.co/tools/phone-number-lookup/ca/257) | British Columbia | 604 | 24 May 2025 | 20 | 18 | 2024-09-05 | yes |
| [942](https://spaw.co/tools/phone-number-lookup/ca/942) | Ontario | 416 | 26 April 2025 | 57 | 44 | 2024-11-04 | yes |
| [357](https://spaw.co/tools/phone-number-lookup/us/357) | California | 559 | 26 March 2025 | 9 | 0 | 2026-09-18 | not yet |
| [837](https://spaw.co/tools/phone-number-lookup/us/837) | California | 530 | 31 January 2025 | 11 | 0 | 2026-08-27 | not yet |
| [621](https://spaw.co/tools/phone-number-lookup/us/621) | Texas | 346 | 23 January 2025 | 0 | 0 | — | not yet |
| [738](https://spaw.co/tools/phone-number-lookup/us/738) | California | 323 | 1 November 2024 | 29 | 20 | 2025-09-08 | yes |
An area code can be in service before any of its codes is assigned: 465 has been in service since 18 June 2026, and NANPA’s central office code file of 14 October 2026 lists none of its codes.
## Assigned, not yet in service
| Area code | Where | Overlay of | Planned in-service date (NANPA) | Relief planning |
| --- | --- | --- | --- | --- |
| 273 | Quebec | 418 | 27 February 2027 | under way |
| 565 | Georgia | 912 | 6 December 2027 | under way |
| 851 | Nova Scotia and Prince Edward Island | 902 | 27 May 2028 | under way |
| 568 | Alberta | 825 | 10 June 2028 | under way |
| 451 | North Carolina | 828 | no date set | under way |
| 761 | Kentucky | 502 | no date set | under way |
## Relief planning under way
| Area code | Where | Shares its area with | In service since |
| --- | --- | --- | --- |
| 273 | Quebec | 367, 418, 581 | not yet |
| 451 | North Carolina | 828 | not yet |
| [502](https://spaw.co/tools/phone-number-lookup/us/502) | Kentucky | 761 | 1947 |
| [509](https://spaw.co/tools/phone-number-lookup/us/509) | Washington State | — | 1957 |
| 565 | Georgia | 912 | not yet |
| 568 | Alberta | 368, 403, 587, 780, 825 | not yet |
| [580](https://spaw.co/tools/phone-number-lookup/us/580) | Oklahoma | — | 1 November 1997 |
| [626](https://spaw.co/tools/phone-number-lookup/us/626) | California | — | 14 June 1997 |
| [657](https://spaw.co/tools/phone-number-lookup/us/657) | California | 714 | 23 September 2008 |
| [714](https://spaw.co/tools/phone-number-lookup/us/714) | California | 657 | 1951 |
| 761 | Kentucky | 502 | not yet |
| [828](https://spaw.co/tools/phone-number-lookup/us/828) | North Carolina | — | 22 March 1998 |
| 851 | Nova Scotia and Prince Edward Island | 782, 902 | not yet |
| [912](https://spaw.co/tools/phone-number-lookup/us/912) | Georgia | 565 | 1954 |
| [949](https://spaw.co/tools/phone-number-lookup/us/949) | California | — | 18 April 1998 |
| [956](https://spaw.co/tools/phone-number-lookup/us/956) | Texas | — | 7 July 1997 |
## Non-geographic codes
NANPA also put 4 non-geographic codes in service in the same window (532 on 4 November 2024, 538 on 28 October 2025, 542 on 26 May 2026 and 543 on 21 September 2026). They are non-geographic 5XX codes, used across the whole plan and tied to no state or province, so they are not listed as area codes.
## Sources
- NANPA’s NPA report (`nanpa-npa-report`, file of 14 October 2026)
- NANPA’s central office code file (`nanpa-co-codes`, file of 14 October 2026)
- the CNAC’s central office code file (`cnac-co-codes`, file of 11 October 2026)
- libphonenumber 9.0.41, the numbering metadata a lookup checks first
---
# Spaw API reference
Base URL: `https://spaw.co/api/v1`. Machine-readable description: https://spaw.co/openapi.json (also /openapi.yaml).
Spaw verifies email addresses, phone numbers, IP addresses, postal addresses and company identifiers over plain REST, and compares those signals against each other: JSON in and out, typed errors, and one credit balance across every endpoint. An email lookup runs RFC syntax parsing, live DNS and MX checks, SPF and DMARC records, open disposable and role lists, and a mailbox-level SMTP handshake; the other signals answer from datasets held on our own servers, and every answer names the datasets it came from. No verification ever sends mail, and nothing is ever sent to a number or an address you check.
## Authentication
Every request carries a secret API key as a bearer token. Create and revoke keys on the dashboard's API keys page; a key is shown once at creation and stored hashed.
```http
Authorization: Bearer sk_live_…
Content-Type: application/json
```
There are two kinds of keys. Secret `sk_live_` keys authenticate servers and unlock the full API. Publishable `pk_` keys are safe in browser page source: they can only run single lookups through the four browser endpoints — `POST /api/v1/email/public`, `POST /api/v1/phone/public` (which never runs the live carrier-network check), `POST /api/v1/ip/public` and `POST /api/v1/address/public` — and only from the domains they are locked to.
A key only works once the account behind it has confirmed its email address. Until then every request under `/api/v1`, and every MCP tool call, answers `403 EMAIL_NOT_VERIFIED` with the standard error envelope, and nothing is charged. Open the link in the verification email, or sign in and request a new one from the verification notice, then retry the same call. The browser endpoint and the bounce webhook URLs are unaffected.
## Response envelope
Successful lookups answer `{ "success": true, "data": …, "meta": … }`. The `meta` block carries `credits_used`, `credits_remaining`, `cache_hit`, and `request_id` (email lookups add `suppressed` and `fair_use_billed`, and `settling` when a callback was requested; IP lookups add `ip_lookups_prepaid`). Failures answer `{ "success": false, "error": { "code", "message", "request_id" } }` with a stable code to branch on; validation failures add `error.errors` keyed by field. Every failure answers in that shape, including the ones the framework used to answer for itself: a record that is not on the account is `404 NOT_FOUND`, a wrong HTTP verb is `405 METHOD_NOT_ALLOWED` with an `Allow` header, and a failure on this side is `500 SERVER_ERROR`. Every response also carries an `X-Request-Id` header that matches every log line the request produced. Quote it when writing to support.
## Billing
You pay for answers, not attempts, and anything you paid to check once is free to check again for seven days.
| Outcome | Credits |
| --- | --- |
| Fresh lookup with a usable answer (deliverable or risky verdict, a domain that accepts mail, a valid phone number, an address that stands as written, a register that answers) | 1 |
| Five fresh IP lookups that place an address | 1 |
| Email answer whose mailbox check was uncertain (`unverified`, including a handshake partner outage) | 0 |
| Undeliverable email verdict within the fair-use allowance | 0 |
| Undeliverable email verdict past the fair-use allowance (`meta.fair_use_billed` true) | 1 |
| Invalid input or no match | 0 |
| Repeat of a recent lookup, within 7 days | 0 |
| Repeat asked for fresh with `refresh` or an exceeded `max_age`, when answered | 1 |
| Rejected or failed request | 0 |
| Monthly free grant, every account | +100 |
An uncertain mailbox answer writes no seven-day repeat marker, so a later lookup of the address is a fresh one: the uncertain probe result is reused for up to 15 minutes (`refresh` and `max_age` do not set it aside), then the mailbox is asked again, and the lookup bills 1 credit only once the mailbox is confirmed.
Fair use: undeliverable email verdicts that needed a mailbox check are free up to 500 in any 30 days, or the number of deliverable and risky email answers billed in those 30 days when that is more. Past the allowance each one bills 1 credit and answers `meta.fair_use_billed: true`. Verdicts settled without a mailbox check (bad syntax, no working mail server, a username the provider never allows), suppressed answers, test addresses and repeats are always free and never counted.
IP lookups are billed in blocks of five. The answered IP lookup that opens a block reports `credits_used: 1` and leaves four prepaid on the account; the next four answered IP lookups report `credits_used: 0`. `meta.ip_lookups_prepaid` says how many are left after the call, and one `ip_lookup` row in the ledger stands for a block of five. Reserved, private and unallocated ranges, unknown addresses and repeats are free and never use a prepaid lookup. ASN and range lookups are not IP lookups and bill 1 credit each.
Credit packs are one-time purchases from the dashboard's Billing page (from $9 for 1,000 credits); plans deposit the same credits every month for less (from $7 a month, or yearly for ten months' price) and can be cancelled at any time. Plan credits are spent first. Pack credits never expire; plan credits roll over up to three months' allowance and stay usable for 60 days after you cancel. `GET /api/v1/account` returns the live balance and the plan for pre-flight checks.
## Rate limits
Every key may make 5 requests per second; a batch or bulk call counts as one request. Over the limit, the API answers `429 RATE_LIMITED` with a `Retry-After` header.
## Test addresses
Six fixed addresses on the reserved `spaw.test` domain always answer the same canonical result, cost no credits, touch no DNS or mail server, and never appear in your history or suppression list. A plus-tag (`deliverable+ci@spaw.test`) works too, on every endpoint including batch, bulk, and the browser endpoint.
| Address | Answer |
| --- | --- |
| deliverable@spaw.test | deliverable · mailbox confirmed, risk_score 0 |
| unverified@spaw.test | risky · reason unverified, smtp_checked false, smtp_reason greylisted, mailbox_confidence set |
| catch-all@spaw.test | risky · reason catch_all, mailbox_exists null |
| disposable@spaw.test | risky · reason disposable, risk_score 90 |
| role@spaw.test | risky · reason role |
| undeliverable@spaw.test | undeliverable · reason mailbox_not_found, smtp_reason no_mailbox |
## Data sources
Every lookup's `data.sources` array names the dataset and version behind the answer (`rfc-5322-syntax`, `live-dns`, `disposable-domains`, `role-addresses`, `free-provider-domains`, `typo-domains`, `provider-username-rules`, `mx-provider-patterns`, `rdap-registration`, the mailbox probe's provider and date, `delivery-feedback`, `suppression-list`, or `test-addresses`), so an answer can always be audited.
## Endpoints
Grouped by product, and inside each product by the kind of endpoint.
### Email verification
#### Lookups
| Method | Path | Summary |
| --- | --- | --- |
| POST | `/api/v1/email` | [Verify an email address](https://spaw.co/docs/api/verify-email) |
| GET | `/api/v1/email/domain/{domain}` | [Inspect a domain's mail setup](https://spaw.co/docs/api/domain-intelligence) |
#### Batch & browser
| Method | Path | Summary |
| --- | --- | --- |
| POST | `/api/v1/email/batch` | [Verify up to 50 addresses in one call](https://spaw.co/docs/api/verify-email-batch) |
| POST | `/api/v1/email/public` | [Verify an address from the browser with a publishable key](https://spaw.co/docs/api/verify-email-public) |
#### Bulk jobs
| Method | Path | Summary |
| --- | --- | --- |
| GET | `/api/v1/email/bulk` | [List the account's bulk verification runs](https://spaw.co/docs/api/list-bulk-jobs) |
| POST | `/api/v1/email/bulk` | [Queue a bulk verification run](https://spaw.co/docs/api/create-bulk-job) |
| GET | `/api/v1/email/bulk/{jobId}` | [Poll a bulk job](https://spaw.co/docs/api/get-bulk-job) |
| POST | `/api/v1/email/bulk/{jobId}/cancel` | [Cancel a bulk job](https://spaw.co/docs/api/cancel-bulk-job) |
| POST | `/api/v1/email/bulk/{jobId}/resume` | [Resume a bulk job that ran out of credits](https://spaw.co/docs/api/resume-bulk-job) |
| POST | `/api/v1/email/bulk/{jobId}/webhook/redeliver` | [Send a finished run's completion webhook again](https://spaw.co/docs/api/redeliver-bulk-webhook) |
| GET | `/api/v1/email/bulk/{jobId}/results` | [Download a bulk job's results as CSV](https://spaw.co/docs/api/download-bulk-results) |
#### Monitored lists
| Method | Path | Summary |
| --- | --- | --- |
| GET | `/api/v1/email/monitors` | [List the account's monitors](https://spaw.co/docs/api/list-monitors) |
| POST | `/api/v1/email/monitors` | [Monitor a list on a schedule](https://spaw.co/docs/api/create-monitor) |
| GET | `/api/v1/email/monitors/{monitorId}` | [Read a monitor and its last results](https://spaw.co/docs/api/get-monitor) |
| PATCH | `/api/v1/email/monitors/{monitorId}` | [Change a monitor](https://spaw.co/docs/api/update-monitor) |
| DELETE | `/api/v1/email/monitors/{monitorId}` | [Stop monitoring a list](https://spaw.co/docs/api/delete-monitor) |
| POST | `/api/v1/email/monitors/{monitorId}/run` | [Re-verify a monitored list now](https://spaw.co/docs/api/run-monitor) |
| GET | `/api/v1/email/domain-monitors` | [List the account's domain monitors](https://spaw.co/docs/api/list-domain-monitors) |
| POST | `/api/v1/email/domain-monitors` | [Monitor a list of domains on a schedule](https://spaw.co/docs/api/create-domain-monitor) |
| GET | `/api/v1/email/domain-monitors/{monitorId}` | [Read a domain monitor and its last results](https://spaw.co/docs/api/get-domain-monitor) |
| PATCH | `/api/v1/email/domain-monitors/{monitorId}` | [Change a domain monitor](https://spaw.co/docs/api/update-domain-monitor) |
| DELETE | `/api/v1/email/domain-monitors/{monitorId}` | [Stop monitoring a domain list](https://spaw.co/docs/api/delete-domain-monitor) |
| POST | `/api/v1/email/domain-monitors/{monitorId}/run` | [Re-check a monitored domain list now](https://spaw.co/docs/api/run-domain-monitor) |
#### Sending policy
| Method | Path | Summary |
| --- | --- | --- |
| GET | `/api/v1/email/policy` | [Read the account's sending policy](https://spaw.co/docs/api/get-email-policy) |
| PUT | `/api/v1/email/policy` | [Change one or more rules of the sending policy](https://spaw.co/docs/api/update-email-policy) |
| DELETE | `/api/v1/email/policy` | [Restore the default sending policy](https://spaw.co/docs/api/reset-email-policy) |
#### Outcome feedback
| Method | Path | Summary |
| --- | --- | --- |
| POST | `/api/v1/email/feedback` | [Report delivery outcomes](https://spaw.co/docs/api/report-delivery-feedback) |
| GET | `/api/v1/email/feedback/summary` | [Measured accuracy](https://spaw.co/docs/api/feedback-summary) |
| POST | `/api/v1/email/feedback/{provider}/{feedbackKey}` | [Receive bounce webhooks from an email provider](https://spaw.co/docs/api/esp-bounce-webhook) |
#### Suppression lists
| Method | Path | Summary |
| --- | --- | --- |
| GET | `/api/v1/email/suppressions` | [List the suppression list](https://spaw.co/docs/api/list-suppressions) |
| POST | `/api/v1/email/suppressions` | [Import known-bad addresses](https://spaw.co/docs/api/add-suppressions) |
| DELETE | `/api/v1/email/suppressions` | [Remove addresses from the suppression list](https://spaw.co/docs/api/remove-suppressions) |
| DELETE | `/api/v1/email/suppressions/{suppressionId}` | [Remove a suppression entry](https://spaw.co/docs/api/remove-suppression) |
#### History
| Method | Path | Summary |
| --- | --- | --- |
| GET | `/api/v1/email/history` | [List the account's verification history](https://spaw.co/docs/api/list-history) |
### Phone intelligence
#### Lookups
| Method | Path | Summary |
| --- | --- | --- |
| POST | `/api/v1/phone` | [Validate and enrich a phone number](https://spaw.co/docs/api/validate-phone) |
| GET | `/api/v1/phone/block/{country}/{prefix}` | [Report a number range from the regulator's table](https://spaw.co/docs/api/phone-block-report) |
#### Batch & browser
| Method | Path | Summary |
| --- | --- | --- |
| POST | `/api/v1/phone/batch` | [Validate up to 1,000 numbers in one call](https://spaw.co/docs/api/lookup-phone-batch) |
| POST | `/api/v1/phone/public` | [Validate a number from the browser with a publishable key](https://spaw.co/docs/api/validate-phone-public) |
#### Bulk jobs
| Method | Path | Summary |
| --- | --- | --- |
| GET | `/api/v1/phone/bulk` | [List the account's bulk phone lookup runs](https://spaw.co/docs/api/list-phone-bulk-jobs) |
| POST | `/api/v1/phone/bulk` | [Queue a bulk phone lookup run](https://spaw.co/docs/api/create-phone-bulk-job) |
| GET | `/api/v1/phone/bulk/{jobId}` | [Poll a bulk phone job](https://spaw.co/docs/api/get-phone-bulk-job) |
| POST | `/api/v1/phone/bulk/{jobId}/cancel` | [Cancel a bulk phone job](https://spaw.co/docs/api/cancel-phone-bulk-job) |
| POST | `/api/v1/phone/bulk/{jobId}/resume` | [Resume a bulk job that ran out of credits](https://spaw.co/docs/api/resume-phone-bulk-job) |
| POST | `/api/v1/phone/bulk/{jobId}/webhook/redeliver` | [Send a finished run's completion webhook again](https://spaw.co/docs/api/redeliver-phone-bulk-webhook) |
| GET | `/api/v1/phone/bulk/{jobId}/results` | [Download a bulk phone job's results](https://spaw.co/docs/api/download-phone-bulk-results) |
#### Monitored lists
| Method | Path | Summary |
| --- | --- | --- |
| GET | `/api/v1/phone/monitors` | [List the account's phone monitors](https://spaw.co/docs/api/list-phone-monitors) |
| POST | `/api/v1/phone/monitors` | [Monitor a phone list on a schedule](https://spaw.co/docs/api/create-phone-monitor) |
| GET | `/api/v1/phone/monitors/{monitorId}` | [Read a phone monitor and its last results](https://spaw.co/docs/api/get-phone-monitor) |
| PATCH | `/api/v1/phone/monitors/{monitorId}` | [Change a monitor](https://spaw.co/docs/api/update-phone-monitor) |
| DELETE | `/api/v1/phone/monitors/{monitorId}` | [Stop monitoring a phone list](https://spaw.co/docs/api/delete-phone-monitor) |
| POST | `/api/v1/phone/monitors/{monitorId}/run` | [Re-check a monitored phone list now](https://spaw.co/docs/api/run-phone-monitor) |
#### Sending policy
| Method | Path | Summary |
| --- | --- | --- |
| GET | `/api/v1/phone/policy` | [Read the account's SMS policy](https://spaw.co/docs/api/get-phone-policy) |
| PUT | `/api/v1/phone/policy` | [Change one or more rules of the SMS policy](https://spaw.co/docs/api/update-phone-policy) |
| DELETE | `/api/v1/phone/policy` | [Restore the default SMS policy](https://spaw.co/docs/api/reset-phone-policy) |
#### Outcome feedback
| Method | Path | Summary |
| --- | --- | --- |
| POST | `/api/v1/phone/feedback/{provider}/{feedbackKey}` | [Receive delivery status webhooks from an SMS provider](https://spaw.co/docs/api/sms-status-webhook) |
| POST | `/api/v1/phone/feedback` | [Report SMS outcomes](https://spaw.co/docs/api/report-phone-feedback) |
| GET | `/api/v1/phone/feedback/summary` | [Reported SMS outcomes](https://spaw.co/docs/api/phone-feedback-summary) |
#### Suppression lists
| Method | Path | Summary |
| --- | --- | --- |
| GET | `/api/v1/phone/suppressions` | [List the phone suppression list](https://spaw.co/docs/api/list-phone-suppressions) |
| POST | `/api/v1/phone/suppressions` | [Import known-dead numbers](https://spaw.co/docs/api/add-phone-suppressions) |
| DELETE | `/api/v1/phone/suppressions` | [Remove numbers from the phone suppression list](https://spaw.co/docs/api/remove-phone-suppressions) |
| DELETE | `/api/v1/phone/suppressions/{suppressionId}` | [Remove a phone suppression entry](https://spaw.co/docs/api/remove-phone-suppression) |
### IP intelligence
#### Lookups
| Method | Path | Summary |
| --- | --- | --- |
| POST | `/api/v1/ip` | [Locate and risk-score an IP address](https://spaw.co/docs/api/lookup-ip) |
| GET | `/api/v1/ip/{ip}` | [Look up an address by path](https://spaw.co/docs/api/get-ip) |
| GET | `/api/v1/ip/me` | [Look up the address the call came from](https://spaw.co/docs/api/get-my-ip) |
| GET | `/api/v1/ip/asn/{asn}` | [Read an autonomous system and its prefixes](https://spaw.co/docs/api/get-ip-asn) |
| GET | `/api/v1/ip/range` | [Read what the feeds say about a whole prefix](https://spaw.co/docs/api/lookup-ip-range) |
#### Batch & browser
| Method | Path | Summary |
| --- | --- | --- |
| POST | `/api/v1/ip/batch` | [Look up up to 1,000 addresses in one call](https://spaw.co/docs/api/lookup-ip-batch) |
| POST | `/api/v1/ip/public` | [Look up an address from the browser with a publishable key](https://spaw.co/docs/api/lookup-ip-public) |
#### Bulk jobs
| Method | Path | Summary |
| --- | --- | --- |
| GET | `/api/v1/ip/bulk` | [List the account's bulk IP lookup runs](https://spaw.co/docs/api/list-ip-bulk-jobs) |
| POST | `/api/v1/ip/bulk` | [Queue a bulk IP lookup run](https://spaw.co/docs/api/create-ip-bulk-job) |
| GET | `/api/v1/ip/bulk/{jobId}` | [Poll a bulk IP job](https://spaw.co/docs/api/get-ip-bulk-job) |
| POST | `/api/v1/ip/bulk/{jobId}/cancel` | [Cancel a bulk IP job](https://spaw.co/docs/api/cancel-ip-bulk-job) |
| POST | `/api/v1/ip/bulk/{jobId}/resume` | [Resume a bulk job that ran out of credits](https://spaw.co/docs/api/resume-ip-bulk-job) |
| POST | `/api/v1/ip/bulk/{jobId}/webhook/redeliver` | [Send a finished run's completion webhook again](https://spaw.co/docs/api/redeliver-ip-bulk-webhook) |
| GET | `/api/v1/ip/bulk/{jobId}/results` | [Download a bulk IP job's results](https://spaw.co/docs/api/download-ip-bulk-results) |
#### Monitored lists
| Method | Path | Summary |
| --- | --- | --- |
| GET | `/api/v1/ip/monitors` | [List the account's IP monitors](https://spaw.co/docs/api/list-ip-monitors) |
| POST | `/api/v1/ip/monitors` | [Monitor your own addresses on a schedule](https://spaw.co/docs/api/create-ip-monitor) |
| GET | `/api/v1/ip/monitors/{monitorId}` | [Read an IP monitor and its last results](https://spaw.co/docs/api/get-ip-monitor) |
| PATCH | `/api/v1/ip/monitors/{monitorId}` | [Change a monitor](https://spaw.co/docs/api/update-ip-monitor) |
| DELETE | `/api/v1/ip/monitors/{monitorId}` | [Stop monitoring an IP list](https://spaw.co/docs/api/delete-ip-monitor) |
| POST | `/api/v1/ip/monitors/{monitorId}/run` | [Re-check a monitored IP list now](https://spaw.co/docs/api/run-ip-monitor) |
#### Your lists
| Method | Path | Summary |
| --- | --- | --- |
| GET | `/api/v1/ip/lists` | [List the account's allow and deny lists](https://spaw.co/docs/api/list-ip-lists) |
| POST | `/api/v1/ip/lists` | [Create an allow or deny list](https://spaw.co/docs/api/create-ip-list) |
| GET | `/api/v1/ip/lists/{listId}` | [Read a list and its entries](https://spaw.co/docs/api/get-ip-list) |
| PATCH | `/api/v1/ip/lists/{listId}` | [Change a list](https://spaw.co/docs/api/update-ip-list) |
| DELETE | `/api/v1/ip/lists/{listId}` | [Remove a list](https://spaw.co/docs/api/delete-ip-list) |
#### Outcome feedback
| Method | Path | Summary |
| --- | --- | --- |
| POST | `/api/v1/ip/feedback` | [Report IP outcomes](https://spaw.co/docs/api/report-ip-feedback) |
| GET | `/api/v1/ip/feedback/summary` | [Reported IP outcomes](https://spaw.co/docs/api/ip-feedback-summary) |
### Address verification
#### Lookups
| Method | Path | Summary |
| --- | --- | --- |
| POST | `/api/v1/address` | [Validate and enrich a postal address](https://spaw.co/docs/api/validate-address) |
| GET | `/api/v1/address/postcode/{postcode}` | [List the addresses at a postcode](https://spaw.co/docs/api/find-addresses-by-postcode) |
| GET | `/api/v1/address/streets` | [List the streets at a postcode](https://spaw.co/docs/api/list-address-streets) |
#### Batch & browser
| Method | Path | Summary |
| --- | --- | --- |
| POST | `/api/v1/address/batch` | [Validate up to 100 addresses in one call](https://spaw.co/docs/api/lookup-address-batch) |
| POST | `/api/v1/address/public` | [Validate an address from the browser with a publishable key](https://spaw.co/docs/api/validate-address-public) |
#### Bulk jobs
| Method | Path | Summary |
| --- | --- | --- |
| GET | `/api/v1/address/bulk` | [List the account's bulk address lookup runs](https://spaw.co/docs/api/list-address-bulk-jobs) |
| POST | `/api/v1/address/bulk` | [Queue a bulk address lookup run](https://spaw.co/docs/api/create-address-bulk-job) |
| GET | `/api/v1/address/bulk/{jobId}` | [Poll a bulk address job](https://spaw.co/docs/api/get-address-bulk-job) |
| POST | `/api/v1/address/bulk/{jobId}/cancel` | [Cancel a bulk address job](https://spaw.co/docs/api/cancel-address-bulk-job) |
| POST | `/api/v1/address/bulk/{jobId}/resume` | [Resume a bulk job that ran out of credits](https://spaw.co/docs/api/resume-address-bulk-job) |
| POST | `/api/v1/address/bulk/{jobId}/webhook/redeliver` | [Send a finished run's completion webhook again](https://spaw.co/docs/api/redeliver-address-bulk-webhook) |
| GET | `/api/v1/address/bulk/{jobId}/results` | [Download a bulk address job's results](https://spaw.co/docs/api/download-address-bulk-results) |
#### Monitored lists
| Method | Path | Summary |
| --- | --- | --- |
| GET | `/api/v1/address/monitors` | [List the account's address monitors](https://spaw.co/docs/api/list-address-monitors) |
| POST | `/api/v1/address/monitors` | [Monitor a list of addresses on a schedule](https://spaw.co/docs/api/create-address-monitor) |
| GET | `/api/v1/address/monitors/{monitorId}` | [Read an address monitor and its last results](https://spaw.co/docs/api/get-address-monitor) |
| PATCH | `/api/v1/address/monitors/{monitorId}` | [Change a monitor](https://spaw.co/docs/api/update-address-monitor) |
| DELETE | `/api/v1/address/monitors/{monitorId}` | [Stop monitoring an address list](https://spaw.co/docs/api/delete-address-monitor) |
| POST | `/api/v1/address/monitors/{monitorId}/run` | [Re-check a monitored address list now](https://spaw.co/docs/api/run-address-monitor) |
#### Sending policy
| Method | Path | Summary |
| --- | --- | --- |
| GET | `/api/v1/address/policy` | [Read the account's shipping policy](https://spaw.co/docs/api/get-address-policy) |
| PUT | `/api/v1/address/policy` | [Change one or more rules of the shipping policy](https://spaw.co/docs/api/update-address-policy) |
| DELETE | `/api/v1/address/policy` | [Restore the default shipping policy](https://spaw.co/docs/api/reset-address-policy) |
#### Outcome feedback
| Method | Path | Summary |
| --- | --- | --- |
| POST | `/api/v1/address/feedback` | [Report what happened to post you sent](https://spaw.co/docs/api/report-address-feedback) |
| GET | `/api/v1/address/feedback/summary` | [Reported postal outcomes](https://spaw.co/docs/api/address-feedback-summary) |
#### Suppression lists
| Method | Path | Summary |
| --- | --- | --- |
| GET | `/api/v1/address/suppressions` | [List the address suppression list](https://spaw.co/docs/api/list-address-suppressions) |
| POST | `/api/v1/address/suppressions` | [Import known-dead addresses](https://spaw.co/docs/api/add-address-suppressions) |
| DELETE | `/api/v1/address/suppressions` | [Remove addresses from the address suppression list](https://spaw.co/docs/api/remove-address-suppressions) |
| DELETE | `/api/v1/address/suppressions/{suppressionId}` | [Remove an address suppression entry](https://spaw.co/docs/api/remove-address-suppression) |
### Business verification
#### Lookups
| Method | Path | Summary |
| --- | --- | --- |
| POST | `/api/v1/entity` | [Check a business identifier against the registers](https://spaw.co/docs/api/verify-entity) |
#### Batch & browser
| Method | Path | Summary |
| --- | --- | --- |
| POST | `/api/v1/entity/batch` | [Check up to 500 identifiers in one call](https://spaw.co/docs/api/lookup-entity-batch) |
#### Monitored lists
| Method | Path | Summary |
| --- | --- | --- |
| GET | `/api/v1/entity/monitors` | [List business monitors](https://spaw.co/docs/api/list-entity-monitors) |
| POST | `/api/v1/entity/monitors` | [Save a business monitor](https://spaw.co/docs/api/create-entity-monitor) |
| GET | `/api/v1/entity/monitors/{monitorId}` | [Get a business monitor](https://spaw.co/docs/api/get-entity-monitor) |
| PATCH | `/api/v1/entity/monitors/{monitorId}` | [Change a monitor](https://spaw.co/docs/api/update-entity-monitor) |
| DELETE | `/api/v1/entity/monitors/{monitorId}` | [Delete a business monitor](https://spaw.co/docs/api/delete-entity-monitor) |
| POST | `/api/v1/entity/monitors/{monitorId}/run` | [Run a business monitor now](https://spaw.co/docs/api/run-entity-monitor) |
### Cross-signal consistency
#### Lookups
| Method | Path | Summary |
| --- | --- | --- |
| POST | `/api/v1/consistency` | [Check several signals against each other](https://spaw.co/docs/api/check-consistency) |
#### Bulk jobs
| Method | Path | Summary |
| --- | --- | --- |
| GET | `/api/v1/consistency/bulk` | [List the account's bulk consistency runs](https://spaw.co/docs/api/list-consistency-bulk-jobs) |
| POST | `/api/v1/consistency/bulk` | [Queue a bulk consistency run](https://spaw.co/docs/api/create-consistency-bulk-job) |
| GET | `/api/v1/consistency/bulk/{jobId}` | [Poll a bulk consistency job](https://spaw.co/docs/api/get-consistency-bulk-job) |
| POST | `/api/v1/consistency/bulk/{jobId}/cancel` | [Cancel a bulk consistency job](https://spaw.co/docs/api/cancel-consistency-bulk-job) |
| POST | `/api/v1/consistency/bulk/{jobId}/resume` | [Resume a bulk consistency job that ran out of credits](https://spaw.co/docs/api/resume-consistency-bulk-job) |
| POST | `/api/v1/consistency/bulk/{jobId}/webhook/redeliver` | [Send a finished run's completion webhook again](https://spaw.co/docs/api/redeliver-consistency-bulk-webhook) |
| GET | `/api/v1/consistency/bulk/{jobId}/results` | [Download a bulk consistency job's results](https://spaw.co/docs/api/download-consistency-bulk-results) |
### Account & status
#### Account
| Method | Path | Summary |
| --- | --- | --- |
| GET | `/api/v1/account` | [Read the account and credit balance](https://spaw.co/docs/api/get-account) |
| GET | `/api/v1/account/requests/{requestId}` | [Look up one request by its id](https://spaw.co/docs/api/get-request) |
| GET | `/api/v1/account/ledger` | [Read the credit ledger](https://spaw.co/docs/api/list-ledger) |
#### Service status
| Method | Path | Summary |
| --- | --- | --- |
| GET | `/api/v1/status/data` | [Health, latency and data freshness](https://spaw.co/docs/api/get-data-status) |
### Webhooks
#### Testing
| Method | Path | Summary |
| --- | --- | --- |
| POST | `/api/v1/webhooks/test` | [Send a test webhook](https://spaw.co/docs/api/send-test-webhook) |
Every error code is explained at https://spaw.co/docs/errors and every verdict reason at https://spaw.co/docs/reasons.
---
# Verify an email address
`POST /api/v1/email`
- Authentication: Secret API key as a bearer token
- Billing: 1 credit for a fresh deliverable or risky verdict whose mailbox check was definite; an unverified answer (the mailbox check could not say, including a provider outage) is free, and so are invalid input, 7-day repeats and test addresses. An undeliverable verdict is free within your email fair use and bills 1 credit past it, with `meta.fair_use_billed` true.
- Group: Email
Runs the full pipeline on one address and answers with a verdict, a machine-readable reason, an auditable 0–100 risk score with the signals behind it, and 46 named fields. The checks run in order: address extraction (display names, `mailto:` prefixes, spreadsheet quotes and invisible characters are stripped), RFC 5322 syntax, the shape of the username (naming pattern, name tokens, machine-made or placeholder look), live MX resolution including the null-MX and implicit-MX rules, the relay, disposable, role, free-provider and typo-squat lists, provider-specific username rules, the MX hosts against the threat blocklists and the parking nameservers, SPF and DMARC records, the domain's registration and expiry from RDAP, and finally a mailbox-level SMTP handshake that ends before any message is transmitted, read against the address's own not-found history and how many other accounts checked it today.
The mailbox probe runs only when the domain accepts mail and no free signal already settled the answer, so a disposable domain or a typo-squat never reaches the mail server. Confirmed mailbox answers are shared across customers for up to a week, which is why `smtp_checked_at` can predate the lookup. When mail servers refuse to say whether an inbox exists (greylisting, timeouts), the answer degrades honestly to a `risky` verdict with reason `unverified`, `smtp_checked: false` and `mailbox_exists: null`, plus a recomputable `mailbox_confidence` estimate; an unverified answer whose `smtp_reason` is `provider_unavailable` is free.
A single lookup never consults your suppression list, and its verdict clears only an entry a verdict added: an import, or an entry your own bounce and complaint reports added, stays until you remove it (a reported bounce also leaves when a delivery to that address is reported; a complaint only when you remove it). The batch, bulk and monitor paths do honor it.
### Asking for a fresh answer
Repeats inside the seven-day window are free and served from the earlier answer. Pass `refresh: true` to discard that answer and the shared mailbox answer, run a real handshake again, and bill the lookup as fresh when it is answered; or pass `max_age` in seconds to do so only when the earlier answer is older than that. `meta.refreshed` says whether an earlier answer was discarded for the call. An uncertain (`unverified`) mailbox answer under 15 minutes old is the one exception: it is free, so neither control sets it aside, and the call answers it again. `timeout` caps how long the mailbox handshake may wait for this call; a handshake that runs out of time answers `unverified` with `smtp_reason: timeout` instead of holding the request.
### Settling an unverified answer later
Pass `callback_url` and `callback_secret` and an unverified answer is re-checked after 5 and 20 minutes. The settled verdict is POSTed to the callback as `{ "event": "email.settled", "request_id", "settled", "attempt", "data", "meta" }`, signed with `callback_secret` the way every webhook here is: `X-Spaw-Signature-V2` carries `t={unix seconds},v2={lowercase hex}`, the HMAC-SHA256 of `v2:{t}:{raw body}`, with the older `X-Spaw-Signature` over the body alone beside it until 1 March 2027. The response to this request then carries `meta.settling: true`. An unverified answer is free and leaves nothing to repeat, so each re-check is billed like a fresh lookup: 1 credit when it confirms the mailbox, nothing while it still cannot say (`settled` is false when the last re-check still could not say), and an undeliverable answer counts against your email fair use. A re-check your balance cannot pay for is not run: the last callback then has `settled` false, `data` null, `meta.credits_used` 0 and `meta.stopped_reason: "insufficient_credits"`.
## Request body
| Field | Type | Required | Description |
| --- | --- | --- | --- |
| `email` | string | yes | The address to verify. Wrappers people paste (a display name, a `mailto:` prefix, quotes, trailing punctuation) are stripped before checking. At most 254 characters. |
| `callback_url` | string | null | no | An https URL. When the answer comes back unverified (greylisted, timed out, provider unavailable), the address is re-checked after 5 and 20 minutes and the settled verdict is POSTed here. At most 2,048 characters. |
| `callback_secret` | string | null | no | Required with `callback_url`. Signs the callback body exactly as the bulk webhook is signed — `X-Spaw-Signature-V2` carries `t={unix seconds},v2={lowercase hex}`, the HMAC-SHA256 of `v2:{t}:{raw body}`. 16 to 128 characters. |
| `refresh` | boolean | null | no | Discard the answer from the seven-day repeat window and the shared mailbox answer, run the handshake again, and bill as a fresh lookup when answered. |
| `max_age` | integer | null | no | Seconds. Refresh only when the earlier answer is older than this; a younger answer is served free as usual. |
| `timeout` | integer | null | no | Seconds the mailbox handshake may wait for this call. When it runs out the answer is `unverified` with `smtp_reason` `timeout`. |
| `fields` | string | null | no | A comma-separated list of the answer's own field names to return, in the answer's order. Unknown names are ignored, a blank list means everything, and `sources` comes back only when named. The meta block is never narrowed. At most 1,000 characters. |
## Example request
```bash
curl -X POST https://spaw.co/api/v1/email \
-H "Authorization: Bearer sk_live_…" \
-H "Content-Type: application/json" \
-d '{
"email": "mia@acme.com"
}'
```
## Responses
### 200 — The verdict and every field, whether or not the address is deliverable.
```json
{
"success": true,
"data": {
"email": "mia@acme.com",
"normalized_email": "mia@acme.com",
"is_alias": false,
"is_gibberish": false,
"looks_generated": false,
"local_part_pattern": "first",
"first_name": "mia",
"last_name": null,
"deliverable": "deliverable",
"reason": null,
"risk_score": 0,
"risk_level": "low",
"risk_signals": [],
"syntax_valid": true,
"domain": "acme.com",
"domain_category": "other",
"mx_found": true,
"mx_implicit": false,
"mx_provider": "google",
"mx_provider_type": "mailbox_provider",
"mx_blocklisted": false,
"mx_network_type": "hosting",
"has_spf": true,
"dmarc_policy": "reject",
"domain_registered_at": "1998-03-12",
"domain_age_days": 10402,
"domain_expires_at": "2027-03-12",
"parked": false,
"disposable": false,
"is_relay": false,
"relay_service": null,
"role": false,
"role_type": null,
"free_provider": false,
"smtp_checked": true,
"smtp_checked_at": "2026-09-03T10:12:44+00:00",
"mailbox_exists": true,
"previously_invalid": false,
"previously_invalid_at": null,
"catch_all": false,
"smtp_reason": null,
"mailbox_confidence": null,
"widely_seen": false,
"did_you_mean": null,
"ok_to_send": true,
"blocked_by": null,
"sources": [
{
"dataset": "rfc-5322-syntax",
"version": "2026-09-03"
},
{
"dataset": "live-dns",
"version": "2026-09-03"
},
{
"dataset": "disposable-domains",
"version": "2026-09-01"
},
{
"dataset": "mx-provider-patterns",
"version": "2026-08-30"
},
{
"dataset": "mailbox-smtp",
"version": "2026-09-03"
}
]
},
"meta": {
"credits_used": 1,
"credits_remaining": 9,
"cache_hit": false,
"refreshed": false,
"suppressed": false,
"fair_use_billed": false,
"request_id": "req_01m1kgdm4xngzmbmff68g94w0c"
}
}
```
### 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"
}
}
```
### 402 — The balance is empty. The lookup did not run.
```json
{
"success": false,
"error": {
"code": "INSUFFICIENT_CREDITS",
"message": "Your credit balance is empty.",
"request_id": "req_01m1kgdm4xngzmbmff68g94w0c"
}
}
```
### 422 — The request body could not be validated; `error.errors` lists the fields.
```json
{
"success": false,
"error": {
"code": "VALIDATION_FAILED",
"message": "The email field is required.",
"errors": {
"email": [
"The email field is required."
]
},
"request_id": "req_01m1kgdm4xngzmbmff68g94w0c"
}
}
```
### 429 — Over 5 requests per second for the key. Retry after the limit resets.
```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
- `VALIDATION_FAILED` — https://spaw.co/docs/errors/VALIDATION_FAILED
- `INSUFFICIENT_CREDITS` — https://spaw.co/docs/errors/INSUFFICIENT_CREDITS
- `RATE_LIMITED` — https://spaw.co/docs/errors/RATE_LIMITED
- `SANDBOX_VALUE_NOT_ALLOWED` — https://spaw.co/docs/errors/SANDBOX_VALUE_NOT_ALLOWED
- `SANDBOX_UNAVAILABLE` — https://spaw.co/docs/errors/SANDBOX_UNAVAILABLE
---
Canonical page: https://spaw.co/docs/api/verify-email · OpenAPI document: https://spaw.co/openapi.json · All endpoints: https://spaw.co/docs/api
---
# Verify up to 50 addresses in one call
`POST /api/v1/email/batch`
- Authentication: Secret API key as a bearer token
- Billing: Each address bills like a single lookup — 1 credit for a fresh deliverable or risky verdict whose mailbox check was definite, an unverified answer free, an undeliverable one free within your email fair use and 1 credit past it (`fair_use_billed` on the item), everything else free — and the call stops cleanly where the balance ends.
- Group: Email
Verifies a list synchronously under the exact single-lookup billing rules. Every address is prepared first, then all the mailbox probes that are still needed are sent to the verification provider together, so a full batch takes about as long as a handful of single lookups. `data.results[]` mirrors the single-address response per item, in input order, and each item's `meta` carries `credits_used`, `cache_hit`, `suppressed`, `refreshed` and `fair_use_billed`.
Addresses on your suppression list are served from the stored verdict at no cost and marked `suppressed: true`. Repeats inside the list are verified once. The list is walked in order and stops where the balance would run out: running out of credits mid-batch returns the paid partial results with `meta.stopped_reason: "insufficient_credits"` and `processed` lower than `requested`, never discarding billed work. Only a batch whose first lookup is refused answers the typed `402`. A key with a daily credit cap stops the list the same way once the cap is spent: the addresses already paid for come back with `meta.stopped_reason: "key_spend_cap"`, and a call that starts with the cap already spent answers `429 KEY_SPEND_CAP_REACHED` and runs nothing.
A batch counts as one request against the rate limit. `refresh`, `max_age` and `timeout` work exactly as on the single-address endpoint and apply to every address in the list; each item's `meta.refreshed` says whether its earlier answer was discarded.
Every item carries `index`, its zero-based position in the list you sent, and `input`, the address exactly as you sent it. A batch stopped early by `stopped_reason` answers only a prefix of the list, so those two are what line an answer up with the row it came from.
## Request body
| Field | Type | Required | Description |
| --- | --- | --- | --- |
| `emails` | string[] | yes | 1 to 50 addresses; each item at most 254 characters. |
| `refresh` | boolean | null | no | Apply to every address — discard earlier answers, probe again, bill as fresh when answered. Repeats inside the list are still verified once. |
| `max_age` | integer | null | no | Seconds. Refresh only the addresses whose earlier answer is older than this. |
| `timeout` | integer | null | no | Seconds the mailbox handshake may wait, for every address in the call. |
## Example request
```bash
curl -X POST https://spaw.co/api/v1/email/batch \
-H "Authorization: Bearer sk_live_…" \
-H "Content-Type: application/json" \
-d '{
"emails": [
"mia@acme.com",
"info@example.org",
"deliverable@spaw.test"
]
}'
```
## Responses
### 200 — One result per processed address, plus the batch totals.
```json
{
"success": true,
"data": {
"results": [
{
"index": 0,
"input": "mia@acme.com",
"data": {
"email": "mia@acme.com",
"deliverable": "deliverable",
"reason": null,
"risk_score": 0,
"risk_level": "low"
},
"meta": {
"credits_used": 1,
"cache_hit": false,
"suppressed": false,
"refreshed": false,
"fair_use_billed": false
}
},
{
"index": 1,
"input": "info@example.org",
"data": {
"email": "info@example.org",
"deliverable": "risky",
"reason": "role",
"risk_score": 30,
"risk_level": "medium"
},
"meta": {
"credits_used": 1,
"cache_hit": false,
"suppressed": false,
"refreshed": false,
"fair_use_billed": false
}
},
{
"index": 2,
"input": "deliverable@spaw.test",
"data": {
"email": "deliverable@spaw.test",
"deliverable": "deliverable",
"reason": null,
"risk_score": 0,
"risk_level": "low"
},
"meta": {
"credits_used": 0,
"cache_hit": false,
"suppressed": false,
"refreshed": false,
"fair_use_billed": false
}
}
]
},
"meta": {
"requested": 3,
"processed": 3,
"credits_used": 2,
"credits_remaining": 7,
"stopped_reason": null,
"request_id": "req_01m1kgdrtqdvwnks99vfgx2rcw"
}
}
```
### 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"
}
}
```
### 402 — The balance is empty. The lookup did not run.
```json
{
"success": false,
"error": {
"code": "INSUFFICIENT_CREDITS",
"message": "Your credit balance is empty.",
"request_id": "req_01m1kgdm4xngzmbmff68g94w0c"
}
}
```
### 422 — The request body could not be validated; `error.errors` lists the fields.
```json
{
"success": false,
"error": {
"code": "VALIDATION_FAILED",
"message": "The email field is required.",
"errors": {
"email": [
"The email field is required."
]
},
"request_id": "req_01m1kgdm4xngzmbmff68g94w0c"
}
}
```
### 429 — Over 5 requests per second for the key. Retry after the limit resets.
```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
- `VALIDATION_FAILED` — https://spaw.co/docs/errors/VALIDATION_FAILED
- `INSUFFICIENT_CREDITS` — https://spaw.co/docs/errors/INSUFFICIENT_CREDITS
- `KEY_SPEND_CAP_REACHED` — https://spaw.co/docs/errors/KEY_SPEND_CAP_REACHED
- `RATE_LIMITED` — https://spaw.co/docs/errors/RATE_LIMITED
---
Canonical page: https://spaw.co/docs/api/verify-email-batch · OpenAPI document: https://spaw.co/openapi.json · All endpoints: https://spaw.co/docs/api
---
# Verify an address from the browser with a publishable key
`POST /api/v1/email/public`
- Authentication: Publishable key in the body, checked against the browser Origin header
- Billing: Bills the key owner like a single lookup; capped by the key's daily credit cap when one is set.
- Group: Email
The endpoint behind the form widget (`https://spaw.co/spaw-form.js`). It is authenticated by a publishable `pk_` key in the body plus the browser's `Origin` header, which must match one of the domains the key is locked to. A missing `Origin` is rejected on purpose: servers use a secret key and `POST /api/v1/email` instead.
The lookup bills the key's owner under the normal rules and answers `{ "success", "data" }` with **no meta block**, so page visitors never see the owner's balance. Because the key sits in page source, give it a daily credit cap in the dashboard: once the cap is spent the endpoint answers `429 KEY_SPEND_CAP_REACHED` until the next day. Free answers (undeliverable, cache hits, test addresses) never count toward the cap.
For forms open to the public, store a Cloudflare Turnstile site key and secret on the publishable key. The endpoint then requires a confirmed `turnstile_token` with every lookup and answers `403 TURNSTILE_FAILED` without one, before any credit is spent. Tokens are single-use. The form widget obtains a fresh token per lookup when the script tag carries `data-turnstile-site-key`.
Throttled at 20 requests per minute per IP.
## Parameters
| Name | In | Type | Required | Description |
| --- | --- | --- | --- | --- |
| `Origin` | header | string | yes | Sent by browsers automatically. Its host must be on the key's allowed-domain list. |
## Request body
| Field | Type | Required | Description |
| --- | --- | --- | --- |
| `key` | string | yes | A publishable key, which starts with `pk_`. At most 64 characters. |
| `email` | string | yes | The address to verify. At most 254 characters. |
| `fields` | string | null | no | A comma-separated list of the answer's own field names to return, in the answer's order. Unknown names are ignored, a blank list means everything, and `sources` comes back only when named. The meta block is never narrowed. At most 1,000 characters. |
| `turnstile_token` | string | null | no | Required when the key carries a Turnstile pair. A single-use token from the Turnstile widget on the page. At most 2,048 characters. |
## Example request
```bash
curl -X POST https://spaw.co/api/v1/email/public \
-H "Origin: https://www.example.com" \
-H "Content-Type: application/json" \
-d '{
"key": "pk_live_…",
"email": "mia@acme.com"
}'
```
## Responses
### 200 — The same fields as `POST /api/v1/email`, without a meta block.
```json
{
"success": true,
"data": {
"email": "mia@acme.com",
"deliverable": "deliverable",
"reason": null,
"risk_score": 0,
"risk_level": "low",
"did_you_mean": null,
"ok_to_send": true,
"blocked_by": null
}
}
```
### 401 — The publishable key does not exist or was revoked.
```json
{
"success": false,
"error": {
"code": "INVALID_PUBLISHABLE_KEY",
"message": "That publishable key does not exist or was revoked.",
"request_id": "req_01m1kgdm4xngzmbmff68g94w0c"
}
}
```
### 402 — The balance is empty. The lookup did not run.
```json
{
"success": false,
"error": {
"code": "INSUFFICIENT_CREDITS",
"message": "Your credit balance is empty.",
"request_id": "req_01m1kgdm4xngzmbmff68g94w0c"
}
}
```
### 403 — The page's origin is not on the key's allowed-domain list (`ORIGIN_NOT_ALLOWED`), or the key requires a Turnstile token that was missing or not confirmed (`TURNSTILE_FAILED`).
```json
{
"success": false,
"error": {
"code": "ORIGIN_NOT_ALLOWED",
"message": "This publishable key cannot be used from this origin.",
"request_id": "req_01m1kgdm4xngzmbmff68g94w0c"
}
}
```
### 422 — The request body could not be validated; `error.errors` lists the fields.
```json
{
"success": false,
"error": {
"code": "VALIDATION_FAILED",
"message": "The email field is required.",
"errors": {
"email": [
"The email field is required."
]
},
"request_id": "req_01m1kgdm4xngzmbmff68g94w0c"
}
}
```
### 429 — The key has spent its owner's daily credit cap (`KEY_SPEND_CAP_REACHED`), or the IP exceeded 20 requests per minute (`RATE_LIMITED`).
```json
{
"success": false,
"error": {
"code": "KEY_SPEND_CAP_REACHED",
"message": "This publishable key has reached its daily credit cap. The counter resets each day.",
"request_id": "req_01m1kgdm4xngzmbmff68g94w0c"
}
}
```
## Error codes
- `INVALID_PUBLISHABLE_KEY` — https://spaw.co/docs/errors/INVALID_PUBLISHABLE_KEY
- `ORIGIN_NOT_ALLOWED` — https://spaw.co/docs/errors/ORIGIN_NOT_ALLOWED
- `TURNSTILE_FAILED` — https://spaw.co/docs/errors/TURNSTILE_FAILED
- `KEY_SPEND_CAP_REACHED` — https://spaw.co/docs/errors/KEY_SPEND_CAP_REACHED
- `VALIDATION_FAILED` — https://spaw.co/docs/errors/VALIDATION_FAILED
- `INSUFFICIENT_CREDITS` — https://spaw.co/docs/errors/INSUFFICIENT_CREDITS
- `RATE_LIMITED` — https://spaw.co/docs/errors/RATE_LIMITED
---
Canonical page: https://spaw.co/docs/api/verify-email-public · OpenAPI document: https://spaw.co/openapi.json · All endpoints: https://spaw.co/docs/api
---
# Inspect a domain's mail setup
`GET /api/v1/email/domain/{domain}`
- Authentication: Secret API key as a bearer token
- Billing: 1 credit only when the domain accepts mail (`mx_found` true) and the lookup is fresh; invalid or dead domains and 7-day repeats are free.
- Group: Domain
The domain-level signals of an email lookup, answered once for a whole domain: whether it can receive mail (MX records, or the RFC 5321 implicit fallback), who runs its mail, whether it publishes SPF and which DMARC policy, when it was registered, whether it is on the disposable or free-provider lists, and whether any address there has answered catch-all within the last week.
Invalid input is an answer, not a validation error: a string that is not a hostname answers `200` with `valid: false` and every other field null. A domain with no usable mail server is free. The per-account 7-day repeat cache applies.
## Parameters
| Name | In | Type | Required | Description |
| --- | --- | --- | --- | --- |
| `domain` | path | string | yes | The domain to inspect. Lowercased and converted to punycode before checking. At most 254 characters. |
## Example request
```bash
curl https://spaw.co/api/v1/email/domain/acme.com \
-H "Authorization: Bearer sk_live_…"
```
## Responses
### 200 — The domain's signals; `valid: false` for input that is not a hostname.
```json
{
"success": true,
"data": {
"domain": "acme.com",
"valid": true,
"domain_category": "other",
"mx_found": true,
"mx_implicit": false,
"catch_all": null,
"mx_provider": "google",
"mx_provider_type": "mailbox_provider",
"mx_blocklisted": false,
"mx_network_type": "hosting",
"has_spf": true,
"spf_all": "-all",
"spf_lookup_count": 4,
"dmarc_policy": "reject",
"dmarc_pct": 100,
"dmarc_rua": true,
"dkim_selectors": [
"google"
],
"mta_sts": true,
"tls_rpt": true,
"bimi": false,
"domain_registered_at": "1998-03-12",
"domain_age_days": 10402,
"domain_expires_at": "2027-03-12",
"domain_updated_at": "2026-02-14",
"registrar": "Example Registrar, Inc.",
"registrant_redacted": true,
"parked": false,
"disposable": false,
"is_relay": false,
"relay_service": null,
"free_provider": false,
"sources": [
{
"dataset": "live-dns",
"version": "2026-09-03"
},
{
"dataset": "disposable-domains",
"version": "2026-09-01"
},
{
"dataset": "free-provider-domains",
"version": "2026-09-01"
},
{
"dataset": "mx-provider-patterns",
"version": "2026-08-30"
},
{
"dataset": "rdap-registration",
"version": "2026-09-03"
},
{
"dataset": "forwarding-services",
"version": "2026-10-08"
},
{
"dataset": "domain-categories",
"version": "2026-09-12"
},
{
"dataset": "parking-nameservers",
"version": "2026-09-12"
},
{
"dataset": "threat-blocklists",
"version": "2026-09-12"
},
{
"dataset": "mail-policies",
"version": "2026-09-12"
}
]
},
"meta": {
"credits_used": 1,
"credits_remaining": 8,
"cache_hit": false,
"request_id": "req_01m1kgdm4xngzmbmff68g94w0c"
}
}
```
### 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"
}
}
```
### 402 — The balance is empty. The lookup did not run.
```json
{
"success": false,
"error": {
"code": "INSUFFICIENT_CREDITS",
"message": "Your credit balance is empty.",
"request_id": "req_01m1kgdm4xngzmbmff68g94w0c"
}
}
```
### 429 — Over 5 requests per second for the key. Retry after the limit resets.
```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
- `INSUFFICIENT_CREDITS` — https://spaw.co/docs/errors/INSUFFICIENT_CREDITS
- `RATE_LIMITED` — https://spaw.co/docs/errors/RATE_LIMITED
---
Canonical page: https://spaw.co/docs/api/domain-intelligence · OpenAPI document: https://spaw.co/openapi.json · All endpoints: https://spaw.co/docs/api
---
# List the account's bulk verification runs
`GET /api/v1/email/bulk`
- Authentication: Secret API key as a bearer token
- Billing: Free.
- Group: Bulk
Every bulk run this account has started, newest first, 100 per page. A job id is handed out once, at creation, and everything about a run hangs off it — its counts, its results, its cancel and resume calls — so this is how a caller that lost one finds the run again, and how a dashboard lists what is still in flight.
`status`, `since` and `until` are optional and combine; a bare date in `until` means the whole of that day, and an unknown status or an unparseable date is a `422` rather than an empty page. The rows are the same job objects `GET /api/v1/email/bulk/{jobId}` answers. The webhook secret is never among them: it is returned once, at creation.
Runs are kept as long as the lookup history, and `meta.retention_days` says how long that is — a run older than that is deleted with its files and cannot be listed.
## Parameters
| Name | In | Type | Required | Description |
| --- | --- | --- | --- | --- |
| `status` | query | string | no | Only runs in this status. One of: queued, processing, completed, failed, cancelled. |
| `since` | query | string | no | Only runs created at or after this date or timestamp. |
| `until` | query | string | no | Only runs created at or before this. A bare date means the whole of that day. |
| `page` | query | integer | no | The page to return. Default: 1. |
## Example request
```bash
curl https://spaw.co/api/v1/email/bulk \
-H "Authorization: Bearer sk_live_…"
```
## Responses
### 200 — One page of runs, newest first.
```json
{
"success": true,
"data": {
"jobs": [
{
"id": 512,
"status": "completed",
"total": 2000,
"duplicate_count": 12,
"processed": 2000,
"deliverable": 1380,
"risky": 310,
"undeliverable": 310,
"credits_used": 1690,
"stopped_reason": null,
"cancel_requested": false,
"webhook_status": "delivered",
"webhook_detail": "HTTP 200",
"created_at": "2026-09-03T10:12:44+00:00",
"finished_at": "2026-09-03T10:19:02+00:00"
}
]
},
"meta": {
"page": 1,
"per_page": 100,
"total": 1,
"retention_days": 30
}
}
```
### 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"
}
}
```
### 422 — The request body could not be validated; `error.errors` lists the fields.
```json
{
"success": false,
"error": {
"code": "VALIDATION_FAILED",
"message": "The email field is required.",
"errors": {
"email": [
"The email field is required."
]
},
"request_id": "req_01m1kgdm4xngzmbmff68g94w0c"
}
}
```
### 429 — Over 5 requests per second for the key. Retry after the limit resets.
```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
- `VALIDATION_FAILED` — https://spaw.co/docs/errors/VALIDATION_FAILED
- `RATE_LIMITED` — https://spaw.co/docs/errors/RATE_LIMITED
---
Canonical page: https://spaw.co/docs/api/list-bulk-jobs · OpenAPI document: https://spaw.co/openapi.json · All endpoints: https://spaw.co/docs/api
---
# Queue a bulk verification run
`POST /api/v1/email/bulk`
- Authentication: Secret API key as a bearer token
- Billing: `preview: true` is free and queues nothing. A started run bills each row like a single lookup as it is processed; repeats inside the run and 7-day repeats are free, so a run costs at most the `credits_upper_bound` the preview answered and usually less.
- Group: Bulk
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-V2` header carries two comma-separated elements, `t={unix seconds},v2={lowercase hex}`: the hex is the HMAC-SHA256 of the string `v2:{t}:{raw body}`, keyed with `webhook_secret`, which this response returns once — and again only on an idempotent replay of the same request. Check the timestamp as well as the digest and refuse anything more than five minutes from your own clock: a signature over the body alone never goes stale, so a captured delivery could be posted back at you a year later and still verify. Connection errors and 5xx answers are retried twice with a short backoff, three attempts in all.
```php
$elements = [];
foreach (explode(',', (string) $request->header('X-Spaw-Signature-V2')) as $element) {
$pair = explode('=', trim($element), 2);
if (count($pair) === 2) {
$elements[$pair[0]] = $pair[1];
}
}
$timestamp = $elements['t'] ?? '';
abort_unless(ctype_digit($timestamp) && abs(time() - (int) $timestamp) <= 300, 401);
$expected = hash_hmac('sha256', "v2:{$timestamp}:".$request->getContent(), $secret);
abort_unless(hash_equals($expected, $elements['v2'] ?? ''), 401);
```
The original `X-Spaw-Signature` — the same HMAC over the raw body alone, with no timestamp — is still sent beside it until 1 March 2027, so a receiver written against it keeps working while you move over. After that only the timestamped header is sent.
### Retrying safely
Send an `Idempotency-Key` header (1 to 128 printable characters, unique per request — a UUID stored with whatever triggered the run) and creation becomes safe to retry after a timeout or a dropped connection. A repeat with the same key and the same addresses and webhook URL answers the job the first attempt created, as `200` instead of `202`, with the same body including `webhook_secret` and the header `Idempotent-Replayed: true`; nothing is queued twice. The same key with a different request answers `409 IDEMPOTENCY_KEY_REUSED`. Keys are scoped to your account and live as long as the job (30 days).
### Reading a list before you start it
Send `preview: true` with the same body — or `csv` with a raw CSV instead of the item list — and the call answers `200` with `data.draft` instead of queueing anything: what the list was read as, which column filled which field and whether a heading named it or we guessed, the first rows as parsed, the repeats, the balance, and `credits_upper_bound`. Nothing is charged.
`credits_upper_bound` is a CEILING, not a quote. Rows come back free for reasons no preview can know in advance — a repeat inside the seven-day window, an entry on your suppression list, a verdict that is not charged for, a provider outage — so a run usually costs less than the ceiling and never more. It is priced against the mapping in play, so correcting a column at start re-prices the run rather than holding you to the figure the guess produced — ask for the draft again after a correction if you want the new number. What it actually cost is `credits_used` on the finished job.
Start it with `{"draft_id": }`, adding `mapping` to correct a column we read wrongly. Starting consumes the draft; one left unstarted is deleted, with the rows it holds, after `retention_hours`.
## Parameters
| Name | In | Type | Required | Description |
| --- | --- | --- | --- | --- |
| `Idempotency-Key` | header | string | no | Optional. Makes the creation safe to retry; a repeat with the same key replays the job it created instead of queueing another. 1 to 128 characters. |
## Request body
| Field | Type | Required | Description |
| --- | --- | --- | --- |
| `emails` | string[] | no | 1 to 100,000 addresses; each item at most 254 characters. Required unless the body carries `csv` with `preview: true`, or `draft_id`. |
| `preview` | boolean | no | Read the list and answer `data.draft` — what we made of it, the repeats, the ceiling on the cost and the balance — with nothing queued and nothing billed. Start it afterwards with `draft_id`. Default: . |
| `csv` | string | null | no | A raw CSV to read instead of the item list, with the column mapping answered back. Preview only, on purpose: a CSV's columns have to be worked out, and working them out and billing for the answer in one request is what the preview exists to stop. At most 5,000,000 characters. |
| `draft_id` | integer | null | no | Start the run a preview answered with. The rows and the options come from the draft, so nothing else in the body applies except `mapping`. A draft can be started once; an `Idempotency-Key` still makes the call safe to retry. |
| `mapping` | object | null | no | Corrects the draft's column mapping, as column index to field — {"1": "address_line1", "2": "postal_code"}. It REPLACES the detected mapping rather than merging into it, so a column can be taken off a field as well as put on one. Goes with `draft_id`, and only for a draft read from a `csv`. |
| `webhook_url` | string | null | no | An https URL to POST when the job finishes. The response includes the signing secret — shown at creation and on an idempotent replay of the same request, never otherwise. At most 2,048 characters. |
## Example request
```bash
curl -X POST https://spaw.co/api/v1/email/bulk \
-H "Authorization: Bearer sk_live_…" \
-H "Content-Type: application/json" \
-d '{
"emails": [
"mia@acme.com",
"info@example.org"
],
"webhook_url": "https://www.example.com/hooks/spaw"
}'
```
## Responses
### 202 — The job was queued.
```json
{
"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"
}
}
}
```
### 200 — Either a preview — `preview: true` answers `data.draft` and queues nothing — or a retry with an `Idempotency-Key` already used for this exact request, which answers `data.job` with `Idempotent-Replayed: true`.
```json
{
"success": true,
"data": {
"job": {
"id": 512,
"status": "processing",
"total": 2,
"duplicate_count": 0,
"processed": 1,
"deliverable": 1,
"risky": 0,
"undeliverable": 0,
"credits_used": 1,
"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"
}
}
}
```
### 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"
}
}
```
### 409 — The `Idempotency-Key` was already used for a different list or webhook URL.
```json
{
"success": false,
"error": {
"code": "IDEMPOTENCY_KEY_REUSED",
"message": "This Idempotency-Key was already used for a different request.",
"request_id": "req_01m1kgdm4xngzmbmff68g94w0c"
}
}
```
### 422 — The request body could not be validated; `error.errors` lists the fields.
```json
{
"success": false,
"error": {
"code": "VALIDATION_FAILED",
"message": "The email field is required.",
"errors": {
"email": [
"The email field is required."
]
},
"request_id": "req_01m1kgdm4xngzmbmff68g94w0c"
}
}
```
### 429 — Over 5 requests per second for the key. Retry after the limit resets.
```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
- `VALIDATION_FAILED` — https://spaw.co/docs/errors/VALIDATION_FAILED
- `IDEMPOTENCY_KEY_REUSED` — https://spaw.co/docs/errors/IDEMPOTENCY_KEY_REUSED
- `RATE_LIMITED` — https://spaw.co/docs/errors/RATE_LIMITED
---
Canonical page: https://spaw.co/docs/api/create-bulk-job · OpenAPI document: https://spaw.co/openapi.json · All endpoints: https://spaw.co/docs/api
---
# Poll a bulk job
`GET /api/v1/email/bulk/{jobId}`
- Authentication: Secret API key as a bearer token
- Billing: Free.
- Group: Bulk
The job's current status and running counts. `status` moves from `queued` to `processing` and ends in `completed`, `failed`, or `cancelled`; `processed` and the verdict counts advance as chunks finish. A job that belongs to another account answers `404`.
## Parameters
| Name | In | Type | Required | Description |
| --- | --- | --- | --- | --- |
| `jobId` | path | integer | yes | The id returned at creation. |
## Example request
```bash
curl https://spaw.co/api/v1/email/bulk/512 \
-H "Authorization: Bearer sk_live_…"
```
## Responses
### 200 — The job.
```json
{
"success": true,
"data": {
"job": {
"id": 512,
"status": "processing",
"total": 2000,
"duplicate_count": 12,
"processed": 900,
"deliverable": 610,
"risky": 140,
"undeliverable": 150,
"credits_used": 750,
"stopped_reason": null,
"cancel_requested": false,
"webhook_status": null,
"created_at": "2026-09-03T10:12:44+00:00",
"finished_at": null
}
}
}
```
### 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"
}
}
```
### 429 — Over 5 requests per second for the key. Retry after the limit resets.
```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
- `RATE_LIMITED` — https://spaw.co/docs/errors/RATE_LIMITED
---
Canonical page: https://spaw.co/docs/api/get-bulk-job · OpenAPI document: https://spaw.co/openapi.json · All endpoints: https://spaw.co/docs/api
---
# Cancel a bulk job
`POST /api/v1/email/bulk/{jobId}/cancel`
- Authentication: Secret API key as a bearer token
- Billing: Free.
- Group: Bulk
Asks a queued or running job to stop. A queued job is cancelled on the spot; a running one stops at its next checkpoint and keeps the rows already written downloadable through the results endpoint. Rows already verified stay billed. The completion webhook fires with status `cancelled` just as it does for `completed` and `failed`. A job that already reached a terminal status answers `409 JOB_ALREADY_FINISHED`.
## 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/email/bulk/512/cancel \
-H "Authorization: Bearer sk_live_…"
```
## Responses
### 200 — The job, with `cancel_requested` set.
```json
{
"success": true,
"data": {
"job": {
"id": 512,
"status": "processing",
"total": 2000,
"duplicate_count": 12,
"processed": 900,
"deliverable": 610,
"risky": 140,
"undeliverable": 150,
"credits_used": 750,
"stopped_reason": null,
"cancel_requested": true,
"webhook_status": null,
"created_at": "2026-09-03T10:12:44+00:00",
"finished_at": null
}
}
}
```
### 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 job already finished.
```json
{
"success": false,
"error": {
"code": "JOB_ALREADY_FINISHED",
"message": "The job has already finished and cannot be cancelled.",
"request_id": "req_01m1kgdm4xngzmbmff68g94w0c"
}
}
```
### 429 — Over 5 requests per second for the key. Retry after the limit resets.
```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
- `JOB_ALREADY_FINISHED` — https://spaw.co/docs/errors/JOB_ALREADY_FINISHED
- `RATE_LIMITED` — https://spaw.co/docs/errors/RATE_LIMITED
---
Canonical page: https://spaw.co/docs/api/cancel-bulk-job · OpenAPI document: https://spaw.co/openapi.json · All endpoints: https://spaw.co/docs/api
---
# Resume a bulk job that ran out of credits
`POST /api/v1/email/bulk/{jobId}/resume`
- Authentication: Secret API key as a bearer token
- Billing: Free to call. The resumed run bills the rows it reaches exactly as the first one did, and a row answered in the last seven days is a free repeat.
- Group: Bulk
Runs a job that stopped when the credit balance ran out again, from the top of the list it was given, instead of asking you to upload it a second time. Rows verified in the last seven days come back as free repeats, so only the rows the stopped run never reached are charged.
The job keeps its id, its webhook and its cumulative `credits_used`, which is the real spend on the account. `processed` and the verdict counts restart at zero: the part files they were counted from are deleted when a run settles, so the resumed run rebuilds them, and the result file the stopped run left is replaced as the rows come back.
Only a run whose `stopped_reason` is `insufficient_credits` and whose `status` has settled can be resumed; anything else answers `409 JOB_NOT_RESUMABLE`, and so does a second resume of a run that is already going again — claiming the run is one conditional write, so two calls can never queue it twice. A run whose list has passed out of the retention window answers `409 JOB_INPUT_EXPIRED`. An empty balance answers `402 INSUFFICIENT_CREDITS` rather than queueing a run that would stop again on the first row it never reached.
## Parameters
| Name | In | Type | Required | Description |
| --- | --- | --- | --- | --- |
| `jobId` | path | integer | yes | |
## Example request
```bash
curl -X POST https://spaw.co/api/v1/email/bulk/{jobId}/resume \
-H "Authorization: Bearer sk_live_…"
```
## Responses
### 202 — The list is queued again.
```json
{
"success": true,
"data": {
"job": {
"id": 512,
"status": "queued",
"total": 2000,
"duplicate_count": 12,
"processed": 0,
"deliverable": 0,
"risky": 0,
"undeliverable": 0,
"credits_used": 750,
"stopped_reason": null,
"cancel_requested": false,
"webhook_status": null,
"created_at": "2026-09-03T10:12:44+00:00",
"finished_at": null
}
}
}
```
### 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"
}
}
```
### 402 — The balance is empty. The lookup did not run.
```json
{
"success": false,
"error": {
"code": "INSUFFICIENT_CREDITS",
"message": "Your credit balance is empty.",
"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 is not in a state a resume can recover (`JOB_NOT_RESUMABLE`), or its list is no longer stored (`JOB_INPUT_EXPIRED`).
```json
{
"success": false,
"error": {
"code": "JOB_NOT_RESUMABLE",
"message": "Only a run that stopped because the credit balance ran out can be resumed, and only once it has finished settling.",
"request_id": "req_01m1kgdm4xngzmbmff68g94w0c"
}
}
```
### 429 — Over 5 requests per second for the key. Retry after the limit resets.
```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
- `JOB_NOT_RESUMABLE` — https://spaw.co/docs/errors/JOB_NOT_RESUMABLE
- `JOB_INPUT_EXPIRED` — https://spaw.co/docs/errors/JOB_INPUT_EXPIRED
- `INSUFFICIENT_CREDITS` — https://spaw.co/docs/errors/INSUFFICIENT_CREDITS
- `RATE_LIMITED` — https://spaw.co/docs/errors/RATE_LIMITED
---
Canonical page: https://spaw.co/docs/api/resume-bulk-job · OpenAPI document: https://spaw.co/openapi.json · All endpoints: https://spaw.co/docs/api
---
# Send a finished run's completion webhook again
`POST /api/v1/email/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: Bulk
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 verdict 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_email_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/email/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/email/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": 2000,
"duplicate_count": 12,
"processed": 2000,
"deliverable": 1400,
"risky": 260,
"undeliverable": 340,
"credits_used": 1750,
"stopped_reason": null,
"cancel_requested": false,
"webhook_status": "pending",
"webhook_detail": null,
"created_at": "2026-09-03T10:12:44+00:00",
"finished_at": "2026-09-03T10:41: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-bulk-webhook · OpenAPI document: https://spaw.co/openapi.json · All endpoints: https://spaw.co/docs/api
---
# Download a bulk job's results as CSV
`GET /api/v1/email/bulk/{jobId}/results`
- Authentication: Secret API key as a bearer token
- Billing: Free.
- Group: Bulk
Streams the run's result file: a finished job's full results, or the partial file a mid-run cancellation kept. The file starts with the input's own columns (for an API list, a single `email` column; for a dashboard upload, every column of the uploaded file), followed by `verified_email` and the verdict fields, one row per input row in the original order:
`verified_email, deliverable, reason, risk_score, risk_level, risk_signals, normalized_email, is_alias, is_gibberish, looks_generated, local_part_pattern, first_name, last_name, disposable, is_relay, relay_service, role, role_type, free_provider, domain_category, mx_found, mx_implicit, mx_provider, mx_provider_type, mx_blocklisted, mx_network_type, has_spf, dmarc_policy, domain_registered_at, domain_age_days, domain_expires_at, parked, smtp_checked, smtp_checked_at, mailbox_exists, previously_invalid, previously_invalid_at, catch_all, smtp_reason, mailbox_confidence, widely_seen, did_you_mean, ok_to_send, blocked_by, credits_used, cache_hit, suppressed`
Add `?variant=deliverable` (or `risky`, `undeliverable`) to download only those rows. A job still running answers `409 RESULTS_NOT_READY`; one cancelled before any address was processed answers `409 JOB_CANCELLED`, and one that failed before any address was processed `409 JOB_FAILED`. A run that was cancelled or failed part-way keeps the rows it did write, and they stay downloadable.
`format=json` answers the same rows, filtered the same way, as objects keyed by the result file's own header instead of a CSV attachment: `{ "success": true, "data": { "job": …, "results": [ … ] }, "meta": { "variant", "request_id" } }`. It is streamed row by row exactly as the CSV is, so a 100,000-row run costs no more to answer either way. A CSV cell carries no type, so only the two the writer encodes come back typed: an empty cell is `null` and `true`/`false` are booleans. Everything else is the string as written — a number is never silently made numeric, because postcodes, phone numbers and house numbers lose their leading zeros that way — and the columns of an uploaded file come back exactly as they were uploaded.
## Parameters
| Name | In | Type | Required | Description |
| --- | --- | --- | --- | --- |
| `jobId` | path | integer | yes | The id returned at creation. |
| `variant` | query | string | no | Which rows to include. One of: full, deliverable, risky, undeliverable. Default: full. |
| `format` | query | string | no | `csv` streams the file as an attachment; `json` streams the same rows as objects. One of: csv, json. Default: csv. |
## Example request
```bash
curl https://spaw.co/api/v1/email/bulk/512/results \
-H "Authorization: Bearer sk_live_…"
```
## Responses
### 200 — The result rows: a CSV attachment named `spaw-bulk-{jobId}.csv` (with `-{variant}` appended for a filtered download), or streamed JSON with `format=json`.
```csv
email,verified_email,deliverable,reason,risk_score,risk_level,risk_signals,normalized_email,is_alias,is_gibberish,looks_generated,local_part_pattern,first_name,last_name,disposable,is_relay,relay_service,role,role_type,free_provider,domain_category,mx_found,mx_implicit,mx_provider,mx_provider_type,mx_blocklisted,mx_network_type,has_spf,dmarc_policy,domain_registered_at,domain_age_days,domain_expires_at,parked,smtp_checked,smtp_checked_at,mailbox_exists,previously_invalid,previously_invalid_at,catch_all,smtp_reason,mailbox_confidence,widely_seen,did_you_mean,ok_to_send,blocked_by,credits_used,cache_hit,suppressed
mia@acme.com,mia@acme.com,deliverable,,0,low,,mia@acme.com,false,false,false,first,mia,,false,false,,false,,false,other,true,false,google,mailbox_provider,false,hosting,true,reject,1998-03-12,10402,2027-03-12,false,true,2026-09-03T10:12:44+00:00,true,false,,false,,,false,,true,,1,false,false
```
### 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 results are not available yet (`RESULTS_NOT_READY`) or never will be (`JOB_CANCELLED`, `JOB_FAILED`).
```json
{
"success": false,
"error": {
"code": "RESULTS_NOT_READY",
"message": "The job has not finished yet. Poll its status until it reports completed.",
"request_id": "req_01m1kgdm4xngzmbmff68g94w0c"
}
}
```
### 422 — The `variant` query parameter is not one of the allowed values.
```json
{
"success": false,
"error": {
"code": "VALIDATION_FAILED",
"message": "variant must be one of: full, deliverable, risky, undeliverable.",
"request_id": "req_01m1kgdm4xngzmbmff68g94w0c"
}
}
```
### 429 — Over 5 requests per second for the key. Retry after the limit resets.
```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
- `RESULTS_NOT_READY` — https://spaw.co/docs/errors/RESULTS_NOT_READY
- `JOB_CANCELLED` — https://spaw.co/docs/errors/JOB_CANCELLED
- `JOB_FAILED` — https://spaw.co/docs/errors/JOB_FAILED
- `VALIDATION_FAILED` — https://spaw.co/docs/errors/VALIDATION_FAILED
- `RATE_LIMITED` — https://spaw.co/docs/errors/RATE_LIMITED
---
Canonical page: https://spaw.co/docs/api/download-bulk-results · OpenAPI document: https://spaw.co/openapi.json · All endpoints: https://spaw.co/docs/api
---
# List the suppression list
`GET /api/v1/email/suppressions`
- Authentication: Secret API key as a bearer token
- Billing: Free.
- Group: Suppressions
The account's suppression list, newest first, 100 entries per page. Every undeliverable verdict adds the address in normalized form (lowercased, plus-tag stripped, Gmail dots folded) with source `auto`; imports arrive with source `imported`; bounces and complaints reported through the feedback endpoints arrive with source `feedback`. Batch, bulk and monitor runs answer suppressed addresses from the stored verdict at no cost; a single lookup always re-verifies, and a verdict other than undeliverable removes only an entry with source `auto`; a bounce you reported leaves when you report a delivery to that address, and an import or a complaint only when you remove it. A complaint about an address you imported is recorded on that entry (reason `complained`), which stays an import. Imports and complaints never go stale: they are served for as long as they stand. Any other entry untouched for 90 days is re-verified instead of trusted.
Every filter is optional and they combine. `value` looks one address up: it is normalized the same way the list stores it, so `Mia+news@Acme.com` finds the entry filed as `mia@acme.com`. An address that does not parse matches nothing. An unparseable date or an out-of-range `per_page` is a `422`, not a silently unfiltered page.
## Parameters
| Name | In | Type | Required | Description |
| --- | --- | --- | --- | --- |
| `value` | query | string | no | One address, in any spelling of it. Normalized before it is matched. At most 500 characters. |
| `source` | query | string | no | Only entries this source added. One of: auto, imported, feedback. |
| `reason` | query | string | no | Only entries carrying this verdict reason. At most 64 characters. |
| `since` | query | string | no | Only entries added on or after this date. |
| `until` | query | string | no | Only entries added on or before this date. A bare date means the whole of that day. |
| `page` | query | integer | no | The page to return. Default: 1. |
| `per_page` | query | integer | no | Entries per page, up to 500. Default: 100. |
## Example request
```bash
curl https://spaw.co/api/v1/email/suppressions \
-H "Authorization: Bearer sk_live_…"
```
## Responses
### 200 — One page of entries.
```json
{
"success": true,
"data": {
"suppressions": [
{
"id": 9041,
"email": "old@acme.com",
"reason": "mailbox_not_found",
"source": "auto",
"added_at": "2026-09-02T08:30:00+00:00"
},
{
"id": 9040,
"email": "bounced@example.org",
"reason": null,
"source": "imported",
"added_at": "2026-09-01T17:02:11+00:00"
}
]
},
"meta": {
"page": 1,
"per_page": 100,
"total": 2
}
}
```
### 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"
}
}
```
### 429 — Over 5 requests per second for the key. Retry after the limit resets.
```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
- `VALIDATION_FAILED` — https://spaw.co/docs/errors/VALIDATION_FAILED
- `RATE_LIMITED` — https://spaw.co/docs/errors/RATE_LIMITED
---
Canonical page: https://spaw.co/docs/api/list-suppressions · OpenAPI document: https://spaw.co/openapi.json · All endpoints: https://spaw.co/docs/api
---
# Import known-bad addresses
`POST /api/v1/email/suppressions`
- Authentication: Secret API key as a bearer token
- Billing: Free.
- Group: Suppressions
Adds up to 10,000 addresses per request to the suppression list — an email provider's bounce export, a hand-kept blocklist. Addresses are normalized first; ones that do not parse are counted as `invalid`. An address already on the list keeps the verdict stored with it — a verified answer is better to serve than a synthesized one — is counted as `already_suppressed`, and becomes an import too; that verdict is served for 90 days after the entry was last renewed, then the import answers with reason `suppressed`, because nothing verifies an import again. Imported entries are honored from day one at no credit cost: list, bulk and monitor runs answer them with reason `suppressed` and `risk_score` 100. An import is your decision rather than a verdict, so no lookup clears it — not a single re-verify, not one through your publishable key — and no delivery report does either: it leaves the list only when you remove it. A complaint reported about it later is recorded on the entry (reason `complained`, still source `imported`). An import never goes stale, so importing an address again is never needed.
## Request body
| Field | Type | Required | Description |
| --- | --- | --- | --- |
| `emails` | string[] | yes | 1 to 10,000 addresses; each item at most 254 characters. |
## Example request
```bash
curl -X POST https://spaw.co/api/v1/email/suppressions \
-H "Authorization: Bearer sk_live_…" \
-H "Content-Type: application/json" \
-d '{
"emails": [
"bounced@example.org",
"gone@acme.com"
]
}'
```
## Responses
### 200 — How many entries were added, already present, or unparseable.
```json
{
"success": true,
"data": {
"added": 2,
"already_suppressed": 0,
"invalid": 0
}
}
```
### 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"
}
}
```
### 422 — The request body could not be validated; `error.errors` lists the fields.
```json
{
"success": false,
"error": {
"code": "VALIDATION_FAILED",
"message": "The email field is required.",
"errors": {
"email": [
"The email field is required."
]
},
"request_id": "req_01m1kgdm4xngzmbmff68g94w0c"
}
}
```
### 429 — Over 5 requests per second for the key. Retry after the limit resets.
```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
- `VALIDATION_FAILED` — https://spaw.co/docs/errors/VALIDATION_FAILED
- `RATE_LIMITED` — https://spaw.co/docs/errors/RATE_LIMITED
---
Canonical page: https://spaw.co/docs/api/add-suppressions · OpenAPI document: https://spaw.co/openapi.json · All endpoints: https://spaw.co/docs/api
---
# Remove addresses from the suppression list
`DELETE /api/v1/email/suppressions`
- Authentication: Secret API key as a bearer token
- Billing: Free.
- Group: Suppressions
Removes up to 1,000 addresses per request by the address itself, for a caller that knows the address but not the id it was filed under. Each value is normalized the way the list stores it first, so `Mia+news@Acme.com` removes the entry filed as `mia@acme.com`.
The answer names every address it was given, spelled the way you sent it: `removed` are the ones that were on the list and no longer are, `not_found` the ones that were not on it — including an address that does not parse, and one that is on another account's list. Two spellings of one mailbox both count as removed.
The cap is 1,000 rather than the import's 10,000 because the answer names every value back: a request over the cap is a `422` and removes nothing. Removing an entry means the address is verified fresh the next time a list, bulk or monitor run meets it.
## Request body
| Field | Type | Required | Description |
| --- | --- | --- | --- |
| `values` | string[] | yes | 1 to 1,000 addresses, in any spelling of each. |
## Example request
```bash
curl -X DELETE https://spaw.co/api/v1/email/suppressions \
-H "Authorization: Bearer sk_live_…" \
-H "Content-Type: application/json" \
-d '{
"values": [
"Mia+news@Acme.com",
"nobody@example.org"
]
}'
```
## Responses
### 200 — Which of the addresses were on the list and which were not.
```json
{
"success": true,
"data": {
"removed": [
"Mia+news@Acme.com"
],
"not_found": [
"nobody@example.org"
]
}
}
```
### 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"
}
}
```
### 422 — The request body could not be validated; `error.errors` lists the fields.
```json
{
"success": false,
"error": {
"code": "VALIDATION_FAILED",
"message": "The email field is required.",
"errors": {
"email": [
"The email field is required."
]
},
"request_id": "req_01m1kgdm4xngzmbmff68g94w0c"
}
}
```
### 429 — Over 5 requests per second for the key. Retry after the limit resets.
```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
- `VALIDATION_FAILED` — https://spaw.co/docs/errors/VALIDATION_FAILED
- `RATE_LIMITED` — https://spaw.co/docs/errors/RATE_LIMITED
---
Canonical page: https://spaw.co/docs/api/remove-suppressions · OpenAPI document: https://spaw.co/openapi.json · All endpoints: https://spaw.co/docs/api
---
# Remove a suppression entry
`DELETE /api/v1/email/suppressions/{suppressionId}`
- Authentication: Secret API key as a bearer token
- Billing: Free.
- Group: Suppressions
Deletes one entry so the address is verified fresh the next time a list, bulk or monitor run meets it. An entry that belongs to another account answers `404`.
## Parameters
| Name | In | Type | Required | Description |
| --- | --- | --- | --- | --- |
| `suppressionId` | path | integer | yes | The entry's id from the list endpoint. |
## Example request
```bash
curl -X DELETE https://spaw.co/api/v1/email/suppressions/9041 \
-H "Authorization: Bearer sk_live_…"
```
## Responses
### 200 — The entry was removed.
```json
{
"success": true
}
```
### 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"
}
}
```
### 429 — Over 5 requests per second for the key. Retry after the limit resets.
```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
- `RATE_LIMITED` — https://spaw.co/docs/errors/RATE_LIMITED
---
Canonical page: https://spaw.co/docs/api/remove-suppression · OpenAPI document: https://spaw.co/openapi.json · All endpoints: https://spaw.co/docs/api
---
# List the account's monitors
`GET /api/v1/email/monitors`
- Authentication: Secret API key as a bearer token
- Billing: Free.
- Group: Monitors
The saved lists this account re-verifies on a schedule, newest first, 100 per page. Each monitor carries the summary of its last run: the verdict counts, how many addresses decayed (deliverable last time, not any more), the credits the run spent, and `stopped_reason` when it did not finish cleanly: `insufficient_credits` or `key_spend_cap` where the run stopped part-way, `run_failed` where it died outright, and `run_failed_retrying` where it died and the schedule has been pulled in to check again — `retry_at` in the same block says when that becomes due, and the hourly sweep picks it up at or after that, so between 45 minutes and about an hour and three quarters later. A summary carrying a `stopped_reason` of either failure kind has NO counts in it at all: the run produced none, so read a missing count as unknown rather than as zero. The addresses and their verdicts are on the single-monitor endpoint.
## Parameters
| Name | In | Type | Required | Description |
| --- | --- | --- | --- | --- |
| `page` | query | integer | no | The page to return. Default: 1. |
## Example request
```bash
curl https://spaw.co/api/v1/email/monitors \
-H "Authorization: Bearer sk_live_…"
```
## Responses
### 200 — One page of monitors.
```json
{
"success": true,
"data": {
"monitors": [
{
"id": 41,
"name": "Newsletter list",
"cadence": "weekly",
"email_count": 2,
"next_run_at": "2026-09-10T08:00:00+00:00",
"last_run_at": "2026-09-03T08:00:12+00:00",
"last_summary": {
"deliverable": 1,
"risky": 1,
"undeliverable": 0,
"decayed": 0,
"credits_used": 2,
"stopped_reason": null
},
"created_at": "2026-08-27T08:00:00+00:00"
}
]
},
"meta": {
"page": 1,
"per_page": 100,
"total": 1,
"retention_days": 365
}
}
```
### 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"
}
}
```
### 429 — Over 5 requests per second for the key. Retry after the limit resets.
```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
- `RATE_LIMITED` — https://spaw.co/docs/errors/RATE_LIMITED
---
Canonical page: https://spaw.co/docs/api/list-monitors · OpenAPI document: https://spaw.co/openapi.json · All endpoints: https://spaw.co/docs/api
---
# Monitor a list on a schedule
`POST /api/v1/email/monitors`
- Authentication: Secret API key as a bearer token
- Billing: Creation is free. Each run bills every address like a single lookup — 1 credit for a fresh deliverable or risky verdict whose mailbox check was definite; unverified answers, suppressed addresses and 7-day repeats are free, and undeliverable verdicts are free within your email fair use and 1 credit each past it.
- Group: Monitors
Saves up to 500 addresses and re-verifies them every week or every month. The first run starts right away and only sets the baseline; from the next run on, every address that was deliverable last time and no longer is counts as decayed and is reported to the account's email. Runs honor the suppression list and the 7-day repeat cache, so a stable list costs little to keep watching.
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": "email_monitor.run", "monitor": { "id", "name", "cadence", "last_run_at", "next_run_at" }, "summary": { … the same block as `last_summary` … }, "changes": [ … ] }`, where `changes` names the addresses the alert email names. 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 | Required | Description |
| --- | --- | --- | --- |
| `name` | string | yes | A label for the dashboard and the decay alerts. At most 100 characters. |
| `emails` | string[] | yes | 1 to 500 addresses; each item at most 254 characters. |
| `cadence` | string | yes | How often the list is re-verified. One of: weekly, monthly. |
| `webhook_url` | string | null | no | 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 | no | 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
```bash
curl -X POST https://spaw.co/api/v1/email/monitors \
-H "Authorization: Bearer sk_live_…" \
-H "Content-Type: application/json" \
-d '{
"name": "Newsletter list",
"emails": [
"mia@acme.com",
"ben@acme.com"
],
"cadence": "weekly"
}'
```
## Responses
### 201 — The monitor was saved and its baseline run queued.
```json
{
"success": true,
"data": {
"monitor": {
"id": 41,
"name": "Newsletter list",
"cadence": "weekly",
"email_count": 2,
"next_run_at": "2026-09-10T10:12:44+00:00",
"last_run_at": null,
"last_summary": null,
"created_at": "2026-09-03T10:12:44+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"
}
}
```
### 422 — The request body could not be validated; `error.errors` lists the fields.
```json
{
"success": false,
"error": {
"code": "VALIDATION_FAILED",
"message": "The email field is required.",
"errors": {
"email": [
"The email field is required."
]
},
"request_id": "req_01m1kgdm4xngzmbmff68g94w0c"
}
}
```
### 429 — Over 5 requests per second for the key. Retry after the limit resets.
```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
- `VALIDATION_FAILED` — https://spaw.co/docs/errors/VALIDATION_FAILED
- `RATE_LIMITED` — https://spaw.co/docs/errors/RATE_LIMITED
---
Canonical page: https://spaw.co/docs/api/create-monitor · OpenAPI document: https://spaw.co/openapi.json · All endpoints: https://spaw.co/docs/api
---
# Read a monitor and its last results
`GET /api/v1/email/monitors/{monitorId}`
- Authentication: Secret API key as a bearer token
- Billing: Free.
- Group: Monitors
The monitor plus `results`: the verdict of every address from the last run, keyed by the address as it was saved, or null until the first run has finished. A monitor that belongs to another account answers `404`.
## Parameters
| Name | In | Type | Required | Description |
| --- | --- | --- | --- | --- |
| `monitorId` | path | integer | yes | The monitor's id from the list endpoint. |
## Example request
```bash
curl https://spaw.co/api/v1/email/monitors/41 \
-H "Authorization: Bearer sk_live_…"
```
## Responses
### 200 — The monitor.
```json
{
"success": true,
"data": {
"monitor": {
"id": 41,
"name": "Newsletter list",
"cadence": "weekly",
"email_count": 2,
"next_run_at": "2026-09-10T08:00:00+00:00",
"last_run_at": "2026-09-03T08:00:12+00:00",
"last_summary": {
"deliverable": 1,
"risky": 1,
"undeliverable": 0,
"decayed": 0,
"credits_used": 2,
"stopped_reason": null
},
"created_at": "2026-08-27T08:00:00+00:00",
"results": {
"mia@acme.com": "deliverable",
"ben@acme.com": "risky"
}
}
}
}
```
### 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"
}
}
```
### 429 — Over 5 requests per second for the key. Retry after the limit resets.
```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
- `RATE_LIMITED` — https://spaw.co/docs/errors/RATE_LIMITED
---
Canonical page: https://spaw.co/docs/api/get-monitor · OpenAPI document: https://spaw.co/openapi.json · All endpoints: https://spaw.co/docs/api
---
# Change a monitor
`PATCH /api/v1/email/monitors/{monitorId}`
- Authentication: Secret API key as a bearer token
- Billing: Free. The change applies to the next run.
- Group: Monitors
Changes a saved monitor in place: rename it, move it to another cadence, replace the addresses, or point it at a webhook. Send only the fields you are changing; a body that names none of them answers `422` rather than quietly changing nothing, because that is the shape a misspelled field name takes.
Replacing `emails` replaces the list. What the last run recorded about the addresses that stayed is kept, so their next run still compares against the answer they already had; what it recorded about the ones that are gone is deleted with them; and an entry that is new starts with no baseline, so its first run only sets one. A rename never touches a stored answer — they are keyed by the entry, never by the list.
Changing `cadence` neither skips a run nor starts a second one: a run already scheduled sooner than the new interval still happens, and a longer interval applies from the one after it. Nothing is queued by this call — the change applies to the next run, and `POST .../run` is there to have it now.
Sending a `webhook_url` that differs from the one stored mints a new signing secret and returns it once, here; sending the URL it already has keeps the secret it is signing with, and `null` removes the webhook. A monitor that belongs to another account answers `404`.
## Parameters
| Name | In | Type | Required | Description |
| --- | --- | --- | --- | --- |
| `monitorId` | path | integer | yes | The monitor's id from the list endpoint. |
## Request body
| Field | Type | Required | Description |
| --- | --- | --- | --- |
| `name` | string | no | At most 100 characters. |
| `emails` | string[] | no | |
| `cadence` | string | no | One of: weekly, monthly. |
| `webhook_url` | string | null | no | At most 2,048 characters. |
| `webhook_events` | string | no | One of: changes, every_run. |
## Example request
```bash
curl -X PATCH https://spaw.co/api/v1/email/monitors/41 \
-H "Authorization: Bearer sk_live_…" \
-H "Content-Type: application/json" \
-d '{
"name": "Newsletter list (2026)",
"cadence": "monthly"
}'
```
## Responses
### 200 — The monitor as it now stands.
### 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"
}
}
```
### 422 — The request body could not be validated; `error.errors` lists the fields.
```json
{
"success": false,
"error": {
"code": "VALIDATION_FAILED",
"message": "The email field is required.",
"errors": {
"email": [
"The email field is required."
]
},
"request_id": "req_01m1kgdm4xngzmbmff68g94w0c"
}
}
```
### 429 — Over 5 requests per second for the key. Retry after the limit resets.
```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
- `VALIDATION_FAILED` — https://spaw.co/docs/errors/VALIDATION_FAILED
- `RATE_LIMITED` — https://spaw.co/docs/errors/RATE_LIMITED
---
Canonical page: https://spaw.co/docs/api/update-monitor · OpenAPI document: https://spaw.co/openapi.json · All endpoints: https://spaw.co/docs/api
---
# Stop monitoring a list
`DELETE /api/v1/email/monitors/{monitorId}`
- Authentication: Secret API key as a bearer token
- Billing: Free.
- Group: Monitors
Removes the monitor and its stored results; nothing further is scheduled. A run that is already queued finishes on its own and bills as usual. A monitor that belongs to another account answers `404`.
## Parameters
| Name | In | Type | Required | Description |
| --- | --- | --- | --- | --- |
| `monitorId` | path | integer | yes | The monitor's id from the list endpoint. |
## Example request
```bash
curl -X DELETE https://spaw.co/api/v1/email/monitors/41 \
-H "Authorization: Bearer sk_live_…"
```
## Responses
### 200 — The monitor was removed.
```json
{
"success": true
}
```
### 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"
}
}
```
### 429 — Over 5 requests per second for the key. Retry after the limit resets.
```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
- `RATE_LIMITED` — https://spaw.co/docs/errors/RATE_LIMITED
---
Canonical page: https://spaw.co/docs/api/delete-monitor · OpenAPI document: https://spaw.co/openapi.json · All endpoints: https://spaw.co/docs/api
---
# Re-verify a monitored list now
`POST /api/v1/email/monitors/{monitorId}/run`
- Authentication: Secret API key as a bearer token
- Billing: The run bills every address like a single lookup; suppressed addresses and 7-day repeats are free.
- Group: Monitors
Queues a run right away instead of waiting for the schedule; when it finishes, the next scheduled run is set one cadence from then. Answers `202` with the monitor as it stands before the run; poll it for the new `last_run_at`, `last_summary` and `results`. A monitor that belongs to another account answers `404`.
## Parameters
| Name | In | Type | Required | Description |
| --- | --- | --- | --- | --- |
| `monitorId` | path | integer | yes | The monitor's id from the list endpoint. |
## Example request
```bash
curl -X POST https://spaw.co/api/v1/email/monitors/41/run \
-H "Authorization: Bearer sk_live_…"
```
## Responses
### 202 — The run was queued.
```json
{
"success": true,
"data": {
"monitor": {
"id": 41,
"name": "Newsletter list",
"cadence": "weekly",
"email_count": 2,
"next_run_at": "2026-09-10T08:00:00+00:00",
"last_run_at": "2026-09-03T08:00:12+00:00",
"last_summary": {
"deliverable": 1,
"risky": 1,
"undeliverable": 0,
"decayed": 0,
"credits_used": 2,
"stopped_reason": null
},
"created_at": "2026-08-27T08:00:00+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"
}
}
```
### 429 — Over 5 requests per second for the key. Retry after the limit resets.
```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
- `RATE_LIMITED` — https://spaw.co/docs/errors/RATE_LIMITED
---
Canonical page: https://spaw.co/docs/api/run-monitor · OpenAPI document: https://spaw.co/openapi.json · All endpoints: https://spaw.co/docs/api
---
# Report delivery outcomes
`POST /api/v1/email/feedback`
- Authentication: Secret API key as a bearer token
- Billing: Free.
- Group: Feedback
Tells Spaw what actually happened to an address after you sent to it: `delivered`, `bounced` (hard bounces only), or `complained`. Each outcome is stored against the verdict you had been given for that address in the previous 90 days, which turns verdicts into measured accuracy, and it keeps your suppression list current: a bounce or complaint adds the address with source `feedback`, and a delivery removes an entry a verdict or a bounce added. A complaint is the recipient's objection, not a fact about the mailbox: no delivery, lookup or later bounce clears or relabels it and it never goes stale, so like an import it leaves only when you remove it. On an address you imported, a complaint is recorded on the import (reason `complained`, source still `imported`) and a bounce renews it without changing its reason; a delivery never clears an import. An address you reported delivered within the last 90 days also counts as a confirmed mailbox for your own later lookups (`smtp_reason: "delivered_recently"`), without a probe.
Feedback is free and never logged as a lookup. Items whose address does not parse are skipped and counted, not rejected. Outcomes are kept for 180 days.
## Request body
| Field | Type | Required | Description |
| --- | --- | --- | --- |
| `items` | object[] | yes | 1 to 1,000 outcomes per request. |
| `items[].email` | string | yes | The recipient. Normalized like a lookup; an address that does not parse is skipped and counted, not rejected. At most 254 characters. |
| `items[].outcome` | string | yes | What happened. Report hard bounces only. One of: delivered, bounced, complained. |
| `items[].reason` | string | null | no | The provider's diagnostic, e.g. the SMTP reply. Stored as-is. At most 255 characters. |
| `items[].occurred_at` | string | null | no | When it happened, any parseable date. Defaults to now. A time more than five minutes in the future is refused; one inside that margin is recorded as now. |
## Example request
```bash
curl -X POST https://spaw.co/api/v1/email/feedback \
-H "Authorization: Bearer sk_live_…" \
-H "Content-Type: application/json" \
-d '{
"items": [
{
"email": "mia@acme.com",
"outcome": "bounced",
"reason": "550 5.1.1 no such user"
},
{
"email": "sam@example.org",
"outcome": "delivered",
"occurred_at": "2026-09-03T09:00:00Z"
}
]
}'
```
## Responses
### 202 — The outcomes were recorded.
```json
{
"success": true,
"data": {
"recorded": 2,
"skipped": 0
}
}
```
### 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"
}
}
```
### 422 — The request body could not be validated; `error.errors` lists the fields.
```json
{
"success": false,
"error": {
"code": "VALIDATION_FAILED",
"message": "The email field is required.",
"errors": {
"email": [
"The email field is required."
]
},
"request_id": "req_01m1kgdm4xngzmbmff68g94w0c"
}
}
```
### 429 — Over 5 requests per second for the key. Retry after the limit resets.
```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
- `VALIDATION_FAILED` — https://spaw.co/docs/errors/VALIDATION_FAILED
- `RATE_LIMITED` — https://spaw.co/docs/errors/RATE_LIMITED
---
Canonical page: https://spaw.co/docs/api/report-delivery-feedback · OpenAPI document: https://spaw.co/openapi.json · All endpoints: https://spaw.co/docs/api
---
# Measured accuracy
`GET /api/v1/email/feedback/summary`
- Authentication: Secret API key as a bearer token
- Billing: Free.
- Group: Feedback
Crosses the last 90 days of reported outcomes with the verdicts on file. `by_verdict` counts delivered, bounced and complained per verdict (`deliverable`, `risky`, `undeliverable`, and `unknown` for addresses with no verdict on file), and `measured.deliverable_delivery_rate` and `measured.undeliverable_bounce_rate` are the two numbers that say how right Spaw was for you. A rate is `null` until there is at least one matching outcome. The same figures appear on the History tab of the dashboard.
## Example request
```bash
curl https://spaw.co/api/v1/email/feedback/summary \
-H "Authorization: Bearer sk_live_…"
```
## Responses
### 200 — The summary.
```json
{
"success": true,
"data": {
"window_days": 90,
"total": 1240,
"outcomes": {
"delivered": 1180,
"bounced": 52,
"complained": 8
},
"by_verdict": {
"deliverable": {
"delivered": 1150,
"bounced": 9,
"complained": 6
},
"risky": {
"delivered": 30,
"bounced": 11,
"complained": 2
},
"undeliverable": {
"delivered": 0,
"bounced": 32,
"complained": 0
},
"unknown": {
"delivered": 0,
"bounced": 0,
"complained": 0
}
},
"measured": {
"deliverable_delivery_rate": 0.992,
"undeliverable_bounce_rate": 1
}
}
}
```
### 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"
}
}
```
### 429 — Over 5 requests per second for the key. Retry after the limit resets.
```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
- `RATE_LIMITED` — https://spaw.co/docs/errors/RATE_LIMITED
---
Canonical page: https://spaw.co/docs/api/feedback-summary · OpenAPI document: https://spaw.co/openapi.json · All endpoints: https://spaw.co/docs/api
---
# Receive bounce webhooks from an email provider
`POST /api/v1/email/feedback/{provider}/{feedbackKey}`
- Authentication: Feedback key in the URL
- Billing: Free.
- Group: Feedback
The webhook target for an email provider's bounce, delivery and complaint notifications. Providers cannot send bearer tokens, so the account's feedback key rides in the URL (generate or rotate it on the dashboard's API keys page; rotating invalidates the URLs at once) and the provider name selects the payload adapter. The body is the provider's own payload, decoded as JSON regardless of content type because Amazon SNS posts JSON as `text/plain`. Payloads are read for their documented fields and nothing else; events that say nothing definitive about the address are ignored.
| Provider | Path | Mapping |
| --- | --- | --- |
| postmark | `/api/v1/email/feedback/postmark/{key}` | Bounce (hard types) → bounced · Delivery → delivered · SpamComplaint → complained. Soft bounces are ignored. |
| ses | `/api/v1/email/feedback/ses/{key}` | SNS notifications: Bounce with bounceType Permanent → bounced · Delivery → delivered · Complaint → complained. Subscription confirmations are answered. |
| mailgun | `/api/v1/email/feedback/mailgun/{key}` | delivered → delivered · failed with severity permanent → bounced · complained → complained. Temporary failures are ignored. |
| sendgrid | `/api/v1/email/feedback/sendgrid/{key}` | delivered → delivered · bounce and dropped → bounced · spamreport → complained. Deferred events are ignored. |
Throttled at 120 requests per minute per IP. Outcomes recorded here behave exactly like ones reported through `POST /api/v1/email/feedback`.
## Parameters
| Name | In | Type | Required | Description |
| --- | --- | --- | --- | --- |
| `provider` | path | string | yes | Which provider's payload format to expect. One of: postmark, ses, mailgun, sendgrid. |
| `feedbackKey` | path | string | yes | The account's feedback key, which starts with `fb_`. |
## Request body
The provider's native webhook payload.
## Example request
```bash
curl -X POST https://spaw.co/api/v1/email/feedback/postmark/fb_… \
-H "Content-Type: application/json" \
-d '{
"RecordType": "Bounce",
"Type": "HardBounce",
"Email": "mia@acme.com",
"Description": "The server was unable to deliver your message (ex. unknown user, mailbox not found).",
"BouncedAt": "2026-09-03T10:12:44Z"
}'
```
## Responses
### 200 — The outcomes the payload carried were recorded.
```json
{
"success": true,
"data": {
"recorded": 1,
"skipped": 0
}
}
```
### 401 — The feedback key does not exist or was rotated.
```json
{
"success": false,
"error": {
"code": "INVALID_FEEDBACK_KEY",
"message": "That feedback key does not exist or was rotated.",
"request_id": "req_01m1kgdm4xngzmbmff68g94w0c"
}
}
```
### 404 — The provider path is not one of postmark, ses, mailgun, or sendgrid.
```json
{
"success": false,
"error": {
"code": "UNKNOWN_FEEDBACK_PROVIDER",
"message": "Supported providers: postmark, ses, mailgun, sendgrid.",
"request_id": "req_01m1kgdm4xngzmbmff68g94w0c"
}
}
```
### 429 — Over 5 requests per second for the key. Retry after the limit resets.
```json
{
"success": false,
"error": {
"code": "RATE_LIMITED",
"message": "Too many requests. Retry after the limit resets.",
"request_id": "req_01m1kgdm4xngzmbmff68g94w0c"
}
}
```
## Error codes
- `UNKNOWN_FEEDBACK_PROVIDER` — https://spaw.co/docs/errors/UNKNOWN_FEEDBACK_PROVIDER
- `INVALID_FEEDBACK_KEY` — https://spaw.co/docs/errors/INVALID_FEEDBACK_KEY
- `RATE_LIMITED` — https://spaw.co/docs/errors/RATE_LIMITED
---
Canonical page: https://spaw.co/docs/api/esp-bounce-webhook · OpenAPI document: https://spaw.co/openapi.json · All endpoints: https://spaw.co/docs/api
---
# Receive delivery status webhooks from an SMS provider
`POST /api/v1/phone/feedback/{provider}/{feedbackKey}`
- Authentication: Feedback key in the URL
- Billing: Free.
- Group: Phone
The webhook target for an SMS provider's delivery status callbacks, the phone counterpart of the email bounce webhook. Providers cannot send bearer tokens, so the account's feedback key rides in the URL (the same key as for email, generated or rotated on the dashboard); the provider name picks the payload adapter. Twilio and MessageBird post form fields, Vonage and Sinch JSON; both are read. Only definitive statuses become outcomes; queued, sent, buffered, accepted and unknown are ignored.
| Provider | Path | Mapping |
| --- | --- | --- |
| twilio | `/api/v1/phone/feedback/twilio/{key}` | Message status callback: delivered → delivered · undelivered and failed → undelivered. `To` is the number; the callback carries no timestamp, so the outcome is dated on receipt. |
| vonage | `/api/v1/phone/feedback/vonage/{key}` | SMS API receipts (`msisdn`, `status`, `message-timestamp`) and Messages API status webhooks (`to`, `status`, `timestamp`): delivered → delivered · expired, failed, rejected and undeliverable → undelivered. |
| messagebird | `/api/v1/phone/feedback/messagebird/{key}` | Status reports (`recipient`, `status`, `statusDatetime`), configured as POST: delivered → delivered · delivery_failed and expired → undelivered. |
| sinch | `/api/v1/phone/feedback/sinch/{key}` | Per-recipient reports (`recipient`, `status`, `at`) and batch reports (`statuses[].recipients`): Delivered → delivered · Failed, Expired, Rejected and Aborted → undelivered. |
A failed receipt's error code — Twilio `ErrorCode`, Vonage `err-code` (SMS API) or `error.title` (Messages API), MessageBird `statusErrorCode` (or, without one, `statusReason`), Sinch `code` — is read into the report's `failure_class` from each provider's published code list: `permanent` (Vonage 3 and 4, Messages API 1190, MessageBird 1, Sinch 150 and 151), `transient` (such as Twilio 30003, 30004, 30005 and 30008, Vonage 1, 2, 7 and 8), `not_sms_capable` (Twilio 30006, Vonage 14, MessageBird 10, 11 and 21, Sinch 152) and `sender_side` (such as Twilio 30007, 30022, 30032 and 30034, Vonage 6, 10, 11 and 39, MessageBird 104 and 110, Sinch 402 and 412). A code that says the recipient opted out — Twilio 21610, Vonage 9 and 1240, MessageBird 103 or the status reason `opted out` — is not recorded as a failure at all but as an `opted_out` outcome, which puts the number on your suppression list with reason `opted_out` and never scores it; Sinch publishes no opt-out code. The tables are a best-effort reading of each provider's documentation; a code none of them lists leaves the failure unclassified, which counts exactly as every failure did before classes existed.
Throttled at 120 requests per minute per IP. Outcomes recorded here behave exactly like ones reported through `POST /api/v1/phone/feedback`: they feed the account's own later lookups and its suppression list, and are never billed. A receipt dated in the future is recorded as now.
## Parameters
| Name | In | Type | Required | Description |
| --- | --- | --- | --- | --- |
| `provider` | path | string | yes | Which provider's payload format to expect. One of: twilio, vonage, messagebird, sinch. |
| `feedbackKey` | path | string | yes | The account's feedback key, which starts with `fb_`. |
## Request body
The provider's native callback payload, as JSON or form fields.
## Example request
```bash
curl -X POST https://spaw.co/api/v1/phone/feedback/twilio/fb_… \
-H "Content-Type: application/json" \
-d '{
"MessageSid": "SM0f2b3c4d5e6f7a8b9c0d1e2f3a4b5c6d",
"To": "+447911012345",
"MessageStatus": "undelivered",
"ErrorCode": "30003"
}'
```
## Responses
### 200 — The outcomes the payload carried were recorded.
```json
{
"success": true,
"data": {
"recorded": 1,
"skipped": 0
}
}
```
### 401 — The feedback key does not exist or was rotated.
```json
{
"success": false,
"error": {
"code": "INVALID_FEEDBACK_KEY",
"message": "That feedback key does not exist or was rotated.",
"request_id": "req_01m1kgdm4xngzmbmff68g94w0c"
}
}
```
### 404 — The provider path is not one of twilio, vonage, messagebird, or sinch.
```json
{
"success": false,
"error": {
"code": "UNKNOWN_FEEDBACK_PROVIDER",
"message": "Supported providers: twilio, vonage, messagebird, sinch.",
"request_id": "req_01m1kgdm4xngzmbmff68g94w0c"
}
}
```
### 429 — Over 5 requests per second for the key. Retry after the limit resets.
```json
{
"success": false,
"error": {
"code": "RATE_LIMITED",
"message": "Too many requests. Retry after the limit resets.",
"request_id": "req_01m1kgdm4xngzmbmff68g94w0c"
}
}
```
## Error codes
- `UNKNOWN_FEEDBACK_PROVIDER` — https://spaw.co/docs/errors/UNKNOWN_FEEDBACK_PROVIDER
- `INVALID_FEEDBACK_KEY` — https://spaw.co/docs/errors/INVALID_FEEDBACK_KEY
- `RATE_LIMITED` — https://spaw.co/docs/errors/RATE_LIMITED
---
Canonical page: https://spaw.co/docs/api/sms-status-webhook · OpenAPI document: https://spaw.co/openapi.json · All endpoints: https://spaw.co/docs/api
---
# Read the account's sending policy
`GET /api/v1/email/policy`
- Authentication: Secret API key as a bearer token
- Billing: Free.
- Group: Email
The rules behind `ok_to_send` on every email lookup this account runs: the signals that block, the risk-score ceiling and the mailbox-confidence floor. The policy belongs to the account, so every member reads and changes the same one. An undeliverable verdict is always blocked whatever the policy says.
## Example request
```bash
curl https://spaw.co/api/v1/email/policy \
-H "Authorization: Bearer sk_live_…"
```
## Responses
### 200 — The policy in force.
```json
{
"success": true,
"data": {
"max_risk_score": 60,
"block_signals": [
"disposable",
"likely_typo",
"parked_domain",
"mx_blocklisted",
"no_reply"
],
"min_mailbox_confidence": null,
"is_default": true,
"updated_at": null
}
}
```
## Error codes
- `UNAUTHENTICATED` — https://spaw.co/docs/errors/UNAUTHENTICATED
- `RATE_LIMITED` — https://spaw.co/docs/errors/RATE_LIMITED
---
Canonical page: https://spaw.co/docs/api/get-email-policy · OpenAPI document: https://spaw.co/openapi.json · All endpoints: https://spaw.co/docs/api
---
# Change one or more rules of the sending policy
`PUT /api/v1/email/policy`
- Authentication: Secret API key as a bearer token
- Billing: Free.
- Group: Email
Names only the rules that change; the rest stay as they were, and the change applies to the account's next lookup. `max_risk_score: null` ignores the score, `block_signals: []` blocks on no signal. A body naming none of the three rules is refused with a 422 keyed `policy`, because that is the shape a misspelled key takes.
## Request body
| Field | Type | Required | Description |
| --- | --- | --- | --- |
| `max_risk_score` | integer | null | no | Block when risk_score is above this; null ignores the score. |
| `block_signals` | string[] | null | no | Signals that block whenever they fire, whatever the score. |
| `min_mailbox_confidence` | integer | null | no | Block an unconfirmed mailbox whose mailbox_confidence is below this; null ignores it. |
## Example request
```bash
curl -X PUT https://spaw.co/api/v1/email/policy \
-H "Authorization: Bearer sk_live_…" \
-H "Content-Type: application/json" \
-d '{
"max_risk_score": 40,
"block_signals": [
"disposable",
"likely_typo",
"parked_domain",
"mx_blocklisted",
"no_reply",
"catch_all"
]
}'
```
## Responses
### 200 — The policy now in force.
## Error codes
- `UNAUTHENTICATED` — https://spaw.co/docs/errors/UNAUTHENTICATED
- `RATE_LIMITED` — https://spaw.co/docs/errors/RATE_LIMITED
- `VALIDATION_FAILED` — https://spaw.co/docs/errors/VALIDATION_FAILED
---
Canonical page: https://spaw.co/docs/api/update-email-policy · OpenAPI document: https://spaw.co/openapi.json · All endpoints: https://spaw.co/docs/api
---
# Restore the default sending policy
`DELETE /api/v1/email/policy`
- Authentication: Secret API key as a bearer token
- Billing: Free.
- Group: Email
Puts the published defaults back — block above 60 and on disposable, typo-squat, parked, blocklisted and no-reply — and forgets when the policy was last changed.
## Example request
```bash
curl -X DELETE https://spaw.co/api/v1/email/policy \
-H "Authorization: Bearer sk_live_…"
```
## Responses
### 200 — The defaults, in force again.
## Error codes
- `UNAUTHENTICATED` — https://spaw.co/docs/errors/UNAUTHENTICATED
- `RATE_LIMITED` — https://spaw.co/docs/errors/RATE_LIMITED
---
Canonical page: https://spaw.co/docs/api/reset-email-policy · OpenAPI document: https://spaw.co/openapi.json · All endpoints: https://spaw.co/docs/api
---
# List the account's domain monitors
`GET /api/v1/email/domain-monitors`
- Authentication: Secret API key as a bearer token
- Billing: Free.
- Group: Email
The saved domain lists this account re-checks on a schedule, newest first, 100 per page. Each monitor carries the summary of its last run: how many domains were ok, flagged (parked, mail hosts on a threat blocklist, a registration ending within 30 days, no SPF, no DMARC, or a provider, SPF, DMARC, DKIM or MTA-STS change since the previous run), dead (no usable mail servers) or invalid (not a hostname), how many changed, how many newly need attention, the credits the run spent, and `stopped_reason` when it did not finish cleanly: `insufficient_credits` or `key_spend_cap` where the run stopped part-way, `run_failed` where it died outright, and `run_failed_retrying` where it died and the schedule has been pulled in to check again — `retry_at` in the same block says when that becomes due, and the hourly sweep picks it up at or after that, so between 45 minutes and about an hour and three quarters later. A summary carrying a `stopped_reason` of either failure kind has NO counts in it at all: the run produced none, so read a missing count as unknown rather than as zero. The domains and their answers are on the single-monitor endpoint.
## Parameters
| Name | In | Type | Required | Description |
| --- | --- | --- | --- | --- |
| `page` | query | integer | no | The page to return. Default: 1. |
## Example request
```bash
curl https://spaw.co/api/v1/email/domain-monitors \
-H "Authorization: Bearer sk_live_…"
```
## Responses
### 200 — One page of monitors.
```json
{
"success": true,
"data": {
"monitors": [
{
"id": 4,
"name": "Sending domains",
"cadence": "daily",
"domain_count": 2,
"next_run_at": "2026-09-14T08:00:00+00:00",
"last_run_at": "2026-09-13T08:00:03+00:00",
"last_summary": {
"total": 2,
"ok": 1,
"flagged": 1,
"dead": 0,
"invalid": 0,
"changed": 1,
"newly_flagged": 1,
"credits_used": 0,
"stopped_reason": null
},
"webhook_url": null,
"webhook_events": "changes",
"webhook_status": null,
"webhook_detail": null,
"prunes_after_days": 365,
"created_at": "2026-09-01T08:00:00+00:00"
}
]
},
"meta": {
"page": 1,
"per_page": 100,
"total": 1,
"retention_days": 365
}
}
```
### 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"
}
}
```
### 429 — Over 5 requests per second for the key. Retry after the limit resets.
```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
- `RATE_LIMITED` — https://spaw.co/docs/errors/RATE_LIMITED
---
Canonical page: https://spaw.co/docs/api/list-domain-monitors · OpenAPI document: https://spaw.co/openapi.json · All endpoints: https://spaw.co/docs/api
---
# Monitor a list of domains on a schedule
`POST /api/v1/email/domain-monitors`
- Authentication: Secret API key as a bearer token
- Billing: Creation is free. Each run bills every domain like one domain lookup: 1 credit when the domain accepts mail; dead and invalid domains and 7-day repeats are free, so a domain is billed at most once a week whatever the cadence.
- Group: Email
Saves up to 500 domains — your own sending domains, or the domains your list is full of — and re-checks them every day, week or month through the domain endpoint. Domains are lowercased, converted to punycode and deduplicated. The first run starts right away and only sets the baseline; from the next run on, every domain that changes mail provider, loses its SPF, DMARC, DKIM keys or MTA-STS policy, is parked, lands its mail hosts on a threat blocklist, has a registration ending within 30 days, or stops accepting mail is reported to the account's email with the reason.
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": "domain_monitor.run", "monitor": { "id", "name", "cadence", "last_run_at", "next_run_at" }, "summary": { … the same block as `last_summary` … }, "changes": [ … ] }`, where `changes` names the domains the alert email names. 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; the original `X-Spaw-Signature` is sent beside it until 1 March 2027. Connection errors and 5xx answers are retried twice, 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 | Required | Description |
| --- | --- | --- | --- |
| `name` | string | yes | A label for the alerts. At most 100 characters. |
| `domains` | string[] | yes | 1 to 500 domain names. Spellings of one domain (case, a trailing dot, an internationalised form) count once; anything that is not a hostname answers 422 naming the entry. |
| `cadence` | string | yes | How often the list is re-checked. One of: daily, weekly, monthly. |
| `webhook_url` | string | null | no | 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 | no | When to post. `changes` posts only when the run found something new or stopped early; `every_run` posts after every run. One of: changes, every_run. Default: changes. |
## Example request
```bash
curl -X POST https://spaw.co/api/v1/email/domain-monitors \
-H "Authorization: Bearer sk_live_…" \
-H "Content-Type: application/json" \
-d '{
"name": "Sending domains",
"domains": [
"acme.com",
"news.acme.com"
],
"cadence": "daily"
}'
```
## Responses
### 201 — The monitor was saved and its baseline run queued.
```json
{
"success": true,
"data": {
"monitor": {
"id": 4,
"name": "Sending domains",
"cadence": "daily",
"domain_count": 2,
"next_run_at": "2026-09-14T10:12:44+00:00",
"last_run_at": null,
"last_summary": null,
"webhook_url": null,
"webhook_events": "changes",
"webhook_status": null,
"webhook_detail": null,
"prunes_after_days": 365,
"created_at": "2026-09-13T10:12:44+00:00",
"webhook_secret": null
}
}
}
```
### 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"
}
}
```
### 422 — The request body could not be validated; `error.errors` lists the fields.
```json
{
"success": false,
"error": {
"code": "VALIDATION_FAILED",
"message": "The email field is required.",
"errors": {
"email": [
"The email field is required."
]
},
"request_id": "req_01m1kgdm4xngzmbmff68g94w0c"
}
}
```
### 429 — Over 5 requests per second for the key. Retry after the limit resets.
```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
- `VALIDATION_FAILED` — https://spaw.co/docs/errors/VALIDATION_FAILED
- `RATE_LIMITED` — https://spaw.co/docs/errors/RATE_LIMITED
---
Canonical page: https://spaw.co/docs/api/create-domain-monitor · OpenAPI document: https://spaw.co/openapi.json · All endpoints: https://spaw.co/docs/api
---
# Read a domain monitor and its last results
`GET /api/v1/email/domain-monitors/{monitorId}`
- Authentication: Secret API key as a bearer token
- Billing: Free.
- Group: Email
The monitor plus `results`: for every domain from the last run, its status (`ok`, `flagged`, `dead` or `invalid`), the flags that fired (`parked`, `blocklisted`, `expiring`, `no_spf`, `no_dmarc`, and the ones that compare with the previous run: `provider_changed`, `spf_changed`, `dmarc_changed`, `dkim_lost`, `mta_sts_lost`), whether it changed since the previous run, and the facts the next run compares against; keyed by the domain in punycode, or null until the first run has finished. A monitor that belongs to another account answers `404`.
## Parameters
| Name | In | Type | Required | Description |
| --- | --- | --- | --- | --- |
| `monitorId` | path | integer | yes | The monitor's id from the list endpoint. |
## Example request
```bash
curl https://spaw.co/api/v1/email/domain-monitors/4 \
-H "Authorization: Bearer sk_live_…"
```
## Responses
### 200 — The monitor.
```json
{
"success": true,
"data": {
"monitor": {
"id": 4,
"name": "Sending domains",
"cadence": "daily",
"domain_count": 2,
"next_run_at": "2026-09-14T08:00:00+00:00",
"last_run_at": "2026-09-13T08:00:03+00:00",
"last_summary": {
"total": 2,
"ok": 1,
"flagged": 1,
"dead": 0,
"invalid": 0,
"changed": 1,
"newly_flagged": 1,
"credits_used": 0,
"stopped_reason": null
},
"webhook_url": null,
"webhook_events": "changes",
"webhook_status": null,
"webhook_detail": null,
"prunes_after_days": 365,
"created_at": "2026-09-01T08:00:00+00:00",
"results": {
"acme.com": {
"status": "flagged",
"flags": [
"no_dmarc",
"provider_changed",
"dmarc_changed"
],
"changed": true,
"valid": true,
"mx_found": true,
"mx_provider": "other",
"mx_provider_type": "self_hosted",
"has_spf": true,
"spf_all": "-all",
"dmarc_policy": null,
"dkim_selectors": [],
"mta_sts": true,
"parked": false,
"mx_blocklisted": false,
"domain_expires_at": "2027-03-12"
},
"news.acme.com": {
"status": "ok",
"flags": [],
"changed": false,
"valid": true,
"mx_found": true,
"mx_provider": "google",
"mx_provider_type": "mailbox_provider",
"has_spf": true,
"spf_all": "-all",
"dmarc_policy": "reject",
"dkim_selectors": [
"google"
],
"mta_sts": true,
"parked": false,
"mx_blocklisted": false,
"domain_expires_at": "2027-03-12"
}
}
}
}
}
```
### 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"
}
}
```
### 429 — Over 5 requests per second for the key. Retry after the limit resets.
```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
- `RATE_LIMITED` — https://spaw.co/docs/errors/RATE_LIMITED
---
Canonical page: https://spaw.co/docs/api/get-domain-monitor · OpenAPI document: https://spaw.co/openapi.json · All endpoints: https://spaw.co/docs/api
---
# Change a domain monitor
`PATCH /api/v1/email/domain-monitors/{monitorId}`
- Authentication: Secret API key as a bearer token
- Billing: Free. The change applies to the next run.
- Group: Email
Changes a saved monitor in place: rename it, move it to another cadence, replace the domains, or point it at a webhook. Send only the fields you are changing; a body that names none of them answers `422` rather than quietly changing nothing.
Replacing `domains` replaces the list. What the last run recorded about the domains that stayed is kept, so their next run still compares against the answer they already had; what it recorded about the ones that are gone is deleted with them; a new entry starts with no baseline. A rename never touches a stored answer. Changing `cadence` neither skips a run nor starts a second one. Nothing is queued by this call; `POST .../run` is there to have it now.
Sending a `webhook_url` that differs from the one stored mints a new signing secret and returns it once, here; the URL it already has keeps the secret, and `null` removes the webhook. A monitor that belongs to another account answers `404`.
## Parameters
| Name | In | Type | Required | Description |
| --- | --- | --- | --- | --- |
| `monitorId` | path | integer | yes | The monitor's id from the list endpoint. |
## Request body
| Field | Type | Required | Description |
| --- | --- | --- | --- |
| `name` | string | no | At most 100 characters. |
| `domains` | string[] | no | |
| `cadence` | string | no | One of: daily, weekly, monthly. |
| `webhook_url` | string | null | no | At most 2,048 characters. |
| `webhook_events` | string | no | One of: changes, every_run. |
## Example request
```bash
curl -X PATCH https://spaw.co/api/v1/email/domain-monitors/4 \
-H "Authorization: Bearer sk_live_…" \
-H "Content-Type: application/json" \
-d '{
"domains": [
"acme.com",
"shop.acme.com"
],
"cadence": "weekly"
}'
```
## Responses
### 200 — The monitor as it now stands.
### 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"
}
}
```
### 422 — The request body could not be validated; `error.errors` lists the fields.
```json
{
"success": false,
"error": {
"code": "VALIDATION_FAILED",
"message": "The email field is required.",
"errors": {
"email": [
"The email field is required."
]
},
"request_id": "req_01m1kgdm4xngzmbmff68g94w0c"
}
}
```
### 429 — Over 5 requests per second for the key. Retry after the limit resets.
```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
- `VALIDATION_FAILED` — https://spaw.co/docs/errors/VALIDATION_FAILED
- `RATE_LIMITED` — https://spaw.co/docs/errors/RATE_LIMITED
---
Canonical page: https://spaw.co/docs/api/update-domain-monitor · OpenAPI document: https://spaw.co/openapi.json · All endpoints: https://spaw.co/docs/api
---
# Stop monitoring a domain list
`DELETE /api/v1/email/domain-monitors/{monitorId}`
- Authentication: Secret API key as a bearer token
- Billing: Free.
- Group: Email
Removes the monitor and its stored results; nothing further is scheduled. A run that is already queued finishes on its own and bills as usual. A monitor that belongs to another account answers `404`.
## Parameters
| Name | In | Type | Required | Description |
| --- | --- | --- | --- | --- |
| `monitorId` | path | integer | yes | The monitor's id from the list endpoint. |
## Example request
```bash
curl -X DELETE https://spaw.co/api/v1/email/domain-monitors/4 \
-H "Authorization: Bearer sk_live_…"
```
## Responses
### 200 — The monitor was removed.
```json
{
"success": true
}
```
### 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"
}
}
```
### 429 — Over 5 requests per second for the key. Retry after the limit resets.
```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
- `RATE_LIMITED` — https://spaw.co/docs/errors/RATE_LIMITED
---
Canonical page: https://spaw.co/docs/api/delete-domain-monitor · OpenAPI document: https://spaw.co/openapi.json · All endpoints: https://spaw.co/docs/api
---
# Re-check a monitored domain list now
`POST /api/v1/email/domain-monitors/{monitorId}/run`
- Authentication: Secret API key as a bearer token
- Billing: The run bills every domain like one domain lookup; dead and invalid domains and 7-day repeats are free.
- Group: Email
Queues a run right away instead of waiting for the schedule; when it finishes, the next scheduled run is set one cadence from then. Answers `202` with the monitor as it stands before the run; poll it for the new `last_run_at`, `last_summary` and `results`. A monitor that belongs to another account answers `404`.
## Parameters
| Name | In | Type | Required | Description |
| --- | --- | --- | --- | --- |
| `monitorId` | path | integer | yes | The monitor's id from the list endpoint. |
## Example request
```bash
curl -X POST https://spaw.co/api/v1/email/domain-monitors/4/run \
-H "Authorization: Bearer sk_live_…"
```
## Responses
### 202 — The run was queued.
### 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"
}
}
```
### 429 — Over 5 requests per second for the key. Retry after the limit resets.
```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
- `RATE_LIMITED` — https://spaw.co/docs/errors/RATE_LIMITED
---
Canonical page: https://spaw.co/docs/api/run-domain-monitor · OpenAPI document: https://spaw.co/openapi.json · All endpoints: https://spaw.co/docs/api
---
# List the account's verification history
`GET /api/v1/email/history`
- Authentication: Secret API key as a bearer token
- Billing: Free.
- Group: History
Every email lookup the account ran through any channel — the API, the dashboard, the form widget, batch and bulk runs, monitors, the MCP server — newest first, 100 per page, for the retention window (`meta.retention_days`, 30 days by default). Each row records the verdict, the reason, the risk score, whether the mailbox handshake answered, what the lookup cost, whether it was a 7-day repeat, and the channel that ran it as `via`.
Rows are written after billing, so a refused lookup never appears here, and test addresses are never logged. Filters combine. An unknown verdict or an unparseable date answers `422`.
## Parameters
| Name | In | Type | Required | Description |
| --- | --- | --- | --- | --- |
| `page` | query | integer | no | The page to return. Default: 1. |
| `verdict` | query | string | no | Only lookups with this verdict. One of: deliverable, risky, undeliverable. |
| `email` | query | string | no | Only lookups of this exact address, compared case-insensitively against the address as it was checked. At most 254 characters. |
| `since` | query | string | no | Only lookups on or after this date or timestamp (ISO 8601). |
| `until` | query | string | no | Only lookups on or before this timestamp; a bare date covers the whole day. |
| `via` | query | string | no | Only lookups from this channel, matched exactly: `api · {key name}`, `dashboard`, `widget · {key name}`, `bulk run #{id}`, `monitor · {name}`, or `mcp · {key name}`. At most 120 characters. |
## Example request
```bash
curl https://spaw.co/api/v1/email/history \
-H "Authorization: Bearer sk_live_…"
```
## Responses
### 200 — One page of history rows.
```json
{
"success": true,
"data": {
"lookups": [
{
"id": 9310,
"email": "mia@acme.com",
"deliverable": "deliverable",
"reason": null,
"risk_score": 0,
"mx_provider": "google",
"smtp_checked": true,
"credits_used": 1,
"cache_hit": false,
"via": "api · Production key",
"created_at": "2026-09-03T10:12:44+00:00"
},
{
"id": 9309,
"email": "info@example.org",
"deliverable": "risky",
"reason": "role",
"risk_score": 30,
"mx_provider": "other",
"smtp_checked": true,
"credits_used": 0,
"cache_hit": true,
"via": "dashboard",
"created_at": "2026-09-03T09:58:02+00:00"
}
]
},
"meta": {
"page": 1,
"per_page": 100,
"total": 2,
"retention_days": 30
}
}
```
### 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"
}
}
```
### 422 — The request body could not be validated; `error.errors` lists the fields.
```json
{
"success": false,
"error": {
"code": "VALIDATION_FAILED",
"message": "The email field is required.",
"errors": {
"email": [
"The email field is required."
]
},
"request_id": "req_01m1kgdm4xngzmbmff68g94w0c"
}
}
```
### 429 — Over 5 requests per second for the key. Retry after the limit resets.
```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
- `VALIDATION_FAILED` — https://spaw.co/docs/errors/VALIDATION_FAILED
- `RATE_LIMITED` — https://spaw.co/docs/errors/RATE_LIMITED
---
Canonical page: https://spaw.co/docs/api/list-history · OpenAPI document: https://spaw.co/openapi.json · All endpoints: https://spaw.co/docs/api
---
# Read the account and credit balance
`GET /api/v1/account`
- Authentication: Secret API key as a bearer token
- Billing: Free.
- Group: Account
The account behind the presented key: its name and email, the plan (`free`, or the key of the plan while it is paid up, monthly or yearly: `starter`, `growth`, `scale`, `volume`, `pro`, `business` or `enterprise`), the live credit balance, and the monthly free grant. Useful for dashboards and pre-flight checks before a big run. Reading the balance applies the month's free grant if it has not been applied yet.
## Example request
```bash
curl https://spaw.co/api/v1/account \
-H "Authorization: Bearer sk_live_…"
```
## Responses
### 200 — The account.
```json
{
"success": true,
"data": {
"name": "Mia Kowalski",
"email": "mia@acme.com",
"plan": "free",
"credits": {
"balance": 99,
"monthly_grant": 100
}
}
}
```
### 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"
}
}
```
### 429 — Over 5 requests per second for the key. Retry after the limit resets.
```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
- `RATE_LIMITED` — https://spaw.co/docs/errors/RATE_LIMITED
---
Canonical page: https://spaw.co/docs/api/get-account · OpenAPI document: https://spaw.co/openapi.json · All endpoints: https://spaw.co/docs/api
---
# Look up one request by its id
`GET /api/v1/account/requests/{requestId}`
- Authentication: Secret API key as a bearer token
- Billing: Free.
- Group: Account
Every response carries an `X-Request-Id` header and repeats the same id in its `meta` or `error` block. This reads that id back: the endpoint that was called, the status and typed `error_code` it answered with, the credits it spent and how long it took. It is what a script that caught a failure can call to find out what happened, and what to quote to support.
The row carries no lookup input. The endpoint is named by its route, never by the path it was called on, so this can never read back the address, number or identifier a call was about. Rows are kept for `meta.retention_days` and then deleted; an id older than that, or belonging to another account, answers `404 NOT_FOUND`.
## Parameters
| Name | In | Type | Required | Description |
| --- | --- | --- | --- | --- |
| `requestId` | path | string | yes | The id from the X-Request-Id header, e.g. req_01m1kgdm4xngzmbmff68g94w0c. |
## Example request
```bash
curl https://spaw.co/api/v1/account/requests/{requestId} \
-H "Authorization: Bearer sk_live_…"
```
## Responses
### 200 — The request as it was recorded.
```json
{
"success": true,
"data": {
"request_id": "req_01m1kgdm4xngzmbmff68g94w0c",
"route": "api.v1.email",
"method": "POST",
"status": 402,
"error_code": "INSUFFICIENT_CREDITS",
"credits_used": 0,
"duration_ms": 8,
"via": "api · Production",
"occurred_at": "2026-09-09T09:41:02+00:00"
},
"meta": {
"retention_days": 30,
"request_id": "req_01m22gaxhnj60f71ez5tjcdcvy"
}
}
```
### 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"
}
}
```
### 429 — Over 5 requests per second for the key. Retry after the limit resets.
```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
- `RATE_LIMITED` — https://spaw.co/docs/errors/RATE_LIMITED
---
Canonical page: https://spaw.co/docs/api/get-request · OpenAPI document: https://spaw.co/openapi.json · All endpoints: https://spaw.co/docs/api
---
# Read the credit ledger
`GET /api/v1/account/ledger`
- Authentication: Secret API key as a bearer token
- Billing: Free.
- Group: Account
Every credit movement on the account, newest first: lookups debited, monthly grants, purchased packs and plan deposits. Each row carries the signed `delta`, the `balance_after` it left, the `reason`, an optional `reference` (a Stripe session or invoice id for purchases), and `via`, the label of the key, widget, bulk run or monitor that spent it. Each row spent through a secret key also carries that key's `api_token_id`, which is what tells two keys of the same name apart and what a rename does not move; it is null for the dashboard, for bulk runs and monitors, and for grants and purchases. An `ip_lookup` row is one credit paying for five answered IP lookups: the lookup that opens a block writes it, and the four after it write nothing (rows written before IP lookups were billed in blocks stand for one lookup each). Filter with `since` and `until` (ISO 8601 dates), `reason`, `via` and `api_token_id`; page with `page` and `per_page` (up to 200). The dashboard's Usage page offers the same rows as a CSV download.
## Parameters
| Name | In | Type | Required | Description |
| --- | --- | --- | --- | --- |
| `since` | query | string | no | Only rows on or after this date. |
| `until` | query | string | no | Only rows on or before this date. |
| `reason` | query | string | no | Only rows with this reason, e.g. email_lookup, ip_lookup, monthly_grant, purchase, subscription, plan_credits_expired. |
| `via` | query | string | no | Only rows spent through this label, e.g. "api · Production". |
| `api_token_id` | query | integer | no | Only rows spent by this secret key. An id belonging to another account matches no rows, because the ledger is scoped to the caller first. |
| `page` | query | integer | no | |
| `per_page` | query | integer | no | Default: 50. |
## Example request
```bash
curl https://spaw.co/api/v1/account/ledger \
-H "Authorization: Bearer sk_live_…"
```
## Responses
### 200 — One page of ledger rows.
```json
{
"success": true,
"data": [
{
"id": 9812,
"delta": -1,
"balance_after": 99,
"lookups": 5,
"reason": "ip_lookup",
"reference": null,
"via": "api · Production",
"api_token_id": 41,
"created_at": "2026-09-04T10:12:44+00:00"
},
{
"id": 9801,
"delta": 100,
"balance_after": 100,
"lookups": 0,
"reason": "monthly_grant",
"reference": "2026-09",
"via": null,
"api_token_id": null,
"created_at": "2026-09-01T00:00:03+00:00"
}
],
"meta": {
"page": 1,
"per_page": 50,
"total": 2,
"request_id": "req_01m1kgdm4xngzmbmff68g94w0c"
}
}
```
### 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"
}
}
```
### 422 — The request body could not be validated; `error.errors` lists the fields.
```json
{
"success": false,
"error": {
"code": "VALIDATION_FAILED",
"message": "The email field is required.",
"errors": {
"email": [
"The email field is required."
]
},
"request_id": "req_01m1kgdm4xngzmbmff68g94w0c"
}
}
```
### 429 — Over 5 requests per second for the key. Retry after the limit resets.
```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
- `VALIDATION_FAILED` — https://spaw.co/docs/errors/VALIDATION_FAILED
- `RATE_LIMITED` — https://spaw.co/docs/errors/RATE_LIMITED
---
Canonical page: https://spaw.co/docs/api/list-ledger · OpenAPI document: https://spaw.co/openapi.json · All endpoints: https://spaw.co/docs/api
---
# Health, latency and data freshness
`GET /api/v1/status/data`
- Authentication: No authentication
- Billing: Free.
- Group: Status
The facts behind the public status page, without authentication: the state of each component behind an answer (the database, IP geolocation, the mailbox handshake, the live carrier check, and the server-side renderer behind the website), the sync date, freshness and range counts of every dataset behind the lookups, measured API latency per endpoint group over the last hour and the last 24 hours, and ninety days of measured availability rolled up a day at a time.
`ok` is false when any component is degraded; a component that was never switched on for this deployment reads `not_configured` and does not make it false. A degraded component that is holding off says when it will be retried in `retry_in_seconds`. Percentiles are computed over the requests that succeeded — a refusal that never reached a provider is not evidence the API is fast — and `success_rate` reports the share that succeeded, over `sample` requests of which `answered` were answered. `error_rate` is the share that failed on this side — a 402, 429 or 422 is an answer about the request, not a fault here, so only `error_rate` is a reason to worry. A window with fewer than 20 requests is not published at all, and the percentiles are withheld unless at least 20 of them were answered. The weekly quality measurements published on the status page — the delivery outcomes customers reported back against the email verdicts we gave, and the business-register round trip — are page copy and are deliberately not in this payload: they are measured once a week and dated on the page, and a monitor polling for one would be polling for last Sunday. Point a monitor at it, or read it before a big run to know how fresh the feeds are. Throttled at 60 requests per minute per IP.
## Example request
```bash
curl https://spaw.co/api/v1/status/data
```
## Responses
### 200 — The current status.
```json
{
"success": true,
"data": {
"ok": false,
"checked_at": "2026-09-04T18:00:00+00:00",
"components": [
{
"component": "database",
"name": "Database",
"state": "operational",
"detail": "Queries are answering.",
"retry_in_seconds": null
},
{
"component": "ip-geolocation",
"name": "IP geolocation",
"state": "operational",
"detail": "The geolocation database is installed.",
"retry_in_seconds": null
},
{
"component": "mailbox-handshake",
"name": "Mailbox handshake",
"state": "degraded",
"detail": "Probes are paused after repeated upstream failures, so addresses answer unverified instead of waiting out a timeout. The next attempt is in 240 seconds.",
"retry_in_seconds": 240
},
{
"component": "live-carrier-check",
"name": "Live carrier check",
"state": "not_configured",
"detail": "Not switched on for this deployment: phone verdicts come from the numbering data alone.",
"retry_in_seconds": null
},
{
"component": "ssr",
"name": "Server-side rendering",
"state": "operational",
"detail": "Pages are rendered on the server before they are sent.",
"retry_in_seconds": null
}
],
"datasets": [
{
"dataset": "dbip-city-lite",
"version": "2026-08-01",
"installed": true,
"feeds": null,
"freshness": "current",
"age_days": 34
},
{
"dataset": "datacenter-ranges",
"version": "2026-09-04",
"installed": true,
"feeds": 12,
"freshness": "current",
"age_days": 0
},
{
"dataset": "libphonenumber",
"version": "9.0.37",
"installed": true,
"feeds": null,
"freshness": "not_dated",
"age_days": null
}
],
"latency": [
{
"group": "ip",
"window": "1h",
"p50_ms": 2,
"p95_ms": 6,
"success_rate": 100,
"error_rate": 0,
"sample": 812,
"answered": 812
},
{
"group": "email",
"window": "1h",
"p50_ms": 410,
"p95_ms": 2100,
"success_rate": 98.42,
"error_rate": 0.08,
"sample": 1204,
"answered": 1185
}
],
"availability": {
"days": 90,
"since": "2026-06-13",
"until": "2026-09-10",
"measured_days": 62,
"days_without_requests": 28,
"requests": 184320,
"answered": 184102,
"failed": 218,
"availability": 99.88,
"history": [
{
"date": "2026-09-09",
"requests": 3140,
"answered": 3138,
"failed": 2,
"availability": 99.94,
"state": "operational",
"complete": true
},
{
"date": "2026-09-10",
"requests": 812,
"answered": 812,
"failed": 0,
"availability": 100,
"state": "operational",
"complete": false
},
{
"date": "2026-06-13",
"requests": 0,
"answered": 0,
"failed": 0,
"availability": null,
"state": "no_requests",
"complete": true
}
]
}
}
}
```
### 429 — Over 5 requests per second for the key. Retry after the limit resets.
```json
{
"success": false,
"error": {
"code": "RATE_LIMITED",
"message": "Too many requests. Retry after the limit resets.",
"request_id": "req_01m1kgdm4xngzmbmff68g94w0c"
}
}
```
## Error codes
- `RATE_LIMITED` — https://spaw.co/docs/errors/RATE_LIMITED
---
Canonical page: https://spaw.co/docs/api/get-data-status · OpenAPI document: https://spaw.co/openapi.json · All endpoints: https://spaw.co/docs/api
---
# Validate and enrich a phone number
`POST /api/v1/phone`
- Authentication: Secret API key as a bearer token
- Billing: 1 credit for a fresh lookup that answers `valid` true; invalid numbers (including an unassigned block) and 7-day repeats are free. The base credit is charged once per number every 7 days whether or not the lookup asked for the live check, so a check after a plain lookup costs only the check and a plain lookup after a check is free. Asking for the live check (hlr) adds 4 credits only when it actually answered, so a fresh lookup with a check costs 5 in all. Once the account has paid for a check of the number, a repeated check within 7 days is free while the network's answer is held (a day); a repeat after that asks the network again and is charged the 4 credits, reserved up front. A check the account has not paid for in the last 7 days is charged the 4 credits whenever it answers, even from a held answer. Paying for the check alone does not restart the base credit's 7 days. A fictional number or an unassigned block is never checked, and never charged for the check.
- Group: Phone
Parses a number with libphonenumber metadata and answers whether it is valid for its region, its E.164, national, international and `tel:` forms, the country and its facts (name, calling code, continent, EU membership, currency), the line type, the carrier, the geographic description and the time zones. Pass `country` (an ISO 3166-1 alpha-2 code) to parse numbers written without a country calling code; a secret key created with a default country supplies it whenever a request names none.
Where a regulator publishes its number-block allocations (NANPA for the United States and its territories, the CNAC for Canada, Ofcom for the United Kingdom and the Crown dependencies), the lookup also reads the block the number sits in: `block_status` says whether the block is allocated, and a geographic number in a block nobody holds answers `valid: false` with reason `unassigned_block`. The block holder names the `carrier` (`carrier_source: block_allocation`) and, for the North American plan's ambiguous `fixed_line_or_mobile`, settles the `line_type` when the holder is a wireless carrier (`mobile`) or a virtual-number wholesaler (`voip`); `line_type_source` says where the answer came from. Neither is port-aware: both reflect who the numbers were allocated to, not who serves them today.
Three more free signals: `is_fictional` for ranges reserved for fiction (the NANP 555-01XX block; the drama ranges and numbers of Ofcom, ARCEP, ACMA and ComReg), `is_disposable` for numbers published by SMS-receiving websites, and `is_virtual` for blocks held by virtual-number wholesalers. Emergency numbers and short codes answer `valid: false` with reason `emergency_number` or `short_number`. The `risk_score` sums the weights of the signals in `risk_signals` (fictional 80, disposable 70, disposable_stale 40 for a listing that is no longer fresh (last seen more than a year ago, or more than 90 days ago once every list dropped the number), reported_abuse 60 and reported_undelivered 50 from your own SMS outcome reports, reported_abuse_widely 40 when three or more accounts reported abuse for the number within 30 days, premium_rate 60 (in the UK, the 09 range), reserved_block 40, virtual 40, range_burst 40 when your account looked up several numbers in one 1,000-number range within the window, voip 30, pager 30, digit_pattern 30, numbers_per_client 30 when one client IP tried several numbers, disposable_range 30 for a number not listed itself in a 1,000-number range holding ten or more SMS-receiving listings that still score disposable, international_network 30, shared_cost 20 (including the UK 084 and 087 ranges, which Ofcom's plan classes as capped service numbers), reported_unwanted_calls 20 when consumers complained to the FTC about a North American caller three or more times on at least two days within 90 days; 60 and above is `high`, 30 and above `medium`), each with the dataset that produced it. Signals answer `null` when their dataset is not installed, never a guess.
Every answer ends with `ok_to_send` and `blocked_by`: your account's SMS policy (`GET /api/v1/phone/policy`) applied to the answer — the countries and line types you send to, the signals that block, a ceiling on the score, whether a number your suppression list holds as opted out is blocked and, if you require it, a live check that reached the handset. The defaults block a number held as opted out, a score above 60, and the signals fictional, disposable, reported_abuse, reported_abuse_widely and premium_rate, so the fictional example +1 415 555 0142 answers `ok_to_send: false` with `blocked_by: fictional`. It is a fraud and deliverability decision, not consent and not a do-not-call scrub. `client_ip`, the address of the person behind the lookup, is counted for `client_velocity` and never looked up or billed; `call_window` ("09:00-20:00" in the number's local time) answers `in_call_window`, next to `local_time` and `utc_offsets`, which every answer carries: the zone is where the number was issued, not where the person is, and the answer is clock arithmetic, not compliance advice. An invalid answer to a number sent with a `country` may carry `did_you_mean`, the one valid reading of what was typed (a missing `+`, a foreign international prefix left in); it is never applied. `meta.opted_out` says your suppression list holds the number as opted out (see `POST /api/v1/phone/feedback`). The number is still answered and scored in full, and its score never changes; under the default SMS policy the answer is `ok_to_send: false` with `blocked_by: opted_out`. An OTP or transactional sender may switch `block_opted_out` off, because an opt-out from marketing does not always forbid a verification code and the person typing the number may be its owner asking for one again; honouring an opt-out stays your obligation.
`hlr: true` asks for the live carrier-network check (reachable, ported, current carrier, roaming). The live carrier-network check (`hlr: true`) is not enabled on the service: `hlr_checked` answers false and nothing extra is charged. Once it is, a definitive answer is shared across customers for a day, so a handset is queried at most once a day, and the premium credits are charged only when the network answered.
Eight test numbers in the Washington 555-01XX fiction block answer a fixed result each, cost nothing and leave no repeat marker, so integration tests never spend credits or depend on the day's data: +1 202 555 0100 (mobile), 0101 (landline), 0102 (virtual number), 0103 (disposable), 0104 (unassigned block), 0105 (fictional), 0106 (invalid) and 0113 (in a range crowded with SMS-receiving numbers). Their `sources` name the `test-numbers` dataset.
The endpoint answers under the same envelope, billing and error codes as every other lookup; the phone intelligence guide at https://spaw.co/docs/phone-intelligence explains every field and where it comes from.
## Request body
| Field | Type | Required | Description |
| --- | --- | --- | --- |
| `phone` | string | yes | The number, in any common notation. Include the country calling code or pass `country`. At most 64 characters. |
| `country` | string | null | no | An ISO 3166-1 alpha-2 region code (`US`, `GB`, `PL`) used to parse numbers written without a calling code, and to recognise that region's emergency numbers and short codes. |
| `hlr` | boolean | no | Run the live carrier-network check. The live carrier-network check (`hlr: true`) is not enabled on the service: `hlr_checked` answers false and nothing extra is charged. Once it is, it reserves the premium credits up front and charges them only when the check answers. The base credit is shared with a plain lookup of the same number, so a check within 7 days of one costs only the premium. Default: . |
| `client_ip` | string | null | no | The IP address of the person behind the lookup (your visitor, not your server), such as the address your signup form saw. Only counted: the answer's `client_velocity` is the distinct numbers your account looked up from that address (an IPv6 address by its /64) in the last 60 minutes, and five or more add `numbers_per_client` unless Spaw's own IP data places the address on a mobile carrier or a corporate proxy, or it is private or reserved. Never looked up and never billed. |
| `call_window` | string | null | no | Your calling window in the number's local time, "HH:MM-HH:MM", from the start up to, not including, the end; a window may run past midnight ("21:00-08:00"). Answers `in_call_window`, true only when the local time in every zone the number can be in is inside it. The zone is where the number was issued, not where the person is, and the answer is clock arithmetic, not compliance advice. |
## Example request
```bash
curl -X POST https://spaw.co/api/v1/phone \
-H "Authorization: Bearer sk_sandbox_spaw" \
-H "Content-Type: application/json" \
-d '{
"phone": "+1 202 555 0100"
}'
```
## Responses
### 200 — The parsed number; `valid: false` with a `reason` for numbers that do not parse, are not assignable, or sit in a block nobody holds.
```json
{
"success": true,
"data": {
"valid": true,
"reason": null,
"e164": "+12025550100",
"national": "(202) 555-0100",
"international": "+1 202-555-0100",
"rfc3966": "tel:+1-202-555-0100",
"country": "US",
"country_name": "United States",
"calling_code": 1,
"continent": "NA",
"is_eu": false,
"currency": "USD",
"line_type": "mobile",
"line_type_source": "block_allocation",
"carrier": "Spaw Test Wireless",
"carrier_source": "block_allocation",
"block_status": "allocated",
"region": "Washington D.C.",
"is_geographic": true,
"timezones": [
"America/New_York"
],
"extension": null,
"is_fictional": false,
"is_virtual": false,
"is_disposable": false,
"disposable_seen_at": null,
"risk_score": 0,
"risk_level": "low",
"risk_signals": [],
"hlr_checked": false,
"reachable": null,
"ported": null,
"current_carrier": null,
"roaming": null,
"carrier_code": null,
"carrier_brand": null,
"block_pooled": null,
"block_assigned_at": null,
"region_level": "area",
"local_time": "2026-09-24T09:30:00-04:00",
"utc_offsets": [
"-04:00"
],
"in_call_window": null,
"did_you_mean": null,
"range_seen_by_you": null,
"client_velocity": null,
"destination_new_for_you": null,
"complaints_90d": null,
"complaints_last_at": null,
"ok_to_send": true,
"blocked_by": null,
"sources": [
{
"dataset": "test-numbers",
"version": "bundled"
}
]
},
"meta": {
"credits_used": 0,
"credits_remaining": 10,
"cache_hit": false,
"request_id": "req_01m1kgdm4xngzmbmff68g94w0c",
"suppressed": false,
"opted_out": false
}
}
```
### 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"
}
}
```
### 402 — The balance is empty. The lookup did not run.
```json
{
"success": false,
"error": {
"code": "INSUFFICIENT_CREDITS",
"message": "Your credit balance is empty.",
"request_id": "req_01m1kgdm4xngzmbmff68g94w0c"
}
}
```
### 422 — The request body could not be validated; `error.errors` lists the fields.
```json
{
"success": false,
"error": {
"code": "VALIDATION_FAILED",
"message": "The email field is required.",
"errors": {
"email": [
"The email field is required."
]
},
"request_id": "req_01m1kgdm4xngzmbmff68g94w0c"
}
}
```
### 429 — Over 5 requests per second for the key. Retry after the limit resets.
```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
- `VALIDATION_FAILED` — https://spaw.co/docs/errors/VALIDATION_FAILED
- `INSUFFICIENT_CREDITS` — https://spaw.co/docs/errors/INSUFFICIENT_CREDITS
- `RATE_LIMITED` — https://spaw.co/docs/errors/RATE_LIMITED
- `SANDBOX_VALUE_NOT_ALLOWED` — https://spaw.co/docs/errors/SANDBOX_VALUE_NOT_ALLOWED
- `SANDBOX_UNAVAILABLE` — https://spaw.co/docs/errors/SANDBOX_UNAVAILABLE
---
Canonical page: https://spaw.co/docs/api/validate-phone · OpenAPI document: https://spaw.co/openapi.json · All endpoints: https://spaw.co/docs/api
---
# Validate up to 1,000 numbers in one call
`POST /api/v1/phone/batch`
- Authentication: Secret API key as a bearer token
- Billing: Each number bills like a single lookup: 1 credit for a fresh valid answer; invalid numbers and 7-day repeats are free. The call stops cleanly where the balance ends.
- Group: Phone
Runs the single-number lookup for every item, in input order, under the exact single-lookup billing rules. Every answer comes from local metadata and the synced regulator tables, so a full batch of 1,000 takes well under a second. `data.results[]` mirrors the single-number response per item and each item's `meta` carries `credits_used`, `cache_hit`, `suppressed` and `opted_out`.
Repeats inside the list are looked up once and answer as cache hits. The list is walked in order and stops where the balance would run out: running out of credits mid-batch returns the paid partial results with `meta.stopped_reason: "insufficient_credits"` and `processed` lower than `requested`, never discarding billed work. Only a batch whose first lookup is refused answers the typed `402`. A key with a daily credit cap stops the list the same way once the cap is spent: the numbers already paid for come back with `meta.stopped_reason: "key_spend_cap"`, and a call that starts with the cap already spent answers `429 KEY_SPEND_CAP_REACHED` and runs nothing. A string that is not a number is an answer (`valid: false`), not a validation error.
A batch counts as one request against the rate limit. `country` applies to every number in the list that does not name its own. `hlr: true` adds the live carrier-network check to every item: the premium credits are reserved per item before its lookup and charged only where the network answered. `call_window` answers `in_call_window` for every item.
Every item carries `index`, its zero-based position in the list you sent, and `input`, the number exactly as you sent it — the answer's `e164` is what the plan made of it. A batch stopped early by `stopped_reason` answers only a prefix of the list, so those two are what line an answer up with the row it came from.
## Request body
| Field | Type | Required | Description |
| --- | --- | --- | --- |
| `phones` | string[] | yes | 1 to 1,000 numbers, in any common notation. Each item is a plain string, or an object carrying its own country. |
| `phones[].phone` | string | yes | The number, in any common notation. At most 64 characters. |
| `phones[].country` | string | null | no | 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 | no | 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 | no | 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 under the single-lookup rules (free where the account paid for that number's check in the last 7 days and the network's answer is still held), and the balance is gated per item before any network query. Default: . |
| `call_window` | string | null | no | Your calling window in each number's local time, "HH:MM-HH:MM" from the start up to, not including, the end (a window may run past midnight), applied to every item. Answers `in_call_window`; the zone is where the number was issued, not where the person is, and the answer is arithmetic, not compliance advice. |
## Example request
```bash
curl -X POST https://spaw.co/api/v1/phone/batch \
-H "Authorization: Bearer sk_live_…" \
-H "Content-Type: application/json" \
-d '{
"phones": [
"(415) 555-0142",
{
"phone": "020 3013 0018",
"country": "GB"
},
"not a number"
],
"country": "US"
}'
```
## Responses
### 200 — One result per processed number, plus the batch totals.
```json
{
"success": true,
"data": {
"results": [
{
"index": 0,
"input": "(415) 555-0142",
"data": {
"valid": true,
"reason": null,
"e164": "+14155550142",
"country": "US",
"line_type": "fixed_line_or_mobile",
"is_fictional": true,
"risk_score": 80,
"risk_level": "high",
"ok_to_send": false,
"blocked_by": "fictional"
},
"meta": {
"credits_used": 1,
"cache_hit": false,
"suppressed": false,
"opted_out": false
}
},
{
"index": 1,
"input": "020 3013 0018",
"data": {
"valid": false,
"reason": "unassigned_block",
"e164": "+442030130018",
"country": "GB",
"block_status": "unassigned",
"is_fictional": false,
"risk_score": null,
"risk_level": null,
"ok_to_send": false,
"blocked_by": "invalid"
},
"meta": {
"credits_used": 0,
"cache_hit": false,
"suppressed": false,
"opted_out": false
}
},
{
"index": 2,
"input": "not a number",
"data": {
"valid": false,
"reason": "not_a_number",
"e164": null,
"country": null,
"risk_score": null,
"risk_level": null,
"ok_to_send": false,
"blocked_by": "invalid"
},
"meta": {
"credits_used": 0,
"cache_hit": false,
"suppressed": false,
"opted_out": false
}
}
]
},
"meta": {
"requested": 3,
"processed": 3,
"credits_used": 1,
"credits_remaining": 9,
"stopped_reason": null,
"request_id": "req_01m1kgdrtqdvwnks99vfgx2rcw"
}
}
```
### 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"
}
}
```
### 402 — The balance is empty. The lookup did not run.
```json
{
"success": false,
"error": {
"code": "INSUFFICIENT_CREDITS",
"message": "Your credit balance is empty.",
"request_id": "req_01m1kgdm4xngzmbmff68g94w0c"
}
}
```
### 422 — The request body could not be validated; `error.errors` lists the fields.
```json
{
"success": false,
"error": {
"code": "VALIDATION_FAILED",
"message": "The email field is required.",
"errors": {
"email": [
"The email field is required."
]
},
"request_id": "req_01m1kgdm4xngzmbmff68g94w0c"
}
}
```
### 429 — Over 5 requests per second for the key. Retry after the limit resets.
```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
- `VALIDATION_FAILED` — https://spaw.co/docs/errors/VALIDATION_FAILED
- `INSUFFICIENT_CREDITS` — https://spaw.co/docs/errors/INSUFFICIENT_CREDITS
- `KEY_SPEND_CAP_REACHED` — https://spaw.co/docs/errors/KEY_SPEND_CAP_REACHED
- `RATE_LIMITED` — https://spaw.co/docs/errors/RATE_LIMITED
---
Canonical page: https://spaw.co/docs/api/lookup-phone-batch · OpenAPI document: https://spaw.co/openapi.json · All endpoints: https://spaw.co/docs/api
---
# Report a number range from the regulator's table
`GET /api/v1/phone/block/{country}/{prefix}`
- Authentication: Secret API key as a bearer token
- Billing: 1 credit per answered report, free for the next seven days for the same prefix. A prefix or country with nothing to report costs nothing.
- Group: Phone
What the national regulator's own allocation table says about every block under one prefix: how many blocks are allocated, unassigned or reserved, who holds them and what share each holds, the network brand behind each holder and the share each brand holds, how much of the range is mobile or VoIP, and the place, time zones and line types the numbering plan names for it. For the North American tables it also counts the codes pooled into thousands-blocks — whose holder may not hold every number in them — and spans the dates the codes were assigned. The report counts codes; a lookup of a US number names the holder of the number's own thousands-block where NANPA lists it. Original allocation only — never who serves a number today, which no open table publishes.
It is the endpoint behind the public area-code pages, so screening a whole area code before a campaign, or checking a range a signup wave arrived from, is one call rather than a page scrape. `country` is an ISO 3166-1 alpha-2 code and `prefix` the digits of the range with no calling code: `/v1/phone/block/us/415`, `/v1/phone/block/gb/20`. A prefix written the way it is dialled inside the country, with the trunk prefix in front, is read without it, and the answer's `prefix`, the charge and the repeat marker are the stored form's: `gb/020` as `20`, `gb/0161` as `161` and `us/1212` as `212`. Italy, Poland and Mexico dial no trunk prefix, so their digits are read as written. A longer prefix narrows the report to the blocks underneath it.
Guernsey, Jersey and the Isle of Man (`gg`, `je`, `im`) are answered from GB's table, as their lookups are, keeping only the blocks whose numbers are theirs. A country whose numbering plan Spaw does not carry, and a prefix no block starts with (for a Crown dependency, one that reaches outside its own numbers, such as `/v1/phone/block/gg/20`), both answer `404 PHONE_BLOCK_NOT_FOUND` and cost nothing.
## Parameters
| Name | In | Type | Required | Description |
| --- | --- | --- | --- | --- |
| `country` | path | string | yes | An ISO 3166-1 alpha-2 code, in either case. 2 to 2 characters. |
| `prefix` | path | string | yes | The range, digits only and no calling code. A leading national trunk prefix (the UK's 0, the North American 1) is dropped before the table is read. |
## Example request
```bash
curl https://spaw.co/api/v1/phone/block/us/415 \
-H "Authorization: Bearer sk_live_…"
```
## Responses
### 200 — The report for that range.
```json
{
"success": true,
"data": {
"country": "US",
"country_name": "United States",
"calling_code": 1,
"prefix": "415",
"international_prefix": "+1 415",
"place": "San Francisco, CA",
"places": [],
"timezones": [
"America/Los_Angeles"
],
"line_types": [
"fixed_line_or_mobile"
],
"example": "+1 415-200-0000",
"blocks": {
"total": 792,
"allocated": 604,
"unassigned": 171,
"reserved": 17,
"pooled": 431
},
"assigned": {
"dated": 262,
"first": "2001-01-12",
"last": "2026-08-27"
},
"block_length": 6,
"kinds": {
"mobile": 41.2,
"voip": 12.7,
"unsettled": 46.1
},
"holders": [
{
"holder": "PACIFIC BELL",
"brand": "AT&T",
"kind": null,
"blocks": 96,
"share": 15.9
},
{
"holder": "T-MOBILE USA, INC.",
"brand": "T-Mobile",
"kind": "mobile",
"blocks": 71,
"share": 11.8
}
],
"brands": [
{
"brand": "AT&T",
"blocks": 158,
"share": 26.2
},
{
"brand": "T-Mobile",
"blocks": 104,
"share": 17.2
}
],
"feed": {
"key": "nanpa",
"dataset": "nanpa-co-codes",
"version": "2026-09-04",
"complete": true
}
},
"meta": {
"credits_used": 1,
"credits_remaining": 9,
"cache_hit": false,
"request_id": "req_01m1kgdm4xngzmbmff68g94w0c"
}
}
```
### 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"
}
}
```
### 402 — The balance is empty. The lookup did not run.
```json
{
"success": false,
"error": {
"code": "INSUFFICIENT_CREDITS",
"message": "Your credit balance is empty.",
"request_id": "req_01m1kgdm4xngzmbmff68g94w0c"
}
}
```
### 404 — No numbering plan covers the country, or no block starts with the prefix. Nothing was charged.
```json
{
"success": false,
"error": {
"code": "PHONE_BLOCK_NOT_FOUND",
"message": "No number block in US starts with 999.",
"request_id": "req_01m1kgdm4xngzmbmff68g94w0c"
}
}
```
### 429 — Over 5 requests per second for the key. Retry after the limit resets.
```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
- `INSUFFICIENT_CREDITS` — https://spaw.co/docs/errors/INSUFFICIENT_CREDITS
- `PHONE_BLOCK_NOT_FOUND` — https://spaw.co/docs/errors/PHONE_BLOCK_NOT_FOUND
- `RATE_LIMITED` — https://spaw.co/docs/errors/RATE_LIMITED
---
Canonical page: https://spaw.co/docs/api/phone-block-report · OpenAPI document: https://spaw.co/openapi.json · All endpoints: https://spaw.co/docs/api
---
# List the account's bulk phone lookup runs
`GET /api/v1/phone/bulk`
- Authentication: Secret API key as a bearer token
- Billing: Free.
- Group: Phone
Every bulk run this account has started, newest first, 100 per page. A job id is handed out once, at creation, and everything about a run hangs off it — its counts, its results, its cancel and resume calls — so this is how a caller that lost one finds the run again, and how a dashboard lists what is still in flight.
`status`, `since` and `until` are optional and combine; a bare date in `until` means the whole of that day, and an unknown status or an unparseable date is a `422` rather than an empty page. The rows are the same job objects `GET /api/v1/phone/bulk/{jobId}` answers. The webhook secret is never among them: it is returned once, at creation.
Runs are kept as long as the lookup history, and `meta.retention_days` says how long that is — a run older than that is deleted with its files and cannot be listed.
## Parameters
| Name | In | Type | Required | Description |
| --- | --- | --- | --- | --- |
| `status` | query | string | no | Only runs in this status. One of: queued, processing, completed, failed, cancelled. |
| `since` | query | string | no | Only runs created at or after this date or timestamp. |
| `until` | query | string | no | Only runs created at or before this. A bare date means the whole of that day. |
| `page` | query | integer | no | The page to return. Default: 1. |
## Example request
```bash
curl https://spaw.co/api/v1/phone/bulk \
-H "Authorization: Bearer sk_live_…"
```
## Responses
### 200 — One page of runs, newest first.
```json
{
"success": true,
"data": {
"jobs": [
{
"id": 88,
"status": "completed",
"total": 5000,
"duplicate_count": 40,
"processed": 5000,
"valid": 4610,
"invalid": 390,
"low": 4200,
"medium": 380,
"high": 30,
"credits_used": 4610,
"stopped_reason": null,
"cancel_requested": false,
"country": "US",
"live_check": false,
"webhook_status": null,
"webhook_detail": null,
"created_at": "2026-09-05T09:00:00+00:00",
"finished_at": "2026-09-05T09:04:11+00:00"
}
]
},
"meta": {
"page": 1,
"per_page": 100,
"total": 1,
"retention_days": 30
}
}
```
### 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"
}
}
```
### 422 — The request body could not be validated; `error.errors` lists the fields.
```json
{
"success": false,
"error": {
"code": "VALIDATION_FAILED",
"message": "The email field is required.",
"errors": {
"email": [
"The email field is required."
]
},
"request_id": "req_01m1kgdm4xngzmbmff68g94w0c"
}
}
```
### 429 — Over 5 requests per second for the key. Retry after the limit resets.
```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
- `VALIDATION_FAILED` — https://spaw.co/docs/errors/VALIDATION_FAILED
- `RATE_LIMITED` — https://spaw.co/docs/errors/RATE_LIMITED
---
Canonical page: https://spaw.co/docs/api/list-phone-bulk-jobs · OpenAPI document: https://spaw.co/openapi.json · All endpoints: https://spaw.co/docs/api
---
# Queue a bulk phone lookup run
`POST /api/v1/phone/bulk`
- Authentication: Secret API key as a bearer token
- Billing: `preview: true` is free and queues nothing. A started run bills each row like a single lookup as it is processed; invalid numbers, repeats inside the run and 7-day repeats are free, so a run costs at most the `credits_upper_bound` the preview answered and usually less.
- Group: Phone
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 with `webhook_secret` exactly like bulk email and IP runs: `X-Spaw-Signature-V2` carries `t={unix seconds},v2={lowercase hex}`, the HMAC-SHA256 of `v2:{t}:{raw body}`, and a receiver should refuse a `t` more than five minutes from its own clock so a captured delivery cannot be replayed at it later. The original `X-Spaw-Signature`, over the body alone, is sent beside it until 1 March 2027. 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`.
### Reading a list before you start it
Send `preview: true` with the same body — or `csv` with a raw CSV instead of the item list — and the call answers `200` with `data.draft` instead of queueing anything: what the list was read as, which column filled which field and whether a heading named it or we guessed, the first rows as parsed, the repeats, the balance, and `credits_upper_bound`. Nothing is charged.
`credits_upper_bound` is a CEILING, not a quote. Rows come back free for reasons no preview can know in advance — a repeat inside the seven-day window, an entry on your suppression list, a verdict that is not charged for, a provider outage — so a run usually costs less than the ceiling and never more. It is priced against the mapping in play, so correcting a column at start re-prices the run rather than holding you to the figure the guess produced — ask for the draft again after a correction if you want the new number. What it actually cost is `credits_used` on the finished job.
Start it with `{"draft_id": }`, adding `mapping` to correct a column we read wrongly. Starting consumes the draft; one left unstarted is deleted, with the rows it holds, after `retention_hours`.
## Parameters
| Name | In | Type | Required | Description |
| --- | --- | --- | --- | --- |
| `Idempotency-Key` | header | string | no | 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 | Required | Description |
| --- | --- | --- | --- |
| `phones` | string[] | no | 1 to 100,000 numbers, in any common notation. Each item is a plain string, or an object carrying its own country. Required unless the body carries `csv` with `preview: true`, or `draft_id`. |
| `phones[].phone` | string | yes | The number, in any common notation. At most 64 characters. |
| `phones[].country` | string | null | no | 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 | no | 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 | no | 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 under the single-lookup rules (free where the account paid for that number's check in the last 7 days and the network's answer is still held), and the balance is gated per item before any network query. Default: . |
| `preview` | boolean | no | Read the list and answer `data.draft` — what we made of it, the repeats, the ceiling on the cost and the balance — with nothing queued and nothing billed. Start it afterwards with `draft_id`. Default: . |
| `csv` | string | null | no | A raw CSV to read instead of the item list, with the column mapping answered back. Preview only, on purpose: a CSV's columns have to be worked out, and working them out and billing for the answer in one request is what the preview exists to stop. At most 5,000,000 characters. |
| `draft_id` | integer | null | no | Start the run a preview answered with. The rows and the options come from the draft, so nothing else in the body applies except `mapping`. A draft can be started once; an `Idempotency-Key` still makes the call safe to retry. |
| `mapping` | object | null | no | Corrects the draft's column mapping, as column index to field — {"1": "address_line1", "2": "postal_code"}. It REPLACES the detected mapping rather than merging into it, so a column can be taken off a field as well as put on one. Goes with `draft_id`, and only for a draft read from a `csv`. |
| `webhook_url` | string | null | no | An https URL to notify when the run finishes. |
## Example request
```bash
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
### 202 — The job was queued.
```json
{
"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"
}
}
}
```
### 200 — Either a preview — `preview: true` answers `data.draft` and queues nothing — or a retry with an `Idempotency-Key` already used for this exact request, which answers `data.job` with `Idempotent-Replayed: true`.
```json
{
"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
}
}
}
```
### 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"
}
}
```
### 409 — The `Idempotency-Key` was already used for a different list, country or webhook URL.
```json
{
"success": false,
"error": {
"code": "IDEMPOTENCY_KEY_REUSED",
"message": "This Idempotency-Key was already used for a different request.",
"request_id": "req_01m1kgdm4xngzmbmff68g94w0c"
}
}
```
### 422 — The request body could not be validated; `error.errors` lists the fields.
```json
{
"success": false,
"error": {
"code": "VALIDATION_FAILED",
"message": "The email field is required.",
"errors": {
"email": [
"The email field is required."
]
},
"request_id": "req_01m1kgdm4xngzmbmff68g94w0c"
}
}
```
### 429 — Over 5 requests per second for the key. Retry after the limit resets.
```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
- `VALIDATION_FAILED` — https://spaw.co/docs/errors/VALIDATION_FAILED
- `IDEMPOTENCY_KEY_REUSED` — https://spaw.co/docs/errors/IDEMPOTENCY_KEY_REUSED
- `RATE_LIMITED` — https://spaw.co/docs/errors/RATE_LIMITED
---
Canonical page: https://spaw.co/docs/api/create-phone-bulk-job · OpenAPI document: https://spaw.co/openapi.json · All endpoints: https://spaw.co/docs/api
---
# Poll a bulk phone job
`GET /api/v1/phone/bulk/{jobId}`
- Authentication: Secret API key as a bearer token
- Billing: Free.
- Group: Phone
The job's current status and running counts. `status` moves from `queued` to `processing` and ends in `completed`, `failed`, or `cancelled`; `processed`, the `valid` and `invalid` counts and the `low`, `medium` and `high` risk counts advance as chunks finish. A job that belongs to another account answers `404`.
## Parameters
| Name | In | Type | Required | Description |
| --- | --- | --- | --- | --- |
| `jobId` | path | integer | yes | |
## Example request
```bash
curl https://spaw.co/api/v1/phone/bulk/{jobId} \
-H "Authorization: Bearer sk_live_…"
```
## Responses
### 200 — The job.
```json
{
"success": true,
"data": {
"job": {
"id": 733,
"status": "completed",
"total": 2,
"duplicate_count": 0,
"processed": 2,
"valid": 2,
"invalid": 0,
"low": 1,
"medium": 0,
"high": 1,
"credits_used": 2,
"stopped_reason": null,
"cancel_requested": false,
"country": "US",
"webhook_status": "delivered",
"created_at": "2026-09-04T10:12:44+00:00",
"finished_at": "2026-09-04T10:12:46+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"
}
}
```
### 429 — Over 5 requests per second for the key. Retry after the limit resets.
```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
- `RATE_LIMITED` — https://spaw.co/docs/errors/RATE_LIMITED
---
Canonical page: https://spaw.co/docs/api/get-phone-bulk-job · OpenAPI document: https://spaw.co/openapi.json · All endpoints: https://spaw.co/docs/api
---
# Cancel a bulk phone job
`POST /api/v1/phone/bulk/{jobId}/cancel`
- Authentication: Secret API key as a bearer token
- Billing: Free.
- Group: Phone
Asks a queued or running job to stop at its next checkpoint. Everything answered so far stays billed and stays in the result file. A job that already finished answers `409 JOB_ALREADY_FINISHED`.
## Parameters
| Name | In | Type | Required | Description |
| --- | --- | --- | --- | --- |
| `jobId` | path | integer | yes | |
## Example request
```bash
curl -X POST https://spaw.co/api/v1/phone/bulk/{jobId}/cancel \
-H "Authorization: Bearer sk_live_…"
```
## Responses
### 202 — Cancellation was requested.
```json
{
"success": true,
"data": {
"job": {
"id": 733,
"status": "processing",
"total": 20000,
"duplicate_count": 12,
"processed": 4000,
"valid": 3700,
"invalid": 300,
"low": 3100,
"medium": 400,
"high": 200,
"credits_used": 3700,
"stopped_reason": null,
"cancel_requested": true,
"country": null,
"webhook_status": null,
"created_at": "2026-09-04T10:12:44+00:00",
"finished_at": null
}
}
}
```
### 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 job already finished.
```json
{
"success": false,
"error": {
"code": "JOB_ALREADY_FINISHED",
"message": "This job has already finished.",
"request_id": "req_01m1kgdm4xngzmbmff68g94w0c"
}
}
```
### 429 — Over 5 requests per second for the key. Retry after the limit resets.
```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
- `JOB_ALREADY_FINISHED` — https://spaw.co/docs/errors/JOB_ALREADY_FINISHED
- `RATE_LIMITED` — https://spaw.co/docs/errors/RATE_LIMITED
---
Canonical page: https://spaw.co/docs/api/cancel-phone-bulk-job · OpenAPI document: https://spaw.co/openapi.json · All endpoints: https://spaw.co/docs/api
---
# Resume a bulk job that ran out of credits
`POST /api/v1/phone/bulk/{jobId}/resume`
- Authentication: Secret API key as a bearer token
- Billing: Free to call. The resumed run bills the rows it reaches exactly as the first one did, and a row answered in the last seven days is a free repeat; with `live_check`, a row whose network answer is more than a day old is asked again and charged the premium.
- Group: Phone
Runs a job that stopped when the credit balance ran out again, from the top of the list it was given, instead of asking you to upload it a second time. Rows looked up in the last seven days come back as free repeats, so only the rows the stopped run never reached are charged. On a run with `live_check`, a number whose network answer is more than a day old is asked again and charged the premium, even inside the seven days, so resume within a day to keep those rows free.
The job keeps its id, its webhook and its cumulative `credits_used`, which is the real spend on the account. `processed` and the verdict counts restart at zero: the part files they were counted from are deleted when a run settles, so the resumed run rebuilds them, and the result file the stopped run left is replaced as the rows come back.
Only a run whose `stopped_reason` is `insufficient_credits` and whose `status` has settled can be resumed; anything else answers `409 JOB_NOT_RESUMABLE`, and so does a second resume of a run that is already going again — claiming the run is one conditional write, so two calls can never queue it twice. A run whose list has passed out of the retention window answers `409 JOB_INPUT_EXPIRED`. An empty balance answers `402 INSUFFICIENT_CREDITS` rather than queueing a run that would stop again on the first row it never reached.
## Parameters
| Name | In | Type | Required | Description |
| --- | --- | --- | --- | --- |
| `jobId` | path | integer | yes | |
## Example request
```bash
curl -X POST https://spaw.co/api/v1/phone/bulk/{jobId}/resume \
-H "Authorization: Bearer sk_live_…"
```
## Responses
### 202 — The list is queued again.
```json
{
"success": true,
"data": {
"job": {
"id": 733,
"status": "queued",
"total": 20000,
"duplicate_count": 12,
"processed": 0,
"valid": 0,
"invalid": 0,
"low": 0,
"medium": 0,
"high": 0,
"credits_used": 3700,
"stopped_reason": null,
"cancel_requested": false,
"country": null,
"webhook_status": null,
"created_at": "2026-09-04T10:12:44+00:00",
"finished_at": null
}
}
}
```
### 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"
}
}
```
### 402 — The balance is empty. The lookup did not run.
```json
{
"success": false,
"error": {
"code": "INSUFFICIENT_CREDITS",
"message": "Your credit balance is empty.",
"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 is not in a state a resume can recover (`JOB_NOT_RESUMABLE`), or its list is no longer stored (`JOB_INPUT_EXPIRED`).
```json
{
"success": false,
"error": {
"code": "JOB_NOT_RESUMABLE",
"message": "Only a run that stopped because the credit balance ran out can be resumed, and only once it has finished settling.",
"request_id": "req_01m1kgdm4xngzmbmff68g94w0c"
}
}
```
### 429 — Over 5 requests per second for the key. Retry after the limit resets.
```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
- `JOB_NOT_RESUMABLE` — https://spaw.co/docs/errors/JOB_NOT_RESUMABLE
- `JOB_INPUT_EXPIRED` — https://spaw.co/docs/errors/JOB_INPUT_EXPIRED
- `INSUFFICIENT_CREDITS` — https://spaw.co/docs/errors/INSUFFICIENT_CREDITS
- `RATE_LIMITED` — https://spaw.co/docs/errors/RATE_LIMITED
---
Canonical page: https://spaw.co/docs/api/resume-phone-bulk-job · OpenAPI document: https://spaw.co/openapi.json · All endpoints: https://spaw.co/docs/api
---
# Send a finished run's completion webhook again
`POST /api/v1/phone/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: Phone
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_phone_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/phone/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/phone/bulk/733/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": 733,
"status": "completed",
"total": 20000,
"duplicate_count": 12,
"processed": 20000,
"valid": 17400,
"invalid": 2600,
"low": 15200,
"medium": 1900,
"high": 300,
"credits_used": 17400,
"stopped_reason": null,
"cancel_requested": false,
"country": null,
"webhook_status": "pending",
"webhook_detail": null,
"created_at": "2026-09-04T10:12:44+00:00",
"finished_at": "2026-09-04T10:58:11+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-phone-bulk-webhook · OpenAPI document: https://spaw.co/openapi.json · All endpoints: https://spaw.co/docs/api
---
# Download a bulk phone job's results
`GET /api/v1/phone/bulk/{jobId}/results`
- Authentication: Secret API key as a bearer token
- Billing: Free.
- Group: Phone
The result file as CSV: one row per input number, in input order, with the number as submitted, every field of the single-number response as a column except four a run never fills (`range_seen_by_you`, `client_velocity` and `destination_new_for_you`, because a run is not counted in your own traffic, and `in_call_window`, because a run takes no call window; the live-check fields stay empty unless the run asked for the check, and `local_time` and `utc_offsets` are the clock when the row was processed), the credits it cost, whether the suppression list answered it (`suppressed`), and whether an earlier row of the list already held the same number in any spelling (`is_duplicate`, with `duplicate_of_row` naming that row's 1-based position in the list, header not counted; a repeat is looked up once and is free). Time zones, UTC offsets and sources are joined by `;` (sources as `dataset@version`), risk signals as `signal:weight` pairs. Fields containing spaces or commas are quoted. Add `?variant=` to download only some rows: `valid` or `invalid`; one risk level (`low`, `medium`, `high`); `deduped`, the valid rows that are the first in the list to hold their number; `mobile`, the valid rows whose `line_type` is mobile — most North American numbers answer `fixed_line_or_mobile` because mobiles and landlines share blocks there, and are not in it; `ok`, the rows your phone policy answered `ok_to_send` true for; or `blocked`, the rows it held back, with `blocked_by` saying why. The default `full` returns everything. Available once the job is `completed`, or `cancelled` or `failed` with at least one processed row — those rows were billed, so they stay downloadable. A job still running answers `409 RESULTS_NOT_READY`, a cancelled job with nothing processed `409 JOB_CANCELLED`, and a failed job with nothing processed `409 JOB_FAILED`.
`format=json` answers the same rows, filtered the same way, as objects keyed by the result file's own header instead of a CSV attachment: `{ "success": true, "data": { "job": …, "results": [ … ] }, "meta": { "variant", "request_id" } }`. It is streamed row by row exactly as the CSV is, so a 100,000-row run costs no more to answer either way. A CSV cell carries no type, so only the two the writer encodes come back typed: an empty cell is `null` and `true`/`false` are booleans. Everything else is the string as written — a number is never silently made numeric, because postcodes, phone numbers and house numbers lose their leading zeros that way — and the columns of an uploaded file come back exactly as they were uploaded.
## Parameters
| Name | In | Type | Required | Description |
| --- | --- | --- | --- | --- |
| `jobId` | path | integer | yes | |
| `variant` | query | string | no | Restrict the file to the valid or invalid rows, one risk level, the first row of each valid number (`deduped`), the valid mobile rows, or the rows your phone policy lets through (`ok`) or holds back (`blocked`). Defaults to full. One of: full, valid, invalid, low, medium, high, deduped, mobile, ok, blocked. |
| `format` | query | string | no | `csv` streams the file as an attachment; `json` streams the same rows as objects. One of: csv, json. Default: csv. |
## Example request
```bash
curl https://spaw.co/api/v1/phone/bulk/{jobId}/results \
-H "Authorization: Bearer sk_live_…"
```
## Responses
### 200 — The result rows, as a CSV attachment or as streamed JSON.
```csv
phone,valid,reason,e164,national,international,rfc3966,country,country_name,calling_code,continent,is_eu,currency,line_type,line_type_source,carrier,carrier_source,block_status,region,is_geographic,timezones,extension,is_fictional,is_virtual,is_disposable,disposable_seen_at,risk_score,risk_level,risk_signals,hlr_checked,reachable,ported,current_carrier,roaming,carrier_code,carrier_brand,block_pooled,block_assigned_at,region_level,local_time,utc_offsets,did_you_mean,complaints_90d,complaints_last_at,ok_to_send,blocked_by,sources,credits_used,suppressed,is_duplicate,duplicate_of_row
"+1 415 555 0142",true,,+14155550142,"(415) 555-0142","+1 415-555-0142",tel:+1-415-555-0142,US,"United States",1,NA,false,USD,fixed_line_or_mobile,numbering_plan,,,reserved,"San Francisco, CA",true,America/Los_Angeles,,true,false,false,,100,high,fictional:80;reserved_block:40,false,,,,,,,,,locality,2026-09-24T14:20:06-07:00,-07:00,,0,,false,fictional,libphonenumber@9.0.37;nanpa-co-codes@2026-09-24;fictional-ranges@2026-09-25;disposable-phone-numbers@2026-09-24;us-complaint-reports@2026-09-24,1,false,false,
```
### 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 job is still running (`RESULTS_NOT_READY`), or was cancelled or failed before any row was processed (`JOB_CANCELLED`, `JOB_FAILED`).
```json
{
"success": false,
"error": {
"code": "RESULTS_NOT_READY",
"message": "The results are not ready yet.",
"request_id": "req_01m1kgdm4xngzmbmff68g94w0c"
}
}
```
### 429 — Over 5 requests per second for the key. Retry after the limit resets.
```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
- `VALIDATION_FAILED` — https://spaw.co/docs/errors/VALIDATION_FAILED
- `RESULTS_NOT_READY` — https://spaw.co/docs/errors/RESULTS_NOT_READY
- `JOB_CANCELLED` — https://spaw.co/docs/errors/JOB_CANCELLED
- `JOB_FAILED` — https://spaw.co/docs/errors/JOB_FAILED
- `RATE_LIMITED` — https://spaw.co/docs/errors/RATE_LIMITED
---
Canonical page: https://spaw.co/docs/api/download-phone-bulk-results · OpenAPI document: https://spaw.co/openapi.json · All endpoints: https://spaw.co/docs/api
---
# Validate a number from the browser with a publishable key
`POST /api/v1/phone/public`
- Authentication: Publishable key in the body, checked against the browser Origin header
- Billing: Bills the key owner like a single lookup; capped by the key's daily credit cap when one is set.
- Group: Phone
The browser counterpart of `POST /api/v1/phone`, for checking a phone field in a signup form on the client without exposing a secret key; it is what the form helper's `data-spaw-phone` inputs call. It is authenticated by a publishable `pk_` key in the body plus the browser's `Origin` header, which must match one of the domains the key is locked to. A missing `Origin` is rejected on purpose: servers use a secret key. Pass `country` (an ISO 3166-1 alpha-2 code, usually from a country select on the same form) so numbers typed without a calling code parse.
The lookup bills the key's owner under the normal rules (a fresh valid answer costs one credit; invalid numbers, unassigned blocks and 7-day repeats are free) and answers `{ "success", "data" }` with **no meta block**, so page visitors never see the owner's balance. `ok_to_send` and `blocked_by` are the key owner's SMS policy applied to the answer: with `block_opted_out` on (the default), a number the owner's suppression list holds as opted out answers `blocked_by: opted_out`, so the page can ask the visitor to opt in again. The live carrier check is never offered here: an `hlr` field in the body is ignored. Because the key sits in page source, give it a daily credit cap in the dashboard: once the cap is spent the endpoint answers `429 KEY_SPEND_CAP_REACHED` until the next day. Free answers never count toward the cap. A key that carries a Cloudflare Turnstile pair requires a confirmed `turnstile_token` and answers `403 TURNSTILE_FAILED` without one, before any credit is spent.
Throttled at 20 requests per minute per IP.
## Parameters
| Name | In | Type | Required | Description |
| --- | --- | --- | --- | --- |
| `Origin` | header | string | yes | Sent by browsers automatically. Its host must be on the key's allowed-domain list. |
## Request body
| Field | Type | Required | Description |
| --- | --- | --- | --- |
| `key` | string | yes | A publishable key, which starts with `pk_`. At most 64 characters. |
| `phone` | string | yes | The number, in any common notation. Include the country calling code or pass `country`. At most 64 characters. |
| `country` | string | null | no | An ISO 3166-1 alpha-2 region code used to parse numbers written without a calling code, and to recognise that region's emergency numbers and short codes. |
| `turnstile_token` | string | null | no | Required when the key carries a Turnstile pair. A single-use token from the Turnstile widget on the page. At most 2,048 characters. |
## Example request
```bash
curl -X POST https://spaw.co/api/v1/phone/public \
-H "Origin: https://www.example.com" \
-H "Content-Type: application/json" \
-d '{
"key": "pk_live_…",
"phone": "07911 012345",
"country": "GB"
}'
```
## Responses
### 200 — The same fields as `POST /api/v1/phone`, without a meta block. `range_seen_by_you`, `client_velocity` and `destination_new_for_you` are always null here, because the answer reaches the visitor's browser; the visitor's own address is what `numbers_per_client` counts, and the signals still score.
```json
{
"success": true,
"data": {
"valid": true,
"reason": null,
"e164": "+447911012345",
"international": "+44 7911 012345",
"country": "GB",
"line_type": "mobile",
"line_type_source": "numbering_plan",
"carrier": "Marathon Telecom Limited",
"carrier_source": "block_allocation",
"block_status": "allocated",
"is_fictional": false,
"is_virtual": false,
"is_disposable": false,
"risk_score": 0,
"risk_level": "low",
"hlr_checked": false,
"ok_to_send": true,
"blocked_by": null
}
}
```
### 401 — The publishable key does not exist or was revoked.
```json
{
"success": false,
"error": {
"code": "INVALID_PUBLISHABLE_KEY",
"message": "That publishable key does not exist or was revoked.",
"request_id": "req_01m1kgdm4xngzmbmff68g94w0c"
}
}
```
### 402 — The balance is empty. The lookup did not run.
```json
{
"success": false,
"error": {
"code": "INSUFFICIENT_CREDITS",
"message": "Your credit balance is empty.",
"request_id": "req_01m1kgdm4xngzmbmff68g94w0c"
}
}
```
### 403 — The page's origin is not on the key's allowed-domain list (`ORIGIN_NOT_ALLOWED`), or the key requires a Turnstile token that was missing or not confirmed (`TURNSTILE_FAILED`).
```json
{
"success": false,
"error": {
"code": "ORIGIN_NOT_ALLOWED",
"message": "This publishable key cannot be used from this origin.",
"request_id": "req_01m1kgdm4xngzmbmff68g94w0c"
}
}
```
### 422 — The request body could not be validated; `error.errors` lists the fields.
```json
{
"success": false,
"error": {
"code": "VALIDATION_FAILED",
"message": "The email field is required.",
"errors": {
"email": [
"The email field is required."
]
},
"request_id": "req_01m1kgdm4xngzmbmff68g94w0c"
}
}
```
### 429 — The key has spent its owner's daily credit cap (`KEY_SPEND_CAP_REACHED`), or the IP exceeded 20 requests per minute (`RATE_LIMITED`).
```json
{
"success": false,
"error": {
"code": "KEY_SPEND_CAP_REACHED",
"message": "This publishable key has reached its daily credit cap. The counter resets each day.",
"request_id": "req_01m1kgdm4xngzmbmff68g94w0c"
}
}
```
## Error codes
- `INVALID_PUBLISHABLE_KEY` — https://spaw.co/docs/errors/INVALID_PUBLISHABLE_KEY
- `ORIGIN_NOT_ALLOWED` — https://spaw.co/docs/errors/ORIGIN_NOT_ALLOWED
- `TURNSTILE_FAILED` — https://spaw.co/docs/errors/TURNSTILE_FAILED
- `KEY_SPEND_CAP_REACHED` — https://spaw.co/docs/errors/KEY_SPEND_CAP_REACHED
- `VALIDATION_FAILED` — https://spaw.co/docs/errors/VALIDATION_FAILED
- `INSUFFICIENT_CREDITS` — https://spaw.co/docs/errors/INSUFFICIENT_CREDITS
- `RATE_LIMITED` — https://spaw.co/docs/errors/RATE_LIMITED
---
Canonical page: https://spaw.co/docs/api/validate-phone-public · OpenAPI document: https://spaw.co/openapi.json · All endpoints: https://spaw.co/docs/api
---
# Report SMS outcomes
`POST /api/v1/phone/feedback`
- Authentication: Secret API key as a bearer token
- Billing: Free.
- Group: Phone
Tells Spaw what happened after you sent an SMS to a number: `delivered`, `undelivered` (the carrier reported it could not be delivered), `abused` (the code was used by someone who was not the recipient, or the number was involved in fraud), `verified` (the recipient entered the code the SMS carried) or `opted_out` (the recipient unsubscribed, for instance by replying STOP). Your own reports feed your own later lookups of that number as risk signals with the dataset `delivery-feedback`: a number whose latest report within 90 days is a failure about the number scores `reported_undelivered` (+50), a number with any abuse report within 90 days scores `reported_abuse` (+60), and a delivery or a verification reported after a failure cancels that failure. No other account ever sees your reports.
`verified` counts as a delivery everywhere a delivery counts: it cancels an earlier failure and clears a suppression entry your failure reports added. An abuse report is not a failure: the entry it adds, reason `abuse`, stays until you remove it — no delivery, verification, lookup or age takes it off, even once the report has left the 90 days it scores for. Only an `abused` item in the request that carries it adds or relabels an `abuse` entry, so an abuse entry you removed does not come back from a later delivered, verified or undelivered report while the old report is still in its 90 days; only the next abuse report restores it (an opt-out, or failures that would suppress any number, still add it with reason `opted_out` or `undelivered`). On a number you imported, an abuse report is recorded on the import, which keeps source `imported`. Report it beside your delivery receipts and the summary's `verified_share` reads as your OTP conversion — traffic pumping is delivered and never verified.
`opted_out` is never a risk signal: it says nothing about whether the number works. It puts the number on your suppression list with reason `opted_out` — recorded on the entry you imported, if you did, which keeps source `imported` — where batch, bulk and monitor runs serve it suppressed. A single lookup answers and scores it in full with `meta.opted_out: true` and, under the default SMS policy, `ok_to_send: false` with `blocked_by: opted_out`; an account that sends one-time codes may switch `block_opted_out` off (`PUT /api/v1/phone/policy`). Nothing but removing the entry takes it off — not a later delivery, a verification, a lookup or age — and removing it stands until the next opt-out is reported. It is your own record: honouring an opt-out stays your obligation, and Spaw is not a consent record.
An `undelivered` item may name its `failure_class`, read from your SMS provider's error code: `permanent` (the number no longer exists or is barred for good), `transient` (the handset was off, out of coverage or busy), `not_sms_capable` (a landline or another number that cannot take SMS) or `sender_side` (a carrier filter, an unregistered campaign or sender, a rate limit — your side of the send, not the number). Only failures about the number count: `permanent`, `transient` and failures sent without a class feed `reported_undelivered` and your suppression list, where one `permanent` failure suppresses the number at once and the others need two with no delivery after them. `not_sms_capable` and `sender_side` failures are kept and counted in the summary, never scored. `occurred_at` may not be more than five minutes in the future; a time inside that margin is recorded as now.
Numbers are normalised like a lookup; national notation needs a `country` on the item or on the request. Items whose number does not parse as a valid subscriber number are skipped and counted, not rejected. Feedback is free and never logged as a lookup. Outcomes are kept for 180 days.
## Request body
| Field | Type | Required | Description |
| --- | --- | --- | --- |
| `items` | object[] | yes | 1 to 1,000 outcomes per request. |
| `items[].phone` | string | yes | The number, in any common notation. At most 64 characters. |
| `items[].outcome` | string | yes | verified: the recipient entered the code, which counts as a delivery. opted_out: the recipient unsubscribed; never scored, and it suppresses the number with reason opted_out until you remove the entry. One of: delivered, undelivered, abused, verified, opted_out. |
| `items[].failure_class` | string | null | no | For an undelivered outcome only: what kind of failure it was. permanent (the number no longer exists or is barred for good) and transient (the handset was off, out of coverage or busy) count against the number, like a failure with no class; not_sms_capable and sender_side are recorded and summarised, never scored. A class on any other outcome is a 422; an opt-out is its own outcome, not a class. One of: permanent, transient, not_sms_capable, sender_side, null. |
| `items[].country` | string | null | no | An ISO 3166-1 alpha-2 region for a number written without a calling code; overrides the request-level country. |
| `items[].occurred_at` | string | null | no | When it happened. Defaults to now. A time more than five minutes in the future is refused; one inside that margin is recorded as now. |
| `country` | string | null | no | An ISO 3166-1 alpha-2 region applied to items that do not name their own, for numbers written without a calling code. |
## Example request
```bash
curl -X POST https://spaw.co/api/v1/phone/feedback \
-H "Authorization: Bearer sk_live_…" \
-H "Content-Type: application/json" \
-d '{
"items": [
{
"phone": "+44 7911 012345",
"outcome": "undelivered",
"failure_class": "transient"
},
{
"phone": "(415) 555-0142",
"country": "US",
"outcome": "delivered",
"occurred_at": "2026-09-05T09:00:00Z"
},
{
"phone": "(415) 555-0142",
"country": "US",
"outcome": "verified",
"occurred_at": "2026-09-05T09:01:10Z"
}
]
}'
```
## Responses
### 202 — The outcomes were recorded.
```json
{
"success": true,
"data": {
"recorded": 2,
"skipped": 0
}
}
```
### 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"
}
}
```
### 422 — The request body could not be validated; `error.errors` lists the fields.
```json
{
"success": false,
"error": {
"code": "VALIDATION_FAILED",
"message": "The email field is required.",
"errors": {
"email": [
"The email field is required."
]
},
"request_id": "req_01m1kgdm4xngzmbmff68g94w0c"
}
}
```
### 429 — Over 5 requests per second for the key. Retry after the limit resets.
```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
- `VALIDATION_FAILED` — https://spaw.co/docs/errors/VALIDATION_FAILED
- `RATE_LIMITED` — https://spaw.co/docs/errors/RATE_LIMITED
---
Canonical page: https://spaw.co/docs/api/report-phone-feedback · OpenAPI document: https://spaw.co/openapi.json · All endpoints: https://spaw.co/docs/api
---
# Reported SMS outcomes
`GET /api/v1/phone/feedback/summary`
- Authentication: Secret API key as a bearer token
- Billing: Free.
- Group: Phone
The last 90 days of outcomes you reported: totals per outcome, the failed deliveries split by failure class in `undelivered_by_class`, how many distinct numbers they cover, `verified_share` (of the distinct numbers you reported delivered or verified, the share you reported verified), and the ten blocks (region plus the first six digits of the national number) with the most failed deliveries about the number, with their delivered, abused and verified counts and their own `verified_share` alongside. A bad list or a wholesaler's range shows up here first, and so does pumped traffic: delivered, never verified, in a block whose share sits far under yours. Your own sending problems — a lapsed campaign registration, a carrier filter — show under `sender_side` instead of in the blocks.
## Example request
```bash
curl https://spaw.co/api/v1/phone/feedback/summary \
-H "Authorization: Bearer sk_live_…"
```
## Responses
### 200 — The summary.
```json
{
"success": true,
"data": {
"window_days": 90,
"total": 3120,
"numbers": 2870,
"outcomes": {
"delivered": 2988,
"undelivered": 119,
"abused": 11,
"verified": 2410,
"opted_out": 2
},
"verified_share": 0.812,
"undelivered_by_class": {
"permanent": 9,
"transient": 64,
"not_sms_capable": 3,
"sender_side": 38,
"unclassified": 5
},
"top_undelivered_blocks": [
{
"country": "US",
"block": "201201",
"undelivered": 38,
"delivered": 2,
"abused": 4,
"verified": 0,
"verified_share": 0
},
{
"country": "GB",
"block": "770090",
"undelivered": 12,
"delivered": 0,
"abused": 0,
"verified": 0,
"verified_share": null
}
]
}
}
```
### 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"
}
}
```
### 429 — Over 5 requests per second for the key. Retry after the limit resets.
```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
- `RATE_LIMITED` — https://spaw.co/docs/errors/RATE_LIMITED
---
Canonical page: https://spaw.co/docs/api/phone-feedback-summary · OpenAPI document: https://spaw.co/openapi.json · All endpoints: https://spaw.co/docs/api
---
# List the phone suppression list
`GET /api/v1/phone/suppressions`
- Authentication: Secret API key as a bearer token
- Billing: Free.
- Group: Phone
The account's phone suppression list, newest first, 100 entries per page. A fresh lookup that finds a geographic number in a block the regulator does not allocate adds it with source `auto` and its stored answer; the account's own SMS outcomes add a number with source `feedback` after an abuse report, one permanent failure, two failures about the number with no later delivery (reasons `abuse` and `undelivered`), or an opt-out (reason `opted_out`); imports arrive with source `imported`. Batch, bulk and monitor runs answer suppressed numbers free from the stored answer, or with reason `suppressed` where there is none; a single lookup always runs fresh. A valid answer, from any lookup, removes only an `auto` entry: an `undelivered` entry leaves when you report a delivery or a verification, and an `abuse`, `opted_out` or `imported` one only when you remove it, so nobody typing a number into your form can take it off the list. An abuse report or opt-out about a number you imported is recorded on that entry, which keeps source `imported`; an abuse or opt-out entry you removed comes back only with a new abuse report or opt-out. Imports, opt-outs and abuse entries never go stale: they are served for as long as they stand. An `auto` or `undelivered` entry untouched for 90 days is re-checked instead of trusted: the fresh answer removes a stale `auto` entry when it comes back valid, and a stale `undelivered` entry stays listed, renewed by a new failure report. A single lookup of a number held as `opted_out` answers `blocked_by: opted_out` under the default SMS policy (see `block_opted_out`).
Every filter is optional and they combine. `value` looks one number up: it is read to E.164 the way the list stores it, so `07911 012345` with `country=GB` finds the entry filed as `+447911012345`. A number that is not a valid subscriber number matches nothing. An unparseable date or an out-of-range `per_page` is a `422`, not a silently unfiltered page.
## Parameters
| Name | In | Type | Required | Description |
| --- | --- | --- | --- | --- |
| `value` | query | string | no | One number, in any notation. Read to E.164 before it is matched. At most 500 characters. |
| `country` | query | string | no | The region `value` is read against when it carries no calling code. 2 to 2 characters. |
| `source` | query | string | no | Only entries this source added. One of: auto, imported, feedback. |
| `reason` | query | string | no | Only entries carrying this reason. One of: unassigned_block, undelivered, abuse, opted_out. |
| `since` | query | string | no | Only entries added on or after this date. |
| `until` | query | string | no | Only entries added on or before this date. A bare date means the whole of that day. |
| `page` | query | integer | no | The page to return. Default: 1. |
| `per_page` | query | integer | no | Entries per page, up to 500. Default: 100. |
## Example request
```bash
curl https://spaw.co/api/v1/phone/suppressions \
-H "Authorization: Bearer sk_live_…"
```
## Responses
### 200 — One page of entries.
```json
{
"success": true,
"data": {
"suppressions": [
{
"id": 311,
"phone": "+14152010142",
"reason": "unassigned_block",
"source": "auto",
"added_at": "2026-09-05T08:30:00+00:00"
},
{
"id": 310,
"phone": "+447911012345",
"reason": "undelivered",
"source": "feedback",
"added_at": "2026-09-04T17:02:11+00:00"
}
]
},
"meta": {
"page": 1,
"per_page": 100,
"total": 2
}
}
```
### 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"
}
}
```
### 429 — Over 5 requests per second for the key. Retry after the limit resets.
```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
- `VALIDATION_FAILED` — https://spaw.co/docs/errors/VALIDATION_FAILED
- `RATE_LIMITED` — https://spaw.co/docs/errors/RATE_LIMITED
---
Canonical page: https://spaw.co/docs/api/list-phone-suppressions · OpenAPI document: https://spaw.co/openapi.json · All endpoints: https://spaw.co/docs/api
---
# Import known-dead numbers
`POST /api/v1/phone/suppressions`
- Authentication: Secret API key as a bearer token
- Billing: Free.
- Group: Phone
Adds up to 10,000 numbers per request to the phone suppression list — an SMS provider's failure export, a hand-kept blocklist. Each item is a number in any notation or an object naming its own `country`; `country` at the top level applies to the rest. Numbers are normalised to E.164 first; ones that are not valid subscriber numbers (including national notation with no country) are counted as `invalid`. A number already on the list is counted as `already_suppressed`: it keeps its reason and any stored answer, and becomes an import too, so no later lookup or report clears it — it leaves only when you remove it. A stored answer is served for 90 days after the entry was last renewed; after that it is dropped, with an `unassigned_block` reason, and the import answers `suppressed`, because nothing looks an import up again. An abuse report or opt-out reported about it later is recorded on the entry (reason `abuse` or `opted_out`, still source `imported`). An import never goes stale, so importing a number again is never needed. Imported entries are honoured from day one at no credit cost: batch, bulk and monitor runs answer them with reason `suppressed`.
## Request body
| Field | Type | Required | Description |
| --- | --- | --- | --- |
| `items` | string[] | yes | 1 to 10,000 numbers, each a string of at most 64 characters or an object with its own country. |
| `items[].phone` | string | no | At most 64 characters. |
| `items[].country` | string | null | no | An ISO 3166-1 alpha-2 region code for this number. |
| `country` | string | null | no | The region for items written without a calling code that name none of their own. |
## Example request
```bash
curl -X POST https://spaw.co/api/v1/phone/suppressions \
-H "Authorization: Bearer sk_live_…" \
-H "Content-Type: application/json" \
-d '{
"items": [
"+1 415 201 0142",
{
"phone": "07911 012345",
"country": "GB"
}
]
}'
```
## Responses
### 200 — How many entries were added, already present, or not valid numbers.
```json
{
"success": true,
"data": {
"added": 2,
"already_suppressed": 0,
"invalid": 0
}
}
```
### 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"
}
}
```
### 422 — The request body could not be validated; `error.errors` lists the fields.
```json
{
"success": false,
"error": {
"code": "VALIDATION_FAILED",
"message": "The email field is required.",
"errors": {
"email": [
"The email field is required."
]
},
"request_id": "req_01m1kgdm4xngzmbmff68g94w0c"
}
}
```
### 429 — Over 5 requests per second for the key. Retry after the limit resets.
```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
- `VALIDATION_FAILED` — https://spaw.co/docs/errors/VALIDATION_FAILED
- `RATE_LIMITED` — https://spaw.co/docs/errors/RATE_LIMITED
---
Canonical page: https://spaw.co/docs/api/add-phone-suppressions · OpenAPI document: https://spaw.co/openapi.json · All endpoints: https://spaw.co/docs/api
---
# Remove numbers from the phone suppression list
`DELETE /api/v1/phone/suppressions`
- Authentication: Secret API key as a bearer token
- Billing: Free.
- Group: Phone
Removes up to 1,000 numbers per request by the number itself, for a caller that knows the number but not the id it was filed under. Each value is read to E.164 first, against `country` when it carries no calling code, so `07911 012345` with `country: "GB"` removes the entry filed as `+447911012345`.
The answer names every number it was given, spelled the way you sent it: `removed` are the ones that were on the list and no longer are, `not_found` the ones that were not on it — including a number that is not a valid subscriber number, and one that is on another account's list.
The cap is 1,000 rather than the import's 10,000 because the answer names every value back: a request over the cap is a `422` and removes nothing. Removing an entry means the number is looked up fresh the next time a batch, bulk or monitor run meets it.
## Request body
| Field | Type | Required | Description |
| --- | --- | --- | --- |
| `values` | string[] | yes | 1 to 1,000 numbers, each in any notation. |
| `country` | string | null | no | The region values written without a calling code are read against. |
## Example request
```bash
curl -X DELETE https://spaw.co/api/v1/phone/suppressions \
-H "Authorization: Bearer sk_live_…" \
-H "Content-Type: application/json" \
-d '{
"values": [
"07911 012345",
"+14152010142"
],
"country": "GB"
}'
```
## Responses
### 200 — Which of the numbers were on the list and which were not.
```json
{
"success": true,
"data": {
"removed": [
"07911 012345"
],
"not_found": [
"+14152010142"
]
}
}
```
### 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"
}
}
```
### 422 — The request body could not be validated; `error.errors` lists the fields.
```json
{
"success": false,
"error": {
"code": "VALIDATION_FAILED",
"message": "The email field is required.",
"errors": {
"email": [
"The email field is required."
]
},
"request_id": "req_01m1kgdm4xngzmbmff68g94w0c"
}
}
```
### 429 — Over 5 requests per second for the key. Retry after the limit resets.
```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
- `VALIDATION_FAILED` — https://spaw.co/docs/errors/VALIDATION_FAILED
- `RATE_LIMITED` — https://spaw.co/docs/errors/RATE_LIMITED
---
Canonical page: https://spaw.co/docs/api/remove-phone-suppressions · OpenAPI document: https://spaw.co/openapi.json · All endpoints: https://spaw.co/docs/api
---
# Remove a phone suppression entry
`DELETE /api/v1/phone/suppressions/{suppressionId}`
- Authentication: Secret API key as a bearer token
- Billing: Free.
- Group: Phone
Deletes one entry so the number is looked up fresh the next time a batch, bulk or monitor run meets it. An entry that belongs to another account answers `404`.
## Parameters
| Name | In | Type | Required | Description |
| --- | --- | --- | --- | --- |
| `suppressionId` | path | integer | yes | The entry's id from the list endpoint. |
## Example request
```bash
curl -X DELETE https://spaw.co/api/v1/phone/suppressions/311 \
-H "Authorization: Bearer sk_live_…"
```
## Responses
### 200 — The entry was removed.
```json
{
"success": true
}
```
### 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"
}
}
```
### 429 — Over 5 requests per second for the key. Retry after the limit resets.
```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
- `RATE_LIMITED` — https://spaw.co/docs/errors/RATE_LIMITED
---
Canonical page: https://spaw.co/docs/api/remove-phone-suppression · OpenAPI document: https://spaw.co/openapi.json · All endpoints: https://spaw.co/docs/api
---
# List the account's phone monitors
`GET /api/v1/phone/monitors`
- Authentication: Secret API key as a bearer token
- Billing: Free.
- Group: Phone
The saved number lists this account re-checks on a schedule, newest first, 100 per page. Each monitor carries the summary of its last run: how many numbers were valid, invalid, in a block nobody holds or unreachable, how many changed status, how many decayed (valid last time, not any more), how many were newly flagged (still valid, but changed), the credits the run spent, and `stopped_reason` when it did not finish cleanly: `insufficient_credits` or `key_spend_cap` where the run stopped part-way, `run_failed` where it died outright, and `run_failed_retrying` where it died and the schedule has been pulled in to check again — `retry_at` in the same block says when that becomes due, and the hourly sweep picks it up at or after that, so between 45 minutes and about an hour and three quarters later. A summary carrying a `stopped_reason` of either failure kind has NO counts in it at all: the run produced none, so read a missing count as unknown rather than as zero. The numbers and their statuses are on the single-monitor endpoint.
## Parameters
| Name | In | Type | Required | Description |
| --- | --- | --- | --- | --- |
| `page` | query | integer | no | The page to return. Default: 1. |
## Example request
```bash
curl https://spaw.co/api/v1/phone/monitors \
-H "Authorization: Bearer sk_live_…"
```
## Responses
### 200 — One page of monitors.
```json
{
"success": true,
"data": {
"monitors": [
{
"id": 7,
"name": "SMS audience",
"cadence": "monthly",
"number_count": 2,
"country": "GB",
"live_check": false,
"next_run_at": "2026-10-05T08:00:00+00:00",
"last_run_at": "2026-09-05T08:00:12+00:00",
"last_summary": {
"total": 2,
"valid": 1,
"invalid": 0,
"unassigned": 1,
"unreachable": 0,
"suppressed": 0,
"changed": 0,
"decayed": 0,
"credits_used": 1,
"stopped_reason": null
},
"created_at": "2026-08-27T08:00:00+00:00"
}
]
},
"meta": {
"page": 1,
"per_page": 100,
"total": 1,
"retention_days": 365
}
}
```
### 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"
}
}
```
### 429 — Over 5 requests per second for the key. Retry after the limit resets.
```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
- `RATE_LIMITED` — https://spaw.co/docs/errors/RATE_LIMITED
---
Canonical page: https://spaw.co/docs/api/list-phone-monitors · OpenAPI document: https://spaw.co/openapi.json · All endpoints: https://spaw.co/docs/api
---
# Monitor a phone list on a schedule
`POST /api/v1/phone/monitors`
- 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).
- Group: Phone
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 | Required | Description |
| --- | --- | --- | --- |
| `name` | string | yes | A label for the dashboard and the decay alerts. At most 100 characters. |
| `numbers` | string[] | yes | 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. |
| `cadence` | string | yes | 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 | no | The region for numbers written without a calling code. |
| `live_check` | boolean | no | Ask for the live carrier check on every run where the service has it enabled. Default: . |
| `webhook_url` | string | null | no | 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 | no | 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
```bash
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"
}'
```
## Responses
### 201 — The monitor was saved and its baseline run queued.
```json
{
"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"
}
}
}
```
### 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"
}
}
```
### 422 — The request body could not be validated; `error.errors` lists the fields.
```json
{
"success": false,
"error": {
"code": "VALIDATION_FAILED",
"message": "The email field is required.",
"errors": {
"email": [
"The email field is required."
]
},
"request_id": "req_01m1kgdm4xngzmbmff68g94w0c"
}
}
```
### 429 — Over 5 requests per second for the key. Retry after the limit resets.
```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
- `VALIDATION_FAILED` — https://spaw.co/docs/errors/VALIDATION_FAILED
- `RATE_LIMITED` — https://spaw.co/docs/errors/RATE_LIMITED
---
Canonical page: https://spaw.co/docs/api/create-phone-monitor · OpenAPI document: https://spaw.co/openapi.json · All endpoints: https://spaw.co/docs/api
---
# Read a phone monitor and its last results
`GET /api/v1/phone/monitors/{monitorId}`
- Authentication: Secret API key as a bearer token
- Billing: Free.
- Group: Phone
The monitor plus `results`: the status of every number from the last run (`valid`, `invalid`, `unassigned`, `unreachable` or `suppressed` — an entry you put on your suppression list yourself, an import, your own failure or abuse reports or an opt-out, which is never counted as decay), keyed by the number as it was saved — its E.164, or the entry as typed where it does not parse — or null until the first run has finished. Beside it, `flags` maps the same numbers to the change flags that run raised (`[]` where nothing changed); it is null until a run has recorded them, which includes a monitor whose last run came before flags existed. A monitor that belongs to another account answers `404`.
## Parameters
| Name | In | Type | Required | Description |
| --- | --- | --- | --- | --- |
| `monitorId` | path | integer | yes | The monitor's id from the list endpoint. |
## Example request
```bash
curl https://spaw.co/api/v1/phone/monitors/7 \
-H "Authorization: Bearer sk_live_…"
```
## Responses
### 200 — The monitor.
```json
{
"success": true,
"data": {
"monitor": {
"id": 7,
"name": "SMS audience",
"cadence": "monthly",
"number_count": 2,
"country": "GB",
"live_check": false,
"next_run_at": "2026-10-05T08:00:00+00:00",
"last_run_at": "2026-09-05T08:00:12+00:00",
"last_summary": {
"total": 2,
"valid": 1,
"invalid": 0,
"unassigned": 1,
"unreachable": 0,
"suppressed": 0,
"changed": 0,
"decayed": 0,
"newly_flagged": 1,
"credits_used": 1,
"stopped_reason": null
},
"created_at": "2026-08-27T08:00:00+00:00",
"results": {
"+447911012345": "valid",
"+442030130018": "unassigned"
},
"flags": {
"+447911012345": [
"became_disposable"
],
"+442030130018": []
}
}
}
}
```
### 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"
}
}
```
### 429 — Over 5 requests per second for the key. Retry after the limit resets.
```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
- `RATE_LIMITED` — https://spaw.co/docs/errors/RATE_LIMITED
---
Canonical page: https://spaw.co/docs/api/get-phone-monitor · OpenAPI document: https://spaw.co/openapi.json · All endpoints: https://spaw.co/docs/api
---
# Change a monitor
`PATCH /api/v1/phone/monitors/{monitorId}`
- Authentication: Secret API key as a bearer token
- Billing: Free. The change applies to the next run.
- Group: Phone
Changes a saved monitor in place: rename it, move it to another cadence, replace the numbers, or point it at a webhook. Send only the fields you are changing; a body that names none of them answers `422` rather than quietly changing nothing, because that is the shape a misspelled field name takes.
Replacing `numbers` replaces the list. What the last run recorded about the numbers that stayed is kept, so their next run still compares against the answer they already had; what it recorded about the ones that are gone is deleted with them; and an entry that is new starts with no baseline, so its first run only sets one. Numbers are saved in E.164 exactly as on creation, read against the `country` this call sends or else the monitor's own, so a number sent again in another notation is the same entry and keeps its baseline. Changing `country` alone reads the saved numbers against it again. A rename never touches a stored answer — they are keyed by the entry, never by the list.
Changing `cadence` neither skips a run nor starts a second one: a run already scheduled sooner than the new interval still happens, and a longer interval applies from the one after it. Nothing is queued by this call — the change applies to the next run, and `POST .../run` is there to have it now.
Sending a `webhook_url` that differs from the one stored mints a new signing secret and returns it once, here; sending the URL it already has keeps the secret it is signing with, and `null` removes the webhook. A monitor that belongs to another account answers `404`.
## Parameters
| Name | In | Type | Required | Description |
| --- | --- | --- | --- | --- |
| `monitorId` | path | integer | yes | The monitor's id from the list endpoint. |
## Request body
| Field | Type | Required | Description |
| --- | --- | --- | --- |
| `name` | string | no | At most 100 characters. |
| `numbers` | string[] | no | |
| `cadence` | string | no | One of: weekly, monthly. |
| `country` | string | null | no | The region numbers written without a calling code are read against. |
| `live_check` | boolean | no | |
| `webhook_url` | string | null | no | At most 2,048 characters. |
| `webhook_events` | string | no | One of: changes, every_run. |
## Example request
```bash
curl -X PATCH https://spaw.co/api/v1/phone/monitors/41 \
-H "Authorization: Bearer sk_live_…" \
-H "Content-Type: application/json" \
-d '{
"name": "SMS audience (EU)",
"country": "DE"
}'
```
## Responses
### 200 — The monitor as it now stands.
### 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"
}
}
```
### 422 — The request body could not be validated; `error.errors` lists the fields.
```json
{
"success": false,
"error": {
"code": "VALIDATION_FAILED",
"message": "The email field is required.",
"errors": {
"email": [
"The email field is required."
]
},
"request_id": "req_01m1kgdm4xngzmbmff68g94w0c"
}
}
```
### 429 — Over 5 requests per second for the key. Retry after the limit resets.
```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
- `VALIDATION_FAILED` — https://spaw.co/docs/errors/VALIDATION_FAILED
- `RATE_LIMITED` — https://spaw.co/docs/errors/RATE_LIMITED
---
Canonical page: https://spaw.co/docs/api/update-phone-monitor · OpenAPI document: https://spaw.co/openapi.json · All endpoints: https://spaw.co/docs/api
---
# Stop monitoring a phone list
`DELETE /api/v1/phone/monitors/{monitorId}`
- Authentication: Secret API key as a bearer token
- Billing: Free.
- Group: Phone
Removes the monitor and its stored results; nothing further is scheduled. A run that is already queued finishes on its own and bills as usual. A monitor that belongs to another account answers `404`.
## Parameters
| Name | In | Type | Required | Description |
| --- | --- | --- | --- | --- |
| `monitorId` | path | integer | yes | The monitor's id from the list endpoint. |
## Example request
```bash
curl -X DELETE https://spaw.co/api/v1/phone/monitors/7 \
-H "Authorization: Bearer sk_live_…"
```
## Responses
### 200 — The monitor was removed.
```json
{
"success": true
}
```
### 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"
}
}
```
### 429 — Over 5 requests per second for the key. Retry after the limit resets.
```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
- `RATE_LIMITED` — https://spaw.co/docs/errors/RATE_LIMITED
---
Canonical page: https://spaw.co/docs/api/delete-phone-monitor · OpenAPI document: https://spaw.co/openapi.json · All endpoints: https://spaw.co/docs/api
---
# Re-check a monitored phone list now
`POST /api/v1/phone/monitors/{monitorId}/run`
- Authentication: Secret API key as a bearer token
- Billing: The run bills every number like a single lookup; suppressed numbers and 7-day repeats are free.
- Group: Phone
Queues a run right away instead of waiting for the schedule; when it finishes, the next scheduled run is set one cadence from then. Answers `202` with the monitor as it stands before the run; poll it for the new `last_run_at`, `last_summary` and `results`. A monitor that belongs to another account answers `404`.
## Parameters
| Name | In | Type | Required | Description |
| --- | --- | --- | --- | --- |
| `monitorId` | path | integer | yes | The monitor's id from the list endpoint. |
## Example request
```bash
curl -X POST https://spaw.co/api/v1/phone/monitors/7/run \
-H "Authorization: Bearer sk_live_…"
```
## Responses
### 202 — The run was queued.
```json
{
"success": true,
"data": {
"monitor": {
"id": 7,
"name": "SMS audience",
"cadence": "monthly",
"number_count": 2,
"country": "GB",
"live_check": false,
"next_run_at": "2026-10-05T08:00:00+00:00",
"last_run_at": "2026-09-05T08:00:12+00:00",
"last_summary": {
"total": 2,
"valid": 1,
"invalid": 0,
"unassigned": 1,
"unreachable": 0,
"suppressed": 0,
"changed": 0,
"decayed": 0,
"credits_used": 1,
"stopped_reason": null
},
"created_at": "2026-08-27T08:00:00+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"
}
}
```
### 429 — Over 5 requests per second for the key. Retry after the limit resets.
```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
- `RATE_LIMITED` — https://spaw.co/docs/errors/RATE_LIMITED
---
Canonical page: https://spaw.co/docs/api/run-phone-monitor · OpenAPI document: https://spaw.co/openapi.json · All endpoints: https://spaw.co/docs/api
---
# Read the account's SMS policy
`GET /api/v1/phone/policy`
- Authentication: Secret API key as a bearer token
- Billing: Free.
- Group: Phone
The rules behind `ok_to_send` on every phone lookup this account runs: the countries it sends to, the line types it refuses, the signals that block, the risk-score ceiling, whether a live check must have reached the handset, and whether a single lookup of a number the suppression list holds as opted out is blocked. The policy belongs to the account, so every member reads and changes the same one. An invalid number is always blocked whatever the policy says.
`ok_to_send` is a fraud and deliverability decision computed from the answer and these rules. It is not consent to message anyone and not a do-not-call scrub; neither is part of the answer.
## Example request
```bash
curl https://spaw.co/api/v1/phone/policy \
-H "Authorization: Bearer sk_live_…"
```
## Responses
### 200 — The policy in force.
```json
{
"success": true,
"data": {
"max_risk_score": 60,
"block_signals": [
"fictional",
"disposable",
"reported_abuse",
"reported_abuse_widely",
"premium_rate"
],
"allowed_countries": null,
"blocked_countries": null,
"blocked_line_types": [],
"require_reachable": false,
"block_opted_out": true,
"is_default": true,
"updated_at": null
}
}
```
## Error codes
- `UNAUTHENTICATED` — https://spaw.co/docs/errors/UNAUTHENTICATED
- `RATE_LIMITED` — https://spaw.co/docs/errors/RATE_LIMITED
---
Canonical page: https://spaw.co/docs/api/get-phone-policy · OpenAPI document: https://spaw.co/openapi.json · All endpoints: https://spaw.co/docs/api
---
# Change one or more rules of the SMS policy
`PUT /api/v1/phone/policy`
- Authentication: Secret API key as a bearer token
- Billing: Free.
- Group: Phone
Names only the rules that change; the rest stay as they were, and the change applies to the account's next lookup. `max_risk_score: null` ignores the score, `block_signals: []` blocks on no signal, `allowed_countries: null` sends to any country and `blocked_countries: null` blocks none. Country codes are read upper-case and kept sorted; an empty `allowed_countries` is refused, because it would block every number.
A country allow-list is the first control against SMS pumping to destinations the business never serves. It is a business choice about where to send, not a verdict that a country's numbers are fraudulent. The country is the number's own, read from its calling code and prefix, so +1 876 is Jamaica rather than the United States; a number with no country (international networks, global freephone) is outside every allow-list.
`blocked_line_types` refuses the line types named; `fixed_line_or_mobile` can never be one, because it is what the numbering plan answers when it cannot tell a mobile from a landline, which is most of North America. UK 084 and 087 numbers answer `shared_cost`, not `premium_rate`, so a sender that refuses them lists both. `require_reachable: true` passes only a lookup that asked for the live check (`hlr`) and got `reachable: true`: a handset the network could not reach is blocked as `unreachable`, and a lookup with no live answer as `unconfirmed` — while the live check is not enabled on the service, that is every lookup.
`block_opted_out` (default true) blocks every lookup of one number — the single lookup, the browser endpoint, the dashboard, the MCP tool and the consistency check — when your own suppression list (that of the user whose key or session made the lookup; each member keeps their own, while the policy is the account's) holds the number as opted out (`meta.opted_out: true`), as `blocked_by: opted_out`, checked right after `invalid`. Batch, bulk and monitor runs serve such a number from the list as `suppressed` either way, and test numbers never read the list. An OTP or transactional sender may want it off: an opt-out from marketing does not always forbid a verification code, and the person typing the number may be its owner asking for one again. Off, the number is decided like any other and `meta.opted_out` still reports it. Honouring an opt-out stays the account's obligation; the rule is not a consent record.
A body naming none of the seven rules is refused with a 422 keyed `policy`, because that is the shape a misspelled key takes.
## Request body
| Field | Type | Required | Description |
| --- | --- | --- | --- |
| `max_risk_score` | integer | null | no | Block when risk_score is above this; null ignores the score. |
| `block_signals` | string[] | null | no | Signals that block whenever they fire, whatever the score. |
| `allowed_countries` | string[] | null | no | Send only to numbers of these regions (ISO 3166-1 alpha-2, as the numbering plan knows them); null sends to any. |
| `blocked_countries` | string[] | null | no | Never send to numbers of these regions; null or an empty list blocks none. |
| `blocked_line_types` | string[] | null | no | Line types that block. fixed_line_or_mobile can never be blocked. |
| `require_reachable` | boolean | no | Block unless a live check reached the handset (hlr_checked true and reachable true). |
| `block_opted_out` | boolean | no | Block a single or browser lookup of a number your suppression list holds as opted out (meta.opted_out true), as blocked_by opted_out. True by default; an OTP or transactional sender may switch it off. Batch, bulk and monitor runs answer such a number suppressed either way. |
## Example request
```bash
curl -X PUT https://spaw.co/api/v1/phone/policy \
-H "Authorization: Bearer sk_live_…" \
-H "Content-Type: application/json" \
-d '{
"max_risk_score": 50,
"allowed_countries": [
"GB",
"IE"
]
}'
```
## Responses
### 200 — The policy now in force.
## Error codes
- `UNAUTHENTICATED` — https://spaw.co/docs/errors/UNAUTHENTICATED
- `RATE_LIMITED` — https://spaw.co/docs/errors/RATE_LIMITED
- `VALIDATION_FAILED` — https://spaw.co/docs/errors/VALIDATION_FAILED
---
Canonical page: https://spaw.co/docs/api/update-phone-policy · OpenAPI document: https://spaw.co/openapi.json · All endpoints: https://spaw.co/docs/api
---
# Restore the default SMS policy
`DELETE /api/v1/phone/policy`
- Authentication: Secret API key as a bearer token
- Billing: Free.
- Group: Phone
Puts the published defaults back — block above 60 and on fictional, disposable, reported_abuse, reported_abuse_widely and premium_rate, any country, no line type, no live-check requirement, block opted-out numbers — and forgets when the policy was last changed.
## Example request
```bash
curl -X DELETE https://spaw.co/api/v1/phone/policy \
-H "Authorization: Bearer sk_live_…"
```
## Responses
### 200 — The defaults, in force again.
## Error codes
- `UNAUTHENTICATED` — https://spaw.co/docs/errors/UNAUTHENTICATED
- `RATE_LIMITED` — https://spaw.co/docs/errors/RATE_LIMITED
---
Canonical page: https://spaw.co/docs/api/reset-phone-policy · OpenAPI document: https://spaw.co/openapi.json · All endpoints: https://spaw.co/docs/api
---
# Locate and risk-score an IP address
`POST /api/v1/ip`
- Authentication: Secret API key as a bearer token
- Billing: 1 credit per 5 fresh lookups that resolve at least one signal (a location, a network, or a positive threat flag: Tor, datacenter, VPN, relay, blocklist, crawler or corporate proxy): the answered lookup that opens a block costs 1 credit and the next four cost nothing, with `meta.ip_lookups_prepaid` counting what is left. Reserved and private ranges, unallocated space, unknown addresses and 7-day repeats are free and use none of the block. Privacy-mode lookups are always fresh.
- Group: IP
Locates an IPv4 or IPv6 address with the DB-IP Lite city and ASN databases, applies the network operator's own published location where a geofeed or provider list carries one (`location_source: "operator"`), and flags datacenter, Tor exit, VPN, iCloud Private Relay and corporate-gateway addresses from open and first-party feeds: the Tor Project's exit list and Onionoo relay data, X4BNet's datacenter and VPN lists, the published ranges of AWS, Google Cloud, Microsoft Azure, Oracle Cloud, DigitalOcean, Linode, Vultr, Cloudflare, Fastly and GitHub's Actions runners, the server lists of Mullvad, NordVPN, Private Internet Access, IVPN, AirVPN and the VPN Gate volunteer relays, Apple's relay egress list, and the egress ranges Zscaler publishes for its clouds. `hosting_provider`, `privacy_service` and `corporate_proxy` name the provider when its own feed contains the address. Cloudflare publishes no list of its WARP egress, so WARP is inferred as Cloudflare's own network outside the CDN ranges it does publish, and `privacy_service` then reads `Cloudflare WARP (inferred)`. `is_anonymous` is true when any of Tor, VPN or relay is. `is_crawler` flags the ranges the search, AI and SEO crawlers publish for verification (Googlebot and Google's fetchers, Bingbot, GPTBot, ChatGPT-User, OAI-SearchBot, PerplexityBot, Perplexity-User, DuckDuckBot, AhrefsBot), with `crawler` and `crawler_kind` naming them; a crawler is not a risk, so it carries no weight and, like relay egress and a corporate gateway, never the datacenter weight.
Country-level facts come with the location: `country_name`, `continent`, `is_eu`, `is_eea`, `privacy_regime` (the comprehensive privacy law where the address is, the region first: ccpa in California, gdpr across the EU and EEA, uk_gdpr, lgpd, …, null when none is known), `sanctions` (the programmes naming the country: `ofac_comprehensive` for Cuba, Iran and North Korea, `ofac_programme` for any country-related OFAC programme; an empty list when none does, null when the country is unknown) and `fatf_list` (`call_for_action` or `increased_monitoring`, as of the FATF's June 2026 plenary) — facts about a place, never a verdict about a person — plus `currency`, `calling_code`, and `timezone` with `timezone_source`: the curated map settles the zone by `region` or `country` where a place keeps one clock, and where it cannot (Texas, most of Canada, Australia, Brazil, Russia) the nearest GeoNames place inside the located country within 250 km does (`nearest_city`); null when neither can. Send `browser_timezone`, the zone the browser reported to your page, and `timezone_matches` says whether it keeps the same clock as the located zone. `network` is the prefix the location answer covers, and `location_confidence` grades the city: low for an anycast, mobile-carrier or satellite address whoever placed it, else high when the operator declared it, otherwise from the measured agreement for that country in the weekly accuracy report. `registry`, `registry_country` and `allocated_at` come from the RIR delegated statistics; a freshly allocated block, or a registry country that disagrees with the geolocation, is worth a second look. `connection_type` says what the address is attached to from positive evidence only — `corporate_proxy`, `hosting`, `satellite`, `mobile`, or `residential` from a dynamic reverse-DNS name — and null never means residential; `is_mobile` and `is_satellite` come from curated, positive-only operator lists, so false means "not listed".
Two more feeds answer questions the others cannot: `is_unallocated` is true when no regional Internet registry has allocated or assigned the space, from the five RIRs' daily statistics, which is the strongest sign of a spoofed or forged address; `is_anycast` marks published anycast ranges such as CDN edges and the public resolvers. `is_blocklisted` is true when a public threat blocklist names the address's netblock or its operator: Spamhaus DROP, Spamhaus ASN-DROP (matched by the address's ASN) and abuse.ch's Feodo Tracker list of active botnet C2 servers, with `blocklist` naming the list. `is_attack_source` is true when an attack-source list names the address — single hosts seen attacking honeypots and sensors in the last days: Emerging Threats' compromised-hosts list, the CINS Army list and blocklist.de — with `attack_source` naming the list; these name hosts rather than netblocks and churn daily, so they sit apart from the blocklists, weigh 50 and are refreshed hourly. Tor exits are read from both the Tor Project's bulk list and its Onionoo relay data, refreshed hourly, so `is_tor` is evaluated for IPv6 too; a thirty-day exit history answers `tor_last_seen`, the date the address was last seen exiting, and an address that left the list within the last week still carries the `recently_tor` weight. Send `abuse_contact: true` to add the network's abuse mailbox from the registry's RDAP record; it is a network lookup, cached per allocation, so it is only made when asked for. `network_details: true` adds the record's name, organisation, range and dates from the same cached lookup, and `hostname: true` adds the reverse DNS name with `hostname_verified` saying whether it forward-confirms, `hostname_kind` reading the naming convention as `dynamic` or `static`, and `hostname_domain` its registrable domain; a verified name under a crawler operator's domain also proves the crawler. The RFC 8805 geofeeds discovered across the registries every week feed the location override too.
Your account's own allow and deny lists (`/api/v1/ip/lists`) apply to every lookup the account is billed for: `account_list` names the list and the entry that matched, an allow-list hit answers `risk_score: 0` with a single `allowlisted` signal of weight 0, and a deny-list hit adds `denylisted` 100. `seen_by_you` counts how often this account looked the address up in the seven days before this call and when it first did, under a keyed hash of the address; it is null under privacy mode and for reserved addresses.
The flags are tri-state: `false` is a checked negative, `null` means the signal was not evaluated (feed not installed, or no feed for that signal covers the address family). Apple's relay egress also appears in the aggregate VPN list, so when Apple's own list says relay and no VPN provider's list names the address, `is_vpn` answers `false`: the first-party declaration outranks the aggregate classification. `risk_score` sums only the signals that were checked — denylisted 100, unallocated 100, blocklist 80, Tor 70, reported_abuse 60, attack_source 50, datacenter 40, reported_abuse_widely 40, recently_tor 40, VPN 30, relay 10, capped at 100; the datacenter weight is skipped when `is_relay`, `is_crawler` or `is_corporate_proxy` is true because relay egress, verified crawlers and corporate gateways sit on infrastructure by design; `reported_abuse` is your own outcome reports for the address inside the window and `reported_abuse_widely` means three or more accounts reported it (see the feedback endpoint) — `risk_signals` lists each contributing signal with the weight applied, and `risk_level` buckets the score with the same thresholds as email: 60 and above high, 30 and above medium. Reserved and private ranges answer `reason: "reserved_range"` with every field null.
Nothing about an IP lookup is stored: no history row, no log line with the address. The only traces are a keyed seven-day marker that makes a repeat free and the keyed counter behind `seen_by_you`. Send `privacy: true` to skip both; every such call is then a fresh, billable lookup. Send `fields`, a comma-separated list of field names, to have `data` carry only those: unknown names are ignored, an empty list means everything, and `sources` is included only if named.
The same lookup is available as `GET /api/v1/ip/{ip}`, and `GET /api/v1/ip/me` answers for the address the call came from; both answer with `Cache-Control: private, max-age=300`. IP endpoints have their own rate limit of 50 requests per second per key.
## Request body
| Field | Type | Required | Description |
| --- | --- | --- | --- |
| `ip` | string | yes | An IPv4 or IPv6 address. |
| `privacy` | boolean | null | no | Keep nothing about this lookup, not even the seven-day repeat marker. The call is billed as a fresh lookup. |
| `abuse_contact` | boolean | null | no | Also look up the network's abuse mailbox in the registry's RDAP record. |
| `network_details` | boolean | null | no | Also read the allocation's RDAP record for the network name, organisation, covered range and registration dates (one registry call, shared with abuse_contact and cached per allocation). |
| `hostname` | boolean | null | no | Also resolve the reverse DNS name and forward-confirm it; a verified name from a crawler operator's domain sets is_crawler. Two DNS queries, cached six hours per address. |
| `browser_timezone` | string | null | no | The IANA zone the browser reported to your page (`Intl.DateTimeFormat().resolvedOptions().timeZone`), compared with the located zone for `timezone_matches`. A zone the server does not know answers 422. At most 64 characters. |
| `fields` | string | null | no | A comma-separated list of the field names to answer with; `data` then carries only those, in the answer's own order. Unknown names are ignored, an empty list means everything, and `sources` is included only if named. `meta` is never narrowed. At most 1,000 characters. |
## Example request
```bash
curl -X POST https://spaw.co/api/v1/ip \
-H "Authorization: Bearer sk_sandbox_spaw" \
-H "Content-Type: application/json" \
-d '{
"ip": "8.8.8.8"
}'
```
## Responses
### 200 — The location, network, flags and score.
```json
{
"success": true,
"data": {
"ip": "8.8.8.8",
"version": 4,
"reason": null,
"network": "8.8.8.0/24",
"continent": "NA",
"country": "US",
"country_name": "United States",
"is_eu": false,
"is_eea": false,
"privacy_regime": "ccpa",
"sanctions": [],
"fatf_list": null,
"region": "California",
"city": "Mountain View",
"latitude": 37.4056,
"longitude": -122.0775,
"location_source": "database",
"location_confidence": "low",
"timezone": "America/Los_Angeles",
"timezone_source": "region",
"timezone_matches": null,
"currency": "USD",
"calling_code": 1,
"asn": 15169,
"org": "Google LLC",
"asn_type": "hosting",
"connection_type": "hosting",
"is_mobile": false,
"is_satellite": false,
"registry": "arin",
"registry_country": "US",
"allocated_at": "2023-12-28",
"abuse_contact": null,
"network_details": null,
"hostname": null,
"hostname_verified": null,
"hostname_kind": null,
"hostname_domain": null,
"is_datacenter": true,
"hosting_provider": null,
"is_corporate_proxy": false,
"corporate_proxy": null,
"is_crawler": false,
"crawler": null,
"crawler_kind": null,
"is_anycast": true,
"is_unallocated": false,
"is_blocklisted": false,
"blocklist": null,
"is_attack_source": false,
"attack_source": null,
"is_tor": false,
"tor_last_seen": null,
"is_vpn": false,
"is_relay": false,
"privacy_service": null,
"is_anonymous": false,
"account_list": null,
"risk_score": 40,
"risk_level": "medium",
"risk_signals": [
{
"signal": "datacenter",
"weight": 40,
"dataset": "datacenter-ranges"
}
],
"seen_by_you": {
"count": 0,
"first_seen_at": null,
"window_days": 7
},
"sources": [
{
"dataset": "dbip-city-lite",
"version": "2026-09"
},
{
"dataset": "dbip-asn-lite",
"version": "2026-09"
},
{
"dataset": "tor-exit-list",
"version": "2026-09-12"
},
{
"dataset": "datacenter-ranges",
"version": "2026-09-12"
},
{
"dataset": "vpn-ranges",
"version": "2026-09-12"
},
{
"dataset": "relay-ranges",
"version": "2026-09-12"
},
{
"dataset": "anycast-ranges",
"version": "2026-09-12"
},
{
"dataset": "rir-allocations",
"version": "2026-09-12"
},
{
"dataset": "threat-blocklists",
"version": "2026-09-12"
},
{
"dataset": "attack-source-lists",
"version": "2026-09-12"
},
{
"dataset": "crawler-ranges",
"version": "2026-09-12"
},
{
"dataset": "corporate-proxy-ranges",
"version": "2026-09-12"
},
{
"dataset": "satellite-asns",
"version": "2026-09-12"
},
{
"dataset": "mobile-carrier-asns",
"version": "2026-09-12"
},
{
"dataset": "tor-exit-history",
"version": "2026-09-12"
},
{
"dataset": "operator-geofeeds",
"version": "2026-09-06"
},
{
"dataset": "compliance-lists",
"version": "2026-09-12"
}
]
},
"meta": {
"credits_used": 1,
"credits_remaining": 8,
"cache_hit": false,
"request_id": "req_01m1kgdm4xngzmbmff68g94w0c",
"ip_lookups_prepaid": 4
}
}
```
### 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"
}
}
```
### 402 — The balance is empty. The lookup did not run.
```json
{
"success": false,
"error": {
"code": "INSUFFICIENT_CREDITS",
"message": "Your credit balance is empty.",
"request_id": "req_01m1kgdm4xngzmbmff68g94w0c"
}
}
```
### 422 — The request body could not be validated; `error.errors` lists the fields.
```json
{
"success": false,
"error": {
"code": "VALIDATION_FAILED",
"message": "The email field is required.",
"errors": {
"email": [
"The email field is required."
]
},
"request_id": "req_01m1kgdm4xngzmbmff68g94w0c"
}
}
```
### 429 — Over 5 requests per second for the key. Retry after the limit resets.
```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
- `VALIDATION_FAILED` — https://spaw.co/docs/errors/VALIDATION_FAILED
- `INSUFFICIENT_CREDITS` — https://spaw.co/docs/errors/INSUFFICIENT_CREDITS
- `RATE_LIMITED` — https://spaw.co/docs/errors/RATE_LIMITED
- `SANDBOX_VALUE_NOT_ALLOWED` — https://spaw.co/docs/errors/SANDBOX_VALUE_NOT_ALLOWED
- `SANDBOX_UNAVAILABLE` — https://spaw.co/docs/errors/SANDBOX_UNAVAILABLE
---
Canonical page: https://spaw.co/docs/api/lookup-ip · OpenAPI document: https://spaw.co/openapi.json · All endpoints: https://spaw.co/docs/api
---
# Look up an address by path
`GET /api/v1/ip/{ip}`
- Authentication: Secret API key as a bearer token
- Billing: As `POST /api/v1/ip`.
- Group: IP
Exactly `POST /api/v1/ip` with the address in the path and the options in the query string, for callers that prefer a plain GET: `GET /api/v1/ip/8.8.8.8?privacy=1`. The response, billing and rate limit are the same; `browser_timezone` and `fields` ride in the query string too. The answer carries `Cache-Control: private, max-age=300`, so a client cache may keep it for five minutes — the feeds behind it change once a day.
## Parameters
| Name | In | Type | Required | Description |
| --- | --- | --- | --- | --- |
| `ip` | path | string | yes | An IPv4 or IPv6 address. |
| `privacy` | query | boolean | no | Keep nothing about this lookup, not even the seven-day repeat marker. |
| `abuse_contact` | query | boolean | no | Also look up the network's abuse mailbox from the registry's RDAP record. |
| `network_details` | query | boolean | no | Also read the allocation's RDAP record for the network name, organisation, range and registration dates. |
| `hostname` | query | boolean | no | Also resolve and forward-confirm the reverse DNS name; a verified crawler name sets is_crawler. |
| `browser_timezone` | query | string | no | The IANA zone the browser reported to your page, compared with the located zone for timezone_matches; a zone the server does not know answers 422. At most 64 characters. |
| `fields` | query | string | no | A comma-separated list of the fields to answer with; unknown names are ignored and sources is included only if named. At most 1,000 characters. |
## Example request
```bash
curl https://spaw.co/api/v1/ip/8.8.8.8 \
-H "Authorization: Bearer sk_sandbox_spaw"
```
## Responses
### 200 — The same body as `POST /api/v1/ip`.
```json
{
"success": true,
"data": {
"ip": "8.8.8.8",
"version": 4,
"reason": null,
"country": "US",
"city": "Mountain View",
"is_datacenter": true,
"risk_score": 40,
"risk_level": "medium"
},
"meta": {
"credits_used": 1,
"credits_remaining": 8,
"cache_hit": false,
"request_id": "req_01m1kgdm4xngzmbmff68g94w0c",
"ip_lookups_prepaid": 4
}
}
```
### 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"
}
}
```
### 402 — The balance is empty. The lookup did not run.
```json
{
"success": false,
"error": {
"code": "INSUFFICIENT_CREDITS",
"message": "Your credit balance is empty.",
"request_id": "req_01m1kgdm4xngzmbmff68g94w0c"
}
}
```
### 422 — The request body could not be validated; `error.errors` lists the fields.
```json
{
"success": false,
"error": {
"code": "VALIDATION_FAILED",
"message": "The email field is required.",
"errors": {
"email": [
"The email field is required."
]
},
"request_id": "req_01m1kgdm4xngzmbmff68g94w0c"
}
}
```
### 429 — Over 5 requests per second for the key. Retry after the limit resets.
```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
- `VALIDATION_FAILED` — https://spaw.co/docs/errors/VALIDATION_FAILED
- `INSUFFICIENT_CREDITS` — https://spaw.co/docs/errors/INSUFFICIENT_CREDITS
- `RATE_LIMITED` — https://spaw.co/docs/errors/RATE_LIMITED
- `SANDBOX_VALUE_NOT_ALLOWED` — https://spaw.co/docs/errors/SANDBOX_VALUE_NOT_ALLOWED
- `SANDBOX_UNAVAILABLE` — https://spaw.co/docs/errors/SANDBOX_UNAVAILABLE
---
Canonical page: https://spaw.co/docs/api/get-ip · OpenAPI document: https://spaw.co/openapi.json · All endpoints: https://spaw.co/docs/api
---
# Look up the address the call came from
`GET /api/v1/ip/me`
- Authentication: Secret API key as a bearer token
- Billing: As `POST /api/v1/ip`.
- Group: IP
Runs the standard lookup for the address your request arrived from, as seen through the trusted proxy in front of the service. Useful from a server to confirm what the outside world sees, and from scripts that need their own egress classified. A reserved address, such as a call from the same machine, answers `reason: "reserved_range"` free of charge. `browser_timezone` and `fields` ride in the query string, and the answer carries `Cache-Control: private, max-age=300`.
## Parameters
| Name | In | Type | Required | Description |
| --- | --- | --- | --- | --- |
| `privacy` | query | boolean | no | Keep nothing about this lookup, not even the seven-day repeat marker. |
| `abuse_contact` | query | boolean | no | Also look up the network's abuse mailbox from the registry's RDAP record. |
| `network_details` | query | boolean | no | Also read the allocation's RDAP record for the network name, organisation, range and registration dates. |
| `hostname` | query | boolean | no | Also resolve and forward-confirm the reverse DNS name; a verified crawler name sets is_crawler. |
| `browser_timezone` | query | string | no | The IANA zone the browser reported to your page, compared with the located zone for timezone_matches; a zone the server does not know answers 422. At most 64 characters. |
| `fields` | query | string | no | A comma-separated list of the fields to answer with; unknown names are ignored and sources is included only if named. At most 1,000 characters. |
## Example request
```bash
curl https://spaw.co/api/v1/ip/me \
-H "Authorization: Bearer sk_live_…"
```
## Responses
### 200 — The same body as `POST /api/v1/ip`, for the calling address.
```json
{
"success": true,
"data": {
"ip": "203.0.113.9",
"version": 4,
"reason": null,
"country": "DE",
"city": "Berlin",
"is_datacenter": false,
"risk_score": 0,
"risk_level": "low"
},
"meta": {
"credits_used": 0,
"credits_remaining": 8,
"cache_hit": false,
"request_id": "req_01m1kgdm4xngzmbmff68g94w0c",
"ip_lookups_prepaid": 3
}
}
```
### 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"
}
}
```
### 402 — The balance is empty. The lookup did not run.
```json
{
"success": false,
"error": {
"code": "INSUFFICIENT_CREDITS",
"message": "Your credit balance is empty.",
"request_id": "req_01m1kgdm4xngzmbmff68g94w0c"
}
}
```
### 422 — The request body could not be validated; `error.errors` lists the fields.
```json
{
"success": false,
"error": {
"code": "VALIDATION_FAILED",
"message": "The email field is required.",
"errors": {
"email": [
"The email field is required."
]
},
"request_id": "req_01m1kgdm4xngzmbmff68g94w0c"
}
}
```
### 429 — Over 5 requests per second for the key. Retry after the limit resets.
```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
- `VALIDATION_FAILED` — https://spaw.co/docs/errors/VALIDATION_FAILED
- `INSUFFICIENT_CREDITS` — https://spaw.co/docs/errors/INSUFFICIENT_CREDITS
- `RATE_LIMITED` — https://spaw.co/docs/errors/RATE_LIMITED
---
Canonical page: https://spaw.co/docs/api/get-my-ip · OpenAPI document: https://spaw.co/openapi.json · All endpoints: https://spaw.co/docs/api
---
# Look up up to 1,000 addresses in one call
`POST /api/v1/ip/batch`
- Authentication: Secret API key as a bearer token
- Billing: Each address bills like a single lookup — 1 credit per 5 fresh answered lookups, prepaid ones used first; reserved and private ranges, unallocated space, unknown addresses and 7-day repeats free — and the call stops cleanly where the balance ends.
- Group: IP
Runs the single-address lookup for every item, in input order, under the exact single-lookup billing rules. Every answer comes from local databases and compiled feeds, so a full batch of 1,000 takes well under a second. `data.results[]` mirrors the single-address response per item and each item's `meta` carries `credits_used`, `cache_hit` and `ip_lookups_prepaid`. One credit pays for five answered addresses, so most items cost 0: an item that opens a block costs 1 and leaves 4 prepaid, and the lookups the account already held prepaid are used first.
Repeats inside the list are looked up once and answer as cache hits. The list is walked in order and stops where the balance would run out: running out of credits mid-batch returns the paid partial results with `meta.stopped_reason: "insufficient_credits"` and `processed` lower than `requested`, never discarding billed work. Only a batch whose first lookup is refused answers the typed `402`. A key with a daily credit cap stops the list the same way once the cap is spent and the lookups its last credit bought are used up: the addresses already paid for come back with `meta.stopped_reason: "key_spend_cap"`, and a call that starts with the cap already spent answers `429 KEY_SPEND_CAP_REACHED` and runs nothing. An item that is not an IP address fails validation for the whole request.
A batch counts as one request against the rate limit. `privacy` applies to every address in the list.
Every item carries `index`, its zero-based position in the list you sent, and `input`, the address exactly as you sent it — the answer's `ip` is its canonical form. A batch stopped early by `stopped_reason` answers only a prefix of the list, so those two are what line an answer up with the row it came from.
## Request body
| Field | Type | Required | Description |
| --- | --- | --- | --- |
| `ips` | string[] | yes | 1 to 1,000 IPv4 or IPv6 addresses. |
| `privacy` | boolean | null | no | Keep nothing about these lookups; every address is billed as a fresh lookup. |
## Example request
```bash
curl -X POST https://spaw.co/api/v1/ip/batch \
-H "Authorization: Bearer sk_live_…" \
-H "Content-Type: application/json" \
-d '{
"ips": [
"8.8.8.8",
"103.124.165.2",
"192.168.1.1"
]
}'
```
## Responses
### 200 — One result per processed address, plus the batch totals.
```json
{
"success": true,
"data": {
"results": [
{
"index": 0,
"input": "8.8.8.8",
"data": {
"ip": "8.8.8.8",
"version": 4,
"reason": null,
"country": "US",
"is_datacenter": true,
"risk_score": 40,
"risk_level": "medium"
},
"meta": {
"credits_used": 1,
"cache_hit": false,
"ip_lookups_prepaid": 4
}
},
{
"index": 1,
"input": "103.124.165.2",
"data": {
"ip": "103.124.165.2",
"version": 4,
"reason": null,
"country": "AL",
"is_vpn": true,
"privacy_service": "Mullvad",
"risk_score": 70,
"risk_level": "high"
},
"meta": {
"credits_used": 0,
"cache_hit": false,
"ip_lookups_prepaid": 3
}
},
{
"index": 2,
"input": "192.168.1.1",
"data": {
"ip": "192.168.1.1",
"version": 4,
"reason": "reserved_range",
"country": null,
"risk_score": null,
"risk_level": null
},
"meta": {
"credits_used": 0,
"cache_hit": false,
"ip_lookups_prepaid": 3
}
}
]
},
"meta": {
"requested": 3,
"processed": 3,
"credits_used": 1,
"credits_remaining": 8,
"ip_lookups_prepaid": 3,
"stopped_reason": null,
"request_id": "req_01m1kgdrtqdvwnks99vfgx2rcw"
}
}
```
### 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"
}
}
```
### 402 — The balance is empty. The lookup did not run.
```json
{
"success": false,
"error": {
"code": "INSUFFICIENT_CREDITS",
"message": "Your credit balance is empty.",
"request_id": "req_01m1kgdm4xngzmbmff68g94w0c"
}
}
```
### 422 — The request body could not be validated; `error.errors` lists the fields.
```json
{
"success": false,
"error": {
"code": "VALIDATION_FAILED",
"message": "The email field is required.",
"errors": {
"email": [
"The email field is required."
]
},
"request_id": "req_01m1kgdm4xngzmbmff68g94w0c"
}
}
```
### 429 — Over 5 requests per second for the key. Retry after the limit resets.
```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
- `VALIDATION_FAILED` — https://spaw.co/docs/errors/VALIDATION_FAILED
- `INSUFFICIENT_CREDITS` — https://spaw.co/docs/errors/INSUFFICIENT_CREDITS
- `KEY_SPEND_CAP_REACHED` — https://spaw.co/docs/errors/KEY_SPEND_CAP_REACHED
- `RATE_LIMITED` — https://spaw.co/docs/errors/RATE_LIMITED
---
Canonical page: https://spaw.co/docs/api/lookup-ip-batch · OpenAPI document: https://spaw.co/openapi.json · All endpoints: https://spaw.co/docs/api
---
# Look up an address from the browser with a publishable key
`POST /api/v1/ip/public`
- Authentication: Publishable key in the body, checked against the browser Origin header
- Billing: Bills the key owner like a single lookup; capped by the key's daily credit cap when one is set.
- Group: IP
The browser counterpart of `POST /api/v1/ip`, for localising a page or screening a form on the client without exposing a secret key. It is authenticated by a publishable `pk_` key in the body plus the browser's `Origin` header, which must match one of the domains the key is locked to. A missing `Origin` is rejected on purpose: servers use a secret key. Leave `ip` out to look up the visitor's own address, which is the usual case. Send `browser_timezone`, the zone the page read from the browser (`Intl.DateTimeFormat().resolvedOptions().timeZone`), to have it compared with the located zone in `timezone_matches`, and `fields` to receive only the fields the page needs, so nothing else reaches the visitor's browser.
The lookup bills the key's owner under the normal rules and answers `{ "success", "data" }` with **no meta block**, so page visitors never see the owner's balance. Because the key sits in page source, give it a daily credit cap in the dashboard: once the cap is spent the endpoint answers `429 KEY_SPEND_CAP_REACHED` until the next day. Free answers (reserved ranges, cache hits) never count toward the cap. A key that carries a Cloudflare Turnstile pair requires a confirmed `turnstile_token` and answers `403 TURNSTILE_FAILED` without one, before any credit is spent.
Throttled at 20 requests per minute per IP.
## Parameters
| Name | In | Type | Required | Description |
| --- | --- | --- | --- | --- |
| `Origin` | header | string | yes | Sent by browsers automatically. Its host must be on the key's allowed-domain list. |
## Request body
| Field | Type | Required | Description |
| --- | --- | --- | --- |
| `key` | string | yes | A publishable key, which starts with `pk_`. At most 64 characters. |
| `ip` | string | null | no | The address to look up. Omit it for the visitor's own address. |
| `turnstile_token` | string | null | no | Required when the key carries a Turnstile pair. A single-use token from the Turnstile widget on the page. At most 2,048 characters. |
| `browser_timezone` | string | null | no | The IANA zone the browser reported, compared with the located zone for timezone_matches; a zone the server does not know answers 422. At most 64 characters. |
| `fields` | string | null | no | A comma-separated list of the fields to answer with; unknown names are ignored and sources is included only if named. At most 1,000 characters. |
## Example request
```bash
curl -X POST https://spaw.co/api/v1/ip/public \
-H "Origin: https://www.example.com" \
-H "Content-Type: application/json" \
-d '{
"key": "pk_live_…"
}'
```
## Responses
### 200 — The same fields as `POST /api/v1/ip`, without a meta block.
```json
{
"success": true,
"data": {
"ip": "203.0.113.9",
"version": 4,
"reason": null,
"country": "DE",
"country_name": "Germany",
"is_eu": true,
"city": "Berlin",
"timezone": "Europe/Berlin",
"currency": "EUR",
"is_anonymous": false,
"risk_score": 0,
"risk_level": "low"
}
}
```
### 401 — The publishable key does not exist or was revoked.
```json
{
"success": false,
"error": {
"code": "INVALID_PUBLISHABLE_KEY",
"message": "That publishable key does not exist or was revoked.",
"request_id": "req_01m1kgdm4xngzmbmff68g94w0c"
}
}
```
### 402 — The balance is empty. The lookup did not run.
```json
{
"success": false,
"error": {
"code": "INSUFFICIENT_CREDITS",
"message": "Your credit balance is empty.",
"request_id": "req_01m1kgdm4xngzmbmff68g94w0c"
}
}
```
### 403 — The page's origin is not on the key's allowed-domain list (`ORIGIN_NOT_ALLOWED`), or the key requires a Turnstile token that was missing or not confirmed (`TURNSTILE_FAILED`).
```json
{
"success": false,
"error": {
"code": "ORIGIN_NOT_ALLOWED",
"message": "This publishable key cannot be used from this origin.",
"request_id": "req_01m1kgdm4xngzmbmff68g94w0c"
}
}
```
### 422 — The request body could not be validated; `error.errors` lists the fields.
```json
{
"success": false,
"error": {
"code": "VALIDATION_FAILED",
"message": "The email field is required.",
"errors": {
"email": [
"The email field is required."
]
},
"request_id": "req_01m1kgdm4xngzmbmff68g94w0c"
}
}
```
### 429 — The key has spent its owner's daily credit cap (`KEY_SPEND_CAP_REACHED`), or the IP exceeded 20 requests per minute (`RATE_LIMITED`).
```json
{
"success": false,
"error": {
"code": "KEY_SPEND_CAP_REACHED",
"message": "This publishable key has reached its daily credit cap. The counter resets each day.",
"request_id": "req_01m1kgdm4xngzmbmff68g94w0c"
}
}
```
## Error codes
- `INVALID_PUBLISHABLE_KEY` — https://spaw.co/docs/errors/INVALID_PUBLISHABLE_KEY
- `ORIGIN_NOT_ALLOWED` — https://spaw.co/docs/errors/ORIGIN_NOT_ALLOWED
- `TURNSTILE_FAILED` — https://spaw.co/docs/errors/TURNSTILE_FAILED
- `KEY_SPEND_CAP_REACHED` — https://spaw.co/docs/errors/KEY_SPEND_CAP_REACHED
- `VALIDATION_FAILED` — https://spaw.co/docs/errors/VALIDATION_FAILED
- `INSUFFICIENT_CREDITS` — https://spaw.co/docs/errors/INSUFFICIENT_CREDITS
- `RATE_LIMITED` — https://spaw.co/docs/errors/RATE_LIMITED
---
Canonical page: https://spaw.co/docs/api/lookup-ip-public · OpenAPI document: https://spaw.co/openapi.json · All endpoints: https://spaw.co/docs/api
---
# List the account's bulk IP lookup runs
`GET /api/v1/ip/bulk`
- Authentication: Secret API key as a bearer token
- Billing: Free.
- Group: IP
Every bulk run this account has started, newest first, 100 per page. A job id is handed out once, at creation, and everything about a run hangs off it — its counts, its results, its cancel and resume calls — so this is how a caller that lost one finds the run again, and how a dashboard lists what is still in flight.
`status`, `since` and `until` are optional and combine; a bare date in `until` means the whole of that day, and an unknown status or an unparseable date is a `422` rather than an empty page. The rows are the same job objects `GET /api/v1/ip/bulk/{jobId}` answers. The webhook secret is never among them: it is returned once, at creation.
Runs are kept as long as the lookup history, and `meta.retention_days` says how long that is — a run older than that is deleted with its files and cannot be listed.
## Parameters
| Name | In | Type | Required | Description |
| --- | --- | --- | --- | --- |
| `status` | query | string | no | Only runs in this status. One of: queued, processing, completed, failed, cancelled. |
| `since` | query | string | no | Only runs created at or after this date or timestamp. |
| `until` | query | string | no | Only runs created at or before this. A bare date means the whole of that day. |
| `page` | query | integer | no | The page to return. Default: 1. |
## Example request
```bash
curl https://spaw.co/api/v1/ip/bulk \
-H "Authorization: Bearer sk_live_…"
```
## Responses
### 200 — One page of runs, newest first.
```json
{
"success": true,
"data": {
"jobs": [
{
"id": 31,
"status": "processing",
"total": 40000,
"duplicate_count": 120,
"processed": 12000,
"low": 11400,
"medium": 500,
"high": 100,
"credits_used": 11880,
"stopped_reason": null,
"cancel_requested": false,
"webhook_status": null,
"webhook_detail": null,
"created_at": "2026-09-04T14:00:00+00:00",
"finished_at": null
}
]
},
"meta": {
"page": 1,
"per_page": 100,
"total": 1,
"retention_days": 30
}
}
```
### 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"
}
}
```
### 422 — The request body could not be validated; `error.errors` lists the fields.
```json
{
"success": false,
"error": {
"code": "VALIDATION_FAILED",
"message": "The email field is required.",
"errors": {
"email": [
"The email field is required."
]
},
"request_id": "req_01m1kgdm4xngzmbmff68g94w0c"
}
}
```
### 429 — Over 5 requests per second for the key. Retry after the limit resets.
```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
- `VALIDATION_FAILED` — https://spaw.co/docs/errors/VALIDATION_FAILED
- `RATE_LIMITED` — https://spaw.co/docs/errors/RATE_LIMITED
---
Canonical page: https://spaw.co/docs/api/list-ip-bulk-jobs · OpenAPI document: https://spaw.co/openapi.json · All endpoints: https://spaw.co/docs/api
---
# Queue a bulk IP lookup run
`POST /api/v1/ip/bulk`
- Authentication: Secret API key as a bearer token
- Billing: `preview: true` is free and queues nothing. A started run bills each row like a single lookup as it is processed; repeats inside the run and 7-day repeats are free, so what a run adds to your spend usually comes to no more than the `credits_upper_bound` the preview answered (other IP traffic can use lookups from a block the run opened, which can put the job's own `credits_used` slightly above it).
- Group: IP
Queues up to 100,000 addresses and processes them in the background. Creation answers `202` with the job's id and status. Poll `GET /api/v1/ip/bulk/{jobId}` for progress, then download the result CSV from `GET /api/v1/ip/bulk/{jobId}/results`: one row per input address with every response field as a column. Jobs and their files are kept for 30 days.
Rows repeating an address already in the list 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 an IP lookup is stored, the input list and the result file are the only copies and both are deleted with the job.
With a `webhook_url`, the finish (`completed`, `failed`, or `cancelled`) is POSTed there as `{ "event": "bulk_ip_job.finished", "job": { … } }`, signed with `webhook_secret` exactly like bulk email runs: `X-Spaw-Signature-V2` carries `t={unix seconds},v2={lowercase hex}`, the HMAC-SHA256 of `v2:{t}:{raw body}`, and a receiver should refuse a `t` more than five minutes from its own clock so a captured delivery cannot be replayed at it later. The original `X-Spaw-Signature`, over the body alone, is sent beside it until 1 March 2027. 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`.
### Reading a list before you start it
Send `preview: true` with the same body — or `csv` with a raw CSV instead of the item list — and the call answers `200` with `data.draft` instead of queueing anything: what the list was read as, which column filled which field and whether a heading named it or we guessed, the first rows as parsed, the repeats, the balance, and `credits_upper_bound`. Nothing is charged.
`credits_upper_bound` is a CEILING, not a quote: one credit per five unique addresses, because IP lookups bill in blocks of five. Rows come back free for reasons no preview can know in advance — a repeat inside the seven-day window, a reserved, private or unallocated address, IP lookups already prepaid on the account — so what a run adds to your spend usually comes to less than the ceiling. Other IP traffic on the account can use prepaid lookups from a block the run opened, so the job's own `credits_used` can come out slightly above it. It is priced against the mapping in play, so correcting a column at start re-prices the run rather than holding you to the figure the guess produced — ask for the draft again after a correction if you want the new number. What it actually cost is `credits_used` on the finished job.
Start it with `{"draft_id": }`, adding `mapping` to correct a column we read wrongly. Starting consumes the draft; one left unstarted is deleted, with the rows it holds, after `retention_hours`.
## Parameters
| Name | In | Type | Required | Description |
| --- | --- | --- | --- | --- |
| `Idempotency-Key` | header | string | no | 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 | Required | Description |
| --- | --- | --- | --- |
| `ips` | string[] | no | 1 to 100,000 IPv4 or IPv6 addresses. Required unless the body carries `csv` with `preview: true`, or `draft_id`. |
| `preview` | boolean | no | Read the list and answer `data.draft` — what we made of it, the repeats, the ceiling on the cost and the balance — with nothing queued and nothing billed. Start it afterwards with `draft_id`. Default: . |
| `csv` | string | null | no | A raw CSV to read instead of the item list, with the column mapping answered back. Preview only, on purpose: a CSV's columns have to be worked out, and working them out and billing for the answer in one request is what the preview exists to stop. At most 5,000,000 characters. |
| `draft_id` | integer | null | no | Start the run a preview answered with. The rows and the options come from the draft, so nothing else in the body applies except `mapping`. A draft can be started once; an `Idempotency-Key` still makes the call safe to retry. |
| `mapping` | object | null | no | Corrects the draft's column mapping, as column index to field — {"1": "address_line1", "2": "postal_code"}. It REPLACES the detected mapping rather than merging into it, so a column can be taken off a field as well as put on one. Goes with `draft_id`, and only for a draft read from a `csv`. |
| `webhook_url` | string | null | no | An https URL to notify when the run finishes. |
| `privacy` | boolean | null | no | Keep nothing about these lookups; every address is billed as a fresh lookup. |
## Example request
```bash
curl -X POST https://spaw.co/api/v1/ip/bulk \
-H "Authorization: Bearer sk_live_…" \
-H "Content-Type: application/json" \
-d '{
"ips": [
"8.8.8.8",
"103.124.165.2"
],
"webhook_url": "https://example.com/hooks/spaw"
}'
```
## Responses
### 202 — The job was queued.
```json
{
"success": true,
"data": {
"job": {
"id": 512,
"status": "queued",
"total": 2,
"duplicate_count": 0,
"processed": 0,
"low": 0,
"medium": 0,
"high": 0,
"credits_used": 0,
"stopped_reason": null,
"cancel_requested": false,
"webhook_status": null,
"created_at": "2026-09-04T10:12:44+00:00",
"finished_at": null,
"webhook_secret": "8fJ2…40 characters…Qk1"
}
}
}
```
### 200 — Either a preview — `preview: true` answers `data.draft` and queues nothing — or a retry with an `Idempotency-Key` already used for this exact request, which answers `data.job` with `Idempotent-Replayed: true`.
```json
{
"success": true,
"data": {
"job": {
"id": 512,
"status": "processing",
"total": 2,
"duplicate_count": 0,
"processed": 1,
"low": 0,
"medium": 1,
"high": 0,
"credits_used": 1,
"stopped_reason": null,
"cancel_requested": false,
"webhook_status": null,
"created_at": "2026-09-04T10:12:44+00:00",
"finished_at": null
}
}
}
```
### 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"
}
}
```
### 409 — The `Idempotency-Key` was already used for a different list or webhook URL.
```json
{
"success": false,
"error": {
"code": "IDEMPOTENCY_KEY_REUSED",
"message": "This Idempotency-Key was already used for a different request.",
"request_id": "req_01m1kgdm4xngzmbmff68g94w0c"
}
}
```
### 422 — The request body could not be validated; `error.errors` lists the fields.
```json
{
"success": false,
"error": {
"code": "VALIDATION_FAILED",
"message": "The email field is required.",
"errors": {
"email": [
"The email field is required."
]
},
"request_id": "req_01m1kgdm4xngzmbmff68g94w0c"
}
}
```
### 429 — Over 5 requests per second for the key. Retry after the limit resets.
```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
- `VALIDATION_FAILED` — https://spaw.co/docs/errors/VALIDATION_FAILED
- `IDEMPOTENCY_KEY_REUSED` — https://spaw.co/docs/errors/IDEMPOTENCY_KEY_REUSED
- `RATE_LIMITED` — https://spaw.co/docs/errors/RATE_LIMITED
---
Canonical page: https://spaw.co/docs/api/create-ip-bulk-job · OpenAPI document: https://spaw.co/openapi.json · All endpoints: https://spaw.co/docs/api
---
# Poll a bulk IP job
`GET /api/v1/ip/bulk/{jobId}`
- Authentication: Secret API key as a bearer token
- Billing: Free.
- Group: IP
The job's current status and running counts. `status` moves from `queued` to `processing` and ends in `completed`, `failed`, or `cancelled`; `processed` and the `low`, `medium` and `high` counts advance as chunks finish. Once the run has settled, `summary` adds up what the rows say — the top ten countries and networks, how many rows carried each signal, how many hit one of your lists, and the risk split — built from the same cells the result file holds; it is null until then. A job that belongs to another account answers `404`.
## Parameters
| Name | In | Type | Required | Description |
| --- | --- | --- | --- | --- |
| `jobId` | path | integer | yes | |
## Example request
```bash
curl https://spaw.co/api/v1/ip/bulk/{jobId} \
-H "Authorization: Bearer sk_live_…"
```
## Responses
### 200 — The job.
```json
{
"success": true,
"data": {
"job": {
"id": 512,
"status": "completed",
"total": 2,
"duplicate_count": 0,
"processed": 2,
"low": 0,
"medium": 1,
"high": 1,
"summary": {
"rows": 2,
"reserved": 0,
"countries": [
{
"country": "US",
"rows": 1
},
{
"country": "AL",
"rows": 1
}
],
"networks": [
{
"asn": 15169,
"org": "Google LLC",
"rows": 1
},
{
"asn": 42237,
"org": "w1n ltd",
"rows": 1
}
],
"signals": {
"datacenter": 2,
"corporate_proxy": 0,
"crawler": 0,
"tor": 0,
"vpn": 1,
"relay": 0,
"anonymous": 1,
"blocklisted": 0,
"attack_source": 0,
"mobile": 0,
"satellite": 0
},
"listed": {
"allow": 0,
"deny": 0
},
"risk": {
"low": 0,
"medium": 1,
"high": 1
}
},
"credits_used": 2,
"stopped_reason": null,
"cancel_requested": false,
"webhook_status": "delivered",
"created_at": "2026-09-04T10:12:44+00:00",
"finished_at": "2026-09-04T10:12:46+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"
}
}
```
### 429 — Over 5 requests per second for the key. Retry after the limit resets.
```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
- `RATE_LIMITED` — https://spaw.co/docs/errors/RATE_LIMITED
---
Canonical page: https://spaw.co/docs/api/get-ip-bulk-job · OpenAPI document: https://spaw.co/openapi.json · All endpoints: https://spaw.co/docs/api
---
# Cancel a bulk IP job
`POST /api/v1/ip/bulk/{jobId}/cancel`
- Authentication: Secret API key as a bearer token
- Billing: Free.
- Group: IP
Asks a queued or running job to stop at its next checkpoint. Everything answered so far stays billed and stays in the result file. A job that already finished answers `409 JOB_ALREADY_FINISHED`.
## Parameters
| Name | In | Type | Required | Description |
| --- | --- | --- | --- | --- |
| `jobId` | path | integer | yes | |
## Example request
```bash
curl -X POST https://spaw.co/api/v1/ip/bulk/{jobId}/cancel \
-H "Authorization: Bearer sk_live_…"
```
## Responses
### 202 — Cancellation was requested.
```json
{
"success": true,
"data": {
"job": {
"id": 512,
"status": "processing",
"total": 20000,
"duplicate_count": 12,
"processed": 4000,
"low": 3100,
"medium": 700,
"high": 200,
"credits_used": 3900,
"stopped_reason": null,
"cancel_requested": true,
"webhook_status": null,
"created_at": "2026-09-04T10:12:44+00:00",
"finished_at": null
}
}
}
```
### 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 job already finished.
```json
{
"success": false,
"error": {
"code": "JOB_ALREADY_FINISHED",
"message": "This job has already finished.",
"request_id": "req_01m1kgdm4xngzmbmff68g94w0c"
}
}
```
### 429 — Over 5 requests per second for the key. Retry after the limit resets.
```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
- `JOB_ALREADY_FINISHED` — https://spaw.co/docs/errors/JOB_ALREADY_FINISHED
- `RATE_LIMITED` — https://spaw.co/docs/errors/RATE_LIMITED
---
Canonical page: https://spaw.co/docs/api/cancel-ip-bulk-job · OpenAPI document: https://spaw.co/openapi.json · All endpoints: https://spaw.co/docs/api
---
# Resume a bulk job that ran out of credits
`POST /api/v1/ip/bulk/{jobId}/resume`
- Authentication: Secret API key as a bearer token
- Billing: Free to call. The resumed run bills the rows it reaches exactly as the first one did, and a row answered in the last seven days is a free repeat.
- Group: IP
Runs a job that stopped when the credit balance ran out again, from the top of the list it was given, instead of asking you to upload it a second time. Rows looked up in the last seven days come back as free repeats, so only the rows the stopped run never reached are charged.
The job keeps its id, its webhook and its cumulative `credits_used`, which is the real spend on the account. `processed` and the verdict counts restart at zero: the part files they were counted from are deleted when a run settles, so the resumed run rebuilds them, and the result file the stopped run left is replaced as the rows come back.
Only a run whose `stopped_reason` is `insufficient_credits` and whose `status` has settled can be resumed; anything else answers `409 JOB_NOT_RESUMABLE`, and so does a second resume of a run that is already going again — claiming the run is one conditional write, so two calls can never queue it twice. A run whose list has passed out of the retention window answers `409 JOB_INPUT_EXPIRED`. An empty balance answers `402 INSUFFICIENT_CREDITS` rather than queueing a run that would stop again on the first row it never reached.
## Parameters
| Name | In | Type | Required | Description |
| --- | --- | --- | --- | --- |
| `jobId` | path | integer | yes | |
## Example request
```bash
curl -X POST https://spaw.co/api/v1/ip/bulk/{jobId}/resume \
-H "Authorization: Bearer sk_live_…"
```
## Responses
### 202 — The list is queued again.
```json
{
"success": true,
"data": {
"job": {
"id": 512,
"status": "queued",
"total": 20000,
"duplicate_count": 12,
"processed": 0,
"low": 0,
"medium": 0,
"high": 0,
"credits_used": 3900,
"stopped_reason": null,
"cancel_requested": false,
"webhook_status": null,
"created_at": "2026-09-04T10:12:44+00:00",
"finished_at": null
}
}
}
```
### 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"
}
}
```
### 402 — The balance is empty. The lookup did not run.
```json
{
"success": false,
"error": {
"code": "INSUFFICIENT_CREDITS",
"message": "Your credit balance is empty.",
"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 is not in a state a resume can recover (`JOB_NOT_RESUMABLE`), or its list is no longer stored (`JOB_INPUT_EXPIRED`).
```json
{
"success": false,
"error": {
"code": "JOB_NOT_RESUMABLE",
"message": "Only a run that stopped because the credit balance ran out can be resumed, and only once it has finished settling.",
"request_id": "req_01m1kgdm4xngzmbmff68g94w0c"
}
}
```
### 429 — Over 5 requests per second for the key. Retry after the limit resets.
```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
- `JOB_NOT_RESUMABLE` — https://spaw.co/docs/errors/JOB_NOT_RESUMABLE
- `JOB_INPUT_EXPIRED` — https://spaw.co/docs/errors/JOB_INPUT_EXPIRED
- `INSUFFICIENT_CREDITS` — https://spaw.co/docs/errors/INSUFFICIENT_CREDITS
- `RATE_LIMITED` — https://spaw.co/docs/errors/RATE_LIMITED
---
Canonical page: https://spaw.co/docs/api/resume-ip-bulk-job · OpenAPI document: https://spaw.co/openapi.json · All endpoints: https://spaw.co/docs/api
---
# 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
---
# Download a bulk IP job's results
`GET /api/v1/ip/bulk/{jobId}/results`
- Authentication: Secret API key as a bearer token
- Billing: Free.
- Group: IP
The result file as CSV: one row per input address, in input order, with the fields of the single-address response as columns plus the credits it cost. Two structured fields are flattened: `sanctions` as its codes joined with `;`, `account_list` as `kind:name`. Left out, because they belong to one live request rather than a row: `timezone_matches` (needs a browser), `seen_by_you` (would count the run's own rows), and the opt-in network fields (`abuse_contact`, `network_details`, the hostname fields). Fields containing spaces are quoted. Add `?variant=low|medium|high` to download only the rows of one risk level; the default `full` returns everything. Available once the job is `completed`, or `cancelled` or `failed` with at least one processed row — those rows were billed, so they stay downloadable. A job still running answers `409 RESULTS_NOT_READY`, a cancelled job with nothing processed `409 JOB_CANCELLED`, and a failed job with nothing processed `409 JOB_FAILED`.
`format=json` answers the same rows, filtered the same way, as objects keyed by the result file's own header instead of a CSV attachment: `{ "success": true, "data": { "job": …, "results": [ … ] }, "meta": { "variant", "request_id" } }`. It is streamed row by row exactly as the CSV is, so a 100,000-row run costs no more to answer either way. A CSV cell carries no type, so only the two the writer encodes come back typed: an empty cell is `null` and `true`/`false` are booleans. Everything else is the string as written — a number is never silently made numeric, because postcodes, phone numbers and house numbers lose their leading zeros that way — and the columns of an uploaded file come back exactly as they were uploaded.
## Parameters
| Name | In | Type | Required | Description |
| --- | --- | --- | --- | --- |
| `jobId` | path | integer | yes | |
| `variant` | query | string | no | Restrict the file to one risk level. Defaults to full. One of: full, low, medium, high. |
| `format` | query | string | no | `csv` streams the file as an attachment; `json` streams the same rows as objects. One of: csv, json. Default: csv. |
## Example request
```bash
curl https://spaw.co/api/v1/ip/bulk/{jobId}/results \
-H "Authorization: Bearer sk_live_…"
```
## Responses
### 200 — The result rows, as a CSV attachment or as streamed JSON.
```csv
ip,version,reason,network,continent,country,country_name,is_eu,is_eea,privacy_regime,sanctions,fatf_list,region,city,latitude,longitude,location_source,location_confidence,timezone,timezone_source,currency,calling_code,asn,org,asn_type,connection_type,is_mobile,is_satellite,registry,registry_country,allocated_at,is_datacenter,hosting_provider,is_corporate_proxy,corporate_proxy,is_crawler,crawler,crawler_kind,is_blocklisted,blocklist,is_attack_source,attack_source,is_tor,tor_last_seen,is_vpn,is_relay,privacy_service,is_anonymous,account_list,risk_score,risk_level,credits_used
8.8.8.8,4,,8.8.8.0/24,NA,US,United States,false,false,ccpa,,,California,Mountain View,37.4056,-122.0775,database,low,America/Los_Angeles,region,USD,1,15169,Google LLC,hosting,hosting,false,false,arin,US,2023-12-28,true,,false,,false,,,false,,false,,false,,false,false,,false,,40,medium,1
```
### 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 job is still running (`RESULTS_NOT_READY`), or was cancelled or failed before any row was processed (`JOB_CANCELLED`, `JOB_FAILED`).
```json
{
"success": false,
"error": {
"code": "RESULTS_NOT_READY",
"message": "The results are not ready yet.",
"request_id": "req_01m1kgdm4xngzmbmff68g94w0c"
}
}
```
### 429 — Over 5 requests per second for the key. Retry after the limit resets.
```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
- `VALIDATION_FAILED` — https://spaw.co/docs/errors/VALIDATION_FAILED
- `RESULTS_NOT_READY` — https://spaw.co/docs/errors/RESULTS_NOT_READY
- `JOB_CANCELLED` — https://spaw.co/docs/errors/JOB_CANCELLED
- `JOB_FAILED` — https://spaw.co/docs/errors/JOB_FAILED
- `RATE_LIMITED` — https://spaw.co/docs/errors/RATE_LIMITED
---
Canonical page: https://spaw.co/docs/api/download-ip-bulk-results · OpenAPI document: https://spaw.co/openapi.json · All endpoints: https://spaw.co/docs/api
---
# Report IP outcomes
`POST /api/v1/ip/feedback`
- Authentication: Secret API key as a bearer token
- Billing: Free.
- Group: IP
Tells Spaw what an address turned out to be after the fact: `abuse` (spam, scraping, credential stuffing), `fraud` (a confirmed fraudulent order or account), `bot` (automated traffic that got through), or `chargeback`. Your own reports feed your own later lookups of that address: any report inside 30 days scores `reported_abuse` (+60) with the dataset `abuse-feedback`. Once three or more accounts report the same address inside 30 days, every account's lookups of it score `reported_abuse_widely` (+40); only the number of reporting accounts is ever read, never who reported or what. Neither signal turns a null score into a number: an address no feed evaluated stays unscored.
Addresses are canonicalised like a lookup and stored only as a salted SHA-256 hash with the outcome and a timestamp, never in clear; a hash can confirm a later lookup of the same address but cannot be read back into one. Items whose address is malformed or in a reserved range are skipped and counted, not rejected. Feedback is free, never billed and never logged as a lookup. Reports are kept for 90 days.
## Request body
| Field | Type | Required | Description |
| --- | --- | --- | --- |
| `items` | object[] | yes | 1 to 1,000 outcomes per request. |
| `items[].ip` | string | yes | An IPv4 or IPv6 address, in any common notation; IPv4-mapped IPv6 collapses to dotted-quad. At most 64 characters. |
| `items[].outcome` | string | yes | One of: abuse, fraud, bot, chargeback. |
| `items[].occurred_at` | string | null | no | When it happened. Defaults to now. A time more than five minutes in the future is refused; one inside that margin is recorded as now. |
## Example request
```bash
curl -X POST https://spaw.co/api/v1/ip/feedback \
-H "Authorization: Bearer sk_live_…" \
-H "Content-Type: application/json" \
-d '{
"items": [
{
"ip": "203.0.113.9",
"outcome": "fraud"
},
{
"ip": "2001:db8::1",
"outcome": "bot",
"occurred_at": "2026-09-05T09:00:00Z"
}
]
}'
```
## Responses
### 202 — The outcomes were recorded.
```json
{
"success": true,
"data": {
"recorded": 2,
"skipped": 0
}
}
```
### 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"
}
}
```
### 422 — The request body could not be validated; `error.errors` lists the fields.
```json
{
"success": false,
"error": {
"code": "VALIDATION_FAILED",
"message": "The email field is required.",
"errors": {
"email": [
"The email field is required."
]
},
"request_id": "req_01m1kgdm4xngzmbmff68g94w0c"
}
}
```
### 429 — Over 5 requests per second for the key. Retry after the limit resets.
```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
- `VALIDATION_FAILED` — https://spaw.co/docs/errors/VALIDATION_FAILED
- `RATE_LIMITED` — https://spaw.co/docs/errors/RATE_LIMITED
---
Canonical page: https://spaw.co/docs/api/report-ip-feedback · OpenAPI document: https://spaw.co/openapi.json · All endpoints: https://spaw.co/docs/api
---
# Reported IP outcomes
`GET /api/v1/ip/feedback/summary`
- Authentication: Secret API key as a bearer token
- Billing: Free.
- Group: IP
The last 90 days of outcomes you reported: totals per outcome, how many distinct addresses they cover, and the split between IPv4 and IPv6. No address appears, because none is stored.
## Example request
```bash
curl https://spaw.co/api/v1/ip/feedback/summary \
-H "Authorization: Bearer sk_live_…"
```
## Responses
### 200 — The summary.
```json
{
"success": true,
"data": {
"window_days": 90,
"total": 412,
"addresses": 377,
"outcomes": {
"abuse": 208,
"fraud": 96,
"bot": 101,
"chargeback": 7
},
"by_version": {
"4": 401,
"6": 11
}
}
}
```
### 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"
}
}
```
### 429 — Over 5 requests per second for the key. Retry after the limit resets.
```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
- `RATE_LIMITED` — https://spaw.co/docs/errors/RATE_LIMITED
---
Canonical page: https://spaw.co/docs/api/ip-feedback-summary · OpenAPI document: https://spaw.co/openapi.json · All endpoints: https://spaw.co/docs/api
---
# List the account's IP monitors
`GET /api/v1/ip/monitors`
- Authentication: Secret API key as a bearer token
- Billing: Free.
- Group: IP
The saved address lists this account re-checks on a schedule, newest first, 100 per page. Each monitor carries the summary of its last run: how many addresses were clean, flagged (on a threat blocklist, a Tor exit, or scoring high) or reserved, how many changed status, how many newly became flagged, the credits the run spent, and `stopped_reason` when it did not finish cleanly: `insufficient_credits` or `key_spend_cap` where the run stopped part-way, `run_failed` where it died outright, and `run_failed_retrying` where it died and the schedule has been pulled in to check again — `retry_at` in the same block says when that becomes due, and the hourly sweep picks it up at or after that, so between 45 minutes and about an hour and three quarters later. A summary carrying a `stopped_reason` of either failure kind has NO counts in it at all: the run produced none, so read a missing count as unknown rather than as zero. The addresses and their statuses are on the single-monitor endpoint.
## Parameters
| Name | In | Type | Required | Description |
| --- | --- | --- | --- | --- |
| `page` | query | integer | no | The page to return. Default: 1. |
## Example request
```bash
curl https://spaw.co/api/v1/ip/monitors \
-H "Authorization: Bearer sk_live_…"
```
## Responses
### 200 — One page of monitors.
```json
{
"success": true,
"data": {
"monitors": [
{
"id": 4,
"name": "Mail servers",
"cadence": "daily",
"ip_count": 2,
"next_run_at": "2026-09-06T08:00:00+00:00",
"last_run_at": "2026-09-05T08:00:03+00:00",
"last_summary": {
"total": 2,
"clean": 1,
"flagged": 1,
"reserved": 0,
"changed": 1,
"newly_flagged": 1,
"credits_used": 0,
"stopped_reason": null
},
"created_at": "2026-08-27T08:00:00+00:00"
}
]
},
"meta": {
"page": 1,
"per_page": 100,
"total": 1,
"retention_days": 365
}
}
```
### 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"
}
}
```
### 429 — Over 5 requests per second for the key. Retry after the limit resets.
```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
- `RATE_LIMITED` — https://spaw.co/docs/errors/RATE_LIMITED
---
Canonical page: https://spaw.co/docs/api/list-ip-monitors · OpenAPI document: https://spaw.co/openapi.json · All endpoints: https://spaw.co/docs/api
---
# Monitor your own addresses on a schedule
`POST /api/v1/ip/monitors`
- Authentication: Secret API key as a bearer token
- Billing: Creation is free. Each run bills every address like a single lookup: 1 credit per 5 fresh answered lookups; reserved and unallocated addresses and 7-day repeats are free, so a daily list costs at most one credit per five addresses a week.
- Group: IP
Saves up to 500 of your own addresses, your egress addresses, mail servers and office connections, and re-checks them every day, week or month. Addresses are canonicalised and deduplicated. The first run starts right away and only sets the baseline; from the next run on, every address that newly lands on a threat blocklist (Spamhaus DROP, ASN-DROP or the botnet C2 list), becomes a Tor exit, or scores high is reported to the account's email with the reason, so you learn that a mail server sits on a blocklist before your recipients' inboxes do.
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": "ip_monitor.run", "monitor": { "id", "name", "cadence", "last_run_at", "next_run_at" }, "summary": { … the same block as `last_summary` … }, "changes": [ … ] }`, where `changes` names the addresses the alert email names. 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 | Required | Description |
| --- | --- | --- | --- |
| `name` | string | yes | A label for the alerts. At most 100 characters. |
| `ips` | string[] | yes | 1 to 500 IPv4 or IPv6 addresses, or prefixes of at most 256 addresses (/24 to /32, /120 to /128), which are expanded into their addresses before the cap; duplicates and alternative spellings of one address count once. A wider prefix answers 422 — split it, because a /16 is 65,536 lookups a run. |
| `cadence` | string | yes | How often the list is re-checked. One of: daily, weekly, monthly. |
| `webhook_url` | string | null | no | 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 | no | 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
```bash
curl -X POST https://spaw.co/api/v1/ip/monitors \
-H "Authorization: Bearer sk_live_…" \
-H "Content-Type: application/json" \
-d '{
"name": "Mail servers",
"ips": [
"203.0.113.25",
"2001:db8::25"
],
"cadence": "daily"
}'
```
## Responses
### 201 — The monitor was saved and its baseline run queued.
```json
{
"success": true,
"data": {
"monitor": {
"id": 4,
"name": "Mail servers",
"cadence": "daily",
"ip_count": 2,
"next_run_at": "2026-09-06T10:12:44+00:00",
"last_run_at": null,
"last_summary": null,
"created_at": "2026-09-05T10:12:44+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"
}
}
```
### 422 — The request body could not be validated; `error.errors` lists the fields.
```json
{
"success": false,
"error": {
"code": "VALIDATION_FAILED",
"message": "The email field is required.",
"errors": {
"email": [
"The email field is required."
]
},
"request_id": "req_01m1kgdm4xngzmbmff68g94w0c"
}
}
```
### 429 — Over 5 requests per second for the key. Retry after the limit resets.
```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
- `VALIDATION_FAILED` — https://spaw.co/docs/errors/VALIDATION_FAILED
- `RATE_LIMITED` — https://spaw.co/docs/errors/RATE_LIMITED
---
Canonical page: https://spaw.co/docs/api/create-ip-monitor · OpenAPI document: https://spaw.co/openapi.json · All endpoints: https://spaw.co/docs/api
---
# Read an IP monitor and its last results
`GET /api/v1/ip/monitors/{monitorId}`
- Authentication: Secret API key as a bearer token
- Billing: Free.
- Group: IP
The monitor plus `results`: for every address from the last run, its status (`clean`, `flagged` or `reserved`), the flags that fired (`blocklisted`, `attack_source`, `tor`, `high_risk`), the list that names it, its score and level, and whether the status changed since the previous run; keyed by the address in canonical form, or null until the first run has finished. A monitor that belongs to another account answers `404`.
## Parameters
| Name | In | Type | Required | Description |
| --- | --- | --- | --- | --- |
| `monitorId` | path | integer | yes | The monitor's id from the list endpoint. |
## Example request
```bash
curl https://spaw.co/api/v1/ip/monitors/4 \
-H "Authorization: Bearer sk_live_…"
```
## Responses
### 200 — The monitor.
```json
{
"success": true,
"data": {
"monitor": {
"id": 4,
"name": "Mail servers",
"cadence": "daily",
"ip_count": 2,
"next_run_at": "2026-09-06T08:00:00+00:00",
"last_run_at": "2026-09-05T08:00:03+00:00",
"last_summary": {
"total": 2,
"clean": 1,
"flagged": 1,
"reserved": 0,
"changed": 1,
"newly_flagged": 1,
"credits_used": 0,
"stopped_reason": null
},
"created_at": "2026-08-27T08:00:00+00:00",
"results": {
"203.0.113.25": {
"status": "flagged",
"flags": [
"blocklisted",
"high_risk"
],
"blocklist": "Spamhaus DROP",
"risk_score": 80,
"risk_level": "high",
"changed": true
},
"2001:db8::25": {
"status": "clean",
"flags": [],
"blocklist": null,
"risk_score": 0,
"risk_level": "low",
"changed": false
}
}
}
}
}
```
### 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"
}
}
```
### 429 — Over 5 requests per second for the key. Retry after the limit resets.
```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
- `RATE_LIMITED` — https://spaw.co/docs/errors/RATE_LIMITED
---
Canonical page: https://spaw.co/docs/api/get-ip-monitor · OpenAPI document: https://spaw.co/openapi.json · All endpoints: https://spaw.co/docs/api
---
# Change a monitor
`PATCH /api/v1/ip/monitors/{monitorId}`
- Authentication: Secret API key as a bearer token
- Billing: Free. The change applies to the next run.
- Group: IP
Changes a saved monitor in place: rename it, move it to another cadence, replace the addresses, or point it at a webhook. Send only the fields you are changing; a body that names none of them answers `422` rather than quietly changing nothing, because that is the shape a misspelled field name takes.
Replacing `ips` replaces the list. What the last run recorded about the addresses that stayed is kept, so their next run still compares against the answer they already had; what it recorded about the ones that are gone is deleted with them; and an entry that is new starts with no baseline, so its first run only sets one. A rename never touches a stored answer — they are keyed by the entry, never by the list.
Changing `cadence` neither skips a run nor starts a second one: a run already scheduled sooner than the new interval still happens, and a longer interval applies from the one after it. Nothing is queued by this call — the change applies to the next run, and `POST .../run` is there to have it now.
Sending a `webhook_url` that differs from the one stored mints a new signing secret and returns it once, here; sending the URL it already has keeps the secret it is signing with, and `null` removes the webhook. A monitor that belongs to another account answers `404`.
## Parameters
| Name | In | Type | Required | Description |
| --- | --- | --- | --- | --- |
| `monitorId` | path | integer | yes | The monitor's id from the list endpoint. |
## Request body
| Field | Type | Required | Description |
| --- | --- | --- | --- |
| `name` | string | no | At most 100 characters. |
| `ips` | string[] | no | Addresses, or prefixes of at most 256 addresses, expanded before the cap; a wider prefix answers 422. |
| `cadence` | string | no | One of: daily, weekly, monthly. |
| `webhook_url` | string | null | no | At most 2,048 characters. |
| `webhook_events` | string | no | One of: changes, every_run. |
## Example request
```bash
curl -X PATCH https://spaw.co/api/v1/ip/monitors/41 \
-H "Authorization: Bearer sk_live_…" \
-H "Content-Type: application/json" \
-d '{
"ips": [
"203.0.113.7",
"198.51.100.24"
],
"cadence": "daily"
}'
```
## Responses
### 200 — The monitor as it now stands.
### 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"
}
}
```
### 422 — The request body could not be validated; `error.errors` lists the fields.
```json
{
"success": false,
"error": {
"code": "VALIDATION_FAILED",
"message": "The email field is required.",
"errors": {
"email": [
"The email field is required."
]
},
"request_id": "req_01m1kgdm4xngzmbmff68g94w0c"
}
}
```
### 429 — Over 5 requests per second for the key. Retry after the limit resets.
```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
- `VALIDATION_FAILED` — https://spaw.co/docs/errors/VALIDATION_FAILED
- `RATE_LIMITED` — https://spaw.co/docs/errors/RATE_LIMITED
---
Canonical page: https://spaw.co/docs/api/update-ip-monitor · OpenAPI document: https://spaw.co/openapi.json · All endpoints: https://spaw.co/docs/api
---
# Stop monitoring an IP list
`DELETE /api/v1/ip/monitors/{monitorId}`
- Authentication: Secret API key as a bearer token
- Billing: Free.
- Group: IP
Removes the monitor and its stored results; nothing further is scheduled. A run that is already queued finishes on its own and bills as usual. A monitor that belongs to another account answers `404`.
## Parameters
| Name | In | Type | Required | Description |
| --- | --- | --- | --- | --- |
| `monitorId` | path | integer | yes | The monitor's id from the list endpoint. |
## Example request
```bash
curl -X DELETE https://spaw.co/api/v1/ip/monitors/4 \
-H "Authorization: Bearer sk_live_…"
```
## Responses
### 200 — The monitor was removed.
```json
{
"success": true
}
```
### 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"
}
}
```
### 429 — Over 5 requests per second for the key. Retry after the limit resets.
```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
- `RATE_LIMITED` — https://spaw.co/docs/errors/RATE_LIMITED
---
Canonical page: https://spaw.co/docs/api/delete-ip-monitor · OpenAPI document: https://spaw.co/openapi.json · All endpoints: https://spaw.co/docs/api
---
# Re-check a monitored IP list now
`POST /api/v1/ip/monitors/{monitorId}/run`
- Authentication: Secret API key as a bearer token
- Billing: The run bills every address like a single lookup, 1 credit per 5 fresh answered addresses; reserved and unallocated addresses and 7-day repeats are free.
- Group: IP
Queues a run right away instead of waiting for the schedule; when it finishes, the next scheduled run is set one cadence from then. Answers `202` with the monitor as it stands before the run; poll it for the new `last_run_at`, `last_summary` and `results`. A monitor that belongs to another account answers `404`.
## Parameters
| Name | In | Type | Required | Description |
| --- | --- | --- | --- | --- |
| `monitorId` | path | integer | yes | The monitor's id from the list endpoint. |
## Example request
```bash
curl -X POST https://spaw.co/api/v1/ip/monitors/4/run \
-H "Authorization: Bearer sk_live_…"
```
## Responses
### 202 — The run was queued.
```json
{
"success": true,
"data": {
"monitor": {
"id": 4,
"name": "Mail servers",
"cadence": "daily",
"ip_count": 2,
"next_run_at": "2026-09-06T08:00:00+00:00",
"last_run_at": "2026-09-05T08:00:03+00:00",
"last_summary": {
"total": 2,
"clean": 1,
"flagged": 1,
"reserved": 0,
"changed": 1,
"newly_flagged": 1,
"credits_used": 0,
"stopped_reason": null
},
"created_at": "2026-08-27T08:00:00+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"
}
}
```
### 429 — Over 5 requests per second for the key. Retry after the limit resets.
```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
- `RATE_LIMITED` — https://spaw.co/docs/errors/RATE_LIMITED
---
Canonical page: https://spaw.co/docs/api/run-ip-monitor · OpenAPI document: https://spaw.co/openapi.json · All endpoints: https://spaw.co/docs/api
---
# List the account's allow and deny lists
`GET /api/v1/ip/lists`
- Authentication: Secret API key as a bearer token
- Billing: Free.
- Group: IP
Every allow and deny list on the account, oldest first, without the entries. A list belongs to the account rather than to whoever created it: every member's lookups apply it and any member may change or remove it. `meta` carries the two limits, how many lists an account may hold and how many entries one list may carry.
The lists apply to every lookup the account is billed for — the API, the dashboard, batches, bulk runs, monitors and the browser widget. Deny lists are checked before allow lists, so a contradiction fails closed. A lookup that hits one carries `account_list` (`{ id, name, kind, entry }`): an allow-list hit answers `risk_score: 0` with a single `allowlisted` signal of weight 0, a deny-list hit adds `denylisted` 100, and `sources` gains `account-lists` with the list's last change as its version.
## Example request
```bash
curl https://spaw.co/api/v1/ip/lists \
-H "Authorization: Bearer sk_live_…"
```
## Responses
### 200 — The account's lists.
```json
{
"success": true,
"data": {
"lists": [
{
"id": 7,
"name": "Office egress",
"kind": "allow",
"entry_count": 2,
"created_at": "2026-09-12T09:00:00+00:00",
"updated_at": "2026-09-12T09:00:00+00:00"
}
]
},
"meta": {
"max_lists": 20,
"max_entries": 5000
}
}
```
### 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"
}
}
```
### 429 — Over 5 requests per second for the key. Retry after the limit resets.
```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
- `RATE_LIMITED` — https://spaw.co/docs/errors/RATE_LIMITED
---
Canonical page: https://spaw.co/docs/api/list-ip-lists · OpenAPI document: https://spaw.co/openapi.json · All endpoints: https://spaw.co/docs/api
---
# Create an allow or deny list
`POST /api/v1/ip/lists`
- Authentication: Secret API key as a bearer token
- Billing: Free.
- Group: IP
Saves a list of addresses and prefixes the account's lookups should treat as known-good (`allow`) or known-bad (`deny`): office and partner egress, a scraper's ranges, a fraud ring's space. Entries are IPv4 or IPv6 addresses or CIDR prefixes, canonicalised (an IPv4-mapped address collapses to dotted quad, `8.8.8.9/24` to `8.8.8.0/24`) and deduplicated before validation; a start-end range or a hostname is refused with `422`. The list applies from the account's next lookup.
An account may hold `meta.max_lists` lists (20) of `meta.max_entries` entries (5,000) each; the twenty-first answers `409 IP_LIST_LIMIT_REACHED`. Answers `201` with the list and its entries.
## Request body
| Field | Type | Required | Description |
| --- | --- | --- | --- |
| `name` | string | yes | A label, answered back in `account_list.name` on every lookup the list matches. At most 100 characters. |
| `kind` | string | yes | `allow` zeroes the score of a matching address; `deny` adds the denylisted weight of 100. One of: allow, deny. |
| `entries` | string[] | yes | 1 to 5,000 IPv4 or IPv6 addresses or CIDR prefixes; duplicates and alternative spellings count once. |
## Example request
```bash
curl -X POST https://spaw.co/api/v1/ip/lists \
-H "Authorization: Bearer sk_live_…" \
-H "Content-Type: application/json" \
-d '{
"name": "Office egress",
"kind": "allow",
"entries": [
"203.0.113.0/24",
"2001:db8:10::/48"
]
}'
```
## Responses
### 201 — The list was saved and applies from the next lookup.
```json
{
"success": true,
"data": {
"list": {
"id": 7,
"name": "Office egress",
"kind": "allow",
"entry_count": 2,
"created_at": "2026-09-12T09:00:00+00:00",
"updated_at": "2026-09-12T09:00:00+00:00",
"entries": [
"203.0.113.0/24",
"2001:db8:10::/48"
]
}
}
}
```
### 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"
}
}
```
### 409 — The account already holds as many lists as it may (`IP_LIST_LIMIT_REACHED`). Nothing was created.
```json
{
"success": false,
"error": {
"code": "IP_LIST_LIMIT_REACHED",
"message": "This account already holds 20 IP lists. Remove one, or add the entries to a list you have.",
"request_id": "req_01m1kgdm4xngzmbmff68g94w0c"
}
}
```
### 422 — The request body could not be validated; `error.errors` lists the fields.
```json
{
"success": false,
"error": {
"code": "VALIDATION_FAILED",
"message": "The email field is required.",
"errors": {
"email": [
"The email field is required."
]
},
"request_id": "req_01m1kgdm4xngzmbmff68g94w0c"
}
}
```
### 429 — Over 5 requests per second for the key. Retry after the limit resets.
```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
- `VALIDATION_FAILED` — https://spaw.co/docs/errors/VALIDATION_FAILED
- `IP_LIST_LIMIT_REACHED` — https://spaw.co/docs/errors/IP_LIST_LIMIT_REACHED
- `RATE_LIMITED` — https://spaw.co/docs/errors/RATE_LIMITED
---
Canonical page: https://spaw.co/docs/api/create-ip-list · OpenAPI document: https://spaw.co/openapi.json · All endpoints: https://spaw.co/docs/api
---
# Read a list and its entries
`GET /api/v1/ip/lists/{listId}`
- Authentication: Secret API key as a bearer token
- Billing: Free.
- Group: IP
One list with its entries in canonical form. A list that belongs to another account answers `404`.
## Parameters
| Name | In | Type | Required | Description |
| --- | --- | --- | --- | --- |
| `listId` | path | integer | yes | The list's id from the index. |
## Example request
```bash
curl https://spaw.co/api/v1/ip/lists/7 \
-H "Authorization: Bearer sk_live_…"
```
## Responses
### 200 — The list.
```json
{
"success": true,
"data": {
"list": {
"id": 7,
"name": "Office egress",
"kind": "allow",
"entry_count": 2,
"created_at": "2026-09-12T09:00:00+00:00",
"updated_at": "2026-09-12T09:00:00+00:00",
"entries": [
"203.0.113.0/24",
"2001:db8:10::/48"
]
}
}
}
```
### 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"
}
}
```
### 429 — Over 5 requests per second for the key. Retry after the limit resets.
```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
- `RATE_LIMITED` — https://spaw.co/docs/errors/RATE_LIMITED
---
Canonical page: https://spaw.co/docs/api/get-ip-list · OpenAPI document: https://spaw.co/openapi.json · All endpoints: https://spaw.co/docs/api
---
# Change a list
`PATCH /api/v1/ip/lists/{listId}`
- Authentication: Secret API key as a bearer token
- Billing: Free.
- Group: IP
Renames a list, changes its kind, or replaces its entries with the ones sent. Send only the fields you are changing; a body that names none of `name`, `kind` and `entries` answers `422` rather than quietly changing nothing, because that is the shape a misspelled field name takes. Entries are canonicalised and deduplicated exactly as on creation, and the change applies from the account's next lookup. A list that belongs to another account answers `404`.
## Parameters
| Name | In | Type | Required | Description |
| --- | --- | --- | --- | --- |
| `listId` | path | integer | yes | The list's id from the index. |
## Request body
| Field | Type | Required | Description |
| --- | --- | --- | --- |
| `name` | string | no | At most 100 characters. |
| `kind` | string | no | One of: allow, deny. |
| `entries` | string[] | no | Replaces the list's entries. |
## Example request
```bash
curl -X PATCH https://spaw.co/api/v1/ip/lists/7 \
-H "Authorization: Bearer sk_live_…" \
-H "Content-Type: application/json" \
-d '{
"name": "Office and warehouse egress",
"entries": [
"203.0.113.0/24",
"2001:db8:10::/48",
"198.51.100.7"
]
}'
```
## Responses
### 200 — The list as it now stands.
```json
{
"success": true,
"data": {
"list": {
"id": 7,
"name": "Office and warehouse egress",
"kind": "allow",
"entry_count": 3,
"created_at": "2026-09-12T09:00:00+00:00",
"updated_at": "2026-09-12T11:30:00+00:00",
"entries": [
"203.0.113.0/24",
"2001:db8:10::/48",
"198.51.100.7"
]
}
}
}
```
### 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"
}
}
```
### 422 — The request body could not be validated; `error.errors` lists the fields.
```json
{
"success": false,
"error": {
"code": "VALIDATION_FAILED",
"message": "The email field is required.",
"errors": {
"email": [
"The email field is required."
]
},
"request_id": "req_01m1kgdm4xngzmbmff68g94w0c"
}
}
```
### 429 — Over 5 requests per second for the key. Retry after the limit resets.
```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
- `VALIDATION_FAILED` — https://spaw.co/docs/errors/VALIDATION_FAILED
- `RATE_LIMITED` — https://spaw.co/docs/errors/RATE_LIMITED
---
Canonical page: https://spaw.co/docs/api/update-ip-list · OpenAPI document: https://spaw.co/openapi.json · All endpoints: https://spaw.co/docs/api
---
# Remove a list
`DELETE /api/v1/ip/lists/{listId}`
- Authentication: Secret API key as a bearer token
- Billing: Free.
- Group: IP
Removes the list; the account's next lookup no longer applies it. A list that belongs to another account answers `404`.
## Parameters
| Name | In | Type | Required | Description |
| --- | --- | --- | --- | --- |
| `listId` | path | integer | yes | The list's id from the index. |
## Example request
```bash
curl -X DELETE https://spaw.co/api/v1/ip/lists/7 \
-H "Authorization: Bearer sk_live_…"
```
## Responses
### 200 — The list was removed.
```json
{
"success": true
}
```
### 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"
}
}
```
### 429 — Over 5 requests per second for the key. Retry after the limit resets.
```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
- `RATE_LIMITED` — https://spaw.co/docs/errors/RATE_LIMITED
---
Canonical page: https://spaw.co/docs/api/delete-ip-list · OpenAPI document: https://spaw.co/openapi.json · All endpoints: https://spaw.co/docs/api
---
# Read an autonomous system and its prefixes
`GET /api/v1/ip/asn/{asn}`
- Authentication: Secret API key as a bearer token
- Billing: 1 credit per answered system, free for the next seven days for the same number. An unknown number (404) and a server without the index (501) cost nothing.
- Group: IP
The reverse of an address lookup's `asn` field: one autonomous system with the organisation operating it, the registry that allocated the number with the country and date it recorded, what the operator lists say about it (Spamhaus ASN-DROP, the curated satellite and mobile-carrier lists, tri-state like the address flags), and every prefix the DB-IP ASN database assigns to it — `prefix_count` per family is always whole, `prefixes` carries at most 500 per family and `truncated` says when it was cut. It is read from an index the server rebuilds from the ASN database once a month after it syncs; `sources` dates both.
A number the database assigns no prefix to — a reserved or private number, a system that has never held address space, a typo — answers `404 ASN_NOT_FOUND` and costs nothing; a server that has not built the index yet answers `501 ASN_INDEX_UNAVAILABLE`, also free. It is a fact about the database, not the registry: the RIR statistics may well have allocated the number.
## Parameters
| Name | In | Type | Required | Description |
| --- | --- | --- | --- | --- |
| `asn` | path | integer | yes | The autonomous system number, without the AS prefix. |
## Example request
```bash
curl https://spaw.co/api/v1/ip/asn/15169 \
-H "Authorization: Bearer sk_live_…"
```
## Responses
### 200 — The system, its facts and its prefixes.
```json
{
"success": true,
"data": {
"asn": 15169,
"org": "Google LLC",
"asn_type": "hosting",
"registry": "arin",
"registry_country": "US",
"allocated_at": "2000-03-30",
"is_blocklisted": false,
"blocklist": null,
"is_satellite": false,
"is_mobile": false,
"prefix_count": {
"ipv4": 157,
"ipv6": 28
},
"prefixes": {
"ipv4": [
"8.8.4.0/24",
"8.8.8.0/24",
"8.34.208.0/20",
"8.35.192.0/20",
"34.4.0.0/14"
],
"ipv6": [
"2001:4860::/32",
"2404:6800::/32",
"2607:f8b0::/32",
"2800:3f0::/32",
"2a00:1450::/32"
]
},
"truncated": false,
"sources": [
{
"dataset": "asn-prefixes",
"version": "2026-09-12"
},
{
"dataset": "dbip-asn-lite",
"version": "2026-09"
},
{
"dataset": "rir-allocations",
"version": "2026-09-12"
},
{
"dataset": "threat-blocklists",
"version": "2026-09-12"
},
{
"dataset": "satellite-asns",
"version": "2026-09-12"
},
{
"dataset": "mobile-carrier-asns",
"version": "2026-09-12"
}
]
},
"meta": {
"credits_used": 1,
"credits_remaining": 8,
"cache_hit": false,
"request_id": "req_01m1kgdm4xngzmbmff68g94w0c"
}
}
```
### 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"
}
}
```
### 402 — The balance is empty. The lookup did not run.
```json
{
"success": false,
"error": {
"code": "INSUFFICIENT_CREDITS",
"message": "Your credit balance is empty.",
"request_id": "req_01m1kgdm4xngzmbmff68g94w0c"
}
}
```
### 404 — The database assigns no prefix to that number (`ASN_NOT_FOUND`). Nothing was charged.
```json
{
"success": false,
"error": {
"code": "ASN_NOT_FOUND",
"message": "No autonomous system numbered 999999 is in the index.",
"request_id": "req_01m1kgdm4xngzmbmff68g94w0c"
}
}
```
### 429 — Over 5 requests per second for the key. Retry after the limit resets.
```json
{
"success": false,
"error": {
"code": "RATE_LIMITED",
"message": "Too many requests. Retry after the limit resets.",
"request_id": "req_01m1kgdm4xngzmbmff68g94w0c"
}
}
```
### 501 — The ASN prefix index has not been built on this server (`ASN_INDEX_UNAVAILABLE`). Nothing was charged.
```json
{
"success": false,
"error": {
"code": "ASN_INDEX_UNAVAILABLE",
"message": "The ASN prefix index has not been built on this server yet.",
"request_id": "req_01m1kgdm4xngzmbmff68g94w0c"
}
}
```
## Error codes
- `UNAUTHENTICATED` — https://spaw.co/docs/errors/UNAUTHENTICATED
- `INSUFFICIENT_CREDITS` — https://spaw.co/docs/errors/INSUFFICIENT_CREDITS
- `ASN_NOT_FOUND` — https://spaw.co/docs/errors/ASN_NOT_FOUND
- `ASN_INDEX_UNAVAILABLE` — https://spaw.co/docs/errors/ASN_INDEX_UNAVAILABLE
- `RATE_LIMITED` — https://spaw.co/docs/errors/RATE_LIMITED
---
Canonical page: https://spaw.co/docs/api/get-ip-asn · OpenAPI document: https://spaw.co/openapi.json · All endpoints: https://spaw.co/docs/api
---
# Read what the feeds say about a whole prefix
`GET /api/v1/ip/range`
- Authentication: Secret API key as a bearer token
- Billing: 1 credit per answered prefix, free for the next seven days for the same prefix; a prefix inside reserved space costs nothing.
- Group: IP
One prefix at a time, without looking every address up: for each signal group — datacenter, VPN, relay, Tor, anycast, RIR allocation, threat blocklist, crawler and corporate proxy — whether any installed feed overlaps it, how many feed records intersect it (a Tor group counts exits inside the prefix, a datacenter group the provider ranges that touch it) and which named providers do; whose network and country the first and last addresses resolve to and whether the two ends agree; and the registry facts of the first address. `overlaps` is tri-state exactly like the address flags, and record counts are capped at 10,000 per feed with `truncated` saying so, so a count is a floor for a very wide prefix.
The prefix travels as a query parameter, because a slash in a path segment has to be encoded and clients get that wrong: `GET /api/v1/ip/range?prefix=203.0.113.0/24`. A bare address is a /32 or /128, and the prefix comes back canonical (`8.8.8.9/24` as `8.8.8.0/24`). A prefix inside private or special-use space answers `reason: "reserved_range"` with every other field null, free.
## Parameters
| Name | In | Type | Required | Description |
| --- | --- | --- | --- | --- |
| `prefix` | query | string | yes | An IPv4 or IPv6 CIDR prefix, or a bare address. At most 64 characters. |
## Example request
```bash
curl https://spaw.co/api/v1/ip/range \
-H "Authorization: Bearer sk_live_…"
```
## Responses
### 200 — The prefix, its two ends and what each feed group says.
```json
{
"success": true,
"data": {
"prefix": "8.8.8.0/24",
"version": 4,
"reason": null,
"first": "8.8.8.0",
"last": "8.8.8.255",
"addresses": 256,
"network": {
"asn": 15169,
"org": "Google LLC"
},
"network_agrees": true,
"country": "US",
"country_agrees": true,
"registry": "arin",
"registry_country": "US",
"allocated_at": "2023-12-28",
"signals": {
"datacenter": {
"overlaps": true,
"ranges": 1,
"providers": []
},
"vpn": {
"overlaps": false,
"ranges": 0,
"providers": []
},
"relay": {
"overlaps": false,
"ranges": 0,
"providers": []
},
"tor": {
"overlaps": false,
"ranges": 0,
"providers": []
},
"anycast": {
"overlaps": true,
"ranges": 1,
"providers": []
},
"allocated": {
"overlaps": true,
"ranges": 1,
"providers": []
},
"blocklist": {
"overlaps": false,
"ranges": 0,
"providers": []
},
"attack_source": {
"overlaps": false,
"ranges": 0,
"providers": []
},
"crawler": {
"overlaps": false,
"ranges": 0,
"providers": []
},
"corporate_proxy": {
"overlaps": false,
"ranges": 0,
"providers": []
}
},
"truncated": false,
"sources": [
{
"dataset": "dbip-city-lite",
"version": "2026-09"
},
{
"dataset": "dbip-asn-lite",
"version": "2026-09"
},
{
"dataset": "datacenter-ranges",
"version": "2026-09-12"
},
{
"dataset": "vpn-ranges",
"version": "2026-09-12"
},
{
"dataset": "relay-ranges",
"version": "2026-09-12"
},
{
"dataset": "tor-exit-list",
"version": "2026-09-12"
},
{
"dataset": "anycast-ranges",
"version": "2026-09-12"
},
{
"dataset": "rir-allocations",
"version": "2026-09-12"
},
{
"dataset": "threat-blocklists",
"version": "2026-09-12"
},
{
"dataset": "crawler-ranges",
"version": "2026-09-12"
},
{
"dataset": "corporate-proxy-ranges",
"version": "2026-09-12"
}
]
},
"meta": {
"credits_used": 1,
"credits_remaining": 8,
"cache_hit": false,
"request_id": "req_01m1kgdm4xngzmbmff68g94w0c"
}
}
```
### 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"
}
}
```
### 402 — The balance is empty. The lookup did not run.
```json
{
"success": false,
"error": {
"code": "INSUFFICIENT_CREDITS",
"message": "Your credit balance is empty.",
"request_id": "req_01m1kgdm4xngzmbmff68g94w0c"
}
}
```
### 422 — The request body could not be validated; `error.errors` lists the fields.
```json
{
"success": false,
"error": {
"code": "VALIDATION_FAILED",
"message": "The email field is required.",
"errors": {
"email": [
"The email field is required."
]
},
"request_id": "req_01m1kgdm4xngzmbmff68g94w0c"
}
}
```
### 429 — Over 5 requests per second for the key. Retry after the limit resets.
```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
- `VALIDATION_FAILED` — https://spaw.co/docs/errors/VALIDATION_FAILED
- `INSUFFICIENT_CREDITS` — https://spaw.co/docs/errors/INSUFFICIENT_CREDITS
- `RATE_LIMITED` — https://spaw.co/docs/errors/RATE_LIMITED
---
Canonical page: https://spaw.co/docs/api/lookup-ip-range · OpenAPI document: https://spaw.co/openapi.json · All endpoints: https://spaw.co/docs/api
---
# Validate and enrich a postal address
`POST /api/v1/address`
- Authentication: Secret API key as a bearer token
- Billing: 1 credit for a fresh lookup that answers `valid` true; addresses that cannot stand as written and 7-day repeats are free. Two spellings of one address share the repeat marker, so they are charged once. Asking for the deliverability check adds 3 credits only when the partner actually answered.
- Group: Address
Reads a postal address against the country it is addressed to: that operator's own format (which parts it requires, how the lines are ordered, what shape the postcode takes), its naming for boxes and other non-street delivery points, the national address registers and postcode directories, the open company registers, and the branch lists mailbox operators publish themselves.
Send the address either as one written `address`, the way it would appear on an envelope, or as the separated fields (`address_line1`, `address_line2`, `organization`, `building`, `dependent_locality`, `locality`, `administrative_area`, `postal_code`, `po_box`). Both spellings may be combined: a field sent beside a written address wins over what the parser read out of it. `country` (ISO 3166-1 alpha-2) is always required, because an address can only be measured against a country's own format; a code the format table does not cover answers `422`. A written address may end with the country's name, open with a care-of line, or put a building name above the street — each is read for what it is and reported in `changes` rather than mistaken for the town or the street.
The answer gives the address broken into its parts, `formatted` as that country writes it, whether the postcode matches the country's pattern (`postal_code_valid`) and, where a directory is installed, whether it exists (`postcode_status`, denied only by a directory that lists every code), whether the town written on the address is one the directory publishes for that postcode (`locality_matches_postcode`) and whether the region is (`administrative_area_matches_postcode`). `address_type` says what kind of delivery point it is (`street`, `po_box`, `private_mailbox`, `general_delivery`, `parcel_locker`, `rural_route`, `military`, `bulk_mail`) and `address_type_source` says what settled it. An address that cannot stand as written answers `valid: false` with a `reason`: `empty_address`, `missing_country`, `unknown_country`, `missing_street`, `missing_locality`, `missing_administrative_area`, `missing_postal_code`, `invalid_postal_code` or `postcode_not_found`.
Where a national address register is installed, `exists` answers whether the building itself is in it. It is `true` only on a premise match, `false` only when the register that answered has `register_coverage: "complete"` and does not carry this one, and `null` otherwise — including a register whose coverage is partial (the United States National Address Database is one, because states share their data voluntarily) and a register of streets alone. `match_level` says how close the register got (`premise`, `range`, `street`, `postcode`, `none`). A register that locates the building answers its position (`location_source: register`), and where it publishes a use or the units at the building, `building_use` and `is_multi_unit`. `premise_key` is the digest of the building your outcome reports and suppression list are filed under; `street_suggestion` names the street the register most likely meant when the typed one is not there. `confirmation` says, part by part, what stood behind the postcode, the street, the house number, the town and the region.
The `risk_score` sums the weights of the signals in `risk_signals` (undeliverable 80, private_mailbox 60, placeholder 60, mail_drop 50, reported_returned 50, vacant 40, general_delivery 40, po_box 30, unit_unexpected 30, terminated_postcode 30, locality_mismatch 30, region_mismatch 30, not_in_register 20, unit_missing 20, street_not_found 20, number_out_of_range 20, bulk_mail 20; 60 and above is `high`, 30 and above `medium`), each with the dataset that produced it. Being a post-office box is not scored where a box is how that country receives mail; `not_in_register` fires only when `exists` is false, so a register whose coverage is partial never contributes to the score; and a town the directory lists elsewhere in the postcode's area, or a directory that publishes only each code's default place, never scores `locality_mismatch`.
Every signal whose dataset is not installed answers `null`, which means "not evaluated" and never "no". `deliverability: true` asks a licensed partner whether post actually arrives at the address and whether the delivery point is a mail-receiving agency, vacant or residential; while no partner is enabled, `deliverability_checked` answers `false` and nothing extra is charged. `fields` keeps only the named keys of the answer, in the answer's own order; billing and `meta` are never narrowed by it.
Every answer ends with a decision. `ok_to_ship` and `blocked_by` apply the account's shipping policy — the signals that block whenever they fire, a ceiling on the risk score, and whether a register must have confirmed the building (`GET`/`PUT`/`DELETE /api/v1/address/policy`; the defaults block above 60 and on undeliverable, placeholder and vacant) — in a published order: an address that cannot stand as written is blocked whatever the policy says (`blocked_by: invalid`), then the first blocked signal in the weights' order, then `risk_score`, then `unconfirmed`. Batch, bulk, monitors and the browser endpoint apply the same policy.
Two signals read the input against itself and the register. `looks_placeholder` is true when the whole street name or the whole town is what somebody types to get past a form — a test word, a keyboard run, a refusal, a name with no vowel — and never when a real street merely contains one of the words; the `placeholder` weight is 60. `unit_check` reads the unit written against the units the register lists at the building: `missing` (several units listed, none written, weight 20) or `unexpected` (a flat or suite written at a single residence, weight 30); a suite at a non-residential building is ordinary and answers null, and a private-mailbox number never counts as a unit. `seen_by_you` counts your own account's lookups of the same address inside the last seven days, under the premise digest, and never anybody else's. `formatted_lines` is `formatted` as an array, `postal_code_formatted` the postcode as its operator prints it, and `formatted_local` the address in the country's own script where the input was written in one (Japan, China, Korea).
Five documented test addresses answer a fixed result each, free, with no repeat marker and nothing stored: 1 to 5 Spaw Test Street, Springfield, IL 99999 stand for a valid low-risk address, a mail drop, a building a complete register lacks, a terminated postcode, and — written as `5 Spaw Test Street PMB 5` — a private mailbox. The published sandbox key answers them too.
The product is about places, not people: no field says who lives or works at an address, and none is ever added.
## Request body
| Field | Type | Required | Description |
| --- | --- | --- | --- |
| `address` | string | null | no | The whole address written as it would be on an envelope, with the lines separated by commas or newlines. Required unless `address_line1` is sent. At most 500 characters. |
| `address_line1` | string | null | no | The street line — house number and street name, in the order that country writes them. Required unless `address` is sent. At most 255 characters. |
| `address_line2` | string | null | no | A second line — a flat, suite or unit, or a dependent locality. A value that reads as a unit is moved into `unit`. At most 255 characters. |
| `organization` | string | null | no | The company or institution the address belongs to. At most 200 characters. |
| `building` | string | null | no | A building named instead of numbered (Rose Cottage, Kemp House). Left out, a building name above the street line of a written address is separated by the parser and reported in `changes`. At most 100 characters. |
| `dependent_locality` | string | null | no | A neighbourhood, village or district written below the town. At most 100 characters. |
| `locality` | string | null | no | The town or city. At most 100 characters. |
| `administrative_area` | string | null | no | The state, province or region, as a code or a name; it is folded to the code that country uses where one exists. At most 100 characters. |
| `postal_code` | string | null | no | The postcode or ZIP code. At most 32 characters. |
| `po_box` | string | null | no | A post-office box written on its own, when it is not part of the address lines. At most 64 characters. |
| `country` | string | yes | An ISO 3166-1 alpha-2 country code the postal format table covers (`US`, `GB`, `DE`). Case-insensitive. At most 2 characters. |
| `deliverability` | boolean | no | Ask the licensed partner whether post actually arrives at the address. While a partner is configured it reserves the premium credits up front and charges them only when the partner answered; while none is, nothing is reserved and nothing is charged. Default: . |
| `fields` | string | null | no | A comma-separated list of the answer's keys to keep (`formatted,risk_score,confirmation`). Applied after billing; unknown names are ignored, `sources` is dropped unless named, and `meta` is never narrowed. At most 1,000 characters. |
## Example request
```bash
curl -X POST https://spaw.co/api/v1/address \
-H "Authorization: Bearer sk_sandbox_spaw" \
-H "Content-Type: application/json" \
-d '{
"address": "1600 Amphitheatre Parkway, Mountain View, CA 94043",
"country": "US"
}'
```
## Responses
### 200 — The address as the pipeline reads it; `valid: false` with a `reason` for an address that cannot stand as written.
```json
{
"success": true,
"data": {
"valid": true,
"reason": null,
"country": "US",
"country_name": "United States",
"continent": "NA",
"is_eu": false,
"is_eea": false,
"privacy_regime": "ccpa",
"formatted": "1600 Amphitheatre Parkway\nMOUNTAIN VIEW, CA 94043",
"formatted_lines": [
"1600 Amphitheatre Parkway",
"MOUNTAIN VIEW, CA 94043"
],
"formatted_local": null,
"organization": null,
"building": null,
"street": "Amphitheatre Parkway",
"house_number": "1600",
"unit": null,
"po_box": null,
"dependent_locality": null,
"locality": "Mountain View",
"administrative_area": "CA",
"postal_code": "94043",
"postal_code_formatted": "94043",
"postal_code_valid": true,
"postal_code_type": "zip",
"postcode_status": "live",
"locality_matches_postcode": true,
"administrative_area_matches_postcode": true,
"address_type": "street",
"address_type_source": "address_rules",
"is_po_box": false,
"is_private_mailbox": null,
"exists": true,
"match_level": "premise",
"register": "us-national-address-database",
"register_coverage": "partial",
"premise_key": "239f04782171457b",
"building_use": "commercial",
"is_multi_unit": null,
"unit_check": null,
"street_suggestion": null,
"latitude": 37.42201,
"longitude": -122.08409,
"location_source": "register",
"companies_registered": null,
"is_mail_drop": false,
"mail_drop_provider": null,
"mail_drop_source": null,
"deliverability_checked": false,
"is_deliverable": null,
"is_cmra": null,
"is_vacant": null,
"is_residential": null,
"confirmation": {
"postal_code": "postcode_directory",
"street": "register",
"house_number": "register",
"locality": "postcode_directory",
"administrative_area": "postcode_directory"
},
"changes": [],
"looks_placeholder": false,
"seen_by_you": {
"count": 0,
"first_seen_at": null,
"window_days": 7
},
"risk_score": 0,
"risk_level": "low",
"risk_signals": [],
"ok_to_ship": true,
"blocked_by": null,
"sources": [
{
"dataset": "address-formats",
"version": "2026-09-05"
},
{
"dataset": "address-rules",
"version": "2026-09-05"
},
{
"dataset": "us-national-address-database",
"version": "2026-08-14"
},
{
"dataset": "geonames-postal-codes",
"version": "2026-09-16"
},
{
"dataset": "us-tiger-address-ranges",
"version": "2025"
}
]
},
"meta": {
"credits_used": 1,
"credits_remaining": 9,
"cache_hit": false,
"request_id": "req_01m1kgdm4xngzmbmff68g94w0c"
}
}
```
### 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"
}
}
```
### 402 — The balance is empty. The lookup did not run.
```json
{
"success": false,
"error": {
"code": "INSUFFICIENT_CREDITS",
"message": "Your credit balance is empty.",
"request_id": "req_01m1kgdm4xngzmbmff68g94w0c"
}
}
```
### 422 — The request body could not be validated; `error.errors` lists the fields.
```json
{
"success": false,
"error": {
"code": "VALIDATION_FAILED",
"message": "The email field is required.",
"errors": {
"email": [
"The email field is required."
]
},
"request_id": "req_01m1kgdm4xngzmbmff68g94w0c"
}
}
```
### 429 — Over 5 requests per second for the key. Retry after the limit resets.
```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
- `VALIDATION_FAILED` — https://spaw.co/docs/errors/VALIDATION_FAILED
- `INSUFFICIENT_CREDITS` — https://spaw.co/docs/errors/INSUFFICIENT_CREDITS
- `RATE_LIMITED` — https://spaw.co/docs/errors/RATE_LIMITED
- `SANDBOX_VALUE_NOT_ALLOWED` — https://spaw.co/docs/errors/SANDBOX_VALUE_NOT_ALLOWED
- `SANDBOX_UNAVAILABLE` — https://spaw.co/docs/errors/SANDBOX_UNAVAILABLE
---
Canonical page: https://spaw.co/docs/api/validate-address · OpenAPI document: https://spaw.co/openapi.json · All endpoints: https://spaw.co/docs/api
---
# Validate up to 100 addresses in one call
`POST /api/v1/address/batch`
- Authentication: Secret API key as a bearer token
- Billing: Each address bills like a single lookup: 1 credit for a fresh answer that stands as written; addresses that cannot, and 7-day repeats, are free. The call stops cleanly where the balance ends.
- Group: Address
Runs the single-address lookup for every item, in input order, under the exact single-lookup billing rules. Every answer comes from the installed datasets, so no item waits on a network call. `data.results[]` mirrors the single-address response per item and each item's `meta` carries `credits_used` and `cache_hit`.
Each item is either a written address as a plain string, or an object of the same fields the single lookup takes. An item may name its own `country`, which wins over the list-level one; an item left with no country at all is answered `missing_country`, free, rather than refused.
Repeats inside the list are looked up once and answer as cache hits — the repeat marker is keyed by the address as the pipeline folds it, so two spellings of one address count as one. The list is walked in order and stops where the balance would run out: running out of credits mid-batch returns the paid partial results with `meta.stopped_reason: "insufficient_credits"` and `processed` lower than `requested`, never discarding billed work. Only a batch whose first lookup is refused answers the typed `402`. A key with a daily credit cap stops the list the same way once the cap is spent: the addresses already paid for come back with `meta.stopped_reason: "key_spend_cap"`, and a call that starts with the cap already spent answers `429 KEY_SPEND_CAP_REACHED` and runs nothing.
A batch counts as one request against the rate limit. `deliverability` asks the licensed partner about every item; the base and premium credits are gated per item before the lookup, so no paid query is spent on an address that could not be charged.
Batches honour the account's postal suppression list: an address on it is answered from the list without being read again, free, with `meta.suppressed: true` on that item.
Every item carries `index`, its zero-based position in the list you sent, and `input`, the address as you sent it: an object holding the parts you actually gave — `address` for a written line, the separated fields for an object — plus the country the item was measured against. It is echoed, never parsed, so it is not the answer's `formatted`. A batch stopped early by `stopped_reason` answers only a prefix of the list, so those two are what line an answer up with the row it came from.
## Request body
| Field | Type | Required | Description |
| --- | --- | --- | --- |
| `addresses` | string[] | yes | 1 to 100 addresses. Each item is a written address as a string, or an object of the same fields the single lookup takes. |
| `addresses[].address` | string | null | no | The whole address written as it would be on an envelope. At most 500 characters. |
| `addresses[].address_line1` | string | null | no | The street line, when the address arrives separated. At most 255 characters. |
| `addresses[].address_line2` | string | null | no | At most 255 characters. |
| `addresses[].organization` | string | null | no | At most 200 characters. |
| `addresses[].building` | string | null | no | At most 100 characters. |
| `addresses[].dependent_locality` | string | null | no | At most 100 characters. |
| `addresses[].locality` | string | null | no | At most 100 characters. |
| `addresses[].administrative_area` | string | null | no | At most 100 characters. |
| `addresses[].postal_code` | string | null | no | At most 32 characters. |
| `addresses[].po_box` | string | null | no | At most 64 characters. |
| `addresses[].country` | string | null | no | This item's own country. Wins over the list-level one. At most 2 characters. |
| `country` | string | null | no | An ISO 3166-1 alpha-2 code applied to every item that does not name its own. At most 2 characters. |
| `deliverability` | boolean | no | Ask the licensed partner about every address. While no partner is enabled it answers `deliverability_checked` false at no extra cost. Default: . |
## Example request
```bash
curl -X POST https://spaw.co/api/v1/address/batch \
-H "Authorization: Bearer sk_live_…" \
-H "Content-Type: application/json" \
-d '{
"addresses": [
"1600 Amphitheatre Parkway, Mountain View, CA 94043",
{
"address_line1": "221B Baker Street",
"locality": "London",
"postal_code": "NW1 6XE",
"country": "GB"
},
94043
],
"country": "US"
}'
```
## Responses
### 200 — One result per processed address, plus the batch totals.
```json
{
"success": true,
"data": {
"results": [
{
"index": 0,
"input": {
"country": "US",
"address": "1600 Amphitheatre Parkway, Mountain View, CA 94043"
},
"data": {
"valid": true,
"reason": null,
"country": "US",
"street": "Amphitheatre Parkway",
"house_number": "1600",
"locality": "Mountain View",
"administrative_area": "CA",
"postal_code": "94043",
"address_type": "street",
"risk_score": 0,
"risk_level": "low"
},
"meta": {
"credits_used": 1,
"cache_hit": false
}
},
{
"index": 1,
"input": {
"country": "GB",
"address_line1": "221B Baker Street",
"locality": "London",
"postal_code": "NW1 6XE"
},
"data": {
"valid": true,
"reason": null,
"country": "GB",
"street": "Baker Street",
"house_number": "221B",
"locality": "London",
"postal_code": "NW1 6XE",
"address_type": "street",
"risk_score": 0,
"risk_level": "low"
},
"meta": {
"credits_used": 1,
"cache_hit": false
}
},
{
"index": 2,
"input": {
"country": "US",
"address": "94043"
},
"data": {
"valid": false,
"reason": "missing_street",
"country": "US",
"street": null,
"postal_code": "94043",
"risk_score": null,
"risk_level": null
},
"meta": {
"credits_used": 0,
"cache_hit": false
}
}
]
},
"meta": {
"requested": 3,
"processed": 3,
"credits_used": 2,
"credits_remaining": 8,
"stopped_reason": null,
"request_id": "req_01m1kgdrtqdvwnks99vfgx2rcw"
}
}
```
### 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"
}
}
```
### 402 — The balance is empty. The lookup did not run.
```json
{
"success": false,
"error": {
"code": "INSUFFICIENT_CREDITS",
"message": "Your credit balance is empty.",
"request_id": "req_01m1kgdm4xngzmbmff68g94w0c"
}
}
```
### 422 — The request body could not be validated; `error.errors` lists the fields.
```json
{
"success": false,
"error": {
"code": "VALIDATION_FAILED",
"message": "The email field is required.",
"errors": {
"email": [
"The email field is required."
]
},
"request_id": "req_01m1kgdm4xngzmbmff68g94w0c"
}
}
```
### 429 — Over 5 requests per second for the key. Retry after the limit resets.
```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
- `VALIDATION_FAILED` — https://spaw.co/docs/errors/VALIDATION_FAILED
- `INSUFFICIENT_CREDITS` — https://spaw.co/docs/errors/INSUFFICIENT_CREDITS
- `KEY_SPEND_CAP_REACHED` — https://spaw.co/docs/errors/KEY_SPEND_CAP_REACHED
- `RATE_LIMITED` — https://spaw.co/docs/errors/RATE_LIMITED
---
Canonical page: https://spaw.co/docs/api/lookup-address-batch · OpenAPI document: https://spaw.co/openapi.json · All endpoints: https://spaw.co/docs/api
---
# List the account's bulk address lookup runs
`GET /api/v1/address/bulk`
- Authentication: Secret API key as a bearer token
- Billing: Free.
- Group: Address
Every bulk run this account has started, newest first, 100 per page. A job id is handed out once, at creation, and everything about a run hangs off it — its counts, its results, its cancel and resume calls — so this is how a caller that lost one finds the run again, and how a dashboard lists what is still in flight.
`status`, `since` and `until` are optional and combine; a bare date in `until` means the whole of that day, and an unknown status or an unparseable date is a `422` rather than an empty page. The rows are the same job objects `GET /api/v1/address/bulk/{jobId}` answers. The webhook secret is never among them: it is returned once, at creation.
Runs are kept as long as the lookup history, and `meta.retention_days` says how long that is — a run older than that is deleted with its files and cannot be listed.
## Parameters
| Name | In | Type | Required | Description |
| --- | --- | --- | --- | --- |
| `status` | query | string | no | Only runs in this status. One of: queued, processing, completed, failed, cancelled. |
| `since` | query | string | no | Only runs created at or after this date or timestamp. |
| `until` | query | string | no | Only runs created at or before this. A bare date means the whole of that day. |
| `page` | query | integer | no | The page to return. Default: 1. |
## Example request
```bash
curl https://spaw.co/api/v1/address/bulk \
-H "Authorization: Bearer sk_live_…"
```
## Responses
### 200 — One page of runs, newest first.
```json
{
"success": true,
"data": {
"jobs": [
{
"id": 17,
"status": "completed",
"total": 900,
"duplicate_count": 4,
"processed": 900,
"valid": 812,
"invalid": 88,
"low": 760,
"medium": 120,
"high": 20,
"credits_used": 812,
"stopped_reason": null,
"cancel_requested": false,
"country": "GB",
"deliverability": false,
"webhook_status": null,
"webhook_detail": null,
"created_at": "2026-09-05T11:30:00+00:00",
"finished_at": "2026-09-05T11:31:40+00:00"
}
]
},
"meta": {
"page": 1,
"per_page": 100,
"total": 1,
"retention_days": 30
}
}
```
### 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"
}
}
```
### 422 — The request body could not be validated; `error.errors` lists the fields.
```json
{
"success": false,
"error": {
"code": "VALIDATION_FAILED",
"message": "The email field is required.",
"errors": {
"email": [
"The email field is required."
]
},
"request_id": "req_01m1kgdm4xngzmbmff68g94w0c"
}
}
```
### 429 — Over 5 requests per second for the key. Retry after the limit resets.
```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
- `VALIDATION_FAILED` — https://spaw.co/docs/errors/VALIDATION_FAILED
- `RATE_LIMITED` — https://spaw.co/docs/errors/RATE_LIMITED
---
Canonical page: https://spaw.co/docs/api/list-address-bulk-jobs · OpenAPI document: https://spaw.co/openapi.json · All endpoints: https://spaw.co/docs/api
---
# Queue a bulk address lookup run
`POST /api/v1/address/bulk`
- Authentication: Secret API key as a bearer token
- Billing: `preview: true` is free and queues nothing. A started run bills each row like a single lookup as it is processed; addresses that cannot stand as written, repeats inside the run and 7-day repeats are free, so a run costs at most the `credits_upper_bound` the preview answered and usually less.
- Group: Address
Queues up to 100,000 addresses and processes them in the background. Creation answers `202` with the job's id and status. Poll `GET /api/v1/address/bulk/{jobId}` for progress, then download the result CSV from `GET /api/v1/address/bulk/{jobId}/results`: one row per input address with every response field as a column. Jobs and their files are kept for 30 days.
Each item is a written address as a plain string, or an object of the same fields the single lookup takes, and may name its own `country`, which wins over the list-level one; an item left with no country is answered `missing_country`, free. Rows repeating an address already in the list are looked up once and counted in `duplicate_count` — the repeat marker folds case, punctuation and street-suffix wording away, so two spellings of one address count as one. A run that exhausts the balance stops with `stopped_reason: "insufficient_credits"` and keeps everything answered so far. Because nothing about an address lookup is stored, the input list and the result file are the only copies and both are deleted with the job.
With a `webhook_url`, the finish (`completed`, `failed`, or `cancelled`) is POSTed there as `{ "event": "bulk_address_job.finished", "job": { … } }`, signed with `webhook_secret` exactly like bulk email, phone and IP runs: `X-Spaw-Signature-V2` carries `t={unix seconds},v2={lowercase hex}`, the HMAC-SHA256 of `v2:{t}:{raw body}`, and a receiver should refuse a `t` more than five minutes from its own clock so a captured delivery cannot be replayed at it later. The original `X-Spaw-Signature`, over the body alone, is sent beside it until 1 March 2027. 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`.
### Reading a list before you start it
Send `preview: true` with the same body — or `csv` with a raw CSV instead of the item list — and the call answers `200` with `data.draft` instead of queueing anything: what the list was read as, which column filled which field and whether a heading named it or we guessed, the first rows as parsed, the repeats, the balance, and `credits_upper_bound`. Nothing is charged.
`credits_upper_bound` is a CEILING, not a quote. Rows come back free for reasons no preview can know in advance — a repeat inside the seven-day window, an entry on your suppression list, a verdict that is not charged for, a provider outage — so a run usually costs less than the ceiling and never more. It is priced against the mapping in play, so correcting a column at start re-prices the run rather than holding you to the figure the guess produced — ask for the draft again after a correction if you want the new number. What it actually cost is `credits_used` on the finished job.
Start it with `{"draft_id": }`, adding `mapping` to correct a column we read wrongly. Starting consumes the draft; one left unstarted is deleted, with the rows it holds, after `retention_hours`.
## Parameters
| Name | In | Type | Required | Description |
| --- | --- | --- | --- | --- |
| `Idempotency-Key` | header | string | no | 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 | Required | Description |
| --- | --- | --- | --- |
| `addresses` | string[] | no | 1 to 100,000 addresses. Each item is a written address as a string, or an object of the same fields the single lookup takes. Required unless the body carries `csv` with `preview: true`, or `draft_id`. |
| `addresses[].address` | string | null | no | The whole address written as it would be on an envelope. At most 500 characters. |
| `addresses[].address_line1` | string | null | no | The street line, when the address arrives separated. At most 255 characters. |
| `addresses[].address_line2` | string | null | no | At most 255 characters. |
| `addresses[].organization` | string | null | no | At most 200 characters. |
| `addresses[].building` | string | null | no | At most 100 characters. |
| `addresses[].dependent_locality` | string | null | no | At most 100 characters. |
| `addresses[].locality` | string | null | no | At most 100 characters. |
| `addresses[].administrative_area` | string | null | no | At most 100 characters. |
| `addresses[].postal_code` | string | null | no | At most 32 characters. |
| `addresses[].po_box` | string | null | no | At most 64 characters. |
| `addresses[].country` | string | null | no | This item's own country. Wins over the list-level one. At most 2 characters. |
| `country` | string | null | no | An ISO 3166-1 alpha-2 code applied to every row that does not name its own. At most 2 characters. |
| `deliverability` | boolean | no | Ask the licensed partner about every row. While no partner is enabled it answers `deliverability_checked` false at no extra cost; when one is, each row that answers is charged the premium credits on top of the base credit. Default: . |
| `preview` | boolean | no | Read the list and answer `data.draft` — what we made of it, the repeats, the ceiling on the cost and the balance — with nothing queued and nothing billed. Start it afterwards with `draft_id`. Default: . |
| `csv` | string | null | no | A raw CSV to read instead of the item list, with the column mapping answered back. Preview only, on purpose: a CSV's columns have to be worked out, and working them out and billing for the answer in one request is what the preview exists to stop. At most 5,000,000 characters. |
| `draft_id` | integer | null | no | Start the run a preview answered with. The rows and the options come from the draft, so nothing else in the body applies except `mapping`. A draft can be started once; an `Idempotency-Key` still makes the call safe to retry. |
| `mapping` | object | null | no | Corrects the draft's column mapping, as column index to field — {"1": "address_line1", "2": "postal_code"}. It REPLACES the detected mapping rather than merging into it, so a column can be taken off a field as well as put on one. Goes with `draft_id`, and only for a draft read from a `csv`. |
| `webhook_url` | string | null | no | An https URL to notify when the run finishes. |
## Example request
```bash
curl -X POST https://spaw.co/api/v1/address/bulk \
-H "Authorization: Bearer sk_live_…" \
-H "Content-Type: application/json" \
-d '{
"addresses": [
"1600 Amphitheatre Parkway, Mountain View, CA 94043",
{
"address_line1": "221B Baker Street",
"locality": "London",
"postal_code": "NW1 6XE",
"country": "GB"
}
],
"country": "US",
"webhook_url": "https://example.com/hooks/spaw"
}'
```
## Responses
### 202 — The job was queued.
```json
{
"success": true,
"data": {
"job": {
"id": 918,
"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",
"deliverability": false,
"webhook_status": null,
"created_at": "2026-09-05T10:12:44+00:00",
"finished_at": null,
"webhook_secret": "8fJ2…40 characters…Qk1"
}
}
}
```
### 200 — Either a preview — `preview: true` answers `data.draft` and queues nothing — or a retry with an `Idempotency-Key` already used for this exact request, which answers `data.job` with `Idempotent-Replayed: true`.
```json
{
"success": true,
"data": {
"job": {
"id": 918,
"status": "processing",
"total": 2,
"duplicate_count": 0,
"processed": 1,
"valid": 1,
"invalid": 0,
"low": 1,
"medium": 0,
"high": 0,
"credits_used": 1,
"stopped_reason": null,
"cancel_requested": false,
"country": "US",
"deliverability": false,
"webhook_status": null,
"created_at": "2026-09-05T10:12:44+00:00",
"finished_at": null
}
}
}
```
### 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"
}
}
```
### 409 — The `Idempotency-Key` was already used for a different list, country, deliverability flag or webhook URL.
```json
{
"success": false,
"error": {
"code": "IDEMPOTENCY_KEY_REUSED",
"message": "This Idempotency-Key was already used for a different request.",
"request_id": "req_01m1kgdm4xngzmbmff68g94w0c"
}
}
```
### 422 — The request body could not be validated; `error.errors` lists the fields.
```json
{
"success": false,
"error": {
"code": "VALIDATION_FAILED",
"message": "The email field is required.",
"errors": {
"email": [
"The email field is required."
]
},
"request_id": "req_01m1kgdm4xngzmbmff68g94w0c"
}
}
```
### 429 — Over 5 requests per second for the key. Retry after the limit resets.
```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
- `VALIDATION_FAILED` — https://spaw.co/docs/errors/VALIDATION_FAILED
- `IDEMPOTENCY_KEY_REUSED` — https://spaw.co/docs/errors/IDEMPOTENCY_KEY_REUSED
- `RATE_LIMITED` — https://spaw.co/docs/errors/RATE_LIMITED
---
Canonical page: https://spaw.co/docs/api/create-address-bulk-job · OpenAPI document: https://spaw.co/openapi.json · All endpoints: https://spaw.co/docs/api
---
# Poll a bulk address job
`GET /api/v1/address/bulk/{jobId}`
- Authentication: Secret API key as a bearer token
- Billing: Free.
- Group: Address
The job's current status and running counts. `status` moves from `queued` to `processing` and ends in `completed`, `failed`, or `cancelled`; `processed`, the `valid` and `invalid` counts and the `low`, `medium` and `high` risk counts advance as chunks finish. A job that belongs to another account answers `404`.
## Parameters
| Name | In | Type | Required | Description |
| --- | --- | --- | --- | --- |
| `jobId` | path | integer | yes | |
## Example request
```bash
curl https://spaw.co/api/v1/address/bulk/{jobId} \
-H "Authorization: Bearer sk_live_…"
```
## Responses
### 200 — The job.
```json
{
"success": true,
"data": {
"job": {
"id": 918,
"status": "completed",
"total": 2,
"duplicate_count": 0,
"processed": 2,
"valid": 2,
"invalid": 0,
"low": 2,
"medium": 0,
"high": 0,
"credits_used": 2,
"stopped_reason": null,
"cancel_requested": false,
"country": "US",
"deliverability": false,
"webhook_status": "delivered",
"created_at": "2026-09-05T10:12:44+00:00",
"finished_at": "2026-09-05T10:12:46+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"
}
}
```
### 429 — Over 5 requests per second for the key. Retry after the limit resets.
```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
- `RATE_LIMITED` — https://spaw.co/docs/errors/RATE_LIMITED
---
Canonical page: https://spaw.co/docs/api/get-address-bulk-job · OpenAPI document: https://spaw.co/openapi.json · All endpoints: https://spaw.co/docs/api
---
# Cancel a bulk address job
`POST /api/v1/address/bulk/{jobId}/cancel`
- Authentication: Secret API key as a bearer token
- Billing: Free.
- Group: Address
Asks a queued or running job to stop at its next checkpoint. Everything answered so far stays billed and stays in the result file. A job that already finished answers `409 JOB_ALREADY_FINISHED`.
## Parameters
| Name | In | Type | Required | Description |
| --- | --- | --- | --- | --- |
| `jobId` | path | integer | yes | |
## Example request
```bash
curl -X POST https://spaw.co/api/v1/address/bulk/{jobId}/cancel \
-H "Authorization: Bearer sk_live_…"
```
## Responses
### 202 — Cancellation was requested.
```json
{
"success": true,
"data": {
"job": {
"id": 918,
"status": "processing",
"total": 20000,
"duplicate_count": 12,
"processed": 4000,
"valid": 3700,
"invalid": 300,
"low": 3100,
"medium": 600,
"high": 0,
"credits_used": 3700,
"stopped_reason": null,
"cancel_requested": true,
"country": null,
"deliverability": false,
"webhook_status": null,
"created_at": "2026-09-05T10:12:44+00:00",
"finished_at": null
}
}
}
```
### 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 job already finished.
```json
{
"success": false,
"error": {
"code": "JOB_ALREADY_FINISHED",
"message": "This job has already finished.",
"request_id": "req_01m1kgdm4xngzmbmff68g94w0c"
}
}
```
### 429 — Over 5 requests per second for the key. Retry after the limit resets.
```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
- `JOB_ALREADY_FINISHED` — https://spaw.co/docs/errors/JOB_ALREADY_FINISHED
- `RATE_LIMITED` — https://spaw.co/docs/errors/RATE_LIMITED
---
Canonical page: https://spaw.co/docs/api/cancel-address-bulk-job · OpenAPI document: https://spaw.co/openapi.json · All endpoints: https://spaw.co/docs/api
---
# Resume a bulk job that ran out of credits
`POST /api/v1/address/bulk/{jobId}/resume`
- Authentication: Secret API key as a bearer token
- Billing: Free to call. The resumed run bills the rows it reaches exactly as the first one did, and a row answered in the last seven days is a free repeat.
- Group: Address
Runs a job that stopped when the credit balance ran out again, from the top of the list it was given, instead of asking you to upload it a second time. Rows looked up in the last seven days come back as free repeats, so only the rows the stopped run never reached are charged.
The job keeps its id, its webhook and its cumulative `credits_used`, which is the real spend on the account. `processed` and the verdict counts restart at zero: the part files they were counted from are deleted when a run settles, so the resumed run rebuilds them, and the result file the stopped run left is replaced as the rows come back.
Only a run whose `stopped_reason` is `insufficient_credits` and whose `status` has settled can be resumed; anything else answers `409 JOB_NOT_RESUMABLE`, and so does a second resume of a run that is already going again — claiming the run is one conditional write, so two calls can never queue it twice. A run whose list has passed out of the retention window answers `409 JOB_INPUT_EXPIRED`. An empty balance answers `402 INSUFFICIENT_CREDITS` rather than queueing a run that would stop again on the first row it never reached.
## Parameters
| Name | In | Type | Required | Description |
| --- | --- | --- | --- | --- |
| `jobId` | path | integer | yes | |
## Example request
```bash
curl -X POST https://spaw.co/api/v1/address/bulk/{jobId}/resume \
-H "Authorization: Bearer sk_live_…"
```
## Responses
### 202 — The list is queued again.
```json
{
"success": true,
"data": {
"job": {
"id": 918,
"status": "queued",
"total": 20000,
"duplicate_count": 12,
"processed": 0,
"valid": 0,
"invalid": 0,
"low": 0,
"medium": 0,
"high": 0,
"credits_used": 3700,
"stopped_reason": null,
"cancel_requested": false,
"country": null,
"deliverability": false,
"webhook_status": null,
"created_at": "2026-09-05T10:12:44+00:00",
"finished_at": null
}
}
}
```
### 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"
}
}
```
### 402 — The balance is empty. The lookup did not run.
```json
{
"success": false,
"error": {
"code": "INSUFFICIENT_CREDITS",
"message": "Your credit balance is empty.",
"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 is not in a state a resume can recover (`JOB_NOT_RESUMABLE`), or its list is no longer stored (`JOB_INPUT_EXPIRED`).
```json
{
"success": false,
"error": {
"code": "JOB_NOT_RESUMABLE",
"message": "Only a run that stopped because the credit balance ran out can be resumed, and only once it has finished settling.",
"request_id": "req_01m1kgdm4xngzmbmff68g94w0c"
}
}
```
### 429 — Over 5 requests per second for the key. Retry after the limit resets.
```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
- `JOB_NOT_RESUMABLE` — https://spaw.co/docs/errors/JOB_NOT_RESUMABLE
- `JOB_INPUT_EXPIRED` — https://spaw.co/docs/errors/JOB_INPUT_EXPIRED
- `INSUFFICIENT_CREDITS` — https://spaw.co/docs/errors/INSUFFICIENT_CREDITS
- `RATE_LIMITED` — https://spaw.co/docs/errors/RATE_LIMITED
---
Canonical page: https://spaw.co/docs/api/resume-address-bulk-job · OpenAPI document: https://spaw.co/openapi.json · All endpoints: https://spaw.co/docs/api
---
# 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.
- Group: Address
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 | Required | Description |
| --- | --- | --- | --- | --- |
| `jobId` | path | integer | yes | The id returned at creation. |
## Example request
```bash
curl -X POST https://spaw.co/api/v1/address/bulk/918/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": 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"
}
}
}
```
### 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-address-bulk-webhook · OpenAPI document: https://spaw.co/openapi.json · All endpoints: https://spaw.co/docs/api
---
# Download a bulk address job's results
`GET /api/v1/address/bulk/{jobId}/results`
- Authentication: Secret API key as a bearer token
- Billing: Free.
- Group: Address
The result file as CSV: one row per input address, in input order, with the address as submitted, every field of the single-address response as a column, the datasets behind the answer, the credits the row cost, and whether the account's suppression list answered it (`suppressed`). A run created from an uploaded file carries that file's own columns first. Sources are joined by `;` as `dataset@version`, risk signals as `signal:weight`, the confirmation map as `part:source` and the changes by their names; `formatted` writes its lines on one line, joined by `, `. Fields containing spaces or commas are quoted. Add `?variant=valid|invalid|low|medium|high` to download only the rows that stand as written or do not, or one risk level; the default `full` returns everything. Available once the job is `completed`, or `cancelled` or `failed` with at least one processed row — those rows were billed, so they stay downloadable. A job still running answers `409 RESULTS_NOT_READY`, a cancelled job with nothing processed `409 JOB_CANCELLED`, and a failed job with nothing processed `409 JOB_FAILED`.
`format=json` answers the same rows, filtered the same way, as objects keyed by the result file's own header instead of a CSV attachment: `{ "success": true, "data": { "job": …, "results": [ … ] }, "meta": { "variant", "request_id" } }`. It is streamed row by row exactly as the CSV is, so a 100,000-row run costs no more to answer either way. A CSV cell carries no type, so only the two the writer encodes come back typed: an empty cell is `null` and `true`/`false` are booleans. Everything else is the string as written — a number is never silently made numeric, because postcodes, phone numbers and house numbers lose their leading zeros that way — and the columns of an uploaded file come back exactly as they were uploaded.
## Parameters
| Name | In | Type | Required | Description |
| --- | --- | --- | --- | --- |
| `jobId` | path | integer | yes | |
| `variant` | query | string | no | Restrict the file to the rows that stand as written or do not, or to one risk level. Defaults to full. One of: full, valid, invalid, low, medium, high. |
| `format` | query | string | no | `csv` streams the file as an attachment; `json` streams the same rows as objects. One of: csv, json. Default: csv. |
## Example request
```bash
curl https://spaw.co/api/v1/address/bulk/{jobId}/results \
-H "Authorization: Bearer sk_live_…"
```
## Responses
### 200 — The result rows, as a CSV attachment or as streamed JSON.
```csv
address,valid,reason,country,country_name,continent,is_eu,is_eea,privacy_regime,formatted,formatted_lines,formatted_local,organization,building,street,house_number,unit,po_box,dependent_locality,locality,administrative_area,postal_code,postal_code_formatted,postal_code_valid,postal_code_type,postcode_status,locality_matches_postcode,administrative_area_matches_postcode,address_type,address_type_source,is_po_box,is_private_mailbox,exists,match_level,register,register_coverage,premise_key,building_use,is_multi_unit,unit_check,street_suggestion,latitude,longitude,location_source,companies_registered,is_mail_drop,mail_drop_provider,mail_drop_source,deliverability_checked,is_deliverable,is_cmra,is_vacant,is_residential,confirmation,changes,looks_placeholder,risk_score,risk_level,risk_signals,ok_to_ship,blocked_by,sources,credits_used,suppressed
"1600 Amphitheatre Parkway, Mountain View, CA 94043",true,,US,United States,NA,false,false,ccpa,"1600 Amphitheatre Parkway, MOUNTAIN VIEW, CA 94043","1600 Amphitheatre Parkway;MOUNTAIN VIEW, CA 94043",,,,Amphitheatre Parkway,1600,,,,Mountain View,CA,94043,94043,true,zip,,,,street,address_rules,false,,,,,,239f04782171457b,,,,,,,,,,,,false,,,,,postal_code:pattern;street:;house_number:;locality:;administrative_area:,,false,0,low,,true,,address-formats@2026-09-16;address-rules@2026-09-16,1,false
```
### 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 job is still running (`RESULTS_NOT_READY`), or was cancelled or failed before any row was processed (`JOB_CANCELLED`, `JOB_FAILED`).
```json
{
"success": false,
"error": {
"code": "RESULTS_NOT_READY",
"message": "The results are not ready yet.",
"request_id": "req_01m1kgdm4xngzmbmff68g94w0c"
}
}
```
### 429 — Over 5 requests per second for the key. Retry after the limit resets.
```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
- `VALIDATION_FAILED` — https://spaw.co/docs/errors/VALIDATION_FAILED
- `RESULTS_NOT_READY` — https://spaw.co/docs/errors/RESULTS_NOT_READY
- `JOB_CANCELLED` — https://spaw.co/docs/errors/JOB_CANCELLED
- `JOB_FAILED` — https://spaw.co/docs/errors/JOB_FAILED
- `RATE_LIMITED` — https://spaw.co/docs/errors/RATE_LIMITED
---
Canonical page: https://spaw.co/docs/api/download-address-bulk-results · OpenAPI document: https://spaw.co/openapi.json · All endpoints: https://spaw.co/docs/api
---
# Validate an address from the browser with a publishable key
`POST /api/v1/address/public`
- Authentication: Publishable key in the body, checked against the browser Origin header
- Billing: Bills the key owner like a single lookup; capped by the key's daily credit cap when one is set.
- Group: Address
The browser counterpart of `POST /api/v1/address`, for checking an address field in a checkout or signup form on the client without exposing a secret key. It is authenticated by a publishable `pk_` key in the body plus the browser's `Origin` header, which must match one of the domains the key is locked to. A missing `Origin` is rejected on purpose: servers use a secret key.
The body takes the same address fields as the secret-key endpoint, including the required `country`. The lookup bills the key's owner under the normal rules (a fresh address that stands as written costs one credit; addresses that cannot, and 7-day repeats, are free) and answers `{ "success", "data" }` with **no meta block**, so page visitors never see the owner's balance. The licensed deliverability check is never offered here: a `deliverability` field in the body is ignored.
Because the key sits in page source, give it a daily credit cap in the dashboard: once the cap is spent the endpoint answers `429 KEY_SPEND_CAP_REACHED` until the next day. Free answers never count toward the cap. A key that carries a Cloudflare Turnstile pair requires a confirmed `turnstile_token` and answers `403 TURNSTILE_FAILED` without one, before any credit is spent.
Throttled at 20 requests per minute per IP.
## Parameters
| Name | In | Type | Required | Description |
| --- | --- | --- | --- | --- |
| `Origin` | header | string | yes | Sent by browsers automatically. Its host must be on the key's allowed-domain list. |
## Request body
| Field | Type | Required | Description |
| --- | --- | --- | --- |
| `key` | string | yes | A publishable key, which starts with `pk_`. At most 64 characters. |
| `address` | string | null | no | The whole address written as it would be on an envelope. Required unless `address_line1` is sent. At most 500 characters. |
| `address_line1` | string | null | no | The street line. Required unless `address` is sent. At most 255 characters. |
| `address_line2` | string | null | no | At most 255 characters. |
| `organization` | string | null | no | At most 200 characters. |
| `building` | string | null | no | At most 100 characters. |
| `dependent_locality` | string | null | no | At most 100 characters. |
| `locality` | string | null | no | At most 100 characters. |
| `administrative_area` | string | null | no | At most 100 characters. |
| `postal_code` | string | null | no | At most 32 characters. |
| `po_box` | string | null | no | At most 64 characters. |
| `country` | string | yes | An ISO 3166-1 alpha-2 country code, usually from the country select on the same form. At most 2 characters. |
| `turnstile_token` | string | null | no | Required when the key carries a Turnstile pair. A single-use token from the Turnstile widget on the page. At most 2,048 characters. |
| `fields` | string | null | no | A comma-separated list of the answer's keys to keep, applied after billing; unknown names are ignored. At most 1,000 characters. |
## Example request
```bash
curl -X POST https://spaw.co/api/v1/address/public \
-H "Origin: https://www.example.com" \
-H "Content-Type: application/json" \
-d '{
"key": "pk_live_…",
"address_line1": "221B Baker Street",
"locality": "London",
"postal_code": "NW1 6XE",
"country": "GB"
}'
```
## Responses
### 200 — The same fields as `POST /api/v1/address`, without a meta block.
```json
{
"success": true,
"data": {
"valid": true,
"reason": null,
"country": "GB",
"street": "Baker Street",
"house_number": "221B",
"locality": "London",
"postal_code": "NW1 6XE",
"postal_code_valid": true,
"postal_code_type": "postal",
"address_type": "street",
"is_po_box": false,
"deliverability_checked": false,
"risk_score": 0,
"risk_level": "low"
}
}
```
### 401 — The publishable key does not exist or was revoked.
```json
{
"success": false,
"error": {
"code": "INVALID_PUBLISHABLE_KEY",
"message": "That publishable key does not exist or was revoked.",
"request_id": "req_01m1kgdm4xngzmbmff68g94w0c"
}
}
```
### 402 — The balance is empty. The lookup did not run.
```json
{
"success": false,
"error": {
"code": "INSUFFICIENT_CREDITS",
"message": "Your credit balance is empty.",
"request_id": "req_01m1kgdm4xngzmbmff68g94w0c"
}
}
```
### 403 — The page's origin is not on the key's allowed-domain list (`ORIGIN_NOT_ALLOWED`), or the key requires a Turnstile token that was missing or not confirmed (`TURNSTILE_FAILED`).
```json
{
"success": false,
"error": {
"code": "ORIGIN_NOT_ALLOWED",
"message": "This publishable key cannot be used from this origin.",
"request_id": "req_01m1kgdm4xngzmbmff68g94w0c"
}
}
```
### 422 — The request body could not be validated; `error.errors` lists the fields.
```json
{
"success": false,
"error": {
"code": "VALIDATION_FAILED",
"message": "The email field is required.",
"errors": {
"email": [
"The email field is required."
]
},
"request_id": "req_01m1kgdm4xngzmbmff68g94w0c"
}
}
```
### 429 — The key has spent its owner's daily credit cap (`KEY_SPEND_CAP_REACHED`), or the IP exceeded 20 requests per minute (`RATE_LIMITED`).
```json
{
"success": false,
"error": {
"code": "KEY_SPEND_CAP_REACHED",
"message": "This publishable key has reached its daily credit cap. The counter resets each day.",
"request_id": "req_01m1kgdm4xngzmbmff68g94w0c"
}
}
```
## Error codes
- `INVALID_PUBLISHABLE_KEY` — https://spaw.co/docs/errors/INVALID_PUBLISHABLE_KEY
- `ORIGIN_NOT_ALLOWED` — https://spaw.co/docs/errors/ORIGIN_NOT_ALLOWED
- `TURNSTILE_FAILED` — https://spaw.co/docs/errors/TURNSTILE_FAILED
- `KEY_SPEND_CAP_REACHED` — https://spaw.co/docs/errors/KEY_SPEND_CAP_REACHED
- `VALIDATION_FAILED` — https://spaw.co/docs/errors/VALIDATION_FAILED
- `INSUFFICIENT_CREDITS` — https://spaw.co/docs/errors/INSUFFICIENT_CREDITS
- `RATE_LIMITED` — https://spaw.co/docs/errors/RATE_LIMITED
---
Canonical page: https://spaw.co/docs/api/validate-address-public · OpenAPI document: https://spaw.co/openapi.json · All endpoints: https://spaw.co/docs/api
---
# List the addresses at a postcode
`GET /api/v1/address/postcode/{postcode}`
- Authentication: Secret API key as a bearer token
- Billing: From the licensed partner: 25 credits per answered list, priced at twice or more what the partner charges for it, and nothing for a partner that could not answer. From an open register: 1 credit when the listing holds streets at the postcode, free for the next seven days for the same postcode, and free for an empty list. A country neither covers, and a postcode the listing cannot speak for, answer 501 and cost nothing.
- Group: Address
Answers the addresses at one postcode, for callers that want a "find my address" picker under a postcode field.
Two sources answer. A licensed partner, where one is configured, lists every delivery point for the countries it is licensed for and is asked first — a full British list derives from a postal operator's own address file, and serving it needs a licence ([can you look up UK addresses from a postcode for free?](https://spaw.co/guides/a-uk-postcode-is-not-an-address)). Otherwise, wherever an open national register's street listing is installed (Switzerland, Austria, Czechia, Norway, Belgium, Luxembourg, Latvia, Australia and the Netherlands in full, Denmark as its register stood on 26 September 2026, the parts of the United States, France and Brazil their registers carry, Berlin, whose register is filed by postcode, and Great Britain's streets by postcode district from Ordnance Survey's Open Names), the answer is the streets that register lists inside the postcode, in its own spelling, sorted and with no house numbers: `street`, `locality`, `postal_code` and `country` are filled, `house_number`, `unit` and `identifier` stay null. In the United States and its territories a ZIP+4 lists the streets of its five-digit ZIP, and each address's `postal_code` is that ZIP. An empty list is answered only where the registers' coverage is complete and none of them holds an address at the postcode. A country with neither a licensed list nor an open street listing answers `501 ADDRESS_LIST_UNAVAILABLE` and charges nothing, rather than returning an empty list that would read as "there are no addresses here" — and so does a postcode the installed listing cannot speak for, with a message saying which: a code outside the register's area (Northern Irish BT, forces BFPO and BF, and non-geographic BX codes are outside Ordnance Survey's Great Britain data), a postcode in a part of the country no installed register covers (Germany outside Berlin, or a Belgian region whose register is not installed), a postcode the installed register cannot vouch for (a British district Open Names files no street under, since it files each road under one district), and a postcode where a register whose coverage is complete holds addresses but none on a named street (many Czech villages number their houses without one). Nothing else in the address API depends on it.
`country` is an ISO 3166-1 alpha-2 code and defaults to `GB`. The postcode is passed in the path, with or without its space (`NW1 6XE` and `NW16XE` are the same postcode). At most 100 addresses are returned for one postcode; `count` is how many the answer carries.
## Parameters
| Name | In | Type | Required | Description |
| --- | --- | --- | --- | --- |
| `postcode` | path | string | yes | The postcode to list, in the country's own notation. |
| `country` | query | string | no | An ISO 3166-1 alpha-2 code the postal format table covers. Default: GB. |
## Example request
```bash
curl https://spaw.co/api/v1/address/postcode/NW1 6XE \
-H "Authorization: Bearer sk_live_…"
```
## Responses
### 200 — The addresses the partner lists at that postcode, or the streets an open register lists there — an empty list only where the registers' coverage is complete and none of them holds an address at the postcode.
```json
{
"success": true,
"data": {
"postcode": "NW1 6XE",
"country": "GB",
"available": true,
"addresses": [
{
"address_line1": "221B Baker Street",
"address_line2": null,
"organization": null,
"street": "Baker Street",
"house_number": "221B",
"unit": null,
"locality": "London",
"administrative_area": null,
"postal_code": "NW1 6XE",
"country": "GB",
"identifier": "10023456789"
}
],
"count": 1
},
"meta": {
"credits_used": 2,
"credits_remaining": 8,
"request_id": "req_01m1kgdm4xngzmbmff68g94w0c"
}
}
```
### 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"
}
}
```
### 402 — The balance is empty. The lookup did not run.
```json
{
"success": false,
"error": {
"code": "INSUFFICIENT_CREDITS",
"message": "Your credit balance is empty.",
"request_id": "req_01m1kgdm4xngzmbmff68g94w0c"
}
}
```
### 422 — The request body could not be validated; `error.errors` lists the fields.
```json
{
"success": false,
"error": {
"code": "VALIDATION_FAILED",
"message": "The email field is required.",
"errors": {
"email": [
"The email field is required."
]
},
"request_id": "req_01m1kgdm4xngzmbmff68g94w0c"
}
}
```
### 429 — Over 5 requests per second for the key. Retry after the limit resets.
```json
{
"success": false,
"error": {
"code": "RATE_LIMITED",
"message": "Too many requests. Retry after the limit resets.",
"request_id": "req_01m1kgdm4xngzmbmff68g94w0c"
}
}
```
### 501 — Neither a licensed address-list partner nor an open register's street listing covers the country — here Italy, which has neither — or the installed listing cannot speak for the postcode: a code outside the register's area, a postcode in a part of the country no installed register covers, a postcode the installed register cannot vouch for, or one whose addresses are on no named street. The message says which (`The DE street listing does not cover 50667: …` for a Cologne postcode, since the only German register filed by postcode is Berlin's). Nothing was charged.
```json
{
"success": false,
"error": {
"code": "ADDRESS_LIST_UNAVAILABLE",
"message": "Listing the addresses at a postcode needs a licensed address-list partner or an open register with a street listing, and neither is configured for IT.",
"request_id": "req_01m1kgdm4xngzmbmff68g94w0c"
}
}
```
## Error codes
- `UNAUTHENTICATED` — https://spaw.co/docs/errors/UNAUTHENTICATED
- `VALIDATION_FAILED` — https://spaw.co/docs/errors/VALIDATION_FAILED
- `INSUFFICIENT_CREDITS` — https://spaw.co/docs/errors/INSUFFICIENT_CREDITS
- `ADDRESS_LIST_UNAVAILABLE` — https://spaw.co/docs/errors/ADDRESS_LIST_UNAVAILABLE
- `RATE_LIMITED` — https://spaw.co/docs/errors/RATE_LIMITED
---
Canonical page: https://spaw.co/docs/api/find-addresses-by-postcode · OpenAPI document: https://spaw.co/openapi.json · All endpoints: https://spaw.co/docs/api
---
# List the streets at a postcode
`GET /api/v1/address/streets`
- Authentication: Secret API key as a bearer token
- Billing: 1 credit per answered postcode, free for the next seven days for the same postcode; in the United States and its territories every ZIP+4 in one ZIP counts as that ZIP. A country or postcode with nothing to list, and a postcode the listing does not cover, cost nothing.
- Group: Address
Answers every street a national address register lists inside one postcode, in the register's own spelling and sorted: the list behind a street picker under a postcode field, and the list a single lookup's `street_suggestion` is drawn from. One call returns the whole postcode, so a form filters locally as the visitor types rather than calling per keystroke.
Open data only: the registers publish their streets and Spaw lists them back, with no house numbers and nothing about who is there. Available wherever a register with a street listing is installed — Switzerland, Austria, Czechia, Norway, Belgium, Luxembourg, Latvia, Australia and the Netherlands in full, Denmark as its register stood on 26 September 2026, the parts of the United States, France and Brazil their registers carry, Berlin, whose register is filed by postcode (the North Rhine-Westphalia and Rhineland-Palatinate registers are filed by town, so they list no street at a postcode), and Great Britain by postcode district from Ordnance Survey's Open Names. Where several registers share a country region by region, as Belgium's three do, the postcode is read from the register that lists streets there, and `register` names it. A country with no such register, a postcode no register lists a street at, and a postcode the listing does not cover all answer `404 ADDRESS_STREETS_NOT_FOUND` and cost nothing, and the message says which. `No street is listed at 9999 in CH.` is the answer only where the registers' coverage is complete and none of them holds an address at the postcode; a code outside the register's area (Northern Irish BT, forces BFPO and BF, and non-geographic BX codes are outside Ordnance Survey's Great Britain data), a postcode in a part of the country no installed register covers (Germany outside Berlin), a postcode the installed register cannot vouch for (a British district Open Names files no street under) and one where a register whose coverage is complete holds addresses but none on a named street (many Czech villages) each say so, rather than reading as a postcode with no street.
`country` is an ISO 3166-1 alpha-2 code the postal format table covers and `postal_code` the postcode in the country's own notation, with or without its space. In Great Britain the outward code alone (`N1`, `SW1A`) is accepted too, because the listing is filed by postcode district. In the United States and its territories a ZIP+4 answers every street in its five-digit ZIP, because no register files streets by the four-digit add-on; the answer names the ZIP, and every spelling of one ZIP shares the seven-day repeat marker.
## Parameters
| Name | In | Type | Required | Description |
| --- | --- | --- | --- | --- |
| `country` | query | string | yes | An ISO 3166-1 alpha-2 code the postal format table covers, in either case. 2 to 2 characters. |
| `postal_code` | query | string | yes | The postcode to list, in the country's own notation. At most 32 characters. |
## Example request
```bash
curl https://spaw.co/api/v1/address/streets \
-H "Authorization: Bearer sk_live_…"
```
## Responses
### 200 — The streets the register lists at that postcode.
```json
{
"success": true,
"data": {
"country": "CH",
"postal_code": "8001",
"register": "ch-gebaeudeadressverzeichnis",
"streets": [
"Bahnhofstrasse",
"Limmatquai",
"Münsterhof",
"Paradeplatz",
"Rennweg"
],
"count": 5
},
"meta": {
"credits_used": 1,
"credits_remaining": 9,
"cache_hit": false,
"request_id": "req_01m1kgdm4xngzmbmff68g94w0c"
}
}
```
### 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"
}
}
```
### 402 — The balance is empty. The lookup did not run.
```json
{
"success": false,
"error": {
"code": "INSUFFICIENT_CREDITS",
"message": "Your credit balance is empty.",
"request_id": "req_01m1kgdm4xngzmbmff68g94w0c"
}
}
```
### 404 — No register with a street listing covers the country, the registers list no street at the postcode, or the listing does not cover the postcode; the message says which. Nothing was charged.
```json
{
"success": false,
"error": {
"code": "ADDRESS_STREETS_NOT_FOUND",
"message": "No street is listed at 9999 in CH.",
"request_id": "req_01m1kgdm4xngzmbmff68g94w0c"
}
}
```
### 422 — The request body could not be validated; `error.errors` lists the fields.
```json
{
"success": false,
"error": {
"code": "VALIDATION_FAILED",
"message": "The email field is required.",
"errors": {
"email": [
"The email field is required."
]
},
"request_id": "req_01m1kgdm4xngzmbmff68g94w0c"
}
}
```
### 429 — Over 5 requests per second for the key. Retry after the limit resets.
```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
- `VALIDATION_FAILED` — https://spaw.co/docs/errors/VALIDATION_FAILED
- `INSUFFICIENT_CREDITS` — https://spaw.co/docs/errors/INSUFFICIENT_CREDITS
- `ADDRESS_STREETS_NOT_FOUND` — https://spaw.co/docs/errors/ADDRESS_STREETS_NOT_FOUND
- `RATE_LIMITED` — https://spaw.co/docs/errors/RATE_LIMITED
---
Canonical page: https://spaw.co/docs/api/list-address-streets · OpenAPI document: https://spaw.co/openapi.json · All endpoints: https://spaw.co/docs/api
---
# Read the account's shipping policy
`GET /api/v1/address/policy`
- Authentication: Secret API key as a bearer token
- Billing: Free.
- Group: Address
The rules behind `ok_to_ship` on every address lookup this account runs: the signals that block, the risk-score ceiling, and whether a register must have confirmed the building. The policy belongs to the account, so every member reads and changes the same one. An address that cannot stand as written is always blocked whatever the policy says.
## Example request
```bash
curl https://spaw.co/api/v1/address/policy \
-H "Authorization: Bearer sk_live_…"
```
## Responses
### 200 — The policy in force.
```json
{
"success": true,
"data": {
"max_risk_score": 60,
"block_signals": [
"undeliverable",
"placeholder",
"vacant"
],
"require_exists": false,
"is_default": true,
"updated_at": null
}
}
```
## Error codes
- `UNAUTHENTICATED` — https://spaw.co/docs/errors/UNAUTHENTICATED
- `RATE_LIMITED` — https://spaw.co/docs/errors/RATE_LIMITED
---
Canonical page: https://spaw.co/docs/api/get-address-policy · OpenAPI document: https://spaw.co/openapi.json · All endpoints: https://spaw.co/docs/api
---
# Change one or more rules of the shipping policy
`PUT /api/v1/address/policy`
- Authentication: Secret API key as a bearer token
- Billing: Free.
- Group: Address
Names only the rules that change; the rest stay as they were, and the change applies to the account's next lookup. `max_risk_score: null` ignores the score, `block_signals: []` blocks on no signal, `require_exists: true` blocks every address a register did not confirm — which is every address in a country with no register installed, so switch it on only for the countries you ship to. A body naming none of the three rules is refused with a 422 keyed `policy`, because that is the shape a misspelled key takes.
## Request body
| Field | Type | Required | Description |
| --- | --- | --- | --- |
| `max_risk_score` | integer | null | no | Block when risk_score is above this; null ignores the score. |
| `block_signals` | string[] | null | no | Signals that block whenever they fire, whatever the score. |
| `require_exists` | boolean | no | Block unless a register confirmed the building (exists true). |
## Example request
```bash
curl -X PUT https://spaw.co/api/v1/address/policy \
-H "Authorization: Bearer sk_live_…" \
-H "Content-Type: application/json" \
-d '{
"max_risk_score": 40,
"block_signals": [
"undeliverable",
"placeholder",
"vacant",
"mail_drop"
]
}'
```
## Responses
### 200 — The policy now in force.
## Error codes
- `UNAUTHENTICATED` — https://spaw.co/docs/errors/UNAUTHENTICATED
- `RATE_LIMITED` — https://spaw.co/docs/errors/RATE_LIMITED
- `VALIDATION_FAILED` — https://spaw.co/docs/errors/VALIDATION_FAILED
---
Canonical page: https://spaw.co/docs/api/update-address-policy · OpenAPI document: https://spaw.co/openapi.json · All endpoints: https://spaw.co/docs/api
---
# Restore the default shipping policy
`DELETE /api/v1/address/policy`
- Authentication: Secret API key as a bearer token
- Billing: Free.
- Group: Address
Puts the published defaults back — block above 60 and on undeliverable, placeholder and vacant, no register requirement — and forgets when the policy was last changed.
## Example request
```bash
curl -X DELETE https://spaw.co/api/v1/address/policy \
-H "Authorization: Bearer sk_live_…"
```
## Responses
### 200 — The defaults, in force again.
## Error codes
- `UNAUTHENTICATED` — https://spaw.co/docs/errors/UNAUTHENTICATED
- `RATE_LIMITED` — https://spaw.co/docs/errors/RATE_LIMITED
---
Canonical page: https://spaw.co/docs/api/reset-address-policy · OpenAPI document: https://spaw.co/openapi.json · All endpoints: https://spaw.co/docs/api
---
# Report what happened to post you sent
`POST /api/v1/address/feedback`
- Authentication: Secret API key as a bearer token
- Billing: Free.
- Group: Address
Tells Spaw what became of post your account sent to an address: `delivered`, `returned` (it came back), `moved` (the occupant is no longer there) or `refused` (delivery was refused). Your own reports feed your own later lookups of that address, with the dataset `address-feedback`: an address whose most recent report within 90 days is `returned`, `moved` or `refused` rather than `delivered` scores `reported_returned` (+50), and a delivery reported afterwards cancels it.
Reports also keep your postal suppression list: a `moved` or `refused` report suppresses the address at once, a second `returned` report with no later delivery suppresses it, and a `delivered` report clears an entry your reports added.
There is deliberately no cross-account reputation here and no provider webhook. A postal address is where somebody lives, and a shared "post fails here" database would be a database about residences. What you report raises the risk of your own lookups and of nobody else's, and no other account ever sees it.
Each item is a written address or the separated fields, and may name its own `country`, which wins over the request-level one. Addresses are folded the way a lookup folds them, so any spelling of one building lines up; an item that names no building — no postcode or locality, no street, no house number — is skipped and counted, not rejected. Feedback is free, never billed and never logged as a lookup. Reports are kept for 180 days.
## Request body
| Field | Type | Required | Description |
| --- | --- | --- | --- |
| `items` | object[] | yes | 1 to 1,000 reports per request. |
| `items[].address` | string | null | no | The whole address written as it would be on an envelope. Required unless address_line1 is sent. At most 500 characters. |
| `items[].address_line1` | string | null | no | The street line, when the address arrives separated. Required unless address is sent. At most 255 characters. |
| `items[].address_line2` | string | null | no | At most 255 characters. |
| `items[].organization` | string | null | no | At most 200 characters. |
| `items[].building` | string | null | no | At most 100 characters. |
| `items[].dependent_locality` | string | null | no | At most 100 characters. |
| `items[].locality` | string | null | no | At most 100 characters. |
| `items[].administrative_area` | string | null | no | At most 100 characters. |
| `items[].postal_code` | string | null | no | At most 32 characters. |
| `items[].po_box` | string | null | no | At most 64 characters. |
| `items[].country` | string | null | no | This item's own country; overrides the request-level one. At most 2 characters. |
| `items[].outcome` | string | yes | One of: delivered, returned, moved, refused. |
| `items[].occurred_at` | string | null | no | When it happened. Defaults to now. A time more than five minutes in the future is refused; one inside that margin is recorded as now. |
| `country` | string | null | no | An ISO 3166-1 alpha-2 code applied to items that do not name their own. At most 2 characters. |
## Example request
```bash
curl -X POST https://spaw.co/api/v1/address/feedback \
-H "Authorization: Bearer sk_live_…" \
-H "Content-Type: application/json" \
-d '{
"country": "GB",
"items": [
{
"address": "221B Baker Street, London NW1 6XE",
"outcome": "returned"
},
{
"address_line1": "10 Downing Street",
"locality": "London",
"postal_code": "SW1A 2AA",
"outcome": "moved",
"occurred_at": "2026-09-05T09:00:00Z"
}
]
}'
```
## Responses
### 202 — The reports were recorded.
```json
{
"success": true,
"data": {
"recorded": 2,
"skipped": 0
}
}
```
### 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"
}
}
```
### 422 — The request body could not be validated; `error.errors` lists the fields.
```json
{
"success": false,
"error": {
"code": "VALIDATION_FAILED",
"message": "The email field is required.",
"errors": {
"email": [
"The email field is required."
]
},
"request_id": "req_01m1kgdm4xngzmbmff68g94w0c"
}
}
```
### 429 — Over 5 requests per second for the key. Retry after the limit resets.
```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
- `VALIDATION_FAILED` — https://spaw.co/docs/errors/VALIDATION_FAILED
- `RATE_LIMITED` — https://spaw.co/docs/errors/RATE_LIMITED
---
Canonical page: https://spaw.co/docs/api/report-address-feedback · OpenAPI document: https://spaw.co/openapi.json · All endpoints: https://spaw.co/docs/api
---
# Reported postal outcomes
`GET /api/v1/address/feedback/summary`
- Authentication: Secret API key as a bearer token
- Billing: Free.
- Group: Address
The last 90 days of outcomes your account reported: the total, how many distinct addresses they cover, and the count per outcome. Your own rows only.
## Example request
```bash
curl https://spaw.co/api/v1/address/feedback/summary \
-H "Authorization: Bearer sk_live_…"
```
## Responses
### 200 — The summary.
```json
{
"success": true,
"data": {
"window_days": 90,
"total": 1840,
"addresses": 1712,
"outcomes": {
"delivered": 1750,
"returned": 61,
"moved": 22,
"refused": 7
}
}
}
```
### 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"
}
}
```
### 429 — Over 5 requests per second for the key. Retry after the limit resets.
```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
- `RATE_LIMITED` — https://spaw.co/docs/errors/RATE_LIMITED
---
Canonical page: https://spaw.co/docs/api/address-feedback-summary · OpenAPI document: https://spaw.co/openapi.json · All endpoints: https://spaw.co/docs/api
---
# List the address suppression list
`GET /api/v1/address/suppressions`
- Authentication: Secret API key as a bearer token
- Billing: Free.
- Group: Address
The account's postal suppression list, newest first, 100 entries per page. Your own delivery reports add an address with source `feedback`: `moved` or `refused` at once, `returned` on the second report with no later delivery. Imports arrive with source `imported`. Batch, bulk and monitor runs answer a suppressed address free, with `valid` false, reason `suppressed` and `meta.suppressed` true; a single lookup always runs fresh. An import never goes stale. An entry your reports added is re-checked rather than trusted once it is untouched for 90 days: the lookup keeps it while the reports still stand behind it, and drops it once they do not; a new moved, refused or returned report renews it.
The list is per account and is never read for anybody else.
Every filter is optional and they combine. `value` looks one address up: it is folded against its `country`'s postal format the way the list stores it, so `10 downing st, london sw1a2aa` finds the entry filed as `10 Downing Street, London, SW1A 2AA`. `country` is required whenever `value` is sent — an address folds to a building against one country's format, and without it the search would answer "no such entry" for a row you are looking at. An address that names no building matches nothing. An unparseable date or an out-of-range `per_page` is a `422`, not a silently unfiltered page.
## Parameters
| Name | In | Type | Required | Description |
| --- | --- | --- | --- | --- |
| `value` | query | string | no | One written address, in any spelling of it. Folded before it is matched; requires `country`. At most 500 characters. |
| `country` | query | string | no | The ISO 3166-1 alpha-2 country `value` is read against. Required with `value`. 2 to 2 characters. |
| `source` | query | string | no | Only entries this source added. One of: auto, imported, feedback. |
| `reason` | query | string | no | Only entries carrying this reason. One of: returned, moved, refused, imported. |
| `since` | query | string | no | Only entries added on or after this date. |
| `until` | query | string | no | Only entries added on or before this date. A bare date means the whole of that day. |
| `page` | query | integer | no | The page to return. Default: 1. |
| `per_page` | query | integer | no | Entries per page, up to 500. Default: 100. |
## Example request
```bash
curl https://spaw.co/api/v1/address/suppressions \
-H "Authorization: Bearer sk_live_…"
```
## Responses
### 200 — One page of entries.
```json
{
"success": true,
"data": {
"suppressions": [
{
"id": 412,
"address": "221B Baker Street, London NW1 6XE",
"reason": "returned",
"source": "feedback",
"added_at": "2026-09-05T08:30:00+00:00"
},
{
"id": 411,
"address": "10 Downing Street, London, SW1A 2AA",
"reason": "imported",
"source": "imported",
"added_at": "2026-09-04T17:02:11+00:00"
}
]
},
"meta": {
"page": 1,
"per_page": 100,
"total": 2
}
}
```
### 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"
}
}
```
### 429 — Over 5 requests per second for the key. Retry after the limit resets.
```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
- `VALIDATION_FAILED` — https://spaw.co/docs/errors/VALIDATION_FAILED
- `RATE_LIMITED` — https://spaw.co/docs/errors/RATE_LIMITED
---
Canonical page: https://spaw.co/docs/api/list-address-suppressions · OpenAPI document: https://spaw.co/openapi.json · All endpoints: https://spaw.co/docs/api
---
# Import known-dead addresses
`POST /api/v1/address/suppressions`
- Authentication: Secret API key as a bearer token
- Billing: Free.
- Group: Address
Adds up to 10,000 addresses per request to the postal suppression list — a mailing house's return file, a hand-kept list. Each item is a written address or an object of the same fields the single lookup takes, and may name its own `country`; `country` at the top level applies to the rest. Addresses are folded the way a lookup folds them; ones that name no building (no postcode or locality, no street, no house number) are counted as `invalid`. An address already on the list is counted as `already_suppressed`: it keeps its label and reason, and becomes an import too, so no later report or delivery clears it — it leaves only when you remove it. An import never goes stale, so importing an address again is never needed. Imported entries are honoured from day one at no credit cost: batch, bulk and monitor runs answer them with reason `suppressed`.
## Request body
| Field | Type | Required | Description |
| --- | --- | --- | --- |
| `items` | string[] | yes | 1 to 10,000 addresses. Each item is a written address as a string, or an object of the same fields the single lookup takes. |
| `items[].address` | string | null | no | The whole address written as it would be on an envelope. At most 500 characters. |
| `items[].address_line1` | string | null | no | The street line, when the address arrives separated. At most 255 characters. |
| `items[].address_line2` | string | null | no | At most 255 characters. |
| `items[].organization` | string | null | no | At most 200 characters. |
| `items[].building` | string | null | no | At most 100 characters. |
| `items[].dependent_locality` | string | null | no | At most 100 characters. |
| `items[].locality` | string | null | no | At most 100 characters. |
| `items[].administrative_area` | string | null | no | At most 100 characters. |
| `items[].postal_code` | string | null | no | At most 32 characters. |
| `items[].po_box` | string | null | no | At most 64 characters. |
| `items[].country` | string | null | no | This item's own country. Wins over the list-level one. At most 2 characters. |
| `country` | string | null | no | An ISO 3166-1 alpha-2 code applied to every item that does not name its own. At most 2 characters. |
## Example request
```bash
curl -X POST https://spaw.co/api/v1/address/suppressions \
-H "Authorization: Bearer sk_live_…" \
-H "Content-Type: application/json" \
-d '{
"country": "GB",
"items": [
"221B Baker Street, London NW1 6XE",
{
"address_line1": "10 Downing Street",
"locality": "London",
"postal_code": "SW1A 2AA"
}
]
}'
```
## Responses
### 200 — How many entries were added, already present, or named no building.
```json
{
"success": true,
"data": {
"added": 2,
"already_suppressed": 0,
"invalid": 0
}
}
```
### 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"
}
}
```
### 422 — The request body could not be validated; `error.errors` lists the fields.
```json
{
"success": false,
"error": {
"code": "VALIDATION_FAILED",
"message": "The email field is required.",
"errors": {
"email": [
"The email field is required."
]
},
"request_id": "req_01m1kgdm4xngzmbmff68g94w0c"
}
}
```
### 429 — Over 5 requests per second for the key. Retry after the limit resets.
```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
- `VALIDATION_FAILED` — https://spaw.co/docs/errors/VALIDATION_FAILED
- `RATE_LIMITED` — https://spaw.co/docs/errors/RATE_LIMITED
---
Canonical page: https://spaw.co/docs/api/add-address-suppressions · OpenAPI document: https://spaw.co/openapi.json · All endpoints: https://spaw.co/docs/api
---
# Remove addresses from the address suppression list
`DELETE /api/v1/address/suppressions`
- Authentication: Secret API key as a bearer token
- Billing: Free.
- Group: Address
Removes up to 1,000 addresses per request by the address itself, for a caller that knows the address but not the id it was filed under. Each value is a written address or an object of the same fields the import takes, and is folded against `country` the way the list stores it — so `10 downing st, london sw1a2aa` removes the entry filed as `10 Downing Street, London, SW1A 2AA`. `country` is required: an address folds to a building against one country's format, and a value that folds to nothing would be reported as not on a list it is on. An item may still name its own country.
The answer names every address it was given, written the way the list labels it: `removed` are the ones that were on the list and no longer are, `not_found` the ones that were not on it — including an address that names no building, and one that is on another account's list. Two spellings of one building both count as removed.
The cap is 1,000 rather than the import's 10,000 because the answer names every value back: a request over the cap is a `422` and removes nothing. Removing an entry means the address is looked up fresh the next time a batch, bulk or monitor run meets it.
## Request body
| Field | Type | Required | Description |
| --- | --- | --- | --- |
| `values` | string[] | yes | 1 to 1,000 addresses, each a written line or an object of the separated fields. |
| `values[].address` | string | no | At most 500 characters. |
| `values[].address_line1` | string | no | At most 255 characters. |
| `values[].address_line2` | string | no | At most 255 characters. |
| `values[].organization` | string | no | At most 200 characters. |
| `values[].building` | string | no | At most 100 characters. |
| `values[].dependent_locality` | string | no | At most 100 characters. |
| `values[].locality` | string | no | At most 100 characters. |
| `values[].administrative_area` | string | no | At most 100 characters. |
| `values[].postal_code` | string | no | At most 32 characters. |
| `values[].po_box` | string | no | At most 64 characters. |
| `values[].country` | string | null | no | An ISO 3166-1 alpha-2 code for this address. |
| `country` | string | yes | The ISO 3166-1 alpha-2 country every value is read against. |
## Example request
```bash
curl -X DELETE https://spaw.co/api/v1/address/suppressions \
-H "Authorization: Bearer sk_live_…" \
-H "Content-Type: application/json" \
-d '{
"values": [
"10 downing st, london sw1a2aa",
{
"address_line1": "221B Baker Street",
"postal_code": "NW1 6XE"
}
],
"country": "GB"
}'
```
## Responses
### 200 — Which of the addresses were on the list and which were not.
```json
{
"success": true,
"data": {
"removed": [
"10 downing st, london sw1a2aa"
],
"not_found": [
"221B Baker Street, NW1 6XE"
]
}
}
```
### 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"
}
}
```
### 422 — The request body could not be validated; `error.errors` lists the fields.
```json
{
"success": false,
"error": {
"code": "VALIDATION_FAILED",
"message": "The email field is required.",
"errors": {
"email": [
"The email field is required."
]
},
"request_id": "req_01m1kgdm4xngzmbmff68g94w0c"
}
}
```
### 429 — Over 5 requests per second for the key. Retry after the limit resets.
```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
- `VALIDATION_FAILED` — https://spaw.co/docs/errors/VALIDATION_FAILED
- `RATE_LIMITED` — https://spaw.co/docs/errors/RATE_LIMITED
---
Canonical page: https://spaw.co/docs/api/remove-address-suppressions · OpenAPI document: https://spaw.co/openapi.json · All endpoints: https://spaw.co/docs/api
---
# Remove an address suppression entry
`DELETE /api/v1/address/suppressions/{suppressionId}`
- Authentication: Secret API key as a bearer token
- Billing: Free.
- Group: Address
Deletes one entry so the address is looked up fresh the next time a batch, bulk or monitor run meets it. An entry that belongs to another account answers `404`.
## Parameters
| Name | In | Type | Required | Description |
| --- | --- | --- | --- | --- |
| `suppressionId` | path | integer | yes | The entry's id from the list endpoint. |
## Example request
```bash
curl -X DELETE https://spaw.co/api/v1/address/suppressions/412 \
-H "Authorization: Bearer sk_live_…"
```
## Responses
### 200 — The entry was removed.
```json
{
"success": true
}
```
### 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"
}
}
```
### 429 — Over 5 requests per second for the key. Retry after the limit resets.
```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
- `RATE_LIMITED` — https://spaw.co/docs/errors/RATE_LIMITED
---
Canonical page: https://spaw.co/docs/api/remove-address-suppression · OpenAPI document: https://spaw.co/openapi.json · All endpoints: https://spaw.co/docs/api
---
# List the account's address monitors
`GET /api/v1/address/monitors`
- Authentication: Secret API key as a bearer token
- Billing: Free.
- Group: Address
The saved address lists this account re-checks on a schedule, newest first, 100 per page. Each monitor carries the summary of its last run: how many addresses were `ok`, how many fired each of the five conditions, how many were `unverifiable` (valid, but no dataset installed for the country could say anything about the place), how many changed status, how many newly became flagged, the credits the run spent, and `stopped_reason` when it did not finish cleanly: `insufficient_credits` or `key_spend_cap` where the run stopped part-way, `run_failed` where it died outright, and `run_failed_retrying` where it died and the schedule has been pulled in to check again — `retry_at` in the same block says when that becomes due, and the hourly sweep picks it up at or after that, so between 45 minutes and about an hour and three quarters later. A summary carrying a `stopped_reason` of either failure kind has NO counts in it at all: the run produced none, so read a missing count as unknown rather than as zero. The addresses and their statuses are on the single-monitor endpoint.
## Parameters
| Name | In | Type | Required | Description |
| --- | --- | --- | --- | --- |
| `page` | query | integer | no | The page to return. Default: 1. |
## Example request
```bash
curl https://spaw.co/api/v1/address/monitors \
-H "Authorization: Bearer sk_live_…"
```
## Responses
### 200 — One page of monitors.
```json
{
"success": true,
"data": {
"monitors": [
{
"id": 9,
"name": "Supplier offices",
"cadence": "monthly",
"address_count": 2,
"country": "GB",
"deliverability": false,
"next_run_at": "2026-10-05T08:00:00+00:00",
"last_run_at": "2026-09-05T08:00:04+00:00",
"last_summary": {
"total": 2,
"ok": 1,
"invalid": 0,
"not_in_register": 1,
"terminated_postcode": 0,
"mail_drop": 0,
"high_risk": 0,
"unverifiable": 0,
"changed": 1,
"newly_flagged": 1,
"credits_used": 2,
"stopped_reason": null
},
"created_at": "2026-08-05T08:00:00+00:00"
}
]
},
"meta": {
"page": 1,
"per_page": 100,
"total": 1,
"retention_days": 365
}
}
```
### 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"
}
}
```
### 429 — Over 5 requests per second for the key. Retry after the limit resets.
```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
- `RATE_LIMITED` — https://spaw.co/docs/errors/RATE_LIMITED
---
Canonical page: https://spaw.co/docs/api/list-address-monitors · OpenAPI document: https://spaw.co/openapi.json · All endpoints: https://spaw.co/docs/api
---
# Monitor a list of addresses on a schedule
`POST /api/v1/address/monitors`
- Authentication: Secret API key as a bearer token
- Billing: Creation is free. Each run bills every address like a single lookup: 1 credit for a fresh answer that stands as written; addresses that cannot, and 7-day repeats, are free. With `deliverability`, the premium is added only where the licensed partner answered.
- Group: Address
Saves up to 500 addresses — your own sites, a supplier's registered offices, the delivery addresses behind an order book — and re-checks them every week or month. There is no daily cadence: the national registers and postcode directories behind an address answer are refreshed weekly at best, so a daily run would bill for data that cannot have changed.
Each item is a written address as a plain string, or an object of the same fields the single lookup takes, and may name its own `country`; the list-level `country` applies to the rest. The first run starts right away and only sets the baseline. From the next run on, every address that newly leaves the national register, has its postcode terminated, becomes a mail drop, starts scoring high, or stops standing as written is reported to the account's email with the reason, so a customer's address goes stale in your inbox rather than in your post room.
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": "address_monitor.run", "monitor": { "id", "name", "cadence", "last_run_at", "next_run_at" }, "summary": { … the same block as `last_summary` … }, "changes": [ … ] }`, where `changes` names the addresses the alert email names. 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 | Required | Description |
| --- | --- | --- | --- |
| `name` | string | yes | A label for the alerts. At most 100 characters. |
| `addresses` | string[] | yes | 1 to 500 addresses. Each item is a written address as a string, or an object of the same fields the single lookup takes. |
| `addresses[].address` | string | null | no | The whole address written as it would be on an envelope. At most 500 characters. |
| `addresses[].address_line1` | string | null | no | The street line, when the address arrives separated. At most 255 characters. |
| `addresses[].address_line2` | string | null | no | At most 255 characters. |
| `addresses[].organization` | string | null | no | At most 200 characters. |
| `addresses[].building` | string | null | no | At most 100 characters. |
| `addresses[].dependent_locality` | string | null | no | At most 100 characters. |
| `addresses[].locality` | string | null | no | At most 100 characters. |
| `addresses[].administrative_area` | string | null | no | At most 100 characters. |
| `addresses[].postal_code` | string | null | no | At most 32 characters. |
| `addresses[].po_box` | string | null | no | At most 64 characters. |
| `addresses[].country` | string | null | no | This item's own country. Wins over the list-level one. At most 2 characters. |
| `cadence` | string | yes | How often the list is re-checked. There is no daily cadence. One of: weekly, monthly. |
| `country` | string | null | no | An ISO 3166-1 alpha-2 code applied to every entry that does not name its own. At most 2 characters. |
| `deliverability` | boolean | no | Ask the licensed partner whether post arrives at every address on every run. While no partner is enabled it answers `deliverability_checked` false at no extra cost. Default: . |
| `webhook_url` | string | null | no | 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 | no | 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
```bash
curl -X POST https://spaw.co/api/v1/address/monitors \
-H "Authorization: Bearer sk_live_…" \
-H "Content-Type: application/json" \
-d '{
"name": "Supplier offices",
"addresses": [
"221B Baker Street, London, NW1 6XE",
{
"address_line1": "10 Downing Street",
"locality": "London",
"postal_code": "SW1A 2AA"
}
],
"cadence": "monthly",
"country": "GB"
}'
```
## Responses
### 201 — The monitor was saved and its baseline run queued.
```json
{
"success": true,
"data": {
"monitor": {
"id": 9,
"name": "Supplier offices",
"cadence": "monthly",
"address_count": 2,
"country": "GB",
"deliverability": 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"
}
}
}
```
### 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"
}
}
```
### 422 — The request body could not be validated; `error.errors` lists the fields.
```json
{
"success": false,
"error": {
"code": "VALIDATION_FAILED",
"message": "The email field is required.",
"errors": {
"email": [
"The email field is required."
]
},
"request_id": "req_01m1kgdm4xngzmbmff68g94w0c"
}
}
```
### 429 — Over 5 requests per second for the key. Retry after the limit resets.
```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
- `VALIDATION_FAILED` — https://spaw.co/docs/errors/VALIDATION_FAILED
- `RATE_LIMITED` — https://spaw.co/docs/errors/RATE_LIMITED
---
Canonical page: https://spaw.co/docs/api/create-address-monitor · OpenAPI document: https://spaw.co/openapi.json · All endpoints: https://spaw.co/docs/api
---
# Read an address monitor and its last results
`GET /api/v1/address/monitors/{monitorId}`
- Authentication: Secret API key as a bearer token
- Billing: Free.
- Group: Address
The monitor plus `results`: for every address from the last run, its status, the conditions that fired, what the registers answered, its score and level, and whether the status changed since the previous run; keyed by the address as it was saved, or null until the first run has finished.
The status is the first condition that fired, in this order: `invalid` (the address no longer stands as written, with the `reason`), `not_in_register` (a register whose coverage is complete no longer carries it), `terminated_postcode`, `mail_drop`, `high_risk`. An address that fired none is `ok`, or `unverifiable` when the answer stands but no register, postcode directory or company register is installed for its country — which is the honest answer, and is counted apart so you can see how much of a list nobody can check. A monitor that belongs to another account answers `404`.
## Parameters
| Name | In | Type | Required | Description |
| --- | --- | --- | --- | --- |
| `monitorId` | path | integer | yes | The monitor's id from the list endpoint. |
## Example request
```bash
curl https://spaw.co/api/v1/address/monitors/9 \
-H "Authorization: Bearer sk_live_…"
```
## Responses
### 200 — The monitor.
```json
{
"success": true,
"data": {
"monitor": {
"id": 9,
"name": "Supplier offices",
"cadence": "monthly",
"address_count": 2,
"country": "GB",
"deliverability": false,
"next_run_at": "2026-10-05T08:00:00+00:00",
"last_run_at": "2026-09-05T08:00:04+00:00",
"last_summary": {
"total": 2,
"ok": 1,
"invalid": 0,
"not_in_register": 1,
"terminated_postcode": 0,
"mail_drop": 0,
"high_risk": 0,
"unverifiable": 0,
"changed": 1,
"newly_flagged": 1,
"credits_used": 2,
"stopped_reason": null
},
"created_at": "2026-08-05T08:00:00+00:00",
"results": {
"221B Baker Street, London, NW1 6XE": {
"status": "not_in_register",
"flags": [
"not_in_register"
],
"reason": null,
"exists": false,
"postcode_status": "live",
"companies_registered": 0,
"is_mail_drop": false,
"risk_score": 20,
"risk_level": "low",
"changed": true
},
"10 Downing Street, London, SW1A 2AA": {
"status": "ok",
"flags": [],
"reason": null,
"exists": true,
"postcode_status": "live",
"companies_registered": 0,
"is_mail_drop": false,
"risk_score": 0,
"risk_level": "low",
"changed": false
}
}
}
}
}
```
### 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"
}
}
```
### 429 — Over 5 requests per second for the key. Retry after the limit resets.
```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
- `RATE_LIMITED` — https://spaw.co/docs/errors/RATE_LIMITED
---
Canonical page: https://spaw.co/docs/api/get-address-monitor · OpenAPI document: https://spaw.co/openapi.json · All endpoints: https://spaw.co/docs/api
---
# Change a monitor
`PATCH /api/v1/address/monitors/{monitorId}`
- Authentication: Secret API key as a bearer token
- Billing: Free. The change applies to the next run.
- Group: Address
Changes a saved monitor in place: rename it, move it to another cadence, replace the addresses, or point it at a webhook. Send only the fields you are changing; a body that names none of them answers `422` rather than quietly changing nothing, because that is the shape a misspelled field name takes.
Replacing `addresses` replaces the list. What the last run recorded about the addresses that stayed is kept, so their next run still compares against the answer they already had; what it recorded about the ones that are gone is deleted with them; and an entry that is new starts with no baseline, so its first run only sets one. A rename never touches a stored answer — they are keyed by the entry, never by the list.
Changing `cadence` neither skips a run nor starts a second one: a run already scheduled sooner than the new interval still happens, and a longer interval applies from the one after it. Nothing is queued by this call — the change applies to the next run, and `POST .../run` is there to have it now.
Sending a `webhook_url` that differs from the one stored mints a new signing secret and returns it once, here; sending the URL it already has keeps the secret it is signing with, and `null` removes the webhook. A monitor that belongs to another account answers `404`.
## Parameters
| Name | In | Type | Required | Description |
| --- | --- | --- | --- | --- |
| `monitorId` | path | integer | yes | The monitor's id from the list endpoint. |
## Request body
| Field | Type | Required | Description |
| --- | --- | --- | --- |
| `name` | string | no | At most 100 characters. |
| `addresses` | string[] | no | |
| `cadence` | string | no | One of: weekly, monthly. |
| `country` | string | null | no | The country entries that name none belong to. |
| `deliverability` | boolean | no | |
| `webhook_url` | string | null | no | At most 2,048 characters. |
| `webhook_events` | string | no | One of: changes, every_run. |
## Example request
```bash
curl -X PATCH https://spaw.co/api/v1/address/monitors/41 \
-H "Authorization: Bearer sk_live_…" \
-H "Content-Type: application/json" \
-d '{
"name": "Supplier offices (UK)",
"country": "GB"
}'
```
## Responses
### 200 — The monitor as it now stands.
### 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"
}
}
```
### 422 — The request body could not be validated; `error.errors` lists the fields.
```json
{
"success": false,
"error": {
"code": "VALIDATION_FAILED",
"message": "The email field is required.",
"errors": {
"email": [
"The email field is required."
]
},
"request_id": "req_01m1kgdm4xngzmbmff68g94w0c"
}
}
```
### 429 — Over 5 requests per second for the key. Retry after the limit resets.
```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
- `VALIDATION_FAILED` — https://spaw.co/docs/errors/VALIDATION_FAILED
- `RATE_LIMITED` — https://spaw.co/docs/errors/RATE_LIMITED
---
Canonical page: https://spaw.co/docs/api/update-address-monitor · OpenAPI document: https://spaw.co/openapi.json · All endpoints: https://spaw.co/docs/api
---
# Stop monitoring an address list
`DELETE /api/v1/address/monitors/{monitorId}`
- Authentication: Secret API key as a bearer token
- Billing: Free.
- Group: Address
Removes the monitor and its stored results; nothing further is scheduled. A run that is already queued finishes on its own and bills as usual. A monitor that belongs to another account answers `404`.
## Parameters
| Name | In | Type | Required | Description |
| --- | --- | --- | --- | --- |
| `monitorId` | path | integer | yes | The monitor's id from the list endpoint. |
## Example request
```bash
curl -X DELETE https://spaw.co/api/v1/address/monitors/9 \
-H "Authorization: Bearer sk_live_…"
```
## Responses
### 200 — The monitor was removed.
```json
{
"success": true
}
```
### 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"
}
}
```
### 429 — Over 5 requests per second for the key. Retry after the limit resets.
```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
- `RATE_LIMITED` — https://spaw.co/docs/errors/RATE_LIMITED
---
Canonical page: https://spaw.co/docs/api/delete-address-monitor · OpenAPI document: https://spaw.co/openapi.json · All endpoints: https://spaw.co/docs/api
---
# Re-check a monitored address list now
`POST /api/v1/address/monitors/{monitorId}/run`
- Authentication: Secret API key as a bearer token
- Billing: The run bills every address like a single lookup; addresses that do not stand as written, and 7-day repeats, are free.
- Group: Address
Queues a run right away instead of waiting for the schedule; when it finishes, the next scheduled run is set one cadence from then. Answers `202` with the monitor as it stands before the run; poll it for the new `last_run_at`, `last_summary` and `results`. A monitor that belongs to another account answers `404`.
## Parameters
| Name | In | Type | Required | Description |
| --- | --- | --- | --- | --- |
| `monitorId` | path | integer | yes | The monitor's id from the list endpoint. |
## Example request
```bash
curl -X POST https://spaw.co/api/v1/address/monitors/9/run \
-H "Authorization: Bearer sk_live_…"
```
## Responses
### 202 — The run was queued.
```json
{
"success": true,
"data": {
"monitor": {
"id": 9,
"name": "Supplier offices",
"cadence": "monthly",
"address_count": 2,
"country": "GB",
"deliverability": false,
"next_run_at": "2026-10-05T08:00:00+00:00",
"last_run_at": "2026-09-05T08:00:04+00:00",
"last_summary": {
"total": 2,
"ok": 1,
"invalid": 0,
"not_in_register": 1,
"terminated_postcode": 0,
"mail_drop": 0,
"high_risk": 0,
"unverifiable": 0,
"changed": 1,
"newly_flagged": 1,
"credits_used": 2,
"stopped_reason": null
},
"created_at": "2026-08-05T08:00:00+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"
}
}
```
### 429 — Over 5 requests per second for the key. Retry after the limit resets.
```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
- `RATE_LIMITED` — https://spaw.co/docs/errors/RATE_LIMITED
---
Canonical page: https://spaw.co/docs/api/run-address-monitor · OpenAPI document: https://spaw.co/openapi.json · All endpoints: https://spaw.co/docs/api
---
# Check several signals against each other
`POST /api/v1/consistency`
- Authentication: Secret API key as a bearer token
- Billing: Each signal bills exactly as its own single-lookup endpoint: 1 credit per definitive answer, free when the answer is no, and free for a 7-day repeat of the same input on the same account. The IP signal bills one credit per five answered IP lookups, so it usually costs 0 (`ip_lookups_prepaid` on its meta), an email answer whose mailbox check was uncertain is free, and an undeliverable one is free within your email fair use (`fair_use_billed` on its meta). `meta.credits_used` is the sum, and the consistency analysis itself is free. A call that runs out of credits part-way keeps the signals it paid for and reports `meta.stopped_reason`; only a call refused on its first signal answers 402.
- Group: Consistency
Takes any subset of an email address, a phone number, an IP address, a postal address and a business identifier, runs each through the product that owns it, and reports where the answers agree and where they do not.
The per-signal blocks under `signals` are exactly what the single-lookup endpoints return, field for field. All five keys are always present: a signal you did not ask for, or one the call did not reach, is `null`. The signals run in a fixed order — `ip`, `phone`, `entity`, `address`, `email` — because a keyed body has no input order, and because email is the only leg that does DNS and a mailbox probe, so a call that runs out of credits stops at the slowest leg last.
The `consistency` block on top is free. `comparisons` lists each check with both sides quoted — the signal, the field and the value that decided it — so nothing is asserted that you cannot see the working for. A comparison appears only when both of its signals answered; a signal nobody asked about produces silence, not a row. Where one side is `null` the result is `not_compared` with `withheld: "missing_value"`, never a disagreement, because a null field means "not evaluated" and never "no". Where the IP is a VPN exit, a relay or a datacenter egress, the country comparisons answer `not_compared` with `withheld: "low_confidence"`: that location belongs to the operator rather than to the person, and reporting a difference there would manufacture evidence against everyone who uses a VPN.
`country_agreement` rolls up the three country comparisons between the person's own signals — `differ` if any of them differed, `agree` if at least one agreed and none differed, and `null` when none of them could be made. `flags` names the disagreements and the pairings that held: `phone_country_differs_from_address_country`, `ip_country_differs_from_address_country`, `ip_country_differs_from_phone_country`, `disposable_email_from_datacenter_ip`, `disposable_email_from_anonymous_network`, `mail_drop_address_with_free_email_provider`, `disposable_phone_with_disposable_email`, `entity_country_differs_from_address_country`, `entity_name_differs_from_address_organization`. The `ip_address_distance` comparison carries `distance_km` as evidence only, and never raises a flag: there is no honest distance at which a person stops being themselves.
The business identifier is compared against the postal address, the only other signal carrying a company name and a country of its own. `entity_address_country` puts the country of registration beside the country the address stands in; it is deliberately **not** part of `country_agreement`, because where a company is registered is not where a person is, and a Delaware company with a London office would otherwise read as a top-level disagreement. `entity_address_organization` compares the registered legal name with the organisation written on the address, and the only claim it makes is a normalised exact match: both sides are folded to their letters and digits and compared as strings, with case, accents (`Société` and `Societe` fold together), an ampersand against the word `and`, apostrophes, full stops in initials, and company-form words such as limited, ltd, plc, llp and inc taken out first — so a difference here is a difference in the name rather than in how it was typed. There is no similarity score, because no threshold honestly decides whether two names are one company — so `differ` means the two names are not the same name, not that the address belongs to somebody else. A value that folds away to nothing, such as an organisation line reading only "Ltd", withholds the comparison as a missing value.
There is no score, no risk level, no verdict and no recommendation, and there will not be one. The flags are the whole output, and a difference between two fields is a fact about the fields, not a judgement about a person — people travel, move house, keep a foreign number and browse through a VPN. Nothing about the call is stored beyond what each product already logs for its own lookup.
## Request body
| Field | Type | Required | Description |
| --- | --- | --- | --- |
| `email` | string | null | no | The email address to verify. At most 254 characters. |
| `phone` | string | null | no | The phone number to validate, in E.164 or in national notation with `country`. At most 64 characters. |
| `country` | string | null | no | The ISO region the phone number should be read against. It applies to the phone signal only; the postal address carries its own country. At most 2 characters. |
| `ip` | string | null | no | The IPv4 or IPv6 address the request came from. |
| `address` | object | null | no | The postal address, in the same shape `POST /api/v1/address` takes — either a written `address` or the separated fields, always with `country`. Send `organization` to have the company name compared with the register's. |
| `entity` | string | object | no | The business identifier, in the same shape `POST /api/v1/entity` takes. A bare string is read as a LEI, which is the only type needing no country. |
| `entity.identifier` | string | no | At most 64 characters. |
| `entity.type` | string | no | One of: lei, company_number. |
| `entity.country` | string | null | no | Required when `type` is `company_number`. At most 2 characters. |
## Example request
```bash
curl -X POST https://spaw.co/api/v1/consistency \
-H "Authorization: Bearer sk_live_…" \
-H "Content-Type: application/json" \
-d '{
"email": "ada@example.com",
"phone": "+442079460018",
"country": "GB",
"ip": "81.2.69.142",
"address": {
"address": "10 Downing Street, London, SW1A 2AA",
"organization": "ACME HOLDINGS LTD",
"country": "GB"
},
"entity": {
"identifier": "213800QILIUD4ROSUO03",
"type": "lei"
}
}'
```
## Responses
### 200 — Each signal's own answer, and the comparisons between them.
```json
{
"success": true,
"data": {
"consistency": {
"signals_compared": [
"email",
"phone",
"ip",
"address",
"entity"
],
"country_agreement": "differ",
"flags": [
"ip_country_differs_from_address_country",
"ip_country_differs_from_phone_country"
],
"comparisons": [
{
"comparison": "phone_address_country",
"left": {
"signal": "phone",
"field": "country",
"value": "GB"
},
"right": {
"signal": "address",
"field": "country",
"value": "GB"
},
"result": "agree",
"withheld": null,
"flag": null,
"distance_km": null
},
{
"comparison": "ip_address_country",
"left": {
"signal": "ip",
"field": "country",
"value": "DE"
},
"right": {
"signal": "address",
"field": "country",
"value": "GB"
},
"result": "differ",
"withheld": null,
"flag": "ip_country_differs_from_address_country",
"distance_km": null
}
]
}
},
"meta": {
"requested": 5,
"processed": 5,
"credits_used": 5,
"credits_remaining": 995,
"stopped_reason": null,
"request_id": "req_01m1kgdm4xngzmbmff68g94w0c"
}
}
```
### 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"
}
}
```
### 402 — The balance is empty. The lookup did not run.
```json
{
"success": false,
"error": {
"code": "INSUFFICIENT_CREDITS",
"message": "Your credit balance is empty.",
"request_id": "req_01m1kgdm4xngzmbmff68g94w0c"
}
}
```
### 422 — The request body could not be validated; `error.errors` lists the fields.
```json
{
"success": false,
"error": {
"code": "VALIDATION_FAILED",
"message": "The email field is required.",
"errors": {
"email": [
"The email field is required."
]
},
"request_id": "req_01m1kgdm4xngzmbmff68g94w0c"
}
}
```
### 429 — Over 5 requests per second for the key. Retry after the limit resets.
```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
- `VALIDATION_FAILED` — https://spaw.co/docs/errors/VALIDATION_FAILED
- `INSUFFICIENT_CREDITS` — https://spaw.co/docs/errors/INSUFFICIENT_CREDITS
- `RATE_LIMITED` — https://spaw.co/docs/errors/RATE_LIMITED
- `KEY_SCOPE_DENIED` — https://spaw.co/docs/errors/KEY_SCOPE_DENIED
- `EMAIL_NOT_VERIFIED` — https://spaw.co/docs/errors/EMAIL_NOT_VERIFIED
---
Canonical page: https://spaw.co/docs/api/check-consistency · OpenAPI document: https://spaw.co/openapi.json · All endpoints: https://spaw.co/docs/api
---
# List the account's bulk consistency runs
`GET /api/v1/consistency/bulk`
- Authentication: Secret API key as a bearer token
- Billing: Free.
- Group: Consistency
Every bulk consistency run this account has started, newest first, 100 per page. A job id is handed out once, at creation, and everything about a run hangs off it — its counts, its results, its cancel and resume calls — so this is how a caller that lost one finds the run again.
`status`, `since` and `until` are optional and combine; a bare date in `until` means the whole of that day, and an unknown status or an unparseable date is a `422` rather than an empty page. The webhook secret is never among the rows: it is returned once, at creation.
Runs are kept as long as the lookup history, and `meta.retention_days` says how long that is — a run older than that is deleted with its files and cannot be listed.
## Parameters
| Name | In | Type | Required | Description |
| --- | --- | --- | --- | --- |
| `status` | query | string | no | Only runs in this status. One of: queued, processing, completed, failed, cancelled. |
| `since` | query | string | no | Only runs created at or after this date or timestamp. |
| `until` | query | string | no | Only runs created at or before this. A bare date means the whole of that day. |
| `page` | query | integer | no | The page to return. Default: 1. |
## Example request
```bash
curl https://spaw.co/api/v1/consistency/bulk \
-H "Authorization: Bearer sk_live_…"
```
## Responses
### 200 — One page of runs, newest first.
```json
{
"success": true,
"data": {
"jobs": [
{
"id": 41,
"status": "completed",
"total": 900,
"duplicate_count": 3,
"processed": 900,
"flagged": 61,
"unflagged": 802,
"uncompared": 37,
"credits_used": 2410,
"stopped_reason": null,
"cancel_requested": false,
"country": "GB",
"webhook_status": null,
"webhook_detail": null,
"created_at": "2026-09-10T11:30:00+00:00",
"finished_at": "2026-09-10T11:36:12+00:00"
}
]
},
"meta": {
"page": 1,
"per_page": 100,
"total": 1,
"retention_days": 30
}
}
```
### 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"
}
}
```
### 422 — The request body could not be validated; `error.errors` lists the fields.
```json
{
"success": false,
"error": {
"code": "VALIDATION_FAILED",
"message": "The email field is required.",
"errors": {
"email": [
"The email field is required."
]
},
"request_id": "req_01m1kgdm4xngzmbmff68g94w0c"
}
}
```
### 429 — Over 5 requests per second for the key. Retry after the limit resets.
```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
- `VALIDATION_FAILED` — https://spaw.co/docs/errors/VALIDATION_FAILED
- `RATE_LIMITED` — https://spaw.co/docs/errors/RATE_LIMITED
---
Canonical page: https://spaw.co/docs/api/list-consistency-bulk-jobs · OpenAPI document: https://spaw.co/openapi.json · All endpoints: https://spaw.co/docs/api
---
# Queue a bulk consistency run
`POST /api/v1/consistency/bulk`
- Authentication: Secret API key as a bearer token
- Billing: `preview: true` is free and queues nothing. A started run bills each row as the SUM OF THE LEGS IT ACTUALLY RAN, each on that product's own rules — 1 credit for a definitive answer, free when the answer is no, free for a 7-day repeat of the same input on the same account — and the comparison on top is free. So a row costs between nothing and four credits, which is exactly what the same rows would have cost one at a time through `POST /api/v1/consistency`. None of the per-product extras is available here and none is charged: no HLR, no deliverability partner, no privacy mode. Suppression lists are deliberately not consulted, matching the single call: a consistency check is a fresh vetting decision.
- Group: Consistency
The cross-signal check over a whole list. Queues up to 100,000 rows and processes them in the background: creation answers `202` with the job's id and status, `GET /api/v1/consistency/bulk/{jobId}` reports progress, and `GET /api/v1/consistency/bulk/{jobId}/results` streams one CSV row per input row — the four signals as submitted, then what agreed, what did not, and what could not honestly be settled.
Each item carries any subset of `email`, `phone`, `ip` and `address`, and at least one of them. **A row that carries two of the four is a perfectly good consistency check of those two** and is run as one; the comparisons its missing signals would have been part of are simply absent from the row, in the same way a signal you did not send produces silence rather than a row on the single call. A row's `country` applies to that row alone and wins over the list-level one.
Rows repeating an ENTIRE row already in the list are counted in `duplicate_count`. That is stricter than the other four bulk runs on purpose: repeats are free per lookup here, not per row, so two rows sharing an email address but naming different phone numbers get one free leg and one charged one — counting them as duplicates would take a credit off the preview's ceiling that the run then spends.
With a `webhook_url`, the finish (`completed`, `failed`, or `cancelled`) is POSTed there as `{ "event": "bulk_consistency_job.finished", "job": { … } }`, signed with `webhook_secret` exactly like the other four bulk runs: `X-Spaw-Signature-V2` carries `t={unix seconds},v2={lowercase hex}`, the HMAC-SHA256 of `v2:{t}:{raw body}`, and a receiver should refuse a `t` more than five minutes from its own clock so a captured delivery cannot be replayed at it later. The original `X-Spaw-Signature`, over the body alone, is sent beside it until 1 March 2027. 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`.
### Reading a list before you start it
Send `preview: true` with the same body — or `csv` with a raw CSV instead of the item list — and the call answers `200` with `data.draft` instead of queueing anything: what the list was read as, which column filled which signal and whether a heading named it or we guessed, the first rows as parsed, the repeats, the balance, and `credits_upper_bound`. Nothing is charged. The mapping matters more here than anywhere else, because a row is up to four lookups: `credits_per_row` is the number of SIGNALS the columns feed, so a file whose columns only reach an email address and a phone number is priced at two a row rather than four.
`credits_upper_bound` is a CEILING, not a quote. Rows come back free for reasons no preview can know in advance — a leg that repeats inside the seven-day window, a verdict that is not charged for, a signal a row turns out not to carry — so a run usually costs less than the ceiling and never more.
Start it with `{"draft_id": }`, adding `mapping` to correct a column we read wrongly. Starting consumes the draft; one left unstarted is deleted, with the rows it holds, after `retention_hours`.
## Parameters
| Name | In | Type | Required | Description |
| --- | --- | --- | --- | --- |
| `Idempotency-Key` | header | string | no | 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 | Required | Description |
| --- | --- | --- | --- |
| `items` | object[] | no | 1 to 100,000 rows, each naming at least one of the four signals. Required unless the body carries `csv` with `preview: true`, or `draft_id`. |
| `items[].email` | string | null | no | At most 254 characters. |
| `items[].phone` | string | null | no | In E.164, or in national notation with a country. At most 64 characters. |
| `items[].ip` | string | null | no | An IPv4 or IPv6 address. |
| `items[].address` | string | object | null | no | A written address, or an object of the same separated fields `POST /api/v1/address` takes. Its own `country` wins over the item's. |
| `items[].country` | string | null | no | This row's country. It serves two legs — the region the phone number is read against and the country the postal address is measured against — and each leg ignores it when its own table does not cover it. At most 2 characters. |
| `country` | string | null | no | An ISO 3166-1 alpha-2 code applied to every row that does not name its own. It has to be known to the dialling plan or to the postal format table; a code neither knows is a `422` rather than a setting that quietly does nothing. At most 2 characters. |
| `preview` | boolean | no | Read the list and answer `data.draft` — what we made of it, the repeats, the ceiling on the cost and the balance — with nothing queued and nothing billed. Start it afterwards with `draft_id`. Default: . |
| `csv` | string | null | no | A raw CSV to read instead of the item list, with the column mapping answered back. Preview only, on purpose: one header row has to yield an email address, a phone number, an IP address and the address fields at once, and guessing at a customer's columns and billing them for the guess in the same request is what the preview exists to stop. At most 5,000,000 characters. |
| `draft_id` | integer | null | no | Start the run a preview answered with. The rows and the options come from the draft, so nothing else in the body applies except `mapping`. A draft can be started once; an `Idempotency-Key` still makes the call safe to retry. |
| `mapping` | object | null | no | Corrects the draft's column mapping, as column index to field — {"1": "email", "4": "address_line1"}. The fields are `email`, `phone`, `ip`, `country` and the address ones (`address`, `address_line1`, `address_line2`, `organization`, `dependent_locality`, `locality`, `administrative_area`, `postal_code`, `po_box`). It REPLACES the detected mapping rather than merging into it, so a column can be taken off a field as well as put on one. A mapping that feeds none of the four signals is refused: a country column on its own checks nothing. |
| `webhook_url` | string | null | no | An https URL to notify when the run finishes. |
## Example request
```bash
curl -X POST https://spaw.co/api/v1/consistency/bulk \
-H "Authorization: Bearer sk_live_…" \
-H "Content-Type: application/json" \
-d '{
"items": [
{
"email": "ada@acme.com",
"phone": "+442079460018",
"ip": "81.2.69.142",
"address": "10 Downing Street, London, SW1A 2AA",
"country": "GB"
},
{
"email": "grace@acme.com",
"ip": "81.2.69.143"
}
],
"country": "GB",
"webhook_url": "https://example.com/hooks/spaw"
}'
```
## Responses
### 202 — The job was queued.
```json
{
"success": true,
"data": {
"job": {
"id": 41,
"status": "queued",
"total": 2,
"duplicate_count": 0,
"processed": 0,
"flagged": 0,
"unflagged": 0,
"uncompared": 0,
"credits_used": 0,
"stopped_reason": null,
"cancel_requested": false,
"country": "GB",
"webhook_status": null,
"webhook_detail": null,
"created_at": "2026-09-10T10:12:44+00:00",
"finished_at": null,
"webhook_secret": "8fJ2…40 characters…Qk1"
}
}
}
```
### 200 — Either a preview — `preview: true` answers `data.draft` and queues nothing — or a retry with an `Idempotency-Key` already used for this exact request, which answers `data.job` with `Idempotent-Replayed: true`.
```json
{
"success": true,
"data": {
"job": {
"id": 41,
"status": "processing",
"total": 2,
"duplicate_count": 0,
"processed": 1,
"flagged": 1,
"unflagged": 0,
"uncompared": 0,
"credits_used": 3,
"stopped_reason": null,
"cancel_requested": false,
"country": "GB",
"webhook_status": null,
"webhook_detail": null,
"created_at": "2026-09-10T10:12:44+00:00",
"finished_at": null
}
}
}
```
### 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"
}
}
```
### 409 — The `Idempotency-Key` was already used for a different list, country or webhook URL.
```json
{
"success": false,
"error": {
"code": "IDEMPOTENCY_KEY_REUSED",
"message": "This Idempotency-Key was already used for a different request.",
"request_id": "req_01m1kgdm4xngzmbmff68g94w0c"
}
}
```
### 422 — The request body could not be validated; `error.errors` lists the fields.
```json
{
"success": false,
"error": {
"code": "VALIDATION_FAILED",
"message": "The email field is required.",
"errors": {
"email": [
"The email field is required."
]
},
"request_id": "req_01m1kgdm4xngzmbmff68g94w0c"
}
}
```
### 429 — Over 5 requests per second for the key. Retry after the limit resets.
```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
- `VALIDATION_FAILED` — https://spaw.co/docs/errors/VALIDATION_FAILED
- `IDEMPOTENCY_KEY_REUSED` — https://spaw.co/docs/errors/IDEMPOTENCY_KEY_REUSED
- `RATE_LIMITED` — https://spaw.co/docs/errors/RATE_LIMITED
- `KEY_SCOPE_DENIED` — https://spaw.co/docs/errors/KEY_SCOPE_DENIED
- `EMAIL_NOT_VERIFIED` — https://spaw.co/docs/errors/EMAIL_NOT_VERIFIED
---
Canonical page: https://spaw.co/docs/api/create-consistency-bulk-job · OpenAPI document: https://spaw.co/openapi.json · All endpoints: https://spaw.co/docs/api
---
# Poll a bulk consistency job
`GET /api/v1/consistency/bulk/{jobId}`
- Authentication: Secret API key as a bearer token
- Billing: Free.
- Group: Consistency
The job's current status and running counts. `status` moves from `queued` to `processing` and ends in `completed`, `failed`, or `cancelled`; `processed` and the three row counts advance as chunks finish. A job that belongs to another account answers `404`.
`flagged`, `unflagged` and `uncompared` count what the comparison did, and none of them is a verdict about the person a row describes — this product publishes no score, risk level or verdict, and will not. A row is `flagged` when at least one flag was raised on it, `unflagged` when at least two of its signals answered and none of them disagreed, and `uncompared` when fewer than two answered, so there was nothing to compare.
## Parameters
| Name | In | Type | Required | Description |
| --- | --- | --- | --- | --- |
| `jobId` | path | integer | yes | |
## Example request
```bash
curl https://spaw.co/api/v1/consistency/bulk/{jobId} \
-H "Authorization: Bearer sk_live_…"
```
## Responses
### 200 — The job.
```json
{
"success": true,
"data": {
"job": {
"id": 41,
"status": "completed",
"total": 2,
"duplicate_count": 0,
"processed": 2,
"flagged": 1,
"unflagged": 1,
"uncompared": 0,
"credits_used": 5,
"stopped_reason": null,
"cancel_requested": false,
"country": "GB",
"webhook_status": "delivered",
"webhook_detail": "200",
"created_at": "2026-09-10T10:12:44+00:00",
"finished_at": "2026-09-10T10:12:49+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"
}
}
```
### 429 — Over 5 requests per second for the key. Retry after the limit resets.
```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
- `RATE_LIMITED` — https://spaw.co/docs/errors/RATE_LIMITED
---
Canonical page: https://spaw.co/docs/api/get-consistency-bulk-job · OpenAPI document: https://spaw.co/openapi.json · All endpoints: https://spaw.co/docs/api
---
# Cancel a bulk consistency job
`POST /api/v1/consistency/bulk/{jobId}/cancel`
- Authentication: Secret API key as a bearer token
- Billing: Free.
- Group: Consistency
Asks a queued or running job to stop at its next checkpoint. Everything checked so far stays billed and stays in the result file. A job that already finished answers `409 JOB_ALREADY_FINISHED`.
## Parameters
| Name | In | Type | Required | Description |
| --- | --- | --- | --- | --- |
| `jobId` | path | integer | yes | |
## Example request
```bash
curl -X POST https://spaw.co/api/v1/consistency/bulk/{jobId}/cancel \
-H "Authorization: Bearer sk_live_…"
```
## Responses
### 202 — Cancellation was requested.
```json
{
"success": true,
"data": {
"job": {
"id": 41,
"status": "processing",
"total": 20000,
"duplicate_count": 12,
"processed": 4000,
"flagged": 260,
"unflagged": 3600,
"uncompared": 140,
"credits_used": 11200,
"stopped_reason": null,
"cancel_requested": true,
"country": null,
"webhook_status": null,
"webhook_detail": null,
"created_at": "2026-09-10T10:12:44+00:00",
"finished_at": null
}
}
}
```
### 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 job already finished.
```json
{
"success": false,
"error": {
"code": "JOB_ALREADY_FINISHED",
"message": "This job has already finished.",
"request_id": "req_01m1kgdm4xngzmbmff68g94w0c"
}
}
```
### 429 — Over 5 requests per second for the key. Retry after the limit resets.
```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
- `JOB_ALREADY_FINISHED` — https://spaw.co/docs/errors/JOB_ALREADY_FINISHED
- `RATE_LIMITED` — https://spaw.co/docs/errors/RATE_LIMITED
---
Canonical page: https://spaw.co/docs/api/cancel-consistency-bulk-job · OpenAPI document: https://spaw.co/openapi.json · All endpoints: https://spaw.co/docs/api
---
# Resume a bulk consistency job that ran out of credits
`POST /api/v1/consistency/bulk/{jobId}/resume`
- Authentication: Secret API key as a bearer token
- Billing: Free to call. The resumed run bills the legs it reaches exactly as the first one did, and anything answered in the last seven days is a free repeat.
- Group: Consistency
Runs a job that stopped when the credit balance ran out again, from the top of the list it was given, instead of asking you to upload it a second time. Every leg looked up in the last seven days comes back as a free repeat, so only the work the stopped run never reached is charged — including the legs of the ROW it stopped part-way through, which the result file already carries.
The job keeps its id, its webhook and its cumulative `credits_used`, which is the real spend on the account. `processed` and the three row counts restart at zero: the part files they were counted from are deleted when a run settles, so the resumed run rebuilds them.
Only a run whose `stopped_reason` is `insufficient_credits` and whose `status` has settled can be resumed; anything else answers `409 JOB_NOT_RESUMABLE`, and so does a second resume of a run that is already going again. A run whose list has passed out of the retention window answers `409 JOB_INPUT_EXPIRED`. An empty balance answers `402 INSUFFICIENT_CREDITS` rather than queueing a run that would stop again on the first row it never reached.
## Parameters
| Name | In | Type | Required | Description |
| --- | --- | --- | --- | --- |
| `jobId` | path | integer | yes | |
## Example request
```bash
curl -X POST https://spaw.co/api/v1/consistency/bulk/{jobId}/resume \
-H "Authorization: Bearer sk_live_…"
```
## Responses
### 202 — The list is queued again.
```json
{
"success": true,
"data": {
"job": {
"id": 41,
"status": "queued",
"total": 20000,
"duplicate_count": 12,
"processed": 0,
"flagged": 0,
"unflagged": 0,
"uncompared": 0,
"credits_used": 11200,
"stopped_reason": null,
"cancel_requested": false,
"country": null,
"webhook_status": null,
"webhook_detail": null,
"created_at": "2026-09-10T10:12:44+00:00",
"finished_at": null
}
}
}
```
### 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"
}
}
```
### 402 — The balance is empty. The lookup did not run.
```json
{
"success": false,
"error": {
"code": "INSUFFICIENT_CREDITS",
"message": "Your credit balance is empty.",
"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 is not in a state a resume can recover (`JOB_NOT_RESUMABLE`), or its list is no longer stored (`JOB_INPUT_EXPIRED`).
```json
{
"success": false,
"error": {
"code": "JOB_NOT_RESUMABLE",
"message": "Only a run that stopped because the credit balance ran out can be resumed, and only once it has finished settling.",
"request_id": "req_01m1kgdm4xngzmbmff68g94w0c"
}
}
```
### 429 — Over 5 requests per second for the key. Retry after the limit resets.
```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
- `JOB_NOT_RESUMABLE` — https://spaw.co/docs/errors/JOB_NOT_RESUMABLE
- `JOB_INPUT_EXPIRED` — https://spaw.co/docs/errors/JOB_INPUT_EXPIRED
- `INSUFFICIENT_CREDITS` — https://spaw.co/docs/errors/INSUFFICIENT_CREDITS
- `RATE_LIMITED` — https://spaw.co/docs/errors/RATE_LIMITED
---
Canonical page: https://spaw.co/docs/api/resume-consistency-bulk-job · OpenAPI document: https://spaw.co/openapi.json · All endpoints: https://spaw.co/docs/api
---
# Send a finished run's completion webhook again
`POST /api/v1/consistency/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: Consistency
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_consistency_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/consistency/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/consistency/bulk/41/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": 41,
"status": "completed",
"total": 20000,
"duplicate_count": 12,
"processed": 20000,
"flagged": 2400,
"unflagged": 16100,
"uncompared": 1500,
"credits_used": 56200,
"stopped_reason": null,
"cancel_requested": false,
"country": null,
"webhook_status": "pending",
"webhook_detail": null,
"created_at": "2026-09-10T10:12:44+00:00",
"finished_at": "2026-09-10T11:40:55+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-consistency-bulk-webhook · OpenAPI document: https://spaw.co/openapi.json · All endpoints: https://spaw.co/docs/api
---
# Download a bulk consistency job's results
`GET /api/v1/consistency/bulk/{jobId}/results`
- Authentication: Secret API key as a bearer token
- Billing: Free.
- Group: Consistency
The result file as CSV: one row per input row, in input order. A run created from an uploaded file carries that file's own columns first, then the four signals as submitted and the country they were measured against, then the comparison.
The comparison columns are `signals_requested` and `signals_answered` (`;`-joined, in the order the legs ran), `country_agreement`, `flags` (`;`-joined), one cell per comparison — `phone_address_country`, `ip_address_country`, `ip_phone_country`, `email_ip_disposable_datacenter`, `email_ip_disposable_anonymous`, `email_address_free_provider_mail_drop`, `email_phone_disposable` — and `distance_km`. Last come the few per-product fields those comparisons read, so a difference can be checked against the values that decided it, and `credits_used`.
**A comparison cell reads `agree`, `differ`, `not_compared`, `missing_value`, `low_confidence`, or nothing at all.** An empty cell means the comparison was never attempted, because the row did not carry both of its signals. The two withheld reasons are written in place of a bare `not_compared` because they mean different things and a spreadsheet has nowhere else to put them: `missing_value` is a field the product did not evaluate — which means "not evaluated" and never "no" — and `low_confidence` is an IP address whose country belongs to the network operator rather than to the person, a VPN exit, a relay or a datacenter egress. `distance_km` is evidence only and never raises a flag: there is no honest distance at which a person stops being themselves.
`signals_requested` against `signals_answered` is also where a row cut short says so: a run that stopped on an empty balance part-way through a row keeps the legs it had already paid for, and that row answers fewer signals than it asked for.
Add `?variant=flagged|unflagged|uncompared` to download only the rows a flag was raised on, the rows that were compared and raised none, or the rows too thin to compare; the default `full` returns everything. Available once the job is `completed`, or `cancelled` or `failed` with at least one processed row — those rows were billed, so they stay downloadable. A job still running answers `409 RESULTS_NOT_READY`, a cancelled job with nothing processed `409 JOB_CANCELLED`, and a failed job with nothing processed `409 JOB_FAILED`.
`format=json` answers the same rows, filtered the same way, as objects keyed by the result file's own header instead of a CSV attachment. It is streamed row by row exactly as the CSV is. A CSV cell carries no type, so only the two the writer encodes come back typed: an empty cell is `null` and `true`/`false` are booleans. Everything else is the string as written, and the columns of an uploaded file come back exactly as they were uploaded.
## Parameters
| Name | In | Type | Required | Description |
| --- | --- | --- | --- | --- |
| `jobId` | path | integer | yes | |
| `variant` | query | string | no | Restrict the file to the rows a flag was raised on, the rows compared with none, or the rows fewer than two signals answered for. Defaults to full. One of: full, flagged, unflagged, uncompared. |
| `format` | query | string | no | `csv` streams the file as an attachment; `json` streams the same rows as objects. One of: csv, json. Default: csv. |
## Example request
```bash
curl https://spaw.co/api/v1/consistency/bulk/{jobId}/results \
-H "Authorization: Bearer sk_live_…"
```
## Responses
### 200 — The result rows, as a CSV attachment or as streamed JSON.
```csv
email,phone,ip,address,country,signals_requested,signals_answered,country_agreement,flags,phone_address_country,ip_address_country,ip_phone_country,email_ip_disposable_datacenter,email_ip_disposable_anonymous,email_address_free_provider_mail_drop,email_phone_disposable,distance_km,email_deliverable,email_disposable,email_free_provider,phone_valid,phone_country,phone_is_disposable,ip_country,ip_risk_level,ip_is_datacenter,ip_is_anonymous,address_valid,address_country,address_is_mail_drop,credits_used
ada@acme.com,+442079460018,81.2.69.142,"10 Downing Street, London, SW1A 2AA",GB,ip;phone;address;email,ip;phone;address;email,agree,,agree,agree,agree,not_compared,not_compared,not_compared,not_compared,12.4,true,false,false,true,GB,false,GB,low,false,false,true,GB,false,3
```
### 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 job is still running (`RESULTS_NOT_READY`), or was cancelled or failed before any row was processed (`JOB_CANCELLED`, `JOB_FAILED`).
```json
{
"success": false,
"error": {
"code": "RESULTS_NOT_READY",
"message": "The results are not ready yet.",
"request_id": "req_01m1kgdm4xngzmbmff68g94w0c"
}
}
```
### 429 — Over 5 requests per second for the key. Retry after the limit resets.
```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
- `VALIDATION_FAILED` — https://spaw.co/docs/errors/VALIDATION_FAILED
- `RESULTS_NOT_READY` — https://spaw.co/docs/errors/RESULTS_NOT_READY
- `JOB_CANCELLED` — https://spaw.co/docs/errors/JOB_CANCELLED
- `JOB_FAILED` — https://spaw.co/docs/errors/JOB_FAILED
- `RATE_LIMITED` — https://spaw.co/docs/errors/RATE_LIMITED
---
Canonical page: https://spaw.co/docs/api/download-consistency-bulk-results · OpenAPI document: https://spaw.co/openapi.json · All endpoints: https://spaw.co/docs/api
---
# Check a business identifier against the registers
`POST /api/v1/entity`
- Authentication: Secret API key as a bearer token
- Billing: 1 credit when a register answers. A malformed identifier, one no register carries, an unsupported register and a register that is not installed are all free, and a 7-day repeat of the same identifier on the same account is free.
- Group: Entity
Reads a business identifier against the registers this server holds: a Legal Entity Identifier against the GLEIF Level 1 golden copy, or a UK company number against the Companies House free company data product. Both are bulk files synced locally, so no third party sits in the request path and nothing about the lookup leaves the server.
Send `identifier` with a `type` of `lei` or `company_number`. A company number needs the `country` whose register it belongs to, because company numbers are only unique inside their own register; `GB` is the register installed today. A LEI needs no country: the identifier is global by construction. A UK company number is zero-padded to eight characters before it is looked up, so `1234567` and `01234567` are the same company.
The answer gives the entity's legal name, whether it is still live (`status`, with the register's own wording in `status_detail`), the country and jurisdiction it was formed in, its category and legal form, and when it was registered. For a LEI it adds `checksum_valid` — the ISO 7064 MOD 97-10 check digit defined by ISO 17442, which catches a mistyped identifier and says nothing about whether it was ever issued — plus `registration_status` and `next_renewal_at`, because a LEI registration can lapse while the entity behind it is perfectly alive.
Where a LEI names its entry in a national register this server also holds, the two records are compared field by field. `agreements` quotes both sides of each comparison and `flags` names what differs: `name_differs_across_registers`, `status_differs_across_registers`, `dissolved_in_national_register`, `lei_registration_not_current`. Names are compared on their letters and digits with the company-form words removed, so only a real difference is reported. Where there is no counterpart, `agreements` is empty — an absent register is not a disagreement.
`sources` names every register behind the answer with the date of the copy. That matters here: the Companies House snapshot is republished monthly and can be up to five weeks behind the live register, while the GLEIF golden copy is published daily.
The product answers about organisations, not people. No registered address is stored or returned. A registered address is very often somebody's home, and an identifier alone does not say whether a company or a sole trader stands behind it, so the product holds none of them rather than some of them badly.
## Request body
| Field | Type | Required | Description |
| --- | --- | --- | --- |
| `identifier` | string | yes | The Legal Entity Identifier or company number to check. At most 64 characters. |
| `type` | string | yes | Which register the identifier belongs to. One of: lei, company_number. |
| `country` | string | null | no | The ISO 3166-1 alpha-2 country of the company register. Required when `type` is `company_number`; ignored for a LEI. At most 2 characters. |
## Example request
```bash
curl -X POST https://spaw.co/api/v1/entity \
-H "Authorization: Bearer sk_live_…" \
-H "Content-Type: application/json" \
-d '{
"identifier": "213800QILIUD4ROSUO03",
"type": "lei"
}'
```
## Responses
### 200 — The entity as the registers hold it; `found: false` with a `reason` for an identifier none of them could answer.
```json
{
"success": true,
"data": {
"identifier": "213800QILIUD4ROSUO03",
"identifier_type": "lei",
"found": true,
"reason": null,
"name": "ACME HOLDINGS LIMITED",
"status": "active",
"status_detail": "ACTIVE",
"country": "GB",
"jurisdiction": "GB",
"category": "GENERAL",
"legal_form": "H0PO",
"registered_at": "2016-04-21",
"record_updated_at": "2026-08-14",
"checksum_valid": true,
"registration_status": "ISSUED",
"next_renewal_at": "2027-04-21",
"other_register": "uk-companies-house",
"other_identifier": "01234567",
"flags": [],
"agreements": [
{
"field": "name",
"lei": "ACME HOLDINGS LIMITED",
"national_register": "ACME HOLDINGS LIMITED",
"result": "agree"
},
{
"field": "status",
"lei": "active",
"national_register": "active",
"result": "agree"
}
],
"sources": [
{
"dataset": "gleif-lei-level-1",
"version": "2026-09-08"
},
{
"dataset": "uk-companies-house-entities",
"version": "2026-09-01"
}
]
},
"meta": {
"credits_used": 1,
"credits_remaining": 999,
"cache_hit": false,
"request_id": "req_01m1kgdm4xngzmbmff68g94w0c"
}
}
```
### 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"
}
}
```
### 402 — The balance is empty. The lookup did not run.
```json
{
"success": false,
"error": {
"code": "INSUFFICIENT_CREDITS",
"message": "Your credit balance is empty.",
"request_id": "req_01m1kgdm4xngzmbmff68g94w0c"
}
}
```
### 422 — The request body could not be validated; `error.errors` lists the fields.
```json
{
"success": false,
"error": {
"code": "VALIDATION_FAILED",
"message": "The email field is required.",
"errors": {
"email": [
"The email field is required."
]
},
"request_id": "req_01m1kgdm4xngzmbmff68g94w0c"
}
}
```
### 429 — Over 5 requests per second for the key. Retry after the limit resets.
```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
- `VALIDATION_FAILED` — https://spaw.co/docs/errors/VALIDATION_FAILED
- `INSUFFICIENT_CREDITS` — https://spaw.co/docs/errors/INSUFFICIENT_CREDITS
- `RATE_LIMITED` — https://spaw.co/docs/errors/RATE_LIMITED
- `KEY_SCOPE_DENIED` — https://spaw.co/docs/errors/KEY_SCOPE_DENIED
- `EMAIL_NOT_VERIFIED` — https://spaw.co/docs/errors/EMAIL_NOT_VERIFIED
---
Canonical page: https://spaw.co/docs/api/verify-entity · OpenAPI document: https://spaw.co/openapi.json · All endpoints: https://spaw.co/docs/api
---
# Check up to 500 identifiers in one call
`POST /api/v1/entity/batch`
- Authentication: Secret API key as a bearer token
- Billing: Each identifier bills like a single lookup — 1 credit only when a register carried it, free for an unknown, malformed or unsupported identifier, free for a register that is not installed and free for a 7-day repeat — and the call stops cleanly where the balance ends.
- Group: Entity
Runs the single-identifier lookup for every item, in input order, under the exact single-lookup billing rules. Every answer is read from registers synced onto this server, so a full list makes no network call at all. `data.results[]` mirrors the single-identifier response per item and each item's `meta` carries `credits_used` and `cache_hit`.
An item is either a bare identifier string, which is read as a LEI because that is the only type needing no country, or an object with `identifier`, `type` and — for a company number — the `country` whose register it belongs to. Countries are only required per item, never for the list, because a list may mix registers.
The cap is 500, not the 1,000 the IP and phone batches take: those read indexes already held in memory, while each identifier here is a database read, and two when a LEI names a national-register counterpart. It is the same limit a monitored list of identifiers has, and a monitor run walks its list through this same lookup.
`meta.found` is the count this endpoint has that the other batches do not need. Business lookups bill only for an identifier a register actually carried, so a list of 500 unknown identifiers is processed in full and billed nothing, and `processed` on its own would imply a charge that was never made. `credits_used` is `found` less any identifier that was already looked up on this account inside the last seven days.
The list is walked in order and stops where the balance would run out: running out of credits mid-batch returns the paid partial results with `meta.stopped_reason: "insufficient_credits"` and `processed` lower than `requested`, never discarding billed work. Only a batch whose first lookup is refused answers the typed `402`. A key with a daily credit cap stops the list the same way once the cap is spent, reporting `meta.stopped_reason: "key_spend_cap"`; a call that starts with the cap already spent answers `429 KEY_SPEND_CAP_REACHED` and runs nothing.
A batch counts as one request against the rate limit. As with the single lookup, no registered address is stored or returned.
Every item carries `index`, its zero-based position in the list you sent, and `input`, the identifier exactly as you sent it — the answer carries the register's own spelling of it. A batch stopped early by `stopped_reason` answers only a prefix of the list, so those two are what line an answer up with the row it came from.
## Request body
| Field | Type | Required | Description |
| --- | --- | --- | --- |
| `identifiers` | string[] | yes | 1 to 500 identifiers. An item is a LEI string, or an object carrying its own type and country. |
| `identifiers[].identifier` | string | no | At most 64 characters. |
| `identifiers[].type` | string | no | One of: lei, company_number. |
| `identifiers[].country` | string | null | no | Required when `type` is `company_number`. At most 2 characters. |
## Example request
```bash
curl -X POST https://spaw.co/api/v1/entity/batch \
-H "Authorization: Bearer sk_live_…" \
-H "Content-Type: application/json" \
-d '{
"identifiers": [
"213800QILIUD4ROSUO03",
{
"identifier": "01234567",
"type": "company_number",
"country": "GB"
}
]
}'
```
## Responses
### 200 — One result per processed identifier, plus the batch totals.
```json
{
"success": true,
"data": {
"results": [
{
"index": 0,
"input": "213800QILIUD4ROSUO03",
"data": {
"identifier": "213800QILIUD4ROSUO03",
"identifier_type": "lei",
"found": true,
"reason": null,
"name": "ACME HOLDINGS LIMITED",
"status": "active",
"country": "GB",
"checksum_valid": true,
"registration_status": "ISSUED",
"flags": []
},
"meta": {
"credits_used": 1,
"cache_hit": false
}
},
{
"index": 1,
"input": "01234567",
"data": {
"identifier": "01234567",
"identifier_type": "company_number",
"found": true,
"reason": null,
"name": "ACME HOLDINGS LIMITED",
"status": "dissolved",
"status_detail": "Dissolved",
"country": "GB",
"flags": [
"dissolved_in_national_register"
]
},
"meta": {
"credits_used": 1,
"cache_hit": false
}
},
{
"index": 2,
"input": "5493001KJTIIGC8Y1R99",
"data": {
"identifier": "5493001KJTIIGC8Y1R99",
"identifier_type": "lei",
"found": false,
"reason": "unknown_identifier",
"name": null,
"checksum_valid": false,
"flags": []
},
"meta": {
"credits_used": 0,
"cache_hit": false
}
}
]
},
"meta": {
"requested": 3,
"processed": 3,
"found": 2,
"credits_used": 2,
"credits_remaining": 998,
"stopped_reason": null,
"request_id": "req_01m1kgdrtqdvwnks99vfgx2rcw"
}
}
```
### 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"
}
}
```
### 402 — The balance is empty. The lookup did not run.
```json
{
"success": false,
"error": {
"code": "INSUFFICIENT_CREDITS",
"message": "Your credit balance is empty.",
"request_id": "req_01m1kgdm4xngzmbmff68g94w0c"
}
}
```
### 422 — The request body could not be validated; `error.errors` lists the fields.
```json
{
"success": false,
"error": {
"code": "VALIDATION_FAILED",
"message": "The email field is required.",
"errors": {
"email": [
"The email field is required."
]
},
"request_id": "req_01m1kgdm4xngzmbmff68g94w0c"
}
}
```
### 429 — Over 5 requests per second for the key. Retry after the limit resets.
```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
- `VALIDATION_FAILED` — https://spaw.co/docs/errors/VALIDATION_FAILED
- `INSUFFICIENT_CREDITS` — https://spaw.co/docs/errors/INSUFFICIENT_CREDITS
- `KEY_SPEND_CAP_REACHED` — https://spaw.co/docs/errors/KEY_SPEND_CAP_REACHED
- `RATE_LIMITED` — https://spaw.co/docs/errors/RATE_LIMITED
- `KEY_SCOPE_DENIED` — https://spaw.co/docs/errors/KEY_SCOPE_DENIED
- `EMAIL_NOT_VERIFIED` — https://spaw.co/docs/errors/EMAIL_NOT_VERIFIED
---
Canonical page: https://spaw.co/docs/api/lookup-entity-batch · OpenAPI document: https://spaw.co/openapi.json · All endpoints: https://spaw.co/docs/api
---
# List business monitors
`GET /api/v1/entity/monitors`
- Authentication: Secret API key as a bearer token
- Billing: Free. Listing monitors costs nothing; only their runs are billed.
- Group: Entity
Every saved list of business identifiers on the account, newest first, 100 to a page. Each carries the summary of its last run, including `stopped_reason` when it did not finish cleanly: `insufficient_credits` or `key_spend_cap` where the run stopped part-way, `run_failed` where it died outright, and `run_failed_retrying` where it died and the schedule has been pulled in to check again — `retry_at` in the same block says when that becomes due, and the hourly sweep picks it up at or after that, so between 45 minutes and about an hour and three quarters later. A summary carrying a `stopped_reason` of either failure kind has NO counts in it at all: the run produced none, so read a missing count as unknown rather than as zero.
`meta.retention_days` says how long a list nobody runs is kept. A monitor in active use is never pruned — each run advances `last_run_at` — but one that has stopped being run is deleted along with the identifiers it stored, because an identifier can belong to a sole trader as easily as to a company and nothing in it says which.
## Example request
```bash
curl https://spaw.co/api/v1/entity/monitors \
-H "Authorization: Bearer sk_live_…"
```
## Responses
### 200 — The account's business monitors.
### 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"
}
}
```
### 429 — Over 5 requests per second for the key. Retry after the limit resets.
```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
- `RATE_LIMITED` — https://spaw.co/docs/errors/RATE_LIMITED
- `KEY_SCOPE_DENIED` — https://spaw.co/docs/errors/KEY_SCOPE_DENIED
- `EMAIL_NOT_VERIFIED` — https://spaw.co/docs/errors/EMAIL_NOT_VERIFIED
---
Canonical page: https://spaw.co/docs/api/list-entity-monitors · OpenAPI document: https://spaw.co/openapi.json · All endpoints: https://spaw.co/docs/api
---
# Save a business monitor
`POST /api/v1/entity/monitors`
- Authentication: Secret API key as a bearer token
- Billing: Each run bills each identifier exactly as a single lookup: 1 credit when a register answers, free when none does, and free for a 7-day repeat — so a weekly cadence over a monthly register costs almost nothing after the first run. A run that exhausts the balance stops and reports `stopped_reason`.
- Group: Entity
Save up to 500 business identifiers to be re-checked weekly or monthly. There is no daily cadence: the UK company file is republished monthly, so a daily run would bill for data that cannot have changed.
Each entry is an object of `identifier`, `type` (`lei` or `company_number`) and, for a company number, the `country` of its register. A bare string is read as a LEI, because that is the only identifier that needs no country.
The first run happens immediately and only sets the baseline — alerts start with the next one, so saving a list never emails you about what was already true when you saved it.
An email goes out when an entry that was fine last time is not any more: the national register now records it as dissolved, the register marks it inactive, its LEI registration has lapsed, or the two registers have stopped agreeing about it. An identifier no register could answer for is counted as `unverifiable` and never alerted on — that is a limit of the data here, not a fact about the company.
With a `webhook_url`, a finished run is POSTed there as `{ "event": "entity_monitor.run", "monitor": { "id", "name", "cadence", "last_run_at", "next_run_at" }, "summary": { … the same block as `last_summary` … }, "changes": [ … ] }`, where `changes` names the identifiers the alert email names. 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 | Required | Description |
| --- | --- | --- | --- |
| `name` | string | yes | At most 100 characters. |
| `identifiers` | object[] | yes | |
| `identifiers[].identifier` | string | yes | At most 64 characters. |
| `identifiers[].type` | string | no | One of: lei, company_number. Default: lei. |
| `identifiers[].country` | string | null | no | Required when type is company_number. At most 2 characters. |
| `cadence` | string | yes | One of: weekly, monthly. |
| `webhook_url` | string | null | no | 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 | no | 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
```bash
curl -X POST https://spaw.co/api/v1/entity/monitors \
-H "Authorization: Bearer sk_live_…" \
-H "Content-Type: application/json" \
-d '{
"name": "Key suppliers",
"identifiers": [
{
"identifier": "213800QILIUD4ROSUO03",
"type": "lei"
},
{
"identifier": "01234567",
"type": "company_number",
"country": "GB"
}
],
"cadence": "monthly"
}'
```
## Responses
### 201 — The saved monitor. Its first run is already queued.
### 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"
}
}
```
### 422 — The request body could not be validated; `error.errors` lists the fields.
```json
{
"success": false,
"error": {
"code": "VALIDATION_FAILED",
"message": "The email field is required.",
"errors": {
"email": [
"The email field is required."
]
},
"request_id": "req_01m1kgdm4xngzmbmff68g94w0c"
}
}
```
### 429 — Over 5 requests per second for the key. Retry after the limit resets.
```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
- `VALIDATION_FAILED` — https://spaw.co/docs/errors/VALIDATION_FAILED
- `RATE_LIMITED` — https://spaw.co/docs/errors/RATE_LIMITED
- `KEY_SCOPE_DENIED` — https://spaw.co/docs/errors/KEY_SCOPE_DENIED
- `EMAIL_NOT_VERIFIED` — https://spaw.co/docs/errors/EMAIL_NOT_VERIFIED
---
Canonical page: https://spaw.co/docs/api/create-entity-monitor · OpenAPI document: https://spaw.co/openapi.json · All endpoints: https://spaw.co/docs/api
---
# Get a business monitor
`GET /api/v1/entity/monitors/{monitorId}`
- Authentication: Secret API key as a bearer token
- Billing: Free.
- Group: Entity
One monitor with the status of every identifier from its last run.
## Parameters
| Name | In | Type | Required | Description |
| --- | --- | --- | --- | --- |
| `monitorId` | path | integer | yes | |
## Example request
```bash
curl https://spaw.co/api/v1/entity/monitors/{monitorId} \
-H "Authorization: Bearer sk_live_…"
```
## Responses
### 200 — The monitor and its per-identifier results.
### 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"
}
}
```
### 429 — Over 5 requests per second for the key. Retry after the limit resets.
```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
- `RATE_LIMITED` — https://spaw.co/docs/errors/RATE_LIMITED
- `KEY_SCOPE_DENIED` — https://spaw.co/docs/errors/KEY_SCOPE_DENIED
- `EMAIL_NOT_VERIFIED` — https://spaw.co/docs/errors/EMAIL_NOT_VERIFIED
---
Canonical page: https://spaw.co/docs/api/get-entity-monitor · OpenAPI document: https://spaw.co/openapi.json · All endpoints: https://spaw.co/docs/api
---
# Change a monitor
`PATCH /api/v1/entity/monitors/{monitorId}`
- Authentication: Secret API key as a bearer token
- Billing: Free. The change applies to the next run.
- Group: Entity
Changes a saved monitor in place: rename it, move it to another cadence, replace the identifiers, or point it at a webhook. Send only the fields you are changing; a body that names none of them answers `422` rather than quietly changing nothing, because that is the shape a misspelled field name takes.
Replacing `identifiers` replaces the list. What the last run recorded about the identifiers that stayed is kept, so their next run still compares against the answer they already had; what it recorded about the ones that are gone is deleted with them; and an entry that is new starts with no baseline, so its first run only sets one. A rename never touches a stored answer — they are keyed by the entry, never by the list.
Changing `cadence` neither skips a run nor starts a second one: a run already scheduled sooner than the new interval still happens, and a longer interval applies from the one after it. Nothing is queued by this call — the change applies to the next run, and `POST .../run` is there to have it now.
Sending a `webhook_url` that differs from the one stored mints a new signing secret and returns it once, here; sending the URL it already has keeps the secret it is signing with, and `null` removes the webhook. A monitor that belongs to another account answers `404`.
## Parameters
| Name | In | Type | Required | Description |
| --- | --- | --- | --- | --- |
| `monitorId` | path | integer | yes | The monitor's id from the list endpoint. |
## Request body
| Field | Type | Required | Description |
| --- | --- | --- | --- |
| `name` | string | no | At most 100 characters. |
| `identifiers` | string[] | no | |
| `cadence` | string | no | One of: weekly, monthly. |
| `webhook_url` | string | null | no | At most 2,048 characters. |
| `webhook_events` | string | no | One of: changes, every_run. |
## Example request
```bash
curl -X PATCH https://spaw.co/api/v1/entity/monitors/41 \
-H "Authorization: Bearer sk_live_…" \
-H "Content-Type: application/json" \
-d '{
"name": "Key suppliers (2026)",
"cadence": "weekly"
}'
```
## Responses
### 200 — The monitor as it now stands.
### 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"
}
}
```
### 422 — The request body could not be validated; `error.errors` lists the fields.
```json
{
"success": false,
"error": {
"code": "VALIDATION_FAILED",
"message": "The email field is required.",
"errors": {
"email": [
"The email field is required."
]
},
"request_id": "req_01m1kgdm4xngzmbmff68g94w0c"
}
}
```
### 429 — Over 5 requests per second for the key. Retry after the limit resets.
```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
- `VALIDATION_FAILED` — https://spaw.co/docs/errors/VALIDATION_FAILED
- `RATE_LIMITED` — https://spaw.co/docs/errors/RATE_LIMITED
- `KEY_SCOPE_DENIED` — https://spaw.co/docs/errors/KEY_SCOPE_DENIED
- `EMAIL_NOT_VERIFIED` — https://spaw.co/docs/errors/EMAIL_NOT_VERIFIED
---
Canonical page: https://spaw.co/docs/api/update-entity-monitor · OpenAPI document: https://spaw.co/openapi.json · All endpoints: https://spaw.co/docs/api
---
# Delete a business monitor
`DELETE /api/v1/entity/monitors/{monitorId}`
- Authentication: Secret API key as a bearer token
- Billing: Free.
- Group: Entity
Stop monitoring the list. The identifiers and results it stored are deleted with it.
## Parameters
| Name | In | Type | Required | Description |
| --- | --- | --- | --- | --- |
| `monitorId` | path | integer | yes | |
## Example request
```bash
curl -X DELETE https://spaw.co/api/v1/entity/monitors/{monitorId} \
-H "Authorization: Bearer sk_live_…"
```
## Responses
### 200 — The monitor is gone.
### 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"
}
}
```
### 429 — Over 5 requests per second for the key. Retry after the limit resets.
```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
- `RATE_LIMITED` — https://spaw.co/docs/errors/RATE_LIMITED
- `KEY_SCOPE_DENIED` — https://spaw.co/docs/errors/KEY_SCOPE_DENIED
- `EMAIL_NOT_VERIFIED` — https://spaw.co/docs/errors/EMAIL_NOT_VERIFIED
---
Canonical page: https://spaw.co/docs/api/delete-entity-monitor · OpenAPI document: https://spaw.co/openapi.json · All endpoints: https://spaw.co/docs/api
---
# Run a business monitor now
`POST /api/v1/entity/monitors/{monitorId}/run`
- Authentication: Secret API key as a bearer token
- Billing: Billed per identifier exactly like a single lookup, with 7-day repeats free.
- Group: Entity
Re-check the list now instead of waiting for the schedule. The run is queued and answers 202; poll the monitor for its results.
## Parameters
| Name | In | Type | Required | Description |
| --- | --- | --- | --- | --- |
| `monitorId` | path | integer | yes | |
## Example request
```bash
curl -X POST https://spaw.co/api/v1/entity/monitors/{monitorId}/run \
-H "Authorization: Bearer sk_live_…"
```
## Responses
### 202 — The run is queued.
### 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"
}
}
```
### 429 — Over 5 requests per second for the key. Retry after the limit resets.
```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
- `RATE_LIMITED` — https://spaw.co/docs/errors/RATE_LIMITED
- `KEY_SCOPE_DENIED` — https://spaw.co/docs/errors/KEY_SCOPE_DENIED
- `EMAIL_NOT_VERIFIED` — https://spaw.co/docs/errors/EMAIL_NOT_VERIFIED
---
Canonical page: https://spaw.co/docs/api/run-entity-monitor · OpenAPI document: https://spaw.co/openapi.json · All endpoints: https://spaw.co/docs/api
---
# Send a test webhook
`POST /api/v1/webhooks/test`
- Authentication: Secret API key as a bearer token
- Billing: Free. A test delivery costs no credits and is not a lookup. Limited to 10 requests a minute for the key: it is a debugging tool, not a way to generate traffic.
- Group: Webhooks
Posts one sample payload to a URL you name, signed exactly as a real delivery is, and answers with the secret it signed with, both signatures it sent, and the exact bytes all of them were computed over. It is how a signature check gets written and confirmed before a paid run depends on it: until this endpoint existed, the first signed request you ever saw arrived when a bulk job you had queued and paid for happened to finish.
`event` is one of the names this API documents: `bulk_email_job.finished`, `bulk_phone_job.finished`, `bulk_ip_job.finished`, `bulk_address_job.finished`, `bulk_consistency_job.finished`, `email.settled`, `email_monitor.run`, `phone_monitor.run`, `ip_monitor.run`, `address_monitor.run` and `entity_monitor.run`. The body has the shape that event really carries, with fixed sample values and one addition: `test` is `true`. Where an event wraps a whole lookup result — `email.settled` does — the sample carries a handful of that result's fields rather than all of them, because what a signature check needs to see is the envelope and the exact bytes, and a full result copied into this document would be one more thing to drift. That field is there because a receiver that cannot compute an HMAC — most low-code webhook triggers cannot — still has to be able to tell a test delivery from a real one. Nothing else about the shape differs, and nothing in the body comes from your request.
The delivery runs through the same signer every other webhook on the platform runs through, so it behaves the same way: HTTPS only, both signature headers — the timestamped `X-Spaw-Signature-V2` (`t={unix seconds},v2={lowercase hex}`, the HMAC-SHA256 of `v2:{t}:{raw body}`) and the original `X-Spaw-Signature` over the body alone, which is sent until 1 March 2027 — a ten-second timeout, three attempts on a connection error or a 5xx answer, and no redirect followed — a `3xx` answer is reported as the failure it is, because a redirected POST arrives as a GET with no body. A URL whose host resolves onto a private or reserved address is never posted to at all: `delivered` is false and `detail` reads `Refused: private address`.
`status_code` is what your endpoint answered, or `null` when it answered nothing. What it answered in its own body is never read and never returned. The `secret` is minted for this one call and stored nowhere — it is not, and never will be, a real job's or monitor's signing secret, which is shown once at creation and never again.
## Request body
| Field | Type | Required | Description |
| --- | --- | --- | --- |
| `url` | string | yes | The HTTPS URL to post to. Plain http is refused, as it is everywhere a webhook URL is accepted. At most 2,048 characters. |
| `event` | string | yes | Which documented payload to send. One of: bulk_email_job.finished, bulk_phone_job.finished, bulk_ip_job.finished, bulk_address_job.finished, bulk_consistency_job.finished, email.settled, email_monitor.run, phone_monitor.run, ip_monitor.run, address_monitor.run, entity_monitor.run. |
## Example request
```bash
curl -X POST https://spaw.co/api/v1/webhooks/test \
-H "Authorization: Bearer sk_live_…" \
-H "Content-Type: application/json" \
-d '{
"url": "https://hooks.example.com/spaw",
"event": "bulk_email_job.finished"
}'
```
## Responses
### 200 — The delivery was attempted, and this is everything about it.
```json
{
"success": true,
"data": {
"delivered": true,
"status_code": 200,
"detail": "HTTP 200",
"secret": "3nQ8Zk2xVb7Lp1sTfY6wR4mJcH0aD5gU9eKqNvXz",
"signature": "45f6705121de705be218247b0f31a49d895c46d2e9f9d38d150bec2f08b629b9",
"timestamped_signature": "t=1757502000,v2=6a0e5f0f6bd0ba9a4bd6c9bbd0c0b5b6b1a1d0f04a2b9a02de3b1a3d0c4b6f21",
"timestamp": 1757502000,
"body": "{\"test\":true,\"event\":\"bulk_email_job.finished\",\"job\":{\"id\":512,\"status\":\"completed\",\"total\":2,\"processed\":2,\"deliverable\":1,\"risky\":0,\"undeliverable\":1,\"credits_used\":1,\"stopped_reason\":null,\"finished_at\":\"2026-09-03T10:14: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"
}
}
```
### 422 — The request body could not be validated; `error.errors` lists the fields.
```json
{
"success": false,
"error": {
"code": "VALIDATION_FAILED",
"message": "The email field is required.",
"errors": {
"email": [
"The email field is required."
]
},
"request_id": "req_01m1kgdm4xngzmbmff68g94w0c"
}
}
```
### 429 — Over 5 requests per second for the key. Retry after the limit resets.
```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
- `VALIDATION_FAILED` — https://spaw.co/docs/errors/VALIDATION_FAILED
- `RATE_LIMITED` — https://spaw.co/docs/errors/RATE_LIMITED
- `EMAIL_NOT_VERIFIED` — https://spaw.co/docs/errors/EMAIL_NOT_VERIFIED
---
Canonical page: https://spaw.co/docs/api/send-test-webhook · OpenAPI document: https://spaw.co/openapi.json · All endpoints: https://spaw.co/docs/api
---
# Verdict reasons, phone reasons and mailbox-check details
Every Spaw email verification answer carries `deliverable` (deliverable, risky, or undeliverable) and, when the verdict is not deliverable, a machine-readable `reason`. `smtp_reason` adds the detail behind the mailbox handshake. Deliverable and risky answers cost 1 credit, except an unverified one, which is free. Undeliverable verdicts are free too; the ones that needed a mailbox probe count against an email fair use of max(500, your billed email answers) in a rolling 30 days, past which each costs 1 credit. Repeats of a billed address are free for 7 days. Each value is documented at https://spaw.co/docs/reasons/{value}.
A phone validation answer carries `valid` and, when it is false, a `reason` of its own (missing_country, invalid_country_code, too_short, too_long, not_a_number, invalid_length, invalid_number, unassigned_block, emergency_number, short_number). Invalid numbers are always free; the entries below with no verdict are the phone reasons.
## `invalid_syntax`: the input does not parse as an email address
Even after Spaw strips display names, mailto: links, quotes and invisible characters, what is left is not a syntactically valid address, so nothing else was checked.
- Appears in: `reason`
- Verdict: undeliverable
- Billing: Free. Invalid input never costs a credit.
Spaw first pulls the address out of whatever was pasted: a display name such as Mia K , a mailto: link, spreadsheet quotes, trailing punctuation, zero-width and non-breaking characters are all removed. What remains is parsed against RFC 5322. invalid_syntax means that even after that cleanup there is no address to check: a missing @, a domain without a top-level label, spaces inside the username, or a bare word.
Because no domain could be extracted, every other field in the response is null, syntax_valid is false, and the risk score is fixed at 100.
**What to do.** Fix the input before retrying. When the value comes from a form, show the user the exact value that was checked (it is echoed in the email field) so a paste error is easy to spot.
```json
{
"email": "mia@acme",
"deliverable": "undeliverable",
"reason": "invalid_syntax",
"syntax_valid": false,
"risk_score": 100
}
```
Reference: https://spaw.co/docs/reasons/invalid_syntax
## `invalid_local_part`: the username breaks the rules its own provider enforces
The part before the @ cannot exist at the provider that would host the mailbox, so the address is rejected before any mailbox probe is spent on it.
- Appears in: `reason`
- Verdict: undeliverable
- Billing: Free. An undeliverable verdict decided without a mailbox probe never costs a credit.
Large mailbox providers publish rules for usernames: Gmail allows letters, digits and dots up to 30 characters, Outlook, Yahoo, iCloud, AOL and Proton each have their own character sets and length limits. An address that violates the rule of the provider behind its domain cannot exist, and probing it would only waste a handshake.
Spaw applies character-set and maximum-length rules only. Minimum lengths are deliberately not enforced, because legacy accounts predate them.
**What to do.** Treat it like a typo in the username. If the address came from a signup form, ask the user to re-enter it; on an imported list, drop it.
```json
{
"email": "m!a@gmail.com",
"deliverable": "undeliverable",
"reason": "invalid_local_part",
"mx_provider": "google",
"smtp_checked": false
}
```
Reference: https://spaw.co/docs/reasons/invalid_local_part
## `no_mx_records`: the domain has no mail servers at all
Live DNS returned no MX record and no A or AAAA record for the domain, so no host could accept mail for it.
- Appears in: `reason`
- Verdict: undeliverable
- Billing: Free. An undeliverable verdict decided without a mailbox probe never costs a credit.
A domain that publishes neither MX records nor an address record cannot receive email by any route. In practice that means a misspelled domain, a domain that was never set up for email, or one that has lapsed.
The lookup stops before the mailbox handshake: smtp_checked is false and mailbox_exists is null. When a close, real domain exists, did_you_mean carries the suggested correction, verified with its own MX check.
**What to do.** Show did_you_mean when it is present. Otherwise treat the address as unusable and do not retry automatically: DNS answers are cached and a fresh result will not appear for hours.
```json
{
"email": "mia@acme-corp.cmo",
"deliverable": "undeliverable",
"reason": "no_mx_records",
"mx_found": false,
"did_you_mean": "mia@acme-corp.com"
}
```
Reference: https://spaw.co/docs/reasons/no_mx_records
## `null_mx`: the domain declares that it never accepts mail
The domain publishes a null MX record, the standard way of saying it does not receive email.
- Appears in: `reason`
- Verdict: undeliverable
- Billing: Free. An undeliverable verdict decided without a mailbox probe never costs a credit.
RFC 7505 lets a domain publish a single MX record pointing at "." with priority 0, the null MX, to state explicitly that it does not receive email. Website-only domains, brand-protection domains and parked domains use it.
It is a deliberate, authoritative statement, so Spaw does not try the address-record fallback and does not probe. mx_found is false.
**What to do.** Reject the address. There is no correction to suggest, and nothing changes unless the domain owner removes the record.
```json
{
"email": "hello@brand-holding.example",
"deliverable": "undeliverable",
"reason": "null_mx",
"mx_found": false
}
```
Reference: https://spaw.co/docs/reasons/null_mx
## `mx_unresolvable`: the mail servers the domain points at do not exist
The domain publishes MX records, but every target fails to resolve or resolves to an address that cannot receive mail from the internet.
- Appears in: `reason`
- Verdict: undeliverable
- Billing: Free. An undeliverable verdict decided without a mailbox probe never costs a credit.
Every MX target either does not resolve, resolves to a loopback or private address (127.0.0.1, 10.x, 192.168.x), or resolves to nothing routable. Mail sent there bounces at the first hop. It is common on abandoned domains and on domains whose DNS was migrated without the mail records.
Spaw reports mx_found false and skips the handshake. Compare mx_unreachable, the smtp_reason for hosts that resolve but cannot be reached.
**What to do.** Treat the address as undeliverable. If it is your own domain, fix the MX targets; the answer refreshes once DNS caches expire.
```json
{
"email": "ops@dead-mail.example",
"deliverable": "undeliverable",
"reason": "mx_unresolvable",
"mx_found": false
}
```
Reference: https://spaw.co/docs/reasons/mx_unresolvable
## `mailbox_not_found`: the mail server rejected the mailbox during the handshake
The domain accepts mail, but at the RCPT TO step the server answered that this mailbox does not exist. No message was sent.
- Appears in: `reason`
- Verdict: undeliverable
- Billing: Free within your email fair use: undeliverable answers that needed a mailbox probe cost nothing up to max(500, your billed email answers) in a rolling 30 days, then 1 credit each, with meta.fair_use_billed true. A billed answer repeats free for 7 days.
The SMTP conversation reached the recipient check and the server rejected it, typically with a 550 5.1.1 "user unknown" reply. The handshake ends there; no DATA is ever transmitted and nothing lands in an inbox or a spam folder.
The verdict is undeliverable, mailbox_exists is false, and smtp_reason carries the detail, usually no_mailbox. The address is also added to your account's suppression list, so batch, bulk and monitor runs answer it for free until a later verdict or a reported delivery clears it; once it is untouched for 90 days those runs verify it afresh instead of trusting it.
**What to do.** Remove the address from the list. A confirmed missing mailbox is the single strongest bounce predictor, and sending to it damages sender reputation.
```json
{
"email": "nobody@acme.com",
"deliverable": "undeliverable",
"reason": "mailbox_not_found",
"mailbox_exists": false,
"smtp_checked": true,
"smtp_reason": "no_mailbox"
}
```
Reference: https://spaw.co/docs/reasons/mailbox_not_found
## `mailbox_disabled`: the mailbox exists but its provider has disabled it
The server recognised the address but reported the account as disabled, suspended or deactivated. Mail to it is rejected.
- Appears in: `reason`
- Verdict: undeliverable
- Billing: Free within your email fair use: undeliverable answers that needed a mailbox probe cost nothing up to max(500, your billed email answers) in a rolling 30 days, then 1 credit each, with meta.fair_use_billed true. A billed answer repeats free for 7 days.
Unlike a mailbox that was never created, a disabled mailbox was real once: a closed account, a suspended user, a deprovisioned employee. The provider still rejects mail for it, so the verdict is undeliverable.
smtp_reason is disabled. Like mailbox_not_found, the address joins your suppression list.
**What to do.** Remove the address. Disabled accounts rarely come back, and when they do the owner will typically subscribe again.
```json
{
"email": "former.employee@acme.com",
"deliverable": "undeliverable",
"reason": "mailbox_disabled",
"smtp_reason": "disabled",
"mailbox_exists": false
}
```
Reference: https://spaw.co/docs/reasons/mailbox_disabled
## `likely_typo`: the domain is a known misspelling of a real mail provider
The domain is on Spaw's curated map of typo-squats such as gmail.con or hotmial.com. Mail may be accepted there, just not by your customer.
- Appears in: `reason`
- Verdict: risky
- Risk weight: +60
- Billing: 1 credit, like every deliverable or risky answer. Repeats of the same address are free for 7 days.
Spaw keeps a curated map of typo-squat domains, each paired with the domain the user meant. Many typo domains are registered by squatters and even accept mail, which is exactly why they are dangerous: the message is delivered, only not to the person who signed up.
The verdict is risky rather than undeliverable because mail may technically be accepted, and did_you_mean contains the corrected address. A typo-squat that also sits on the disposable list is reported as likely_typo, because the correction is the more useful explanation.
**What to do.** Offer the correction in the form and let the user confirm it. On imported lists, replace the address with did_you_mean only when you can confirm with the contact; otherwise drop it.
```json
{
"email": "mia@gmail.con",
"deliverable": "risky",
"reason": "likely_typo",
"did_you_mean": "mia@gmail.com",
"risk_score": 60
}
```
Reference: https://spaw.co/docs/reasons/likely_typo
## `parked_domain`: the domain is parked or for sale, and still accepts mail
The domain's nameservers belong to a parking or for-sale service, yet it has usable mail servers. Nobody runs mailboxes there; a server that accepts mail for a parked domain is collecting it.
- Appears in: `reason`
- Verdict: risky
- Risk weight: +60
- Billing: 1 credit, like every deliverable or risky answer. Repeats of the same address are free for 7 days.
Parking services (Sedo, ParkingCrew, Bodis, Afternic, Dan and their kind) hold domains that are for sale or idle, and most of them accept no mail at all — those answer no_mx_records. A parked domain that does publish working mail servers is the exception worth naming: the mail is not going to a person who signed up, it is going to whoever collects it.
The check reads the domain's NS records against a curated list of parking nameservers, only once the domain has passed the mail-server check, so the flag is null on a domain with no MX. A registrar's default DNS is never on the list, because real sites run on it too. parked is true, the verdict is risky, and the weight of 60 puts the address in the high band by itself.
**What to do.** Treat the address as unusable for a signup: ask for another one. On an imported list, drop it; a domain that was a real company and is parked now has lost its people.
```json
{
"email": "mia@old-brand.example",
"deliverable": "risky",
"reason": "parked_domain",
"parked": true,
"mx_found": true,
"risk_score": 60
}
```
Reference: https://spaw.co/docs/reasons/parked_domain
## `disposable`: the domain belongs to a disposable, temporary email provider
The domain, or the mail infrastructure behind its MX hosts, is on the open disposable-provider list. The inbox exists for minutes or hours.
- Appears in: `reason`
- Verdict: risky
- Risk weight: +80
- Billing: 1 credit, like every deliverable or risky answer. Repeats of the same address are free for 7 days.
Spaw syncs the open disposable-provider list weekly and extends it with local overrides, and it also matches the operators behind a domain's MX hosts, so a fresh burner domain on a known disposable backend is caught before the list catches up. Whoever signed up with a disposable address does not expect to hear from you again.
Disposable domains are never sent to the mailbox probe, so smtp_checked is false. The risk weight of 80 puts the address in the high band by itself.
**What to do.** For signups, block the address or ask for a different one. For existing lists, do not send: even when the message is accepted nobody reads it, and these domains attract spam-trap conversions.
```json
{
"email": "x9@mailinator.com",
"deliverable": "risky",
"reason": "disposable",
"disposable": true,
"smtp_checked": false,
"risk_score": 90
}
```
Reference: https://spaw.co/docs/reasons/disposable
## `mx_blocklisted`: the domain's mail is served from a network on the threat blocklists
At least one of the domain's MX hosts resolves into a netblock or an autonomous system that the Spamhaus DROP lists or the abuse.ch botnet list name. Mail served from there is a burner-domain signal the disposable list has not caught yet.
- Appears in: `reason`
- Verdict: risky
- Risk weight: +60
- Billing: 1 credit, like every deliverable or risky answer. Repeats of the same address are free for 7 days.
The same blocklist feeds the IP product installs — Spamhaus DROP and ASN-DROP, which name hijacked and criminal netblocks and operators, and the abuse.ch botnet list — are read for the addresses the domain's mail hosts resolve to. No new licence and no new download: it is the is_blocklisted answer of the IP endpoint, asked about the mail servers. A domain whose mail lives on such a network is almost never a company somebody works at.
mx_blocklisted is true, the verdict is risky, and the weight of 60 puts the address in the high band by itself. false is a checked negative; null means the domain has no usable mail servers, the hosts could not be resolved, or no blocklist feed is installed on this server. The mailbox handshake still runs, so mailbox_exists can be true.
**What to do.** Refuse the address on a signup form and ask for another; on a list, drop it. A confirmed mailbox on such a network is not reassurance — it is what a spam operation looks like.
```json
{
"email": "promo@bulk-sender.example",
"deliverable": "risky",
"reason": "mx_blocklisted",
"mx_blocklisted": true,
"mx_network_type": "hosting",
"risk_score": 60
}
```
Reference: https://spaw.co/docs/reasons/mx_blocklisted
## `implicit_mx`: the domain has no MX record and relies on the address-record fallback
No MX record exists; by the letter of RFC 5321 mail would be delivered to the host at the domain's A record, which is almost never a mail server today.
- Appears in: `reason`
- Verdict: risky
- Risk weight: +30
- Billing: 1 credit, like every deliverable or risky answer. Repeats of the same address are free for 7 days.
RFC 5321 says that a domain without MX records receives mail at its A or AAAA address. Real mail domains stopped relying on that decades ago: a web server answering on port 80 does not accept mail. Spaw reports mx_found true and mx_implicit true, and flags the address as risky.
The mailbox handshake still runs. If the host really does accept mail, the probe can confirm or reject the mailbox and the verdict follows that answer.
**What to do.** Treat the address as probably dead unless the handshake confirmed the mailbox. On B2B lists it usually marks a company that lost its mail setup or a domain used only for a website.
```json
{
"email": "info@web-only.example",
"deliverable": "risky",
"reason": "implicit_mx",
"mx_found": true,
"mx_implicit": true
}
```
Reference: https://spaw.co/docs/reasons/implicit_mx
## `mailbox_full`: the mailbox is over its storage quota
The server accepted the address but answered that the mailbox cannot take more mail right now. The account is real and may be read again once space is cleared.
- Appears in: `reason`, `smtp_reason`
- Verdict: risky
- Risk weight: +20
- Billing: 1 credit, like every deliverable or risky answer. Repeats of the same address are free for 7 days.
The handshake reached the recipient check and the server replied with a "mailbox full" class response (a 452 or 552). The mailbox exists, it just has no room, which is why the verdict is risky rather than undeliverable.
As a reason it sets the verdict. As an smtp_reason it explains a probe result on a lookup that another signal may already have settled; the same value appears in both fields for the same address.
**What to do.** Keep the address but hold the send and retry in a few days. For transactional mail that matters, use another channel meanwhile.
```json
{
"email": "mia@acme.com",
"deliverable": "risky",
"reason": "mailbox_full",
"smtp_reason": "mailbox_full",
"mailbox_exists": true
}
```
Reference: https://spaw.co/docs/reasons/mailbox_full
## `no_reply`: a no-reply address: mail to it is discarded by design
The username is a no-reply shape — noreply, no-reply, donotreply, do-not-reply, mailer-daemon, bounces and their spellings. Such an address sends and never reads; whatever you send there goes to nobody.
- Appears in: `reason`
- Verdict: risky
- Risk weight: +20
- Billing: 1 credit, like every deliverable or risky answer. Repeats of the same address are free for 7 days.
A no-reply address is the sender of automated mail, not a mailbox anybody opens: the provider either discards inbound mail to it or lets it pile up unread. It is recognised by pattern rather than by the role list alone, so a spelling the list lacks is still caught, and it forces role true with role_type no_reply.
The mailbox is still probed and often exists, which is exactly the trap: mailbox_exists true and nobody reading. The verdict is risky with reason no_reply, and the weight is added on top of the role weight — 30 for the role and 20 for the no-reply, 50 in all, the medium band.
**What to do.** Never send to it. On a signup form ask for another address; on a list, drop it, since no person signed up with it and no reply will ever come from it.
```json
{
"email": "noreply@acme.com",
"deliverable": "risky",
"reason": "no_reply",
"role": true,
"role_type": "no_reply",
"risk_score": 50
}
```
Reference: https://spaw.co/docs/reasons/no_reply
## `role`: a shared role inbox such as info@ or support@ rather than a person
The username is on the open list of role addresses. The inbox is shared, monitored unevenly, and rarely read by the person who filled in your form.
- Appears in: `reason`
- Verdict: risky
- Risk weight: +30
- Billing: 1 credit, like every deliverable or risky answer. Repeats of the same address are free for 7 days.
The local part matches the open role-address list (info, support, sales, admin, billing and around a thousand more), synced weekly. Role inboxes belong to a function rather than a person, get forwarded and filtered unpredictably, and carry higher complaint rates. role_type says which function: postmaster (the RFC 2142 family), support, sales, info, admin, billing, marketing, careers, legal, or other for a listed username outside those families; a no-reply shape gets its own reason, no_reply.
The mailbox is still probed, so mailbox_exists can be true for a role address; the verdict stays risky.
**What to do.** Accept role addresses on B2B forms where a shared inbox is expected, but keep them out of marketing sends and personalised sequences.
```json
{
"email": "support@acme.com",
"deliverable": "risky",
"reason": "role",
"role": true,
"risk_score": 30
}
```
Reference: https://spaw.co/docs/reasons/role
## `recycled_mailbox`: the mailbox exists now, but once answered that it did not
The handshake confirmed the mailbox today, and an earlier handshake had it rejected as non-existent. A mailbox that was dead and lives again is the shape of a recycled address — the spam-trap kind included.
- Appears in: `reason`
- Verdict: risky
- Risk weight: +30
- Billing: 1 credit, like every deliverable or risky answer. Repeats of the same address are free for 7 days.
Mailbox providers and blocklist operators turn abandoned addresses into traps: an address that bounced for a year starts accepting mail again, and whoever still sends to it is a sender who never cleans a list. Spaw remembers a definitive "no such mailbox" answer for a year under a keyed hash of the address, and when a later probe says the mailbox exists it reports previously_invalid true with the date in previously_invalid_at.
The verdict is risky with reason recycled_mailbox and the weight of 30, the medium band on its own; mailbox_exists is true, because it is. previously_invalid is false for a confirmed mailbox with no such history and null whenever the mailbox is not confirmed. The memory is erased with the address on an erasure request.
**What to do.** Do not send from a list-cleaning run. A recycled address is a real mailbox that may be a trap, so treat it as you would a catch-all: hold it, and only send once the person has re-engaged through another channel.
```json
{
"email": "former.employee@acme.com",
"deliverable": "risky",
"reason": "recycled_mailbox",
"mailbox_exists": true,
"previously_invalid": true,
"previously_invalid_at": "2026-03-02",
"risk_score": 30
}
```
Reference: https://spaw.co/docs/reasons/recycled_mailbox
## `catch_all`: the server accepts every address at the domain, so the mailbox cannot be confirmed
A catch-all (accept-all) server answers OK to any recipient, including ones that do not exist, so the handshake proves nothing about this particular mailbox.
- Appears in: `reason`
- Verdict: risky
- Risk weight: +30
- Billing: 1 credit, like every deliverable or risky answer. Repeats of the same address are free for 7 days.
Corporate domains behind Proofpoint or Mimecast, and many Microsoft 365 tenants, are configured to accept all recipients and sort them out later. Spaw remembers a catch-all answer per domain for seven days and does not spend further probes on that domain. Behind a security gateway (mx_provider_type security_gateway) the catch-all is the gateway's design rather than a squat, so the score carries catch_all_gateway at 15 instead of catch_all at 30; the reason is catch_all either way.
mailbox_exists stays null. mailbox_confidence gives a recomputable 0 to 100 estimate built from the domain's DMARC and SPF, its provider, registration age, the shape of the username, whether that shape follows the naming convention seen in the domain's confirmed deliveries, and the delivery feedback reported for the domain. A relay alias stays at 50 and only a bounce rate of 50% or more or a plus-tag lowers it, because the domain's records describe the relay operator.
**What to do.** Send in moderation and report outcomes through the feedback endpoint: your own delivered reports turn later lookups of that address into confirmed mailboxes, and bounces feed the confidence score for the whole domain.
```json
{
"email": "mia@acme.com",
"deliverable": "risky",
"reason": "catch_all",
"catch_all": true,
"mailbox_exists": null,
"mailbox_confidence": 72
}
```
Reference: https://spaw.co/docs/reasons/catch_all
## `suppressed`: the email address, phone number or postal address is on your account's suppression list
Batch runs, bulk jobs and monitors answered it from your suppression list without running a lookup. Email, phone and postal address each keep their own list, all three per account. Phone and postal entries with no stored answer answer valid false with this reason.
- Appears in: `reason`
- Verdict: undeliverable
- Billing: Free. Suppressed answers are served from the list, with the stored answer where there is one, without running the pipeline.
Your email suppression list holds every address a verdict found undeliverable until a later verdict or a reported delivery clears it, the addresses your own bounce and complaint reports added, and anything you imported. The phone list holds numbers in blocks the regulator does not allocate, numbers your own SMS outcomes wrote off (reason undelivered or abuse), numbers whose recipient opted out (reason opted_out, from your own report or your SMS provider's opt-out code), and imports. The postal list holds addresses your own delivery reports wrote off — the occupant has moved, delivery was refused, or post came back twice with no delivery since — and imports. Batch, bulk and monitor runs consult the list first and answer from the stored result: reason suppressed for entries with no stored answer, the original reason for verified ones, and meta.suppressed true either way. An import that kept a verified answer from before you imported it serves that answer for 90 days after the entry was last renewed, then answers reason suppressed, because nothing looks an import up again.
Single lookups through the API and the dashboard deliberately ignore the list. For email, a recovered verdict removes only an entry a verdict added and a fresh undeliverable one renews any entry, while an import stays until you remove it, a bounce your reports added until a delivery to that address is reported, and a complaint, like a phone opt-out, until you remove it — it never goes stale, and a complaint about an address you imported is recorded on the import, which stays an import. For a postal address, a lookup drops an entry your reports added once the reports behind it have aged out or a delivery was reported since, and never touches an import. A phone lookup clears only an entry an unassigned block added, because a numbering-plan valid says nothing about your own outcomes or a recipient's opt-out. Imports, complaints, opt-outs and abuse reports never go stale: batch, bulk and monitor runs serve them for as long as they are listed. Any other entry older than 90 days is looked up afresh rather than trusted.
A phone opt-out is a consent fact, not a verdict on the number, so it never raises the risk score and never goes stale: no lookup, delivery or entered code clears it, only removing the entry does. An opt-out or abuse report about a number you imported is recorded on that entry, which stays an import. A single phone lookup answers and scores the number in full and says meta.opted_out true, and under the default SMS policy it answers ok_to_send false with blocked_by opted_out (an account that sends one-time codes may switch block_opted_out off); batch, bulk and monitor runs serve it suppressed. Honouring an opt-out stays your obligation — the list is your own record, not a consent system.
Postal suppression is yours alone. Nothing you report about an address is ever read for another account: a shared "post fails here" database would be a database about residences, and this product does not build one.
**What to do.** Nothing for sends: it stays out. To challenge an email entry a verdict added, look that one address up on its own; the fresh answer replaces or clears it. A postal entry your reports added leaves when you report a delivery, or at the next single lookup once those reports have aged out. An import on any of the three lists leaves only when you remove it, and importing an entry that is already listed makes it an import; an email bounce your reports added leaves when a delivery to it is reported, and an email complaint only when you remove it. A phone entry leaves its own way: a lookup clears only an entry an unassigned block added, an undelivered entry leaves on a later delivered or verified report, and an abuse entry, an import or an opt-out only when you remove it; an abuse or opt-out entry you removed comes back only with a new abuse report or opt-out. Any entry can be removed through the suppression endpoints.
```json
{
"email": "old@acme.com",
"deliverable": "undeliverable",
"reason": "suppressed",
"risk_score": 100
}
```
Reference: https://spaw.co/docs/reasons/suppressed
## `unverified`: the mailbox could not be confirmed
The domain accepts mail and nothing else argued against the address, but the mailbox handshake ran and the server would not say whether this inbox exists, so the verdict is risky rather than a guess at deliverable.
- Appears in: `reason`
- Verdict: risky
- Risk weight: +10
- Billing: Free, whatever smtp_reason says: an answer the mailbox check could not settle costs nothing and leaves no 7-day repeat marker, so a later lookup of that address is a fresh one; the uncertain probe result is reused for up to 15 minutes (refresh and max_age do not set it aside), then the mailbox is asked again, and the lookup bills 1 credit only once the mailbox is confirmed.
A probe was sent and came back without a yes or a no: the server deferred the connection (greylisting), went silent (timeout), refused or could not be reached (no_connect, mx_unreachable), or the handshake partner itself was unavailable (provider_unavailable). smtp_reason names which. smtp_checked is false, mailbox_exists is null, and the unverified weight of 10 is added to the risk score.
mailbox_confidence estimates the odds that the specific mailbox exists from the signals already in the answer: DMARC and SPF, the mail provider, the registration age, the shape of the username, and the delivery outcomes every account has reported for the domain, once there are at least five. A relay alias stays at 50 and only a bounce rate of 50% or more or a plus-tag lowers it, because the domain's records describe the relay operator. Confirmed mailboxes answer deliverable, catch-all domains answer catch_all; unverified is the honest state in between.
**What to do.** Send with care and branch on mailbox_confidence, or re-verify later: an unverified answer is free and leaves no repeat marker, so once its 15-minute probe result has expired the next lookup asks the server again, and a callback_url on the lookup settles the answer automatically after 5 and 20 minutes.
```json
{
"email": "mia@acme.com",
"deliverable": "risky",
"reason": "unverified",
"smtp_checked": false,
"smtp_reason": "greylisted",
"mailbox_exists": null,
"mailbox_confidence": 62
}
```
Reference: https://spaw.co/docs/reasons/unverified
## `no_mailbox`: the server answered that the mailbox does not exist
The detail behind a mailbox_not_found verdict: the handshake reached the recipient check and the server rejected the address outright.
- Appears in: `smtp_reason`
- Billing: Free within your email fair use: undeliverable answers that needed a mailbox probe cost nothing up to max(500, your billed email answers) in a rolling 30 days, then 1 credit each, with meta.fair_use_billed true. A billed answer repeats free for 7 days.
The most reliable negative signal SMTP can give. The server was asked whether it would accept mail for the recipient and said no, usually with a 550 5.1.1 reply. Spaw records mailbox_exists false and the verdict mailbox_not_found.
Only an explicit "no such user" answer maps to no_mailbox. Ambiguous rejections and policy blocks are reported as unverified instead of being guessed; an address the handshake partner classifies as non-existent without a finer reason answers mailbox_not_found with no smtp_reason.
**What to do.** Remove the address from the list; it is on your suppression list already.
```json
{
"deliverable": "undeliverable",
"reason": "mailbox_not_found",
"smtp_reason": "no_mailbox",
"mailbox_exists": false
}
```
Reference: https://spaw.co/docs/reasons/no_mailbox
## `disabled`: the server reported the mailbox as disabled or suspended
The detail behind a mailbox_disabled verdict: the account exists in the provider's directory but is blocked from receiving mail.
- Appears in: `smtp_reason`
- Billing: Free within your email fair use: undeliverable answers that needed a mailbox probe cost nothing up to max(500, your billed email answers) in a rolling 30 days, then 1 credit each, with meta.fair_use_billed true. A billed answer repeats free for 7 days.
Closed accounts, suspended users and deprovisioned employees answer this way at providers that distinguish "disabled" from "unknown". Mail is rejected exactly as for a missing mailbox.
**What to do.** Remove the address; it has joined your suppression list.
```json
{
"deliverable": "undeliverable",
"reason": "mailbox_disabled",
"smtp_reason": "disabled",
"mailbox_exists": false
}
```
Reference: https://spaw.co/docs/reasons/disabled
## `greylisted`: the server deferred the handshake and asked the sender to come back later
Greylisting answered the probe with a temporary failure. The mailbox is left unverified and the verdict rests on the free signals.
- Appears in: `smtp_reason`
- Billing: Free: an answer the mailbox check could not settle costs nothing and leaves no 7-day repeat marker; the settling re-checks bill 1 credit only when they confirm the mailbox. A verdict another signal decides, such as role, bills 1 credit as that verdict.
Greylisting is an anti-spam technique: the first connection from an unknown sender gets a temporary 4xx failure and is expected to retry minutes later. A verification probe cannot wait that long, so smtp_checked is false and mailbox_exists is null.
The verdict is risky with reason unverified: the unverified weight of 10 is added to the risk score and a mailbox_confidence estimate is attached, built from everything else Spaw knows about the address.
**What to do.** Pass callback_url with the lookup: Spaw re-checks the address after 5 and 20 minutes and pushes the settled verdict to you, billing 1 credit only if a re-check confirms the mailbox. Or re-verify later: an unverified answer leaves no repeat marker, so once its 15-minute probe result has expired the next lookup asks the server again.
```json
{
"deliverable": "risky",
"reason": "unverified",
"smtp_checked": false,
"smtp_reason": "greylisted",
"mailbox_exists": null,
"mailbox_confidence": 65
}
```
Reference: https://spaw.co/docs/reasons/greylisted
## `timeout`: the mail server did not answer within the probe's time limit
A connection was opened but the server went silent before the recipient reply. The mailbox is left unverified.
- Appears in: `smtp_reason`
- Billing: Free: an answer the mailbox check could not settle costs nothing and leaves no 7-day repeat marker; the settling re-checks bill 1 credit only when they confirm the mailbox. A verdict another signal decides, such as role, bills 1 credit as that verdict.
Some providers slow every session down on purpose to frustrate address harvesting; others are simply overloaded. Either way the probe gave up before an answer, smtp_checked is false and mailbox_exists is null.
**What to do.** Pass callback_url with the lookup: Spaw re-checks the address after 5 and 20 minutes and pushes the settled verdict to you, billing 1 credit only if a re-check confirms the mailbox. Or re-verify later: an unverified answer leaves no repeat marker, so once its 15-minute probe result has expired the next lookup asks the server again.
```json
{
"deliverable": "risky",
"reason": "unverified",
"smtp_checked": false,
"smtp_reason": "timeout",
"mailbox_exists": null
}
```
Reference: https://spaw.co/docs/reasons/timeout
## `no_connect`: no mail server accepted a connection on port 25
The MX hosts resolved but none of them accepted a connection: a firewall, a server that is down, or an MX that points at something other than a mail server.
- Appears in: `smtp_reason`
- Billing: Free: an answer the mailbox check could not settle costs nothing and leaves no 7-day repeat marker; the settling re-checks bill 1 credit only when they confirm the mailbox. A verdict another signal decides, such as role, bills 1 credit as that verdict.
Different from mx_unresolvable, where the hosts do not even resolve. Here DNS is fine and the machines refuse or drop the connection. The mailbox stays unverified and the free signals decide the verdict.
**What to do.** Retry later. If no_connect persists for days on a business domain, its mail is probably broken and the addresses should be treated as risky.
```json
{
"deliverable": "risky",
"reason": "unverified",
"smtp_checked": false,
"smtp_reason": "no_connect",
"mailbox_exists": null
}
```
Reference: https://spaw.co/docs/reasons/no_connect
## `mx_unreachable`: the mail exchangers could not be reached from the verification network
The probe could not reach any of the domain's mail servers: unreachable at the network level rather than a refused connection.
- Appears in: `smtp_reason`
- Billing: Free: an answer the mailbox check could not settle costs nothing and leaves no 7-day repeat marker; the settling re-checks bill 1 credit only when they confirm the mailbox. A verdict another signal decides, such as role, bills 1 credit as that verdict.
Transient routing problems and geo-restricted mail servers produce it. The answer is not cached for long, so a later lookup probes again.
**What to do.** Pass callback_url with the lookup: Spaw re-checks the address after 5 and 20 minutes and pushes the settled verdict to you, billing 1 credit only if a re-check confirms the mailbox. Or re-verify later: an unverified answer leaves no repeat marker, so once its 15-minute probe result has expired the next lookup asks the server again.
```json
{
"deliverable": "risky",
"reason": "unverified",
"smtp_checked": false,
"smtp_reason": "mx_unreachable",
"mailbox_exists": null
}
```
Reference: https://spaw.co/docs/reasons/mx_unreachable
## `delivered_recently`: your own delivery report confirmed this mailbox
Not a probe result: your account reported the address as delivered within the last 90 days, and a real delivery is better evidence than any handshake.
- Appears in: `smtp_reason`
- Billing: 1 credit as a deliverable answer; the handshake itself is skipped.
When you send delivery outcomes through the feedback endpoint or an ESP webhook, a delivered report for an address turns later lookups of it into confirmed mailboxes. The probe is skipped, mailbox_exists is true, smtp_checked_at is the delivery time, and the sources list delivery-feedback instead of the SMTP handshake.
Only your own account's reports count. Other customers' outcomes are never used for your verdicts.
**What to do.** Nothing. Keep reporting outcomes and the share of lookups that needs a paid probe keeps falling.
```json
{
"deliverable": "deliverable",
"mailbox_exists": true,
"smtp_checked": true,
"smtp_reason": "delivered_recently"
}
```
Reference: https://spaw.co/docs/reasons/delivered_recently
## `provider_unavailable`: the verification provider itself could not answer
The partner infrastructure that runs the mailbox handshake was unreachable, returned an error, was held off by Spaw's circuit breaker, or was not asked because a channel's daily probe budget was spent. The lookup was answered from the free signals and not billed.
- Appears in: `smtp_reason`
- Billing: Free. An answer Spaw could not obtain is never billed and never cached, so the next lookup of the address probes again and bills as a fresh lookup when it is answered.
After repeated provider failures Spaw stops sending probes for a few minutes and answers every mailbox check as unverified, so a provider incident never turns into a queue of slow requests. The guest demo has a daily probe budget and answers the same way once it is spent. Such answers are never cached: the next lookup probes again as soon as the provider recovers.
smtp_checked is false and mailbox_exists is null; the verdict is risky with reason unverified, from syntax, DNS, the open lists and the domain signals.
**What to do.** Retry later, or pass callback_url so the settled verdict is pushed to you. If you see it across many lookups, quote a request_id to support@spaw.co.
```json
{
"deliverable": "risky",
"reason": "unverified",
"smtp_checked": false,
"smtp_reason": "provider_unavailable",
"mailbox_exists": null
}
```
Reference: https://spaw.co/docs/reasons/provider_unavailable
## `missing_country`: no country was given, and the input does not carry one
A phone number written in national format (no leading + or 00), or a postal address sent without a country, can only be read against a country, and the request named none.
- Appears in: `reason`
- Billing: Free. Neither an invalid number nor an address that cannot stand as written costs a credit.
Digits alone do not identify a numbering plan: 415 555 0142 is a San Francisco number only if you know it is North American. Spaw never guesses the country from the caller, because an API client running in one country routinely verifies numbers and addresses from another.
Pass country (an ISO 3166-1 alpha-2 code) with numbers your users typed without a calling code, or store numbers in E.164 so the calling code travels with them.
A postal address answers the same reason when no country reaches the lookup. The address endpoints require the country and refuse a request without one before the lookup runs, so this answer comes from the batch, where an item that names no country falls back to the list-level one and there was none.
**What to do.** Send the country parameter (the region of the form, the billing address, or the known origin of the list), or prepend the calling code before verifying a number.
```json
{
"valid": false,
"reason": "missing_country",
"e164": null,
"country": null,
"risk_score": null
}
```
Reference: https://spaw.co/docs/reasons/missing_country
## `invalid_country_code`: the calling code after the + is not assigned to any country
The number starts with a calling code the international numbering plan does not assign, so no region could parse it.
- Appears in: `reason`
- Billing: Free. Invalid numbers never cost a credit.
Country calling codes are assigned by the ITU; leading digits that match none of them cannot belong to any plan. This usually means a typo in the prefix, a national trunk 0 left in place after the plus (+0 20 …), or digits that were never a phone number.
**What to do.** Check the prefix: drop a national trunk 0 after the calling code, or ask the user to re-enter the number with the correct country.
```json
{
"valid": false,
"reason": "invalid_country_code",
"e164": null,
"country": null,
"risk_score": null
}
```
Reference: https://spaw.co/docs/reasons/invalid_country_code
## `too_short`: fewer digits than any number in that plan
After the calling code, the number has fewer digits than the shortest subscriber number of its plan, so it cannot be dialled.
- Appears in: `reason`
- Billing: Free. Invalid numbers never cost a credit.
Every plan has a minimum length per number type; a value below it is a truncated paste, a missing area code, or a short code. Emergency numbers and short codes are recognised separately, as emergency_number and short_number, when a country is known.
**What to do.** Look for a missing area code or a cut-off paste. When the value came from a form, show the user exactly what was checked.
```json
{
"valid": false,
"reason": "too_short",
"e164": null,
"country": null,
"risk_score": null
}
```
Reference: https://spaw.co/docs/reasons/too_short
## `too_long`: more digits than any number in that plan
The number has more digits than the longest subscriber number of its plan, often two numbers run together or an extension typed inline.
- Appears in: `reason`
- Billing: Free. Invalid numbers never cost a credit.
E.164 caps numbers at 15 digits and each plan is shorter still. Extensions written as "ext 123" or "x123" are parsed out and returned in the extension field; digits appended without such a marker make the number too long.
**What to do.** Split off an extension with "ext", or check whether two numbers were pasted as one.
```json
{
"valid": false,
"reason": "too_long",
"e164": null,
"country": null,
"risk_score": null
}
```
Reference: https://spaw.co/docs/reasons/too_long
## `not_a_number`: the input does not look like a phone number
Even after stripping punctuation and vanity letters, what is left is not something a phone can dial: no digits, or characters no plan uses.
- Appears in: `reason`
- Billing: Free. Invalid numbers never cost a credit.
Spaw accepts any common notation: spaces, dashes, dots, brackets, a leading + or 00, tel: links and vanity letters (1-800-FLOWERS). A value that still yields no dialable digits is not a number at all: an email address, a name, placeholder text.
**What to do.** Reject the input and ask for the number again; nothing further can be learned from it.
```json
{
"valid": false,
"reason": "not_a_number",
"e164": null,
"country": null,
"risk_score": null
}
```
Reference: https://spaw.co/docs/reasons/not_a_number
## `invalid_length`: a digit count the plan never uses
The number is neither too short nor too long overall, but its length is one the numbering plan does not assign to any number type.
- Appears in: `reason`
- Billing: Free. Invalid numbers never cost a credit.
Some plans assign several lengths, for example eight and ten digits, with nothing in between; a nine-digit number under such a plan can exist under no type. This is almost always a dropped or doubled digit.
**What to do.** Check for a dropped or doubled digit; the valid lengths per country are published in the libphonenumber metadata named in sources.
```json
{
"valid": false,
"reason": "invalid_length",
"e164": null,
"country": null,
"risk_score": null
}
```
Reference: https://spaw.co/docs/reasons/invalid_length
## `invalid_number`: the right length, but not assignable under the plan
The number has a plausible length but its leading digits are not assigned to any number type in the plan of its country, so no subscriber can hold it.
- Appears in: `reason`
- Billing: Free. Invalid numbers never cost a credit.
Numbering plans assign prefixes to types (geographic, mobile, toll-free, premium); digits outside every assigned prefix, such as a North American exchange starting with 0 or 1, are not a dialable number. Spaw reports what the plan says and never answers valid for a number the plan excludes.
**What to do.** Treat it like a typo. If the number came from a form, ask the user to re-enter it; on an imported list, drop it.
```json
{
"valid": false,
"reason": "invalid_number",
"e164": null,
"country": null,
"risk_score": null
}
```
Reference: https://spaw.co/docs/reasons/invalid_number
## `unassigned_block`: the regulator has not allocated the block the number sits in
The number is well-formed, but the allocation table of its regulator lists its block as unassigned (free, spare, protected, quarantined, frozen, cooling off or held in reserve) or, being complete, does not list it at all: no carrier holds it, so no subscriber can be reached on it.
- Appears in: `reason`
- Billing: Free. Invalid numbers never cost a credit.
Where a regulator publishes its number-block allocations (NANPA, the CNAC, Ofcom, ARCEP, ACMA, ACM, the NAD, the CRT (formerly the IFT), UKE and MIMIT today), Spaw reads the block a number sits in. A block the table itself lists as unassigned has no holder: Ofcom's free, protected, quarantined and reserved blocks, the CNAC's unassigned codes, ARCEP's frozen ranges, ACM's cooling-off ranges, the spare ranges of ACMA and the NAD, UKE's reserves, MIMIT's free and revoked ranges, and the thousands-blocks NANPA lists as available, and not contaminated, inside a pooled US code (while the copy is fresh). So does a geographic number missing from a fresh copy of a complete table (NANPA for the United States and its territories, the CNAC for Canada, ARCEP for France, ACM for the Netherlands, the CRT for Mexico). The plan allows the shape, but nobody can be reached there today.
Unlike the other reasons, the answer keeps the parsed number: e164, the display formats, the country facts and block_status "unassigned" are all filled in, and the table that decided it is named in sources. The inference from an absent block is withheld when the table is older than 45 days, so a code assigned since the last sync is never called free.
A number in one of Ofcom's drama ranges, reserved for fiction, does not answer this reason even where Ofcom's table lists its block as protected: it is valid, with is_fictional true and the fictional risk signal, and block_status still says "unassigned".
**What to do.** Reject it at signup like any invalid number; the block cannot ring. If you believe the number is in service, the version in sources says which copy of the table answered, and the regulator publishes the current one.
```json
{
"valid": false,
"reason": "unassigned_block",
"e164": "+442030130018",
"country": "GB",
"block_status": "unassigned",
"risk_score": null
}
```
Reference: https://spaw.co/docs/reasons/unassigned_block
## `emergency_number`: an emergency number, not a subscriber line
The input is the emergency number of its region (911, 112, 999), which connects to a service and never to a person, so it is not a subscriber number.
- Appears in: `reason`
- Billing: Free. Invalid numbers never cost a credit.
Emergency numbers are recognised from the short-number metadata of the country the request named, or the one the number itself identifies. They are answered by name rather than as too_short so a form can reject them with a clear message.
**What to do.** Reject it. Nothing about a person can be learned from an emergency number, and a system must never dial or text one.
```json
{
"valid": false,
"reason": "emergency_number",
"e164": null,
"country": null,
"risk_score": null
}
```
Reference: https://spaw.co/docs/reasons/emergency_number
## `short_number`: a short code or service number, not a subscriber line
The input is a valid short number for its region, such as a carrier service code (611) or a national helpline (101), rather than a number a person can be reached on.
- Appears in: `reason`
- Billing: Free. Invalid numbers never cost a credit.
Short codes are dialable but belong to services, carriers and campaigns; they have no subscriber, carrier or location in the sense the other fields describe. They are recognised only when a country is known, from the short-number metadata of that country.
**What to do.** Reject it for contact details. If you validate SMS short codes on purpose, the country and the value are all there is to store.
```json
{
"valid": false,
"reason": "short_number",
"e164": null,
"country": null,
"risk_score": null
}
```
Reference: https://spaw.co/docs/reasons/short_number
## `empty_address`: nothing in the request was an address
Neither the written address nor the separated fields carried anything the parser could recognise, so there was nothing to check.
- Appears in: `reason`
- Billing: Free. An address that cannot stand as written never costs a credit.
An address is empty when none of organization, street, house number, PO box, locality, administrative area or postal code came out of the input: a blank string, a value of punctuation and whitespace, or fields that were all empty.
Nothing else runs. The country is echoed back, every other field is null, and no risk score is computed.
**What to do.** Check that the field you send is the one the user filled in. A single written address goes in `address`; separated fields go in `address_line1` and its siblings.
```json
{
"valid": false,
"reason": "empty_address",
"country": "GB",
"formatted": null,
"risk_score": null
}
```
Reference: https://spaw.co/docs/reasons/empty_address
## `unknown_country`: no postal format is published for that country
The country code parses, but the address format table carries no format for it, so there is no layout, no required-field list and no postcode pattern to measure the address against.
- Appears in: `reason`
- Billing: Free. An address that cannot stand as written never costs a credit.
Spaw measures an address against its own country: which parts that operator requires, how the lines are ordered, and what shape the postcode takes. The table behind that (named `address-formats` in `sources`) covers the countries and territories with a published format; a code outside it — a user-defined code such as `ZZ`, or a territory with no format of its own — leaves nothing to check.
The API endpoints validate `country` against the same table before the lookup runs, so a request naming an unknown country answers `422 VALIDATION_FAILED` instead. This reason is what the lookup itself answers.
**What to do.** Send the ISO 3166-1 alpha-2 code of the country the post is addressed to. For a territory with no format of its own, use the code of the operator that delivers there.
```json
{
"valid": false,
"reason": "unknown_country",
"country": "ZZ",
"country_name": null,
"postal_code_type": null,
"risk_score": null
}
```
Reference: https://spaw.co/docs/reasons/unknown_country
## `missing_street`: the country requires a street line and none was found
The operator treats the street line as mandatory, and neither a street nor a delivery point that stands in for one (a box, a rural route, general delivery) was recognised in the address.
- Appears in: `reason`
- Billing: Free. An address that cannot stand as written never costs a credit.
Almost every country requires a first address line. A postcode and a town alone reach a place, not a door, so the address cannot be delivered as written.
A post-office box, a rural route or a general-delivery line stands in for the street line, which is exactly what the operator intends, so none of those answers this reason. What does is an address that carries only geography: "94043", or "Springfield, IL 62701", whose lone line before the state and ZIP is read as the town.
A lone line with no number can be a town or a street, and its shape does not always say which, so the installed data decides where it can. A place the postcode directory lists for the code, or elsewhere in its area, makes the line the town, so "Mountain View, CA 94043" answers this reason with Mountain View as the town. A street a register's street listing files under the postcode (in Great Britain, under its district) makes the line the street, so "Strand, WC2R 0RL" is a valid street address, confirmed by the register. Where the data says both or neither, the shape decides: a line that ends in a street type is read as the street, and one that does not as the town. That is why "London Wall, EC2M 5QD" answers this reason, because Open Names files London Wall under EC2Y. Send the separated fields when you hold them apart.
**What to do.** Send the street line in `address_line1`, or the whole address in `address`. If your form collects the house number separately, write it into the same line in the order that country uses.
```json
{
"valid": false,
"reason": "missing_street",
"country": "US",
"street": null,
"postal_code": "94043",
"risk_score": null
}
```
Reference: https://spaw.co/docs/reasons/missing_street
## `missing_locality`: a country with no postcode to name the town was given neither
The operator treats the locality — the town, city or post town — as mandatory, and the address names neither a town nor a postcode that identifies one.
- Appears in: `reason`
- Billing: Free. An address that cannot stand as written never costs a credit.
A postcode identifies the town, so an address that carries one is not refused for leaving the town out: a British street and postcode, or an American street and ZIP, both stand. This reason is for the countries with no postcodes at all, where the town is the only geography the address has.
A town written on the wrong line, or run into the street line without a separator, can be missed by the parser: send the separated fields when you already hold them apart.
**What to do.** Fill in `locality` (or put the town on its own line in the written address). Where you hold only a postcode, the postcode endpoint of a country whose directory is installed can supply the town.
```json
{
"valid": false,
"reason": "missing_locality",
"country": "BS",
"locality": null,
"postal_code": null,
"risk_score": null
}
```
Reference: https://spaw.co/docs/reasons/missing_locality
## `missing_administrative_area`: a country with no postcodes needs the state or province
The operator requires an administrative area and the country has no postcodes, so the address carries no geographic anchor at all.
- Appears in: `reason`
- Billing: Free. An address that cannot stand as written never costs a credit.
A missing state or province only breaks an address where the postcode cannot stand in for it. In countries with postcodes the postcode already carries the geography and mail arrives without the province written out, so Spaw does not answer this reason there.
In a country with no postal code — the United Arab Emirates, for example, where post is addressed to a box in an emirate — the administrative area is the only thing that says where the address is, and an address without it cannot be routed.
**What to do.** Send `administrative_area` (the emirate, state or province). It is accepted as a code or a name, and folded to the spelling that country uses.
```json
{
"valid": false,
"reason": "missing_administrative_area",
"country": "AE",
"po_box": "PO BOX 111",
"administrative_area": null,
"risk_score": null
}
```
Reference: https://spaw.co/docs/reasons/missing_administrative_area
## `missing_postal_code`: the country requires a postcode and none was given
The operator treats the postal code as mandatory and the address does not carry one.
- Appears in: `reason`
- Billing: Free. An address that cannot stand as written never costs a credit.
Where the postcode is required it is what sorts the item, so an address without one is incomplete however precise the rest of it is.
Spaw takes the postcode out of the line that carries it, from whichever end that country writes it, or reads the `postal_code` field. A postcode typed into the town line in an unusual position can be missed: send it separately when you hold it separately.
**What to do.** Fill in `postal_code`. A directory-covered country can supply it from the rest of the address only where a licensed address-list partner is configured; Spaw never invents one.
```json
{
"valid": false,
"reason": "missing_postal_code",
"country": "GB",
"postal_code": null,
"locality": "London",
"risk_score": null
}
```
Reference: https://spaw.co/docs/reasons/missing_postal_code
## `invalid_postal_code`: the postcode is not the shape that country uses
A postcode was given and checked against the country's published pattern, and it does not match: the wrong length, the wrong mix of letters and digits, or a code from another country.
- Appears in: `reason`
- Billing: Free. An address that cannot stand as written never costs a credit.
Every country with postcodes publishes the shape they take, and Spaw checks the value against that pattern (`postal_code_valid` reports the same fact on answers that are otherwise valid). A five-digit value in the United Kingdom, a four-digit ZIP, or a Dutch code without its letters all fail here.
This is a check of shape, not of existence: a postcode of the right shape that no directory knows answers `postcode_not_found` instead. Country prefixes some operators print ("NL-1012 JS") are stripped before the check.
**What to do.** Show the user the postcode that was checked — it is echoed in `postal_code` — and the country it was checked against; the two disagreeing is the usual cause. `postal_code_type` names what that country calls it.
```json
{
"valid": false,
"reason": "invalid_postal_code",
"country": "US",
"postal_code": "9404",
"postal_code_valid": false,
"postal_code_type": "zip",
"risk_score": null
}
```
Reference: https://spaw.co/docs/reasons/invalid_postal_code
## `postcode_not_found`: the postcode is well formed but is not in the directory
The postcode matches the country's pattern, and the whole postcode directory for that country is installed, and this code is not in it.
- Appears in: `reason`
- Billing: Free. An address that cannot stand as written never costs a credit.
Only a country whose complete directory Spaw carries can say a postcode does not exist; everywhere else an unknown code is simply not checked, and `postcode_status` answers null. The directory is named in `sources` with the version that answered.
A code the directory leaves out by design is never refused either. In Great Britain that is a Northern Irish BT postcode (left out of the installed directory for its licence), a BFPO number, a BF1 code and a non-geographic BX code: each answers `postcode_status: null`, not evaluated, with no town or county comparison.
Nor is the code of a box or forces address refused by a directory that lists places rather than every delivery code: a German Postfach PLZ or an APO ZIP the GeoNames copy lacks answers `postcode_status: null`. The British directory lists box and large-user codes, and the French copy carries La Poste's business codes, so those two still refuse a box code they lack. A ZIP+4 is looked up by its ZIP, the first five digits.
A code that was withdrawn is a different answer: a terminated postcode still exists in the directory, so the address stays valid and carries `postcode_status: "terminated"` with the `terminated_postcode` risk signal. `postcode_not_found` means the code was never issued, which is almost always a typo — a transposed pair, a letter O for a zero.
**What to do.** Treat it as a typo and ask for the postcode again. If you believe the code is current, the directory version in `sources` says which copy answered; a code issued since that copy was published is not in it yet.
```json
{
"valid": false,
"reason": "postcode_not_found",
"country": "GB",
"postal_code": "NW1 6XF",
"postal_code_valid": true,
"postcode_status": null,
"risk_score": null
}
```
Reference: https://spaw.co/docs/reasons/postcode_not_found
## `phone_country_differs_from_address_country`: the phone number and the address name different countries
The number was allocated to one country and the address stands in another. Both answers are good; they simply do not agree.
- Appears in: `flag`
- Billing: Free. The consistency analysis is arithmetic over answers already paid for and never costs a credit of its own.
The phone `country` comes from the numbering plan the number was allocated under, and the address `country` from the register the address was checked against. Neither is a guess.
This is ordinary for people who moved country and kept their number, for expatriates, and for anyone billing a company abroad. It matters when you expected the two to match.
**What to do.** Weigh it beside the rest of the answer. A difference alone is not a reason to refuse anyone.
```json
{
"comparison": "phone_address_country",
"left": {
"signal": "phone",
"field": "country",
"value": "GB"
},
"right": {
"signal": "address",
"field": "country",
"value": "US"
},
"result": "differ",
"flag": "phone_country_differs_from_address_country"
}
```
Reference: https://spaw.co/docs/reasons/phone_country_differs_from_address_country
## `ip_country_differs_from_address_country`: the IP address and the postal address name different countries
The address the request came from geolocates to one country and the postal address stands in another.
- Appears in: `flag`
- Billing: Free. The consistency analysis is arithmetic over answers already paid for and never costs a credit of its own.
The comparison is only made when the IP is not a VPN, a relay or a datacenter egress. On those the country belongs to the operator rather than to the person, so the check is withheld instead of answered.
Travel, a corporate proxy and a household VPN all produce this honestly.
**What to do.** Treat it as one input among several. It is a fact about two fields, not a judgement about a person.
```json
{
"comparison": "ip_address_country",
"left": {
"signal": "ip",
"field": "country",
"value": "DE"
},
"right": {
"signal": "address",
"field": "country",
"value": "US"
},
"result": "differ",
"flag": "ip_country_differs_from_address_country"
}
```
Reference: https://spaw.co/docs/reasons/ip_country_differs_from_address_country
## `ip_country_differs_from_phone_country`: the IP address and the phone number name different countries
The request geolocates to one country and the number was allocated under another country’s numbering plan.
- Appears in: `flag`
- Billing: Free. The consistency analysis is arithmetic over answers already paid for and never costs a credit of its own.
As with the postal comparison, an anonymous, relayed or datacenter IP withholds the check rather than answering it.
A kept foreign number is the common innocent explanation, and it is very common.
**What to do.** Weigh it beside the rest of the answer; on its own it says only that two fields differ.
```json
{
"comparison": "ip_phone_country",
"left": {
"signal": "ip",
"field": "country",
"value": "ES"
},
"right": {
"signal": "phone",
"field": "country",
"value": "GB"
},
"result": "differ",
"flag": "ip_country_differs_from_phone_country"
}
```
Reference: https://spaw.co/docs/reasons/ip_country_differs_from_phone_country
## `disposable_email_from_datacenter_ip`: a disposable address arriving from a datacenter
The email is on a disposable-provider list and the request came from hosting rather than from a consumer network.
- Appears in: `flag`
- Billing: Free. The consistency analysis is arithmetic over answers already paid for and never costs a credit of its own.
Each half is already published by its own product: `disposable` on the email answer, `is_datacenter` on the IP answer. The flag only names that both are true at once.
Automation and server-side integrations legitimately run from datacenters, so this pairing is weaker when your own backend forwards the call than when a browser does.
**What to do.** Useful at signup, where a real person is expected to arrive from a consumer network with an address they can receive mail at.
```json
{
"comparison": "email_ip_disposable_datacenter",
"left": {
"signal": "email",
"field": "disposable",
"value": true
},
"right": {
"signal": "ip",
"field": "is_datacenter",
"value": true
},
"result": "agree",
"flag": "disposable_email_from_datacenter_ip"
}
```
Reference: https://spaw.co/docs/reasons/disposable_email_from_datacenter_ip
## `disposable_email_from_anonymous_network`: a disposable address arriving over an anonymous network
The email is on a disposable-provider list and the request came through a VPN, a proxy or Tor.
- Appears in: `flag`
- Billing: Free. The consistency analysis is arithmetic over answers already paid for and never costs a credit of its own.
`is_anonymous` on the IP answer covers VPN exits, public proxies and Tor. Plenty of people use them for ordinary privacy.
The pairing is what carries the signal, not either half alone.
**What to do.** Weigh it where a durable, reachable address matters. Neither half is misconduct on its own.
```json
{
"comparison": "email_ip_disposable_anonymous",
"left": {
"signal": "email",
"field": "disposable",
"value": true
},
"right": {
"signal": "ip",
"field": "is_anonymous",
"value": true
},
"result": "agree",
"flag": "disposable_email_from_anonymous_network"
}
```
Reference: https://spaw.co/docs/reasons/disposable_email_from_anonymous_network
## `mail_drop_address_with_free_email_provider`: a mail-drop address with a free email provider
The postal address is a commercial mail-receiving agency or mailbox rental, and the email is at a free consumer provider.
- Appears in: `flag`
- Billing: Free. The consistency analysis is arithmetic over answers already paid for and never costs a credit of its own.
`is_mail_drop` comes from the mail-drop lists installed for that country, and answers null where none is installed — silence, never false.
Mail drops are a legitimate service used by travellers, small businesses and people without a fixed address. The flag names a combination, not wrongdoing.
**What to do.** Relevant where you need to reach someone at a residential or registered business address; otherwise ignore it.
```json
{
"comparison": "email_address_free_provider_mail_drop",
"left": {
"signal": "email",
"field": "free_provider",
"value": true
},
"right": {
"signal": "address",
"field": "is_mail_drop",
"value": true
},
"result": "agree",
"flag": "mail_drop_address_with_free_email_provider"
}
```
Reference: https://spaw.co/docs/reasons/mail_drop_address_with_free_email_provider
## `disposable_phone_with_disposable_email`: a disposable number with a disposable address
Both contact details are on disposable lists: the number is a known temporary or receive-only service, and the email is at a burner provider.
- Appears in: `flag`
- Billing: Free. The consistency analysis is arithmetic over answers already paid for and never costs a credit of its own.
Each half is published by its own product as `is_disposable` and `disposable`. Both being true is the flag.
`is_disposable` on the phone answer is null while no disposable-number dataset is installed, and a null side is never read as a no.
**What to do.** The strongest of the pairings for signup vetting, because neither contact detail can receive anything for long.
```json
{
"comparison": "email_phone_disposable",
"left": {
"signal": "email",
"field": "disposable",
"value": true
},
"right": {
"signal": "phone",
"field": "is_disposable",
"value": true
},
"result": "agree",
"flag": "disposable_phone_with_disposable_email"
}
```
Reference: https://spaw.co/docs/reasons/disposable_phone_with_disposable_email
## `entity_country_differs_from_address_country`: the company is registered in one country and the address is in another
The register the business identifier was found in is a register of one country, and the postal address stands in a different one.
- Appears in: `flag`
- Billing: Free. The consistency analysis is arithmetic over answers already paid for and never costs a credit of its own.
Both facts are ordinary on their own. A company registered in one country and trading from an office, a branch or an agent in another is routine, and so is a holding company abroad.
The comparison is between `country` on the business block, which is the country of registration, and `country` on the address block, which is the country the address was checked against. Neither is a guess, and neither is where a person lives.
**What to do.** Read it as what it is: the place of registration is not the place of business. It matters when you expected a domestic counterparty.
```json
{
"comparison": "entity_address_country",
"left": {
"signal": "entity",
"field": "country",
"value": "US"
},
"right": {
"signal": "address",
"field": "country",
"value": "GB"
},
"result": "differ",
"flag": "entity_country_differs_from_address_country"
}
```
Reference: https://spaw.co/docs/reasons/entity_country_differs_from_address_country
## `entity_name_differs_from_address_organization`: the registered name and the organisation on the address are not the same name
The legal name the register carries and the organisation written on the postal address do not match once both are normalised.
- Appears in: `flag`
- Billing: Free. The consistency analysis is arithmetic over answers already paid for and never costs a credit of its own.
The comparison is a normalised exact match and nothing more: both sides are folded to their letters and digits and compared as strings. Case, accents (Société and Societe fold together), an ampersand against the word "and", apostrophes, full stops in initials, and company-form words such as limited, ltd, plc, llp and inc are all taken out first, so a difference here is a difference in the name rather than in how it was typed. There is no similarity score, because no threshold honestly decides whether two names are one company — Acme Group and Acme Holdings are reported as different, and both sides are quoted so you can judge.
So this flag says the two names are not the same name. It does not say the address belongs to somebody else: a trading name, a brand, a subsidiary, a registered-office agent and a recent renaming all produce it. Both sides are quoted so you can settle it yourself.
A name that folds away to nothing — an organisation line reading only "Ltd" — withholds the comparison as a missing value rather than reporting a difference.
**What to do.** Read both names. Where the difference matters, ask for the name as it is registered rather than as it is traded.
```json
{
"comparison": "entity_address_organization",
"left": {
"signal": "entity",
"field": "name",
"value": "ACME HOLDINGS LIMITED"
},
"right": {
"signal": "address",
"field": "organization",
"value": "Acme Group"
},
"result": "differ",
"flag": "entity_name_differs_from_address_organization"
}
```
Reference: https://spaw.co/docs/reasons/entity_name_differs_from_address_organization
## `missing_value`: a comparison was withheld because a value was missing
Both signals answered, but one of the two fields the comparison needs was null, so nothing was compared.
- Appears in: `flag`
- Billing: Free. The consistency analysis is arithmetic over answers already paid for and never costs a credit of its own.
A null field means not evaluated, never no. An invalid phone number carries no country, a reserved IP carries no location, and an address outside every installed register carries no mail-drop answer.
The comparison is reported with result `not_compared` so you can see it was considered rather than silently dropped.
**What to do.** Nothing to act on. Supply the missing signal, or read the per-signal block to see why the field was null.
```json
{
"comparison": "ip_phone_country",
"left": {
"signal": "ip",
"field": "country",
"value": "GB"
},
"right": {
"signal": "phone",
"field": "country",
"value": null
},
"result": "not_compared",
"withheld": "missing_value",
"flag": null
}
```
Reference: https://spaw.co/docs/reasons/missing_value
## `low_confidence`: a comparison was withheld because the location was not the person’s
The IP is a VPN exit, a relay or a datacenter egress, so its country belongs to the operator and comparing it would produce a difference that means nothing.
- Appears in: `flag`
- Billing: Free. The consistency analysis is arithmetic over answers already paid for and never costs a credit of its own.
Withholding is deliberate: answering `differ` here would manufacture evidence against everyone who uses a VPN.
The distance comparison is withheld on the same principle whenever the IP location is not graded high confidence.
**What to do.** Read `is_anonymous`, `is_relay` and `is_datacenter` on the IP block if you want to act on the network itself.
```json
{
"comparison": "ip_address_country",
"left": {
"signal": "ip",
"field": "country",
"value": "NL"
},
"right": {
"signal": "address",
"field": "country",
"value": "US"
},
"result": "not_compared",
"withheld": "low_confidence",
"flag": null
}
```
Reference: https://spaw.co/docs/reasons/low_confidence
## `unknown_identifier`: no register on this server carries the identifier
The identifier is well formed, and the register that would hold it does not have it.
- Appears in: `reason`
- Billing: Free. An identifier no register could answer for never costs a credit.
For a LEI this means it was never issued, or was issued after the copy of the register this server holds. For a company number it means the register has no company under it.
The register behind the answer is named in `sources` with the date of the copy, so you can see how current it is. The Companies House snapshot is republished monthly and can be up to five weeks behind the live register.
**What to do.** Check the identifier against the register itself if the copy here might be too old. A number that has never existed will not appear there either.
```json
{
"identifier": "5493001KJTIIGC8Y1R99",
"identifier_type": "lei",
"found": false,
"reason": "unknown_identifier",
"checksum_valid": false
}
```
Reference: https://spaw.co/docs/reasons/unknown_identifier
## `malformed_identifier`: the identifier is not shaped like one
The value cannot be an identifier of the type asked for, so no register was searched.
- Appears in: `reason`
- Billing: Free. An identifier no register could answer for never costs a credit.
A LEI is exactly twenty characters of digits and capital letters. A UK company number is at most eight characters, and is zero-padded to eight before it is looked up, so "1234567" and "01234567" are the same company.
Nothing was searched, so nothing is billed.
**What to do.** Check the identifier for missing or extra characters. For a LEI, `checksum_valid` will tell you whether the twenty characters carry a valid check digit once the length is right.
```json
{
"identifier": "NOT-A-LEI",
"identifier_type": "lei",
"found": false,
"reason": "malformed_identifier",
"checksum_valid": false
}
```
Reference: https://spaw.co/docs/reasons/malformed_identifier
## `unsupported_register`: no register is held for that identifier type or country
The identifier type is recognised but this server holds no register that could answer it.
- Appears in: `reason`
- Billing: Free. An identifier no register could answer for never costs a credit.
Company numbers are only unique inside their own register, so a country is required and only the registers installed here can answer. GB is the country covered today.
This is a limit of the data on this server, never a statement about the company.
**What to do.** Use a LEI if the entity has one: those are global and need no country.
```json
{
"identifier": "12345678",
"identifier_type": "company_number",
"found": false,
"reason": "unsupported_register"
}
```
Reference: https://spaw.co/docs/reasons/unsupported_register
## `register_unavailable`: the register is not installed on this server
The register that would answer has not been synced here, so the lookup was not attempted.
- Appears in: `reason`
- Billing: Free. An identifier no register could answer for never costs a credit.
Registers are bulk files synced on a schedule. Until one has been installed, every identifier it would answer reports this rather than reporting that the entity does not exist.
Absence of data is never reported as absence of the company. Free, and never cached, so the next lookup answers as soon as the register lands.
**What to do.** Check /status, which lists every dataset this server holds with the date of its copy.
```json
{
"identifier": "5493001KJTIIGC8Y1R12",
"identifier_type": "lei",
"found": false,
"reason": "register_unavailable"
}
```
Reference: https://spaw.co/docs/reasons/register_unavailable
## `lei_registration_not_current`: the LEI registration has lapsed
The entity is in the register, but its LEI registration is no longer current — most often lapsed, meaning nobody renewed it.
- Appears in: `flag`
- Billing: Free. The flags come with an answer that has already been paid for.
A LEI must be renewed each year against the entity's own national register. A lapsed one means the reference data has not been reconfirmed recently, not that the company has gone.
`registration_status` carries the register's own word: ISSUED, LAPSED, RETIRED, ANNULLED, MERGED or DUPLICATE. `next_renewal_at` says when it was last due.
**What to do.** Treat the entity data as unconfirmed rather than wrong. If the counterparty needs a current LEI for a regulatory reason, this is the flag that says they do not have one.
```json
{
"flags": [
"lei_registration_not_current"
],
"registration_status": "LAPSED",
"next_renewal_at": "2025-04-18"
}
```
Reference: https://spaw.co/docs/reasons/lei_registration_not_current
## `name_differs_across_registers`: the two registers name the entity differently
The LEI record and the national register record for the same company carry different legal names.
- Appears in: `flag`
- Billing: Free. The flags come with an answer that has already been paid for.
The comparison ignores case, punctuation and the company-form words (limited, ltd, plc, llp, inc), so only a real difference in the name is reported.
A company that has been renamed is the usual cause: one register has caught up and the other has not. Both names are quoted in `agreements` so you can see which is which.
**What to do.** Read both names before acting. A renamed company is ordinary; a name that is nothing like the other one is worth a question.
```json
{
"flags": [
"name_differs_across_registers"
],
"agreements": [
{
"field": "name",
"lei": "ACME HOLDINGS LIMITED",
"national_register": "ACME GROUP LIMITED",
"result": "differ"
}
]
}
```
Reference: https://spaw.co/docs/reasons/name_differs_across_registers
## `status_differs_across_registers`: the two registers disagree about whether the entity is live
One register has the entity active and the other does not.
- Appears in: `flag`
- Billing: Free. The flags come with an answer that has already been paid for.
The national register is the authority on whether a company exists; the LEI record reflects what was last reported to the issuing organisation.
Both statuses are quoted in `agreements`, with each register's own wording in `status_detail`.
**What to do.** Trust the national register on existence. A disagreement usually means the LEI record is stale.
```json
{
"flags": [
"status_differs_across_registers"
],
"agreements": [
{
"field": "status",
"lei": "active",
"national_register": "dissolved",
"result": "differ"
}
]
}
```
Reference: https://spaw.co/docs/reasons/status_differs_across_registers
## `dissolved_in_national_register`: the company is dissolved in its national register
The register that incorporated the company records it as dissolved.
- Appears in: `flag`
- Billing: Free. The flags come with an answer that has already been paid for.
A dissolved company no longer exists as a legal person. It can still hold a LEI, and that LEI record may still say active, which is why this flag reads the national register rather than the LEI.
`status_detail` carries the register's own words, which distinguish a voluntary strike-off from a liquidation.
**What to do.** This is the strongest signal the product carries. A dissolved counterparty cannot enter a contract.
```json
{
"status": "dissolved",
"status_detail": "Dissolved",
"flags": [
"dissolved_in_national_register"
]
}
```
Reference: https://spaw.co/docs/reasons/dissolved_in_national_register
# API error codes
Every failed request answers `{ "success": false, "error": { "code", "message", "request_id" } }` with one of the codes below. Nothing is billed for a failed request. Each code is documented at https://spaw.co/docs/errors/{CODE}.
## `UNAUTHENTICATED` (HTTP 401): no valid API key was sent
The Authorization header is missing, malformed, or carries a key that does not exist or was revoked.
Every endpoint under /api/v1 except the browser endpoint and the bounce webhooks authenticates with a secret key sent as a bearer token: Authorization: Bearer sk_live_… . Keys are shown once at creation and stored hashed, so a key you cannot find cannot be recovered.
Revoking a key from the dashboard stops its requests immediately with this code.
The one bearer token that belongs to no account is the published sandbox key sk_sandbox_spaw, which answers the documented sample requests on POST /api/v1/email, POST /api/v1/phone, POST /api/v1/ip, GET /api/v1/ip/{ip}, POST /api/v1/address and refuses everything else with its own codes rather than this one.
**What to do.** Send the header exactly as Authorization: Bearer followed by the key. If the key is lost, create a new one on the API keys page and retire the old one.
```json
{
"success": false,
"error": {
"code": "UNAUTHENTICATED",
"message": "Provide a valid API key as a bearer token.",
"request_id": "req_01m1kgdm4xngzmbmff68g94w0c"
}
}
```
Reference: https://spaw.co/docs/errors/UNAUTHENTICATED
## `VALIDATION_FAILED` (HTTP 422): the request body did not pass validation
One or more fields are missing, malformed, or over a limit. error.errors lists each field with its messages.
Typical causes: an email longer than 254 characters, more than 50 addresses in a batch or 100,000 in a bulk job, a callback_url that is not https, callback_secret missing or outside 16 to 128 characters, a bulk results variant that is not full, deliverable, risky or undeliverable, or a feedback outcome that is not delivered, bounced or complained.
It is also how the suppression endpoints refuse a request that is too big or a filter they cannot read: more than 10,000 values in one import or 1,000 in one removal, a per_page above 500, an unparseable since or until, or a search of the address suppression list that names no country. Nothing is imported and nothing is removed.
Nothing runs and nothing is billed. The errors object is keyed by field name, with array items addressed as emails.3.
**What to do.** Read error.errors, fix the named fields, and resend. Validation rules are listed on each endpoint's reference page.
```json
{
"success": false,
"error": {
"code": "VALIDATION_FAILED",
"message": "The email field is required.",
"errors": {
"email": [
"The email field is required."
]
},
"request_id": "req_01m1kgdm4xngzmbmff68g94w0c"
}
}
```
Reference: https://spaw.co/docs/errors/VALIDATION_FAILED
## `RATE_LIMITED` (HTTP 429): more requests than the limit allows
The API allows 5 requests per second per key; each browser endpoint allows 20 lookups a minute per visitor, and the published sandbox key allows 30 a minute per address.
The response carries a Retry-After header with the seconds until the window resets. The request did not run and nothing was billed.
Verifying a list one address at a time is the usual cause. The batch endpoint takes 50 addresses per request and probes them concurrently; bulk jobs take up to 100,000.
The sandbox key is shared by everyone reading the documentation, so its budget is small on purpose and is counted per source address. Create a key of your own to leave it behind.
A few endpoints carry a cap of their own on top of that one, and the bulk webhook redeliveries are the ones to know about: ten a minute, counted per signed-in user rather than per key, and shared by all five products and by the dashboard button beside each run — because every call queues a real signed POST at a server of yours.
**What to do.** Wait for Retry-After, then resend. For lists, switch to the batch or bulk endpoint instead of tightening a client-side loop.
```json
{
"success": false,
"error": {
"code": "RATE_LIMITED",
"message": "Too many requests. Retry after the limit resets.",
"request_id": "req_01m1kgdm4xngzmbmff68g94w0c"
}
}
```
Reference: https://spaw.co/docs/errors/RATE_LIMITED
## `NOT_FOUND` (HTTP 404): no such record on this account
The bulk job, monitor or suppression entry named in the path does not exist, or belongs to another account. Nothing is charged.
Records are looked up inside the calling account, so an id that belongs to somebody else answers exactly as an id that never existed. That is deliberate: telling the difference would confirm that another account holds that id.
A bulk job whose results were pruned answers this too. Result files are kept for the retention window published on the endpoint's reference page and removed with the job afterwards.
The same code answers a path that matches no endpoint at all, which is usually a typo or a stale base URL.
**What to do.** Check the id against the one the creating call returned, and that the key belongs to the same account. For a run created some time ago, download the results before the retention window closes.
```json
{
"success": false,
"error": {
"code": "NOT_FOUND",
"message": "No record with that id on this account.",
"request_id": "req_01m1kgdm4xngzmbmff68g94w0c"
}
}
```
Reference: https://spaw.co/docs/errors/NOT_FOUND
## `INSUFFICIENT_CREDITS` (HTTP 402): the credit balance is empty
The account has no credits left, so the lookup did not run. Free credits renew at the start of each month; packs can be bought from the Billing page.
Spaw checks the balance before doing any work, so a 402 always means nothing was probed and nothing was charged. Every account receives a free grant of credits each calendar month, sized on the pricing page. Pack credits bought through the dashboard never expire; plan credits roll over up to three months' allowance and stay usable for 60 days after you cancel. An IP lookup still runs at a zero balance while the account holds IP lookups prepaid by an earlier credit.
A batch that runs out of credits part-way answers 200 with the results paid for so far and meta.stopped_reason set to insufficient_credits (a key's daily credit cap stops a batch the same way, with stopped_reason key_spend_cap); only a batch whose first lookup is refused returns this error.
**What to do.** Buy a credit pack or start a plan from the Billing page, or wait for the monthly grant. GET /api/v1/account returns the live balance for pre-flight checks.
```json
{
"success": false,
"error": {
"code": "INSUFFICIENT_CREDITS",
"message": "You have run out of credits. Your free credits renew at the start of each month.",
"request_id": "req_01m1kgdm4xngzmbmff68g94w0c"
}
}
```
Reference: https://spaw.co/docs/errors/INSUFFICIENT_CREDITS
## `RESULTS_NOT_READY` (HTTP 409): the bulk job has not finished yet
Results were requested for a bulk job that is still queued or running.
Bulk jobs run in the background in chunks. Until the job reports completed, the results file does not exist. The job status endpoint shows processed counts while it runs.
**What to do.** Poll GET /api/v1/email/bulk/{jobId} until status is completed, or pass webhook_url at creation and wait for the signed completion webhook instead of polling.
```json
{
"success": false,
"error": {
"code": "RESULTS_NOT_READY",
"message": "The job has not finished yet. Poll its status until it reports completed.",
"request_id": "req_01m1kgdm4xngzmbmff68g94w0c"
}
}
```
Reference: https://spaw.co/docs/errors/RESULTS_NOT_READY
## `JOB_ALREADY_FINISHED` (HTTP 409): the bulk job cannot be cancelled any more
A cancel was requested for a bulk job that already reached a terminal status: completed, cancelled, or failed.
Cancellation only makes sense while a job is queued or running. A finished job keeps its counters and its results; nothing about it changes.
**What to do.** Nothing to do. Download the results if the job completed, or create a new job.
```json
{
"success": false,
"error": {
"code": "JOB_ALREADY_FINISHED",
"message": "The job has already finished and cannot be cancelled.",
"request_id": "req_01m1kgdm4xngzmbmff68g94w0c"
}
}
```
Reference: https://spaw.co/docs/errors/JOB_ALREADY_FINISHED
## `JOB_CANCELLED` (HTTP 409): the bulk job was cancelled before producing results
Results were requested for a job that was cancelled before any address was processed, so there is no results file.
A job cancelled part-way keeps its partial results and serves them normally. Only a job cancelled with zero processed addresses answers with this code.
**What to do.** Create a new job with the same addresses.
```json
{
"success": false,
"error": {
"code": "JOB_CANCELLED",
"message": "The job was cancelled before any addresses were processed, so there are no results.",
"request_id": "req_01m1kgdm4xngzmbmff68g94w0c"
}
}
```
Reference: https://spaw.co/docs/errors/JOB_CANCELLED
## `JOB_FAILED` (HTTP 409): the bulk job failed before producing results
Results were requested for a job that failed before a single row was written, so there is no results file.
A run that fails part-way keeps the rows it did write and serves them normally: those rows were billed, so withholding them would charge for work you could never collect. Only a job that failed with zero processed rows answers with this code, and nothing was billed for it.
**What to do.** Create a new job with the same rows. If it fails again, quote the request id when you get in touch.
```json
{
"success": false,
"error": {
"code": "JOB_FAILED",
"message": "The job failed before any addresses were processed, so there are no results. Nothing was billed.",
"request_id": "req_01m1kgdm4xngzmbmff68g94w0c"
}
}
```
Reference: https://spaw.co/docs/errors/JOB_FAILED
## `JOB_NOT_RESUMABLE` (HTTP 409): the bulk job is not in a state a resume can recover
A resume was asked for a run that did not stop because the credit balance ran out, that has not settled yet, or that is already going again.
A run that stops on an empty balance keeps the rows it paid for and the list it was given, so topping up and resuming re-runs that list from the top instead of asking you to upload it again. That is the only stop a resume recovers: a cancelled run was stopped on purpose, a completed one has nothing left to reach, and a run still queued or processing has not finished writing its result file.
Claiming the run is one conditional write, so two resumes sent at once cannot both queue it: the second answers this code and queues nothing. Nothing is charged either way.
**What to do.** Poll the job (`GET /api/v1/email/bulk/{jobId}` and its phone, IP and address twins): a terminal `status` with `stopped_reason: "insufficient_credits"` is the state a resume takes. Anything else needs a new job.
```json
{
"success": false,
"error": {
"code": "JOB_NOT_RESUMABLE",
"message": "Only a run that stopped because the credit balance ran out can be resumed, and only once it has finished settling.",
"request_id": "req_01m1kgdm4xngzmbmff68g94w0c"
}
}
```
Reference: https://spaw.co/docs/errors/JOB_NOT_RESUMABLE
## `JOB_INPUT_EXPIRED` (HTTP 409): the list the bulk job ran on is no longer stored
A resume was asked for a run whose uploaded list has passed out of the retention window, so there is nothing left to re-run.
A run keeps the list it was given for the retention window published on the endpoint's reference page, and the job's row and its files are removed together at the end of it. Resuming replays that stored list from the top, so once it is gone the run cannot be resumed whatever its status says.
Nothing is queued and nothing is charged.
**What to do.** Create a new job with the rows you still need. To keep a long-running list resumable, download its results and re-create it inside the retention window.
```json
{
"success": false,
"error": {
"code": "JOB_INPUT_EXPIRED",
"message": "The list this run was created from is no longer stored, so the run cannot be resumed. Create a new job with the rows you still need.",
"request_id": "req_01m1kgdm4xngzmbmff68g94w0c"
}
}
```
Reference: https://spaw.co/docs/errors/JOB_INPUT_EXPIRED
## `IP_LIST_LIMIT_REACHED` (HTTP 409): the account already holds as many IP lists as it may
A new allow or deny list was asked for when the account already holds the maximum number of lists.
An account keeps a bounded number of IP lists, published as `meta.max_lists` on `GET /api/v1/ip/lists`, because every list is packed and scanned on every IP lookup the account makes. The entries themselves are the thing to grow: one list holds up to `meta.max_entries` addresses and prefixes.
Nothing is created and nothing is charged.
**What to do.** Add the entries to a list you already have with `PATCH /api/v1/ip/lists/{listId}`, or remove a list you no longer need with `DELETE /api/v1/ip/lists/{listId}` and create the new one.
```json
{
"success": false,
"error": {
"code": "IP_LIST_LIMIT_REACHED",
"message": "This account already holds 20 IP lists. Remove one, or add the entries to a list you have.",
"request_id": "req_01m1kgdm4xngzmbmff68g94w0c"
}
}
```
Reference: https://spaw.co/docs/errors/IP_LIST_LIMIT_REACHED
## `JOB_NOT_FINISHED` (HTTP 409): the bulk run has not finished, so there is no completion webhook to send
A redelivery of the completion webhook was asked for a run that is still queued or running.
The completion webhook announces a finished run, so it exists only once the run has settled at completed, failed or cancelled. Until then there is nothing to send again — and sending the run's counts mid-flight would announce a finish that has not happened.
Nothing is delivered and nothing is charged.
**What to do.** Poll the run (`GET /api/v1/email/bulk/{jobId}` and its phone, IP, address and consistency twins) until `status` is terminal, then ask for the redelivery. If the run is still going, the first delivery has not been missed yet: it has not been made.
```json
{
"success": false,
"error": {
"code": "JOB_NOT_FINISHED",
"message": "The run has not finished yet, so there is no completion webhook to send again.",
"request_id": "req_01m1kgdm4xngzmbmff68g94w0c"
}
}
```
Reference: https://spaw.co/docs/errors/JOB_NOT_FINISHED
## `WEBHOOK_NOT_CONFIGURED` (HTTP 409): the bulk run has no webhook URL to deliver to
A redelivery of the completion webhook was asked for a run created without a `webhook_url`, so there is no address to post to and no secret to sign with.
A webhook URL is set once, when the run is created, and the signing secret is minted with it and returned once. A run started without one was never going to be posted about, so there is no first delivery to repeat and nothing this call could invent — a URL supplied now would be an unauthenticated request to send a signed POST at an arbitrary address.
Nothing is delivered and nothing is charged.
**What to do.** Download the results instead (`GET /api/v1/email/bulk/{jobId}/results` and its twins), and pass `webhook_url` when you create the next run so its finish announces itself.
```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"
}
}
```
Reference: https://spaw.co/docs/errors/WEBHOOK_NOT_CONFIGURED
## `INVALID_PUBLISHABLE_KEY` (HTTP 401): the publishable key does not exist or was revoked
The pk_ key sent to the browser endpoint is unknown.
The browser endpoint POST /api/v1/email/public authenticates with a publishable key in the request body, not a bearer token. Publishable keys are created on the API keys page and stay visible there; revoking one stops its lookups immediately.
**What to do.** Check the key on the API keys page. The form widget reads it from its data-key attribute; a stale deploy is the usual cause.
```json
{
"success": false,
"error": {
"code": "INVALID_PUBLISHABLE_KEY",
"message": "That publishable key does not exist or was revoked.",
"request_id": "req_01m1kgdm4xngzmbmff68g94w0c"
}
}
```
Reference: https://spaw.co/docs/errors/INVALID_PUBLISHABLE_KEY
## `ORIGIN_NOT_ALLOWED` (HTTP 403): the page's origin is not on the key's allowed list
The Origin header of the browser request does not match any domain the publishable key allows, or the header is missing.
Each publishable key is locked to the domains you list at creation, so a key copied out of your page source cannot be used from another site. A missing Origin header is rejected on purpose: server-side code should use a secret key.
**What to do.** Add the site's exact host to the key's allowed domains, or create a key for that site. For server-side lookups use a secret sk_live_ key against POST /api/v1/email.
```json
{
"success": false,
"error": {
"code": "ORIGIN_NOT_ALLOWED",
"message": "This publishable key cannot be used from this origin.",
"request_id": "req_01m1kgdm4xngzmbmff68g94w0c"
}
}
```
Reference: https://spaw.co/docs/errors/ORIGIN_NOT_ALLOWED
## `KEY_IP_NOT_ALLOWED` (HTTP 403): the key is used from an address outside its allowlist
The secret key was created with an allowlist of source addresses or CIDR ranges, and this request came from elsewhere.
An allowlist pins a key to the servers that should hold it, so a leaked key is useless from anywhere else. The address checked is the client address as seen through the trusted proxy in front of the service.
Nothing is billed for a refused request.
**What to do.** Call from a listed address, or create a key whose allowlist includes the new range. Allowlists are set when a key is created; Rotate on the API keys page issues a replacement carrying the same one, so change the list by creating a new key rather than by rotating.
```json
{
"success": false,
"error": {
"code": "KEY_IP_NOT_ALLOWED",
"message": "This API key cannot be used from this address. Add the address to the key's allowlist or create a key without one.",
"request_id": "req_01m1kgdm4xngzmbmff68g94w0c"
}
}
```
Reference: https://spaw.co/docs/errors/KEY_IP_NOT_ALLOWED
## `KEY_ACCOUNT_MISMATCH` (HTTP 403): the key belongs to an account its holder has left
A secret key belongs to the account it was created on. The person who holds this one is no longer a member of that account, so the key no longer authorises anything.
This is what a shared account is for: keys are the account's, not one person's, so somebody leaving a team does not take working credentials with them and does not require every other key to be rotated.
Nothing is billed for a refused request.
**What to do.** Ask an owner or an admin on the account to create a new key. Any owner or admin can create, rotate and revoke any key the account holds.
```json
{
"success": false,
"error": {
"code": "KEY_ACCOUNT_MISMATCH",
"message": "This API key belongs to an account you are no longer a member of. Ask an owner or admin for a new key.",
"request_id": "req_01m1kgdm4xngzmbmff68g94w0c"
}
}
```
Reference: https://spaw.co/docs/errors/KEY_ACCOUNT_MISMATCH
## `KEY_SCOPE_DENIED` (HTTP 403): the key lacks the scope this endpoint needs
The secret key was created with a limited set of scopes (email, ip, phone, address, entity, consistency, account), and the endpoint belongs to a family it does not cover.
Scopes let a key for one integration reach only the product it needs: a signup form's key can verify email without being able to spend on IP or phone lookups. A key created without choosing scopes has every scope.
Nothing is billed for a refused request.
**What to do.** Create a key with the scope the integration needs and use that one. Scopes are set at creation; Rotate issues a replacement carrying the same scopes, so a change of scope is a new key rather than a rotation.
```json
{
"success": false,
"error": {
"code": "KEY_SCOPE_DENIED",
"message": "This API key is not allowed to call ip endpoints. Create a key with the ip scope.",
"request_id": "req_01m1kgdm4xngzmbmff68g94w0c"
}
}
```
Reference: https://spaw.co/docs/errors/KEY_SCOPE_DENIED
## `KEY_SPEND_CAP_REACHED` (HTTP 429): the key has spent its daily credit cap
The key, publishable or secret, was created with a daily credit cap and has reached it today. The counter resets each day.
A daily cap protects your balance from a form that is being abused or a script that runs away. Only what a lookup bills counts towards it: undeliverable verdicts inside your email fair use, unverified answers, reserved and unallocated ranges, cache hits and test addresses are free and never move the counter. One credit pays for five answered IP lookups, so only the IP lookup that opens a block moves it, by 1.
For a secret key the counter adds up what single and batch calls bill (meta.credits_used). Bulk runs bill in the background and are not counted, but creating one is refused once the cap is reached. A batch is also stopped by the cap mid-list: it answers 200 with the items already paid for and meta.stopped_reason set to key_spend_cap, so one large batch can never overspend the cap. An IP batch that reaches the cap first uses up the lookups its last credit already bought, which cost nothing more, and stops before it would open another block; a single call is refused as soon as the cap is reached.
**What to do.** Wait for the daily reset, or create a key with a higher cap. The form widget never blocks the visitor's submit when a lookup is refused.
```json
{
"success": false,
"error": {
"code": "KEY_SPEND_CAP_REACHED",
"message": "This publishable key has reached its daily credit cap. The counter resets each day.",
"request_id": "req_01m1kgdm4xngzmbmff68g94w0c"
}
}
```
Reference: https://spaw.co/docs/errors/KEY_SPEND_CAP_REACHED
## `SANDBOX_ENDPOINT_NOT_ALLOWED` (HTTP 403): the sandbox key does not answer this endpoint
The request carried the published sandbox key, which answers a handful of read-only lookups and nothing else.
The sandbox key is printed in the documentation, so everybody has it. It answers POST /api/v1/email, POST /api/v1/phone, POST /api/v1/ip, GET /api/v1/ip/{ip}, POST /api/v1/address and refuses every other endpoint — anything that would spend credits, write something that lasts, read an account, or send a request somewhere on your behalf.
It is not attached to an account at all, so a sandbox call has no balance, no history and no key policy; meta.credits_remaining answers null and meta.sandbox is true.
**What to do.** Create an account and use your own sk_live_ key for this endpoint. The sandbox exists to make the first request runnable, not to be an anonymous tier.
```json
{
"success": false,
"error": {
"code": "SANDBOX_ENDPOINT_NOT_ALLOWED",
"message": "The sandbox key answers POST /api/v1/email, POST /api/v1/phone, POST /api/v1/ip, GET /api/v1/ip/{ip}, POST /api/v1/address. Every other endpoint needs a key of your own.",
"request_id": "req_01m1kgdm4xngzmbmff68g94w0c"
}
}
```
Reference: https://spaw.co/docs/errors/SANDBOX_ENDPOINT_NOT_ALLOWED
## `SANDBOX_VALUE_NOT_ALLOWED` (HTTP 403): the sandbox key does not answer for that value
The endpoint is one the sandbox answers, but only for the sample values the documentation prints, and the request asked about something else.
A sandbox that answered for any input would be a free lookup API. Each operation accepts exactly the values its documentation publishes: the six test addresses on spaw.test, the eight test numbers in the 555-01XX fiction block, and the sample IP and postal address the API reference prints.
The request body has to carry those fields and nothing else. An extra field is refused rather than ignored, which is what keeps the options that would cost something — a callback URL, a forced refresh, the live carrier check, the deliverability check, the reverse-DNS and abuse-contact lookups — out of the sandbox.
The message names the values the endpoint would have answered for.
**What to do.** Send one of the documented sample values, or use your own sk_live_ key to ask about a real address, number or IP. Check the request is JSON: curl -d without Content-Type: application/json sends a form field, which is not one of the accepted bodies.
```json
{
"success": false,
"error": {
"code": "SANDBOX_VALUE_NOT_ALLOWED",
"message": "The sandbox key answers this endpoint only for the documented test addresses (deliverable@spaw.test, unverified@spaw.test, catch-all@spaw.test, disposable@spaw.test, role@spaw.test, undeliverable@spaw.test). Send them as JSON with Content-Type: application/json, or use a key of your own for anything else.",
"request_id": "req_01m1kgdm4xngzmbmff68g94w0c"
}
}
```
Reference: https://spaw.co/docs/errors/SANDBOX_VALUE_NOT_ALLOWED
## `SANDBOX_UNAVAILABLE` (HTTP 503): the sandbox key is switched off here
The request carried the published sandbox key on a deployment where the sandbox is turned off.
The sandbox is a convenience for reading the documentation, not part of the API contract, and a deployment can switch it off. When it is off the key is refused outright rather than throttled.
There is no Retry-After on this answer, deliberately: nothing is going to reset. A 429 here would tell a client to wait and try again forever.
**What to do.** Create an account and use your own sk_live_ key. Nothing about the endpoint itself has changed — only the shared key is unavailable.
```json
{
"success": false,
"error": {
"code": "SANDBOX_UNAVAILABLE",
"message": "The sandbox key is switched off on this deployment. Create an API key of your own to make this call.",
"request_id": "req_01m1kgdm4xngzmbmff68g94w0c"
}
}
```
Reference: https://spaw.co/docs/errors/SANDBOX_UNAVAILABLE
## `TURNSTILE_FAILED` (HTTP 403): the publishable key requires a Cloudflare Turnstile token
The key was created with a Turnstile site key and secret, and the lookup carried no token or one Cloudflare did not confirm.
A Turnstile-protected key needs a fresh turnstile_token with every lookup; tokens are single-use, so one cannot be reused across lookups. Nothing is billed for a refused lookup.
The form widget handles all of this when its script tag carries data-turnstile-site-key: it renders an invisible widget and fetches a token before each lookup.
**What to do.** Use the widget's data-turnstile-site-key attribute, or, with your own client, obtain a token from Turnstile before each request and send it as turnstile_token. A proxy in front of the endpoint must forward the token.
```json
{
"success": false,
"error": {
"code": "TURNSTILE_FAILED",
"message": "This publishable key requires a valid Cloudflare Turnstile token with every lookup.",
"request_id": "req_01m1kgdm4xngzmbmff68g94w0c"
}
}
```
Reference: https://spaw.co/docs/errors/TURNSTILE_FAILED
## `UNKNOWN_FEEDBACK_PROVIDER` (HTTP 404): the bounce webhook path names an unsupported provider
The URL POST /api/v1/email/feedback/{provider}/{key} accepts postmark, ses, mailgun and sendgrid only.
Each provider path parses that provider's own webhook format and maps its events to delivered, bounced or complained. Any other name has no parser and is answered with this code.
**What to do.** Copy the exact webhook URL from the API keys page for your provider. For any other sending service, post normalized outcomes to POST /api/v1/email/feedback with your secret key instead.
```json
{
"success": false,
"error": {
"code": "UNKNOWN_FEEDBACK_PROVIDER",
"message": "Supported providers: postmark, ses, mailgun, sendgrid.",
"request_id": "req_01m1kgdm4xngzmbmff68g94w0c"
}
}
```
Reference: https://spaw.co/docs/errors/UNKNOWN_FEEDBACK_PROVIDER
## `INVALID_FEEDBACK_KEY` (HTTP 401): the feedback key in the webhook URL is unknown
The fb_ key in a bounce webhook URL does not exist or was rotated.
Bounce webhooks authenticate through the key embedded in their URL, since sending providers cannot add custom headers. Rotating the key on the API keys page invalidates every URL that carried the old one.
**What to do.** Generate or reveal the current key on the API keys page and update the webhook URL at your sending provider.
```json
{
"success": false,
"error": {
"code": "INVALID_FEEDBACK_KEY",
"message": "That feedback key does not exist or was rotated.",
"request_id": "req_01m1kgdm4xngzmbmff68g94w0c"
}
}
```
Reference: https://spaw.co/docs/errors/INVALID_FEEDBACK_KEY
## `EMAIL_NOT_VERIFIED` (HTTP 403): the account behind the key has not confirmed its email address
The key is valid, but its account has not opened the verification link yet, so the API and the MCP server refuse to spend for it.
Every account confirms its email address once, right after signing up. Until that happens the dashboard shows the verification notice, and every request under /api/v1 and every MCP tool call answers this code instead of running the lookup.
The browser form endpoint and the bounce webhook URLs are not affected: their keys can only be created from a confirmed session.
**What to do.** Open the link in the verification email, or sign in and request a new one from the verification notice. Nothing is charged for a refused request, so the same call can simply be retried afterwards.
```json
{
"success": false,
"error": {
"code": "EMAIL_NOT_VERIFIED",
"message": "Verify your email address to use the API. Open the verification link Spaw sent you, or request a new one from the dashboard.",
"request_id": "req_01m1kgdm4xngzmbmff68g94w0c"
}
}
```
Reference: https://spaw.co/docs/errors/EMAIL_NOT_VERIFIED
## `IDEMPOTENCY_KEY_REUSED` (HTTP 409): the Idempotency-Key was already used for a different bulk request
POST /api/v1/email/bulk saw an Idempotency-Key it already knows, but the addresses or webhook URL differ from the request that first used it.
An Idempotency-Key makes bulk creation safe to retry: a repeat with the same key and the same request answers the job the first attempt created, with status 200 and the Idempotent-Replayed header, instead of queueing a second run. The key is bound to the exact addresses and webhook URL it was first sent with.
The same key with a different list or webhook is refused with this code so a retry can never be answered with the wrong job. Nothing is queued and nothing is charged.
**What to do.** Generate a fresh key for every distinct request, for example a UUID stored with the record that triggered the run, and reuse it only when retrying that exact request.
```json
{
"success": false,
"error": {
"code": "IDEMPOTENCY_KEY_REUSED",
"message": "This Idempotency-Key was already used for a different request.",
"request_id": "req_01m1kgdm4xngzmbmff68g94w0c"
}
}
```
Reference: https://spaw.co/docs/errors/IDEMPOTENCY_KEY_REUSED
## `ADDRESS_LIST_UNAVAILABLE` (HTTP 501): no licensed list or open street listing covers that country or postcode
GET /api/v1/address/postcode/{postcode} answers from a licensed address-list partner or from an open register's street listing, and neither covers the country asked about, or the installed listing cannot speak for that postcode. Nothing is charged.
Every full list of the delivery points at a postcode is derived from a postal operator's own address file, and serving it needs a licence. Where an open national register publishes its streets, the endpoint answers those streets at the postcode instead, with no house numbers; a country with neither answers this code. Spaw says so plainly rather than answering an empty list, which would read as "there are no addresses at this postcode".
For the same reason a covered country answers it for a postcode its listing cannot speak for, and the message says which case it is: a code outside the register's area (Northern Irish BT, forces BFPO and BF, and non-geographic BX codes are outside Ordnance Survey's Great Britain data), a postcode in a part of the country no installed register covers (Germany outside Berlin, whose other registers are filed by town, or a Belgian region whose register is not installed), a postcode the installed register cannot vouch for (a British district Open Names files no street under, since Ordnance Survey files each road under one district), and a postcode where a register whose coverage is complete holds addresses but none on a named street, as in many Czech villages. An empty list is answered only where the registers' coverage is complete and none of them holds an address at the postcode, and it is free too.
The rest of the address API does not depend on it. Validating an address, its postcode, its type and its risk signals all work from the datasets Spaw ships and syncs, and are unaffected by this code.
**What to do.** Use POST /api/v1/address to validate an address you already hold. If you need postcode-to-address lookup, write to support@spaw.co with the countries you need it for.
```json
{
"success": false,
"error": {
"code": "ADDRESS_LIST_UNAVAILABLE",
"message": "Listing the addresses at a postcode needs a licensed address-list partner or an open register with a street listing, and neither is configured for IT.",
"request_id": "req_01m1kgdm4xngzmbmff68g94w0c"
}
}
```
Reference: https://spaw.co/docs/errors/ADDRESS_LIST_UNAVAILABLE
## `ADDRESS_STREETS_NOT_FOUND` (HTTP 404): no street listing answers for that country or postcode
GET /api/v1/address/streets found nothing to report: no address register Spaw syncs carries a street listing for the country, the registers list no street at that postcode, or the listing does not cover it. Nothing is charged.
The street listing is read from the national address registers Spaw syncs — every street each one lists inside each postcode, in the register's own spelling. A country with no such register, or one whose register was synced before its listing existed, has no streets to answer with and answers this code.
A covered country answers it in three cases, and the message says which. "No street is listed at 9999 in CH." means the registers' coverage is complete and none of them holds an address at the postcode. Where a register whose coverage is complete holds addresses there but none on a named street, as in many Czech villages, the message says so. And a postcode the listing does not cover says that instead of reading as one with no street: a code outside the register's area (Northern Irish BT, forces BFPO and BF, and non-geographic BX codes are outside Ordnance Survey's Great Britain data), a postcode in a part of the country no installed register covers (Germany outside Berlin), or a postcode the installed register cannot vouch for (a British district Open Names files no street under).
Validating a single address is unaffected; it works from the same registers and answers a verdict rather than this code.
**What to do.** Check the country is one whose address register Spaw carries with a street listing, and that the postcode is a real one in that country's own notation. A postcode the message says the listing does not cover answers the same way however it is written, so ask the customer for the street instead.
```json
{
"success": false,
"error": {
"code": "ADDRESS_STREETS_NOT_FOUND",
"message": "No street is listed at 9999 in CH.",
"request_id": "req_01m1kgdm4xngzmbmff68g94w0c"
}
}
```
Reference: https://spaw.co/docs/errors/ADDRESS_STREETS_NOT_FOUND
## `ASN_NOT_FOUND` (HTTP 404): no autonomous system with that number is in the index
GET /api/v1/ip/asn/{asn} found nothing to report: the ASN database assigns no prefix to that number. Nothing is charged.
The ASN endpoint reads an index built from the DB-IP ASN database: every autonomous system that database assigns at least one prefix to. A number that announces nothing the database knows — a reserved or private number, a system that has never held address space, a typo — is not in it.
It is a fact about the database, not about the registry: the RIR statistics may well have allocated the number. Looking up an address is unaffected; an address answers its own asn and org from the same database.
**What to do.** Check the number, or look up an address inside the network you mean and read the asn the answer carries.
```json
{
"success": false,
"error": {
"code": "ASN_NOT_FOUND",
"message": "No autonomous system numbered 999999 is in the index.",
"request_id": "req_01m1kgdm4xngzmbmff68g94w0c"
}
}
```
Reference: https://spaw.co/docs/errors/ASN_NOT_FOUND
## `ASN_INDEX_UNAVAILABLE` (HTTP 501): the ASN prefix index has not been built on this server
GET /api/v1/ip/asn/{asn} needs the index `lookup:index-asns` builds from the ASN database, and it is not installed. Nothing is charged.
The ASN database answers "which system does this address belong to"; the ASN endpoint answers the reverse, "which prefixes does this system hold", and that needs the database turned inside out once a month after it syncs. Until that has run there is nothing to answer from, and saying so is more honest than an empty prefix list.
Every address lookup is unaffected: asn and org on an address come straight from the database.
**What to do.** Retry later. If it persists, the operator has not run lookup:index-asns since the last ASN database sync.
```json
{
"success": false,
"error": {
"code": "ASN_INDEX_UNAVAILABLE",
"message": "The ASN prefix index has not been built on this server yet.",
"request_id": "req_01m1kgdm4xngzmbmff68g94w0c"
}
}
```
Reference: https://spaw.co/docs/errors/ASN_INDEX_UNAVAILABLE
## `PHONE_BLOCK_NOT_FOUND` (HTTP 404): no number block matches that country and prefix
GET /api/v1/phone/block/{country}/{prefix} found nothing to report: either no numbering plan Spaw syncs covers the country, or no block in it starts with the prefix. Nothing is charged.
The block report is read from the regulators' own allocation tables, which Spaw syncs per country. A country whose regulator publishes no such table has no rows and answers this code. The Crown dependencies (GG, JE, IM) are filed under GB's table and answered from it, keeping only their own blocks, so a prefix that reaches outside them — /api/v1/phone/block/gg/20 is London — answers this code too.
A covered country answers it when the prefix simply is not in the plan: the digits are longer than any block, or they name a range the regulator has never opened. Validating a single number is unaffected; it works from the same tables and answers a verdict rather than this code.
**What to do.** Check the country is one whose numbering plan Spaw carries, and shorten the prefix to the area code itself — the report covers every block underneath it.
```json
{
"success": false,
"error": {
"code": "PHONE_BLOCK_NOT_FOUND",
"message": "No number block in US starts with 999.",
"request_id": "req_01m1kgdm4xngzmbmff68g94w0c"
}
}
```
Reference: https://spaw.co/docs/errors/PHONE_BLOCK_NOT_FOUND
## `METHOD_NOT_ALLOWED` (HTTP 405): the endpoint does not accept that HTTP method
The path exists but not for this verb — a GET where the endpoint takes POST, most often. The response carries an Allow header naming the methods it does take. Nothing is charged.
Every lookup endpoint takes POST, because the value being verified belongs in a body rather than a URL that would be logged by proxies along the way. The read-only endpoints (a bulk job's status, a monitor, the account) take GET.
A client that follows a redirect can turn a POST into a GET and arrive here. Call the documented URL directly, with its scheme and no trailing slash.
**What to do.** Read the Allow header, or the endpoint's reference page, and resend with the method it names.
```json
{
"success": false,
"error": {
"code": "METHOD_NOT_ALLOWED",
"message": "This endpoint does not accept that HTTP method. The Allow header names the ones it does.",
"request_id": "req_01m1kgdm4xngzmbmff68g94w0c"
}
}
```
Reference: https://spaw.co/docs/errors/METHOD_NOT_ALLOWED
## `SERVER_ERROR` (HTTP 500): the request failed inside Spaw
Something went wrong on this side. The failure is logged against the request_id in the response, which is the fastest way for support to find it.
This is not a verdict about what you sent. A request that could not be understood answers 422, one asking for a record that is not there answers 404; this code means the request was fine and the server was not.
Unlike every other error, this one may have billed: a failure after the lookup ran but before the response was written leaves the credit spent. Check the balance on the usage page and quote the request_id if it does not look right.
**What to do.** Retry once — most causes are transient. If it repeats, write to support@spaw.co with the request_id and the endpoint, and check the status page for an open incident.
```json
{
"success": false,
"error": {
"code": "SERVER_ERROR",
"message": "Something went wrong on our side. Quote the request id if you get in touch.",
"request_id": "req_01m1kgdm4xngzmbmff68g94w0c"
}
}
```
Reference: https://spaw.co/docs/errors/SERVER_ERROR
---
# Risk signals
Every deliverable or risky Spaw email answer lists the signals that fired in `risk_signals`, each with the weight applied and the dataset it was read from; the weights sum to `risk_score`, capped at 100, and `risk_level` buckets it (medium from 30, high from 60). An undeliverable verdict is always 100 and lists nothing. Four rules bind the list: catch_all_gateway replaces catch_all behind a security gateway, young_domain and new_domain are exclusive, no_reply is added on top of role, and relay cancels disposable. Each signal is documented at https://spaw.co/docs/signals/{signal}.
## `disposable`: the domain is a known burner provider
The mailbox lives at a disposable-address service, so it exists to be thrown away; this is the largest weight there is.
- Weight: +80
- Dataset: disposable-domains
- Read from: `disposable`
- Verdict reasons it comes with: `disposable`
The domain — or a parent that hands out subdomains, or the operator its MX hosts route mail through — is on the open disposable-domain list, refreshed weekly, with a curated allow-list and deny-list on top. A relay service (Firefox Relay, SimpleLogin and their kind) is read before that list and never counts here: the relay weight of 5 replaces this one.
It reads from the disposable field and comes with the disposable verdict reason. The mailbox is not probed, so smtp_checked is false and the unverified weight fires beside it.
**What to do.** Ask for another address at signup; on a list, drop the row. The default sending policy blocks on it.
```json
{
"email": "signup2847@mailinator.com",
"deliverable": "risky",
"reason": "disposable",
"disposable": true,
"risk_signals": [
{
"signal": "disposable",
"weight": 80,
"dataset": "disposable-domains"
}
]
}
```
Reference: https://spaw.co/docs/signals/disposable
## `parked_domain`: the domain's nameservers belong to a parking service
The domain is parked or for sale and still accepts mail, so whoever collects that mail is not your customer.
- Weight: +60
- Dataset: parking-nameservers
- Read from: `parked`
- Verdict reasons it comes with: `parked_domain`
The nameservers match the bundled parking list — Sedo, ParkingCrew, Bodis, Afternic, Dan and their kind. The check runs only when the domain has usable mail servers, because a parked domain with no MX is already undeliverable, and null means the nameserver lookup failed rather than a clean answer.
It reads from the parked field and comes with the parked_domain verdict reason, placed after a typo-squat and before disposable.
**What to do.** Treat it as a harvesting address: do not send, and offer a correction only when did_you_mean carries one. The default sending policy blocks on it.
```json
{
"email": "mia@forgotten-brand.example",
"deliverable": "risky",
"reason": "parked_domain",
"parked": true,
"risk_signals": [
{
"signal": "parked_domain",
"weight": 60,
"dataset": "parking-nameservers"
}
]
}
```
Reference: https://spaw.co/docs/signals/parked_domain
## `likely_typo`: the domain is a known typo-squat of a major provider
The domain is on the curated map of misspellings that resolve and run catch-alls to harvest misdirected mail, and did_you_mean carries the intended address.
- Weight: +60
- Dataset: typo-domains
- Read from: `did_you_mean`
- Verdict reasons it comes with: `likely_typo`
gmial.com and its kind are real domains that accept every address, so a mail-server check cannot catch them; the curated typo-domain map does, and the correction is offered even though the domain resolves. Nothing is probed.
It reads from did_you_mean and comes with the likely_typo verdict reason, which outranks disposable because the correction is the more useful explanation.
**What to do.** Show the suggested address and ask the user to confirm it; never send to the one that was typed.
```json
{
"email": "mia@gmial.com",
"deliverable": "risky",
"reason": "likely_typo",
"did_you_mean": "mia@gmail.com",
"risk_signals": [
{
"signal": "likely_typo",
"weight": 60,
"dataset": "typo-domains"
}
]
}
```
Reference: https://spaw.co/docs/signals/likely_typo
## `mx_blocklisted`: the domain's mail is served from a blocklisted network
At least one MX host resolves into a network on the threat blocklists the IP product installs — Spamhaus DROP and ASN-DROP, abuse.ch Feodo Tracker — a burner signal no domain list has caught yet.
- Weight: +60
- Dataset: threat-blocklists
- Read from: `mx_blocklisted`
- Verdict reasons it comes with: `mx_blocklisted`
The MX hosts' addresses run through the same feeds that flag an IP address. False is a checked negative; null means no host resolved or no blocklist feed is installed. The network class of the first host is reported beside it as mx_network_type.
It reads from the mx_blocklisted field and comes with the mx_blocklisted verdict reason, placed after disposable.
**What to do.** Do not send. An organisation does not route its mail through a hijacked netblock by accident; the default sending policy blocks on it.
```json
{
"email": "mia@shady.example",
"deliverable": "risky",
"reason": "mx_blocklisted",
"mx_blocklisted": true,
"mx_network_type": "hosting",
"risk_signals": [
{
"signal": "mx_blocklisted",
"weight": 60,
"dataset": "threat-blocklists"
}
]
}
```
Reference: https://spaw.co/docs/signals/mx_blocklisted
## `implicit_mx`: the domain has no MX record and relies on the address-record fallback
The domain publishes no MX record, so mail would fall back to its own address under RFC 5321; such web-only domains rarely accept mail and are never probed.
- Weight: +30
- Dataset: live-dns
- Read from: `mx_implicit`
- Verdict reasons it comes with: `implicit_mx`
RFC 5321 lets a domain without MX records receive mail at its A or AAAA record. Almost no domain set up that way runs a mail server, so the handshake is skipped and the address is reported as risky rather than tried.
It reads from the mx_implicit field and comes with the implicit_mx verdict reason.
**What to do.** Expect a bounce; at signup, ask for another address.
```json
{
"email": "mia@brochure-site.example",
"deliverable": "risky",
"reason": "implicit_mx",
"mx_found": true,
"mx_implicit": true,
"risk_signals": [
{
"signal": "implicit_mx",
"weight": 30,
"dataset": "live-dns"
}
]
}
```
Reference: https://spaw.co/docs/signals/implicit_mx
## `role`: the username is a role account rather than a person
info@, support@, billing@ and the rest of the open role-address list name a function, so the inbox may be shared, filtered or unread.
- Weight: +30
- Dataset: role-addresses
- Read from: `role`
- Verdict reasons it comes with: `role`, `no_reply`
The open role list of around a thousand usernames is matched against the plus-tag-free username. A no-reply pattern counts as a role too, even when the list lacks the spelling, and role_type names the family: support, sales, billing, postmaster, no_reply and so on.
It reads from the role field and comes with the role verdict reason — unless the address is a no-reply one, which gets its own reason and whose weight is added on top of this one.
**What to do.** Send transactional mail freely; keep marketing lists to personal addresses, and let the sending policy block the families you do not want.
```json
{
"email": "support@acme.com",
"deliverable": "risky",
"reason": "role",
"role": true,
"role_type": "support",
"risk_signals": [
{
"signal": "role",
"weight": 30,
"dataset": "role-addresses"
}
]
}
```
Reference: https://spaw.co/docs/signals/role
## `catch_all`: the domain accepts every address, so the mailbox cannot be confirmed
The handshake said yes to a recipient that cannot exist, so a yes for this one proves nothing; mailbox_confidence estimates the rest.
- Weight: +30
- Dataset: mailbox-smtp
- Read from: `catch_all`
- Verdict reasons it comes with: `catch_all`
A catch-all answer is remembered per domain for seven days, so later addresses at the domain are answered from memory rather than probed. Behind a security gateway — Proofpoint, Mimecast, Barracuda — the lighter catch_all_gateway weight of 15 takes the place of this one, because accepting every recipient is the gateway's design rather than a squat.
It reads from the catch_all field and comes with the catch_all verdict reason.
**What to do.** Read mailbox_confidence, or set a confidence floor in the sending policy; report outcomes so the domain's naming convention and bounce share sharpen the estimate.
```json
{
"email": "mia@acme.com",
"deliverable": "risky",
"reason": "catch_all",
"catch_all": true,
"mailbox_confidence": 70,
"risk_signals": [
{
"signal": "catch_all",
"weight": 30,
"dataset": "mailbox-smtp"
}
]
}
```
Reference: https://spaw.co/docs/signals/catch_all
## `young_domain`: the domain was registered under 30 days ago
The registry's RDAP record puts the registration less than 30 days back — the age of a domain set up for one campaign.
- Weight: +30
- Dataset: rdap-registration
- Read from: `domain_age_days`
- Verdict reasons it comes with: none — a score-only signal
Registration facts are read for business domains only, never for free providers or burners, and cached a week. Under 30 days this weight applies; from 30 to 179 days the new_domain weight of 10 applies instead. The two are exclusive, and an older or unknown age carries nothing.
It reads from domain_age_days and comes with no verdict reason of its own: a young domain can be a perfectly real startup.
**What to do.** Read it with the rest: young plus no SPF plus a gibberish username is a pattern; young alone is a new company.
```json
{
"email": "mia@newstartup.example",
"deliverable": "deliverable",
"domain_registered_at": "2026-09-01",
"domain_age_days": 12,
"risk_signals": [
{
"signal": "young_domain",
"weight": 30,
"dataset": "rdap-registration"
}
]
}
```
Reference: https://spaw.co/docs/signals/young_domain
## `recycled_mailbox`: the mailbox exists now but once answered that it did not
A definitive no-such-mailbox was recorded for the address earlier and the mailbox answers now — the shape of a recycled address, the spam-trap kind included.
- Weight: +30
- Dataset: mailbox-history
- Read from: `previously_invalid`
- Verdict reasons it comes with: `recycled_mailbox`
The not-found memory is kept for a year under a keyed hash of the address, fed by the handshake and by bounce reports whose reason reads as user unknown. Only a not-found recorded before this answer counts; a mailbox with no such history reads previously_invalid false.
It reads from previously_invalid and previously_invalid_at and comes with the recycled_mailbox verdict reason, placed after role.
**What to do.** Do not add it to a campaign list on the strength of one confirmation: a recycled trap is exactly a dead address that came back.
```json
{
"email": "mia@acme.com",
"deliverable": "risky",
"reason": "recycled_mailbox",
"mailbox_exists": true,
"previously_invalid": true,
"previously_invalid_at": "2026-05-14",
"risk_signals": [
{
"signal": "recycled_mailbox",
"weight": 30,
"dataset": "mailbox-history"
}
]
}
```
Reference: https://spaw.co/docs/signals/recycled_mailbox
## `mailbox_full`: the mailbox exists but is over quota
The handshake found the mailbox and was told it is full, so mail will bounce until the owner clears it.
- Weight: +20
- Dataset: mailbox-smtp
- Read from: `smtp_reason`
- Verdict reasons it comes with: `mailbox_full`
The provider's sub-reason comes back as smtp_reason mailbox_full; the mailbox exists, mailbox_exists is true, and the verdict is risky with the mailbox_full reason.
It reads from smtp_reason.
**What to do.** Retry later rather than dropping the address; pass callback_url to have it re-checked and the settled verdict pushed to you.
```json
{
"email": "mia@acme.com",
"deliverable": "risky",
"reason": "mailbox_full",
"mailbox_exists": true,
"smtp_reason": "mailbox_full",
"risk_signals": [
{
"signal": "mailbox_full",
"weight": 20,
"dataset": "mailbox-smtp"
}
]
}
```
Reference: https://spaw.co/docs/signals/mailbox_full
## `gibberish`: the username reads as keyboard mash
Six letters or more with no vowel, or six consonants in a row, and nothing the bundled name lists recognise — deliberately conservative.
- Weight: +20
- Dataset: local-part-analysis
- Read from: `is_gibberish`
- Verdict reasons it comes with: none — a score-only signal
y counts as a vowel to protect names like krzysztof, anything under six letters is never flagged, digit-only usernames are never flagged, and a username the name dictionary recognises is never mash. It is independent of looks_generated: both can fire on one address.
It reads from is_gibberish and comes with no verdict reason.
**What to do.** Weigh it with the rest: alone it is a hint, beside a young domain or a catch-all it is a pattern.
```json
{
"email": "xkqzwv@acme.com",
"deliverable": "deliverable",
"is_gibberish": true,
"local_part_pattern": "word",
"risk_signals": [
{
"signal": "gibberish",
"weight": 20,
"dataset": "local-part-analysis"
}
]
}
```
Reference: https://spaw.co/docs/signals/gibberish
## `generated`: the username looks machine-made or a placeholder
A hex string, a UUID, a base64-looking run, one or two letters before four digits, a word before six digits, or a placeholder word such as test or asdf.
- Weight: +20
- Dataset: local-part-analysis
- Read from: `looks_generated`
- Verdict reasons it comes with: none — a score-only signal
The rules are fixed and published on the field: hex of sixteen or more characters, a dashed UUID, twenty or more mixed-case alphanumerics with a digit, one or two letters plus four or more digits, any word plus six or more digits, or a placeholder word (test, tester, testing, temp, fake, asdf, qwerty, abc, sample, example, foo, foobar, null, none, noname, dummy, placeholder) with optional trailing digits. mia1987 and krzysztof are not generated.
It reads from looks_generated, comes with no verdict reason, and is independent of gibberish.
**What to do.** At signup, read it as a bot signal beside widely_seen; on a list, it is the row a person never typed.
```json
{
"email": "ab123456@acme.com",
"deliverable": "deliverable",
"looks_generated": true,
"local_part_pattern": "mixed",
"risk_signals": [
{
"signal": "generated",
"weight": 20,
"dataset": "local-part-analysis"
}
]
}
```
Reference: https://spaw.co/docs/signals/generated
## `no_reply`: the address is a no-reply sender
noreply, donotreply, mailer-daemon and their kin are addresses mail is sent from and never read at; this weight is added on top of the role weight.
- Weight: +20
- Dataset: role-types
- Read from: `role_type`
- Verdict reasons it comes with: `no_reply`
The shape is recognised by pattern — as the whole username or as a delimited token, so noreply-billing counts — even when the open role list lacks the spelling, and it forces role true with role_type no_reply. A role address scores 30; a no-reply one 50.
It reads from role_type and comes with the no_reply verdict reason, placed before role.
**What to do.** Never mail it: replies are discarded by design. The default sending policy blocks on it.
```json
{
"email": "no-reply@acme.com",
"deliverable": "risky",
"reason": "no_reply",
"role": true,
"role_type": "no_reply",
"risk_score": 50,
"risk_signals": [
{
"signal": "no_reply",
"weight": 20,
"dataset": "role-types"
}
]
}
```
Reference: https://spaw.co/docs/signals/no_reply
## `widely_seen`: many other accounts checked the address inside a day
At least three other Spaw accounts looked the address up in the last 24 hours — an address hitting many signup forms at once.
- Weight: +20
- Dataset: lookup-velocity
- Read from: `widely_seen`
- Verdict reasons it comes with: none — a score-only signal
The count is kept as a keyed hash of the address with the account ids that saw it and when, for a day; the caller's own repeats never count, and nothing is held about who. Null means nobody could count: the guest demo, the sandbox, test addresses and invalid syntax.
It reads from widely_seen and comes with no verdict reason.
**What to do.** At signup, ask for a second factor or slow the flow: the address is being used by something automated rather than by one person.
```json
{
"email": "mia@acme.com",
"deliverable": "deliverable",
"widely_seen": true,
"risk_signals": [
{
"signal": "widely_seen",
"weight": 20,
"dataset": "lookup-velocity"
}
]
}
```
Reference: https://spaw.co/docs/signals/widely_seen
## `catch_all_gateway`: a catch-all behind a security gateway
The domain accepts every address but its mail runs through Proofpoint, Mimecast or Barracuda, where accepting every recipient is the gateway's design; this lighter weight takes the place of the catch-all one.
- Weight: +15
- Dataset: mailbox-smtp
- Read from: `mx_provider_type`
- Verdict reasons it comes with: `catch_all`
It fires when mx_provider_type is security_gateway and catch_all is true. The gateway accepts recipients and sorts them out behind, so the catch-all says less about the domain than a squat's would; the verdict reason stays catch_all and mailbox_confidence still estimates the mailbox.
Exactly one of catch_all and catch_all_gateway ever fires on an answer.
**What to do.** The same as for catch_all: read mailbox_confidence and report outcomes.
```json
{
"email": "mia@acme.com",
"deliverable": "risky",
"reason": "catch_all",
"catch_all": true,
"mx_provider": "proofpoint",
"mx_provider_type": "security_gateway",
"risk_signals": [
{
"signal": "catch_all_gateway",
"weight": 15,
"dataset": "mailbox-smtp"
}
]
}
```
Reference: https://spaw.co/docs/signals/catch_all_gateway
## `new_domain`: the domain was registered under 180 days ago
The registration is between 30 and 179 days old — past its first month, not yet an established domain.
- Weight: +10
- Dataset: rdap-registration
- Read from: `domain_age_days`
- Verdict reasons it comes with: none — a score-only signal
Exclusive with young_domain: under 30 days that weight applies, from 30 to 179 days this one, older or unknown ages nothing. Registration facts are read for business domains only.
It reads from domain_age_days and comes with no verdict reason.
**What to do.** A hint rather than a verdict: a six-month-old company mails from a domain this age.
```json
{
"email": "mia@newstartup.example",
"deliverable": "deliverable",
"domain_registered_at": "2026-05-01",
"domain_age_days": 135,
"risk_signals": [
{
"signal": "new_domain",
"weight": 10,
"dataset": "rdap-registration"
}
]
}
```
Reference: https://spaw.co/docs/signals/new_domain
## `no_spf`: a business domain publishes no SPF record
The domain has no SPF policy in DNS, which a maintained business domain almost always has; free providers are exempt because theirs is known.
- Weight: +10
- Dataset: live-dns
- Read from: `has_spf`
- Verdict reasons it comes with: none — a score-only signal
has_spf is read only once the domain resolves: false is a checked negative, null a failed lookup, and the weight applies only when free_provider is false.
It reads from has_spf and comes with no verdict reason.
**What to do.** A weak signal on its own; on a domain check it is the first thing a sender should fix.
```json
{
"email": "mia@acme.example",
"deliverable": "deliverable",
"has_spf": false,
"free_provider": false,
"risk_signals": [
{
"signal": "no_spf",
"weight": 10,
"dataset": "live-dns"
}
]
}
```
Reference: https://spaw.co/docs/signals/no_spf
## `unverified`: the mailbox could not be confirmed
The handshake ran and did not settle the mailbox — greylisting, a timeout, an unreachable server, a partner outage — so smtp_checked is false and the verdict is risky.
- Weight: +10
- Dataset: mailbox-smtp
- Read from: `smtp_checked`
- Verdict reasons it comes with: `unverified`
The answer degrades honestly rather than reading a non-answer as a yes; mailbox_confidence estimates the mailbox from the domain's posture and the username's shape. An answer whose reason is unverified is free whatever smtp_reason says, and leaves no repeat marker. The signal can also sit under an earlier reason, such as role or disposable, and that answer bills like any risky one unless smtp_reason is provider_unavailable.
It reads from smtp_checked and comes with the unverified verdict reason, the last in precedence.
**What to do.** Pass callback_url to have the address re-checked after 5 and 20 minutes and the settled verdict pushed to you, billing 1 credit only if a re-check confirms the mailbox.
```json
{
"email": "mia@acme.com",
"deliverable": "risky",
"reason": "unverified",
"smtp_checked": false,
"smtp_reason": "greylisted",
"mailbox_confidence": 65,
"risk_signals": [
{
"signal": "unverified",
"weight": 10,
"dataset": "mailbox-smtp"
}
]
}
```
Reference: https://spaw.co/docs/signals/unverified
## `expiring_domain`: the domain's registration ends within 30 days
RDAP puts the registration's end inside the next 30 days — a domain that may be about to lapse.
- Weight: +10
- Dataset: rdap-registration
- Read from: `domain_expires_at`
- Verdict reasons it comes with: none — a score-only signal
It is read from the same RDAP record as the registration date, for business domains only. A date already past never fires, because a resolving domain has not lapsed, and many registrars renew at the last moment, which is why the weight is small.
It reads from domain_expires_at and comes with no verdict reason.
**What to do.** Nothing on its own; beside a young registration it is the one-year throwaway pattern.
```json
{
"email": "mia@acme.example",
"deliverable": "deliverable",
"domain_expires_at": "2026-09-30",
"risk_signals": [
{
"signal": "expiring_domain",
"weight": 10,
"dataset": "rdap-registration"
}
]
}
```
Reference: https://spaw.co/docs/signals/expiring_domain
## `alias`: the address carries a plus-tag
mia+promo@ is the same inbox as mia@ under a disposable label — a favourite of signup-bonus farmers, and the reason to dedupe on normalized_email.
- Weight: +5
- Dataset: local-part-analysis
- Read from: `is_alias`
- Verdict reasons it comes with: none — a score-only signal
is_alias reads the plus-tag; normalized_email strips it and folds Gmail dots, so tagged variants of one mailbox share one suppression entry and one repeat marker.
It comes with no verdict reason.
**What to do.** Dedupe on normalized_email and read the tag as a hint, not a fault.
```json
{
"email": "mia+promo@acme.com",
"normalized_email": "mia@acme.com",
"deliverable": "deliverable",
"is_alias": true,
"risk_signals": [
{
"signal": "alias",
"weight": 5,
"dataset": "local-part-analysis"
}
]
}
```
Reference: https://spaw.co/docs/signals/alias
## `relay`: the address is a permanent alias at a forwarding service
Firefox Relay, SimpleLogin, Addy.io, DuckDuckGo Email Protection and their kind deliver to a real person's mailbox under a name they control; the identity is withheld, not the mailbox.
- Weight: +5
- Dataset: forwarding-services
- Read from: `is_relay`
- Verdict reasons it comes with: none — a score-only signal
The relay list is read before the burner list, so a relay domain is never disposable: this weight of 5 replaces the disposable 80. The mailbox is probed like any other and domain_category answers relay. A relay whose servers accept every address, as Firefox Relay's and Brave's do, typically answers risky with reason catch_all, so its score carries the catch-all 30 beside this 5.
It reads from is_relay, relay_service names the service, and it comes with no verdict reason.
**What to do.** Accept it. The owner can switch the alias off, which is a reason to stay worth reading rather than a reason to refuse.
```json
{
"email": "quiet.fox.42@mozmail.com",
"deliverable": "risky",
"reason": "catch_all",
"risk_score": 35,
"is_relay": true,
"relay_service": "Firefox Relay",
"disposable": false,
"risk_signals": [
{
"signal": "relay",
"weight": 5,
"dataset": "forwarding-services"
}
]
}
```
Reference: https://spaw.co/docs/signals/relay
---
# Phone risk signals
Every valid Spaw phone answer lists the signals that fired in `risk_signals`, each with the weight applied and the dataset it was read from; the weights sum to `risk_score`, capped at 100, and `risk_level` buckets it (medium from 30, high from 60). An invalid number has no score and lists nothing. Three rules bind the list: disposable and disposable_stale never fire together, disposable_range fires only for a number the lists do not carry, and range_burst and numbers_per_client come from nothing but the account's own lookups. The score is not the decision: `ok_to_send` and `blocked_by` come from the account's SMS policy. These are the phone weights; the email ones, some with the same names and other weights, are at https://spaw.co/docs/signals/{signal}. Each phone signal is documented at https://spaw.co/docs/signals/phone/{signal}.
## `fictional` (phone): the number sits in a range reserved for fiction
The Spaw phone risk signal fictional adds 80 points to risk_score when the number sits in a range set aside for films, television and other works of fiction: the North American 555-0100 to 555-0199 lines, or a range or number Ofcom, ARCEP, ACMA or ComReg sets aside for drama (dataset fictional-ranges).
- Weight: +80
- Dataset: `fictional-ranges`
- Read from: `is_fictional`
- Default SMS policy: blocks it: fictional is one of the default block_signals (fictional, disposable, reported_abuse, reported_abuse_widely, premium_rate).
Numbering administrators keep a few ranges out of service so that a number printed in a story rings nobody: in North America the 555-01XX lines of every area code, in the United Kingdom the drama ranges Ofcom lists for each kind of number, in France ARCEP's six audiovisual blocks, in Australia ACMA's creative-works numbers and in Ireland ComReg's drama ranges. Spaw bundles those ranges as fictional-ranges and versions the file, so every answer names the copy it used.
A fictional number is still valid: it has the plan's shape, is formatted and is charged like any other valid number. It never answers unassigned_block either. Ofcom stores some drama ranges as protected blocks, so such a number answers valid true, is_fictional true, block_status unassigned and this signal. In North America the 555 code itself is held back, so a live lookup of a 555-01XX number usually carries reserved_block beside it: +80 and +40, capped at 100.
**What it cannot tell you.**
- It cannot tell you who typed the number or why: test data, a placeholder in a form and a quote from a film look the same.
- It covers the ranges in the bundled file: the North American 555-01XX lines and the drama ranges and numbers of Ofcom, ARCEP, ACMA and ComReg. is_fictional false means the number is outside those ranges, not that anyone can be reached on it.
**What to do.** Reject it at signup and drop it from lists: nobody can receive a code there. The default SMS policy blocks on it: an answer carrying it is ok_to_send false whatever the score.
The SMS policy is set per account with `PUT /api/v1/phone/policy` (https://spaw.co/docs/api/update-phone-policy) or the dashboard's SMS policy tab, and its `block_signals` may list any signal, this one included.
The sandbox answer for the test number +1 202-555-0105, cut to the fields that matter here. Test numbers are free on every key and name the dataset test-numbers; a live lookup names fictional-ranges.
```json
{
"valid": true,
"e164": "+12025550105",
"country": "US",
"line_type": "mobile",
"is_fictional": true,
"risk_score": 80,
"risk_level": "high",
"risk_signals": [
{
"signal": "fictional",
"weight": 80,
"dataset": "test-numbers"
}
],
"ok_to_send": false,
"blocked_by": "fictional"
}
```
Related:
- [The weights table in the phone guide](https://spaw.co/docs/phone-intelligence#signal-fictional)
- [Glossary: unassigned block](https://spaw.co/glossary/unassigned-block)
- [What an unassigned block means](https://spaw.co/guides/what-an-unassigned-block-means)
- [Glossary: SMS policy](https://spaw.co/glossary/sms-policy)
Reference: https://spaw.co/docs/signals/phone/fictional
## `disposable` (phone): published by an SMS-receiving website
The Spaw phone risk signal disposable adds 70 points to risk_score when an SMS-receiving website publishes the number, so anyone can read a code sent to it: the open lists Spaw merges every day carry it and saw it recently (dataset disposable-phone-numbers).
- Weight: +70
- Dataset: `disposable-phone-numbers`
- Read from: `is_disposable`, `disposable_seen_at`
- Default SMS policy: blocks it: disposable is one of the default block_signals (fictional, disposable, reported_abuse, reported_abuse_widely, premium_rate).
SMS-receiving websites rent numbers and print every message that reaches them on a public page, so a verification code sent there is readable by anyone who opens it. Spaw merges two open lists, from iP.1 Networks and Castle (both MIT), into one table every day and keeps the date each number was last seen in disposable_seen_at.
A listing is fresh, and scores this weight, while the lists still carry the number and it was seen within the last 365 days (an undated listing counts as fresh), or, once every list has dropped it, for 90 days after it was last seen. After that it scores disposable_stale (+40) instead; the two never fire together. A number the lists dropped is kept at its last-seen date for 730 days, so it ages rather than vanishing.
**What it cannot tell you.**
- It cannot tell you that the number is still rented out. The sites recycle numbers, and one they stopped showing may belong to a real subscriber today, which is why the weight drops once a listing is stale.
- It knows only the numbers the open lists publish. Many SMS-receiving numbers are never scraped into a list, so is_disposable false means not listed, not safe; disposable_range covers the unlisted neighbours of listed numbers.
**What to do.** Refuse it for a one-time code or an account recovery number and ask for another. The default SMS policy blocks on it: an answer carrying it is ok_to_send false whatever the score.
The SMS policy is set per account with `PUT /api/v1/phone/policy` (https://spaw.co/docs/api/update-phone-policy) or the dashboard's SMS policy tab, and its `block_signals` may list any signal, this one included.
The sandbox answer for the test number +1 202-555-0103, cut to the fields that matter here. Test numbers are free on every key and name the dataset test-numbers; a live lookup names disposable-phone-numbers.
```json
{
"valid": true,
"e164": "+12025550103",
"country": "US",
"line_type": "mobile",
"is_disposable": true,
"risk_score": 70,
"risk_level": "high",
"risk_signals": [
{
"signal": "disposable",
"weight": 70,
"dataset": "test-numbers"
}
],
"ok_to_send": false,
"blocked_by": "disposable"
}
```
Related:
- [`disposable_stale`](https://spaw.co/docs/signals/phone/disposable_stale): published by an SMS-receiving website, but not recently
- [`disposable_range`](https://spaw.co/docs/signals/phone/disposable_range): not listed, but in a range crowded with SMS-receiving numbers
- [The weights table in the phone guide](https://spaw.co/docs/phone-intelligence#signal-disposable)
- [Stop OTP abuse with block data and disposable numbers](https://spaw.co/guides/stop-otp-abuse-with-block-data-and-disposable-numbers)
- [Glossary: SMS policy](https://spaw.co/glossary/sms-policy)
Reference: https://spaw.co/docs/signals/phone/disposable
## `disposable_stale` (phone): published by an SMS-receiving website, but not recently
The Spaw phone risk signal disposable_stale adds 40 points to risk_score when the open SMS-receiving lists Spaw merges carry the number but last showed it more than 365 days ago, or every list dropped it and last saw it more than 90 days ago (dataset disposable-phone-numbers).
- Weight: +40
- Dataset: `disposable-phone-numbers`
- Read from: `is_disposable`, `disposable_seen_at`
- Default SMS policy: does not block it on its own: +40 is within the default ceiling of 60, which blocks only a score above it.
It takes the place of disposable (+70) once a listing is no longer fresh: still listed and last seen more than 365 days ago, or dropped by every list and last seen more than 90 days ago. is_disposable stays true and disposable_seen_at gives the date, so you can apply a cut-off of your own.
The lists keep numbers long after a site stops showing them, and a number a site gave up may have gone back to its carrier and been issued to someone who never saw the site; the lower weight reflects that. A dropped number leaves the table 730 days after it was last seen.
**What it cannot tell you.**
- It cannot tell you whether the number was reissued: neither the lists nor the regulator tables record a reassignment, so a stale listing is a hint, not a verdict.
- An undated listing never reaches it while a list still carries the number: it counts as fresh.
**What to do.** Weigh it with the rest of the answer, or apply a cut-off of your own to disposable_seen_at. On its own it scores 40 and passes the default SMS policy, which blocks a score above 60; beside other signals the score can pass that.
The SMS policy is set per account with `PUT /api/v1/phone/policy` (https://spaw.co/docs/api/update-phone-policy) or the dashboard's SMS policy tab, and its `block_signals` may list any signal, this one included.
An excerpt of an answer carrying this signal alone, with the number and the other fields left out, because no test number answers it. The score and the decision are worked out as the lookup and the default SMS policy work them out.
```json
{
"valid": true,
"country": "GB",
"line_type": "mobile",
"line_type_source": "numbering_plan",
"is_disposable": true,
"disposable_seen_at": "2025-03-14",
"risk_score": 40,
"risk_level": "medium",
"risk_signals": [
{
"signal": "disposable_stale",
"weight": 40,
"dataset": "disposable-phone-numbers"
}
],
"ok_to_send": true,
"blocked_by": null
}
```
Related:
- [`disposable`](https://spaw.co/docs/signals/phone/disposable): published by an SMS-receiving website
- [`disposable_range`](https://spaw.co/docs/signals/phone/disposable_range): not listed, but in a range crowded with SMS-receiving numbers
- [The weights table in the phone guide](https://spaw.co/docs/phone-intelligence#signal-disposable_stale)
- [Stop OTP abuse with block data and disposable numbers](https://spaw.co/guides/stop-otp-abuse-with-block-data-and-disposable-numbers)
- [Glossary: SMS policy](https://spaw.co/glossary/sms-policy)
Reference: https://spaw.co/docs/signals/phone/disposable_stale
## `reported_abuse` (phone): your own account reported abuse for the number
The Spaw phone risk signal reported_abuse adds 60 points to risk_score when your own account reported an abused outcome for the number within the last 90 days, through the phone feedback endpoint or the dashboard (dataset delivery-feedback).
- Weight: +60
- Dataset: `delivery-feedback`
- Read from: no field of its own
- Default SMS policy: blocks it: reported_abuse is one of the default block_signals (fictional, disposable, reported_abuse, reported_abuse_widely, premium_rate).
POST /api/v1/phone/feedback records what happened after you sent an SMS. An abused report says the code was used by someone who was not the recipient, or the number was involved in fraud. Any such report from your account within the last 90 days adds this weight to your account's later lookups of the number, whatever you reported after it, and the dataset is delivery-feedback.
Your reports feed only your own lookups. Another account sees an abuse report only as part of a count, once 3 or more accounts have reported the same number (reported_abuse_widely). An abuse report also puts the number on your suppression list, where it stays until you remove it.
**What it cannot tell you.**
- It says what your own systems reported, not what the number is: a report made in error keeps scoring until it is older than 90 days.
- It never reflects another account's reports; that is reported_abuse_widely, which reads only a count.
**What to do.** Treat it as your own decision coming back: refuse the number in the flow that was abused. The default SMS policy blocks on it: an answer carrying it is ok_to_send false whatever the score.
The SMS policy is set per account with `PUT /api/v1/phone/policy` (https://spaw.co/docs/api/update-phone-policy) or the dashboard's SMS policy tab, and its `block_signals` may list any signal, this one included.
An excerpt of an answer carrying this signal alone, with the number and the other fields left out, because no test number answers it. The score and the decision are worked out as the lookup and the default SMS policy work them out.
```json
{
"valid": true,
"country": "GB",
"line_type": "mobile",
"line_type_source": "numbering_plan",
"risk_score": 60,
"risk_level": "high",
"risk_signals": [
{
"signal": "reported_abuse",
"weight": 60,
"dataset": "delivery-feedback"
}
],
"ok_to_send": false,
"blocked_by": "reported_abuse"
}
```
Related:
- [`reported_undelivered`](https://spaw.co/docs/signals/phone/reported_undelivered): your own latest report for the number was a failed delivery
- [`reported_abuse_widely`](https://spaw.co/docs/signals/phone/reported_abuse_widely): three or more accounts reported abuse for the number
- [The weights table in the phone guide](https://spaw.co/docs/phone-intelligence#signal-reported_abuse)
- [POST /api/v1/phone/feedback reference](https://spaw.co/docs/api/report-phone-feedback)
- [Glossary: SMS policy](https://spaw.co/glossary/sms-policy)
Reference: https://spaw.co/docs/signals/phone/reported_abuse
## `reported_undelivered` (phone): your own latest report for the number was a failed delivery
The Spaw phone risk signal reported_undelivered adds 50 points to risk_score when the latest report your own account sent for the number within the last 90 days was a failed delivery about the number itself, with no delivery or verification reported after it (dataset delivery-feedback).
- Weight: +50
- Dataset: `delivery-feedback`
- Read from: no field of its own
- Default SMS policy: does not block it on its own: +50 is within the default ceiling of 60, which blocks only a score above it.
An undelivered report sent to POST /api/v1/phone/feedback may name the failure class your SMS provider's error code gives. permanent, transient and failures sent without a class count here. not_sms_capable (a landline) and sender_side (a carrier filter, an unregistered sender, a rate limit: your side of the send) are kept and counted in the feedback summary, never scored. The dataset is delivery-feedback.
A delivered or verified report dated after the failure cancels it, so a number that failed once while the handset was off stops scoring as soon as a later message gets through. Failures also feed your suppression list: one permanent failure suppresses the number at once, other failures need two with no delivery after them.
**What it cannot tell you.**
- It cannot tell you why a message failed beyond the class you reported.
- It reads only your own reports: another account's failures never reach your answer.
**What to do.** If the failure was transient, retry later rather than dropping the number; if failures repeat, stop sending. On its own it scores 50 and passes the default SMS policy, which blocks a score above 60; beside other signals the score can pass that.
The SMS policy is set per account with `PUT /api/v1/phone/policy` (https://spaw.co/docs/api/update-phone-policy) or the dashboard's SMS policy tab, and its `block_signals` may list any signal, this one included.
An excerpt of an answer carrying this signal alone, with the number and the other fields left out, because no test number answers it. The score and the decision are worked out as the lookup and the default SMS policy work them out.
```json
{
"valid": true,
"country": "GB",
"line_type": "mobile",
"line_type_source": "numbering_plan",
"risk_score": 50,
"risk_level": "medium",
"risk_signals": [
{
"signal": "reported_undelivered",
"weight": 50,
"dataset": "delivery-feedback"
}
],
"ok_to_send": true,
"blocked_by": null
}
```
Related:
- [`reported_abuse`](https://spaw.co/docs/signals/phone/reported_abuse): your own account reported abuse for the number
- [`reported_abuse_widely`](https://spaw.co/docs/signals/phone/reported_abuse_widely): three or more accounts reported abuse for the number
- [The weights table in the phone guide](https://spaw.co/docs/phone-intelligence#signal-reported_undelivered)
- [POST /api/v1/phone/feedback reference](https://spaw.co/docs/api/report-phone-feedback)
- [Glossary: SMS policy](https://spaw.co/glossary/sms-policy)
Reference: https://spaw.co/docs/signals/phone/reported_undelivered
## `reported_abuse_widely` (phone): three or more accounts reported abuse for the number
The Spaw phone risk signal reported_abuse_widely adds 40 points to risk_score when 3 or more Spaw accounts reported abuse for the number through the phone feedback endpoint within the last 30 days (dataset delivery-feedback).
- Weight: +40
- Dataset: `delivery-feedback`
- Read from: no field of its own
- Default SMS policy: blocks it: reported_abuse_widely is one of the default block_signals (fictional, disposable, reported_abuse, reported_abuse_widely, premium_rate).
It is the only phone signal built from other accounts' reports, and only a count is ever read: how many distinct accounts filed an abused report for the number inside the window. Nothing about which accounts, what they sent or what else they reported reaches your answer. The dataset is delivery-feedback.
Only reports an account's own systems filed through the API count toward it; an abused report typed into the dashboard feeds that account's own reported_abuse but not this count. A monitored phone list flags a number when it starts carrying this signal.
**What it cannot tell you.**
- It is not a verdict on the subscriber: accounts report what their own systems observed, Spaw does not verify the reports, and a number can be reported in error.
- It needs 3 accounts within 30 days, so a number abused against one or two accounts never reaches it.
**What to do.** Refuse it where a verification code or an account depends on the number. The default SMS policy blocks on it: an answer carrying it is ok_to_send false whatever the score.
The SMS policy is set per account with `PUT /api/v1/phone/policy` (https://spaw.co/docs/api/update-phone-policy) or the dashboard's SMS policy tab, and its `block_signals` may list any signal, this one included.
An excerpt of an answer carrying this signal alone, with the number and the other fields left out, because no test number answers it. The score and the decision are worked out as the lookup and the default SMS policy work them out.
```json
{
"valid": true,
"country": "GB",
"line_type": "mobile",
"line_type_source": "numbering_plan",
"risk_score": 40,
"risk_level": "medium",
"risk_signals": [
{
"signal": "reported_abuse_widely",
"weight": 40,
"dataset": "delivery-feedback"
}
],
"ok_to_send": false,
"blocked_by": "reported_abuse_widely"
}
```
Related:
- [`reported_abuse`](https://spaw.co/docs/signals/phone/reported_abuse): your own account reported abuse for the number
- [`reported_undelivered`](https://spaw.co/docs/signals/phone/reported_undelivered): your own latest report for the number was a failed delivery
- [The weights table in the phone guide](https://spaw.co/docs/phone-intelligence#signal-reported_abuse_widely)
- [POST /api/v1/phone/feedback reference](https://spaw.co/docs/api/report-phone-feedback)
- [Glossary: SMS policy](https://spaw.co/glossary/sms-policy)
Reference: https://spaw.co/docs/signals/phone/reported_abuse_widely
## `premium_rate` (phone): a premium-rate number
The Spaw phone risk signal premium_rate adds 60 points to risk_score when libphonenumber's numbering-plan metadata types the number as premium rate, such as the UK 09 range (dataset libphonenumber).
- Weight: +60
- Dataset: `libphonenumber`
- Read from: `line_type`
- Default SMS policy: blocks it: premium_rate is one of the default block_signals (fictional, disposable, reported_abuse, reported_abuse_widely, premium_rate).
A premium-rate number charges the caller or sender a service fee on top of the call or message, and a message sent there earns someone a share: the business behind artificially inflated traffic. line_type answers premium_rate with line_type_source numbering_plan, and the dataset is libphonenumber.
In the United Kingdom premium rate is the 09 range. The 084 and 087 ranges answer shared_cost instead: libphonenumber's metadata files them as premium rate, but Ofcom's numbering plan classes them as service numbers with a capped charge, and Spaw follows the plan.
**What it cannot tell you.**
- It reads the plan, not the tariff: it cannot tell you what a message to the number costs or whether the number is in service.
- It does not catch 084 and 087, although 0871 to 0873 still fall under Ofcom's premium-rate-services rules. Add shared_cost to blocked_line_types to refuse them too.
**What to do.** Never send a verification code there. blocked_line_types premium_rate refuses the line type whether or not the signal is on your block list. The default SMS policy blocks on it: an answer carrying it is ok_to_send false whatever the score.
The SMS policy is set per account with `PUT /api/v1/phone/policy` (https://spaw.co/docs/api/update-phone-policy) or the dashboard's SMS policy tab, and its `block_signals` may list any signal, this one included.
An excerpt of an answer carrying this signal alone, with the number and the other fields left out, because no test number answers it. The score and the decision are worked out as the lookup and the default SMS policy work them out.
```json
{
"valid": true,
"country": "GB",
"line_type": "premium_rate",
"line_type_source": "numbering_plan",
"risk_score": 60,
"risk_level": "high",
"risk_signals": [
{
"signal": "premium_rate",
"weight": 60,
"dataset": "libphonenumber"
}
],
"ok_to_send": false,
"blocked_by": "premium_rate"
}
```
Related:
- [`voip`](https://spaw.co/docs/signals/phone/voip): the numbering plan types the number as VoIP
- [`pager`](https://spaw.co/docs/signals/phone/pager): the numbering plan types the number as a pager
- [`international_network`](https://spaw.co/docs/signals/phone/international_network): an international network or satellite number
- [`shared_cost`](https://spaw.co/docs/signals/phone/shared_cost): a shared-cost number
- [The weights table in the phone guide](https://spaw.co/docs/phone-intelligence#signal-premium_rate)
- [Glossary: line type](https://spaw.co/glossary/line-type)
- [Glossary: SMS policy](https://spaw.co/glossary/sms-policy)
Reference: https://spaw.co/docs/signals/phone/premium_rate
## `reserved_block` (phone): the regulator's table marks the block reserved
The Spaw phone risk signal reserved_block adds 40 points to risk_score when the allocation table of the number's regulator marks its block as reserved or set aside for special use, as NANPA does for the 555 code (the regulator table of the number's country).
- Weight: +40
- Dataset: the regulator table of the number's country: `nanpa-co-codes`, `cnac-co-codes`, `ofcom-numbering`, `acm-nummerregister`, `nad-number-register`, `uke-numeracja`, `mimit-numerazione`
- Read from: `block_status`
- Default SMS policy: does not block it on its own: +40 is within the default ceiling of 60, which blocks only a score above it.
Where a regulator publishes its number-block allocations, Spaw reads the block a number sits in, and block_status answers allocated, unassigned or reserved. Reserved is a block the table neither gives to a holder nor lists as free: NANPA's central office codes set aside rather than assigned (the 555 code among them), the CNAC's special-use and test codes, Ofcom's designated blocks, ACM's blocked ranges, the NAD's reserved codes, UKE's technical reserve and MIMIT's reserved ranges. Seven of the ten tables Spaw reads mark such blocks; the other three never answer reserved.
A number in a reserved block is still valid, because the plan allows it and the table does not list the block as free, but no subscriber line is expected there. risk_signals names the table that answered, so a US number cites nanpa-co-codes and a UK one ofcom-numbering.
**What it cannot tell you.**
- It cannot tell you what the block is reserved for: the tables name a status, not a purpose.
- Where no regulator table covers the country, block_status is null and the signal cannot fire: not evaluated, not clean. A table is only as current as its last sync, daily for NANPA and weekly for the others.
**What to do.** Reject it at signup unless you expect test or service numbers; on a list, check the row by hand. On its own it scores 40 and passes the default SMS policy, which blocks a score above 60; beside other signals the score can pass that.
The SMS policy is set per account with `PUT /api/v1/phone/policy` (https://spaw.co/docs/api/update-phone-policy) or the dashboard's SMS policy tab, and its `block_signals` may list any signal, this one included.
An excerpt of an answer carrying this signal alone, with the number and the other fields left out, because no test number answers it. The score and the decision are worked out as the lookup and the default SMS policy work them out.
```json
{
"valid": true,
"country": "US",
"line_type": "fixed_line_or_mobile",
"line_type_source": "numbering_plan",
"block_status": "reserved",
"risk_score": 40,
"risk_level": "medium",
"risk_signals": [
{
"signal": "reserved_block",
"weight": 40,
"dataset": "nanpa-co-codes"
}
],
"ok_to_send": true,
"blocked_by": null
}
```
Related:
- [`virtual`](https://spaw.co/docs/signals/phone/virtual): the block is held by a virtual-number wholesaler or VoIP provider
- [The weights table in the phone guide](https://spaw.co/docs/phone-intelligence#signal-reserved_block)
- [Glossary: NPA-NXX](https://spaw.co/glossary/npa-nxx)
- [Glossary: unassigned block](https://spaw.co/glossary/unassigned-block)
- [What an unassigned block means](https://spaw.co/guides/what-an-unassigned-block-means)
- [Glossary: SMS policy](https://spaw.co/glossary/sms-policy)
Reference: https://spaw.co/docs/signals/phone/reserved_block
## `virtual` (phone): the block is held by a virtual-number wholesaler or VoIP provider
The Spaw phone risk signal virtual adds 40 points to risk_score when the regulator's allocation table shows the number's block allocated to a virtual-number wholesaler or VoIP provider (the regulator table of the number's country).
- Weight: +40
- Dataset: the regulator table of the number's country: `nanpa-co-codes`, `cnac-co-codes`, `ofcom-numbering`, `arcep-numbering`, `acma-numbering`, `acm-nummerregister`, `nad-number-register`, `ift-numeracion`, `uke-numeracja`, `mimit-numerazione`
- Read from: `is_virtual`
- Default SMS policy: does not block it on its own: +40 is within the default ceiling of 60, which blocks only a score above it.
Virtual-number providers hold whole blocks and rent numbers from them by the month or the day, so one person can collect many and let them go. Spaw reads the holder of the number's block from the regulator's table and classifies holders by name: is_virtual answers true for a block held by such a provider, false for another holder, and null when no regulator table covers the number. risk_signals names the table that answered.
In North America the holder also settles the line type: a block held by a virtual-number wholesaler answers line_type voip with line_type_source block_allocation, where the plan alone says fixed_line_or_mobile, and the plan-based voip signal does not fire beside it. In a pooled US code the holder is that of the number's own thousands-block where NANPA's block report lists it; in Canada the table is the code holder's alone.
**What it cannot tell you.**
- It names the original allocation, never who serves the number today: a number ported out of a wholesaler's block still carries the signal, and one ported into it does not.
- It does not separate fixed from non-fixed VoIP: a business line on a hosted phone system and a number rented through an app can both sit in such a block.
- Holders are classified by name, so a provider whose name settles nothing is not counted.
**What to do.** Weigh it with the rest: plenty of real people use virtual numbers. For one-time codes, read it beside range_burst and the disposable signals. On its own it scores 40 and passes the default SMS policy, which blocks a score above 60; beside other signals the score can pass that.
The SMS policy is set per account with `PUT /api/v1/phone/policy` (https://spaw.co/docs/api/update-phone-policy) or the dashboard's SMS policy tab, and its `block_signals` may list any signal, this one included.
The sandbox answer for the test number +1 202-555-0102, cut to the fields that matter here. Test numbers are free on every key and name the dataset test-numbers; a live lookup names the regulator table that answered.
```json
{
"valid": true,
"e164": "+12025550102",
"country": "US",
"line_type": "voip",
"line_type_source": "block_allocation",
"carrier": "Spaw Test VoIP",
"is_virtual": true,
"risk_score": 40,
"risk_level": "medium",
"risk_signals": [
{
"signal": "virtual",
"weight": 40,
"dataset": "test-numbers"
}
],
"ok_to_send": true,
"blocked_by": null
}
```
Related:
- [`reserved_block`](https://spaw.co/docs/signals/phone/reserved_block): the regulator's table marks the block reserved
- [The weights table in the phone guide](https://spaw.co/docs/phone-intelligence#signal-virtual)
- [Glossary: NPA-NXX](https://spaw.co/glossary/npa-nxx)
- [Why US numbers cannot be split into mobile and landline](https://spaw.co/guides/why-us-numbers-cannot-be-split-into-mobile-and-landline)
- [Stop OTP abuse with block data and disposable numbers](https://spaw.co/guides/stop-otp-abuse-with-block-data-and-disposable-numbers)
- [Glossary: SMS policy](https://spaw.co/glossary/sms-policy)
Reference: https://spaw.co/docs/signals/phone/virtual
## `range_burst` (phone): your account looked up several numbers in the same range within the window
The Spaw phone risk signal range_burst adds 40 points to risk_score when your own account looked up 5 or more distinct numbers in the same 1,000-number range within 60 minutes, this one included (dataset account-velocity).
- Weight: +40
- Dataset: `account-velocity`
- Read from: `range_seen_by_you`
- Default SMS policy: does not block it on its own: +40 is within the default ceiling of 60, which blocks only a score above it.
The range is the E.164 number without its last three digits (+14152010142 sits in +14152010xxx), a run of digits rather than a regulator's block. range_seen_by_you answers the count and the window on every counted lookup; from 5 distinct numbers the answer carries this signal, with the dataset account-velocity.
SMS pumping walks consecutive numbers in a range where someone earns a share of every message sent there, and each of those numbers looks clean on its own. In North America, where pumping is rare, a burst more likely means someone is testing numbers against your form or opening accounts in bulk.
Single lookups count (POST /api/v1/phone, the dashboard, the MCP verify-phone tool, the consistency check), and so do browser lookups with a publishable key. A browser answer counts only the browser lookups, so it cannot reveal what your backend looked up.
**What it cannot tell you.**
- It reads only your own account's traffic: no other account's lookups are counted, and batch, bulk and monitor runs, the test numbers and the guest demo never count.
- It is a heuristic: not every regulator allocates in thousands, and a list checked one number at a time through the single endpoint trips it. Send lists to the batch endpoint, which is never counted.
**What to do.** Slow the flow or add a challenge for numbers that carry it. On its own it scores 40 and passes the default SMS policy, which blocks a score above 60; beside other signals the score can pass that.
The SMS policy is set per account with `PUT /api/v1/phone/policy` (https://spaw.co/docs/api/update-phone-policy) or the dashboard's SMS policy tab, and its `block_signals` may list any signal, this one included.
An excerpt of an answer carrying this signal alone, with the number and the other fields left out, because no test number answers it. The score and the decision are worked out as the lookup and the default SMS policy work them out.
```json
{
"valid": true,
"country": "GB",
"line_type": "mobile",
"line_type_source": "numbering_plan",
"risk_score": 40,
"risk_level": "medium",
"risk_signals": [
{
"signal": "range_burst",
"weight": 40,
"dataset": "account-velocity"
}
],
"range_seen_by_you": {
"distinct_numbers": 5,
"window_minutes": 60
},
"ok_to_send": true,
"blocked_by": null
}
```
Related:
- [`numbers_per_client`](https://spaw.co/docs/signals/phone/numbers_per_client): one client address tried several numbers within the window
- [The weights table in the phone guide](https://spaw.co/docs/phone-intelligence#signal-range_burst)
- [Your own traffic, in the phone guide](https://spaw.co/docs/phone-intelligence#own-traffic)
- [Glossary: SMS policy](https://spaw.co/glossary/sms-policy)
Reference: https://spaw.co/docs/signals/phone/range_burst
## `voip` (phone): the numbering plan types the number as VoIP
The Spaw phone risk signal voip adds 30 points to risk_score when libphonenumber's numbering-plan metadata types the number as VoIP, such as the UK 056 range (dataset libphonenumber).
- Weight: +30
- Dataset: `libphonenumber`
- Read from: `line_type`
- Default SMS policy: does not block it on its own: +30 is within the default ceiling of 60, which blocks only a score above it.
Some plans set ranges aside for internet telephony: the UK's 056, and some ranges under the international network codes. line_type answers voip with line_type_source numbering_plan, and the dataset is libphonenumber.
It is separate from virtual, which reads the holder of the block from a regulator's table. A North American number whose block holder is a VoIP provider answers line_type voip from block_allocation and carries virtual, not this signal.
**What it cannot tell you.**
- It reads the plan's range, not the service behind the number: Spaw does not separate fixed from non-fixed VoIP, and a range typed VoIP can carry an ordinary business line.
- Many VoIP numbers sit in ranges the plan types as geographic or mobile, where only the block holder can tell them apart.
**What to do.** Weigh it with the rest; blocked_line_types voip refuses the line type outright. On its own it scores 30 and passes the default SMS policy, which blocks a score above 60; beside other signals the score can pass that.
The SMS policy is set per account with `PUT /api/v1/phone/policy` (https://spaw.co/docs/api/update-phone-policy) or the dashboard's SMS policy tab, and its `block_signals` may list any signal, this one included.
An excerpt of an answer carrying this signal alone, with the number and the other fields left out, because no test number answers it. The score and the decision are worked out as the lookup and the default SMS policy work them out.
```json
{
"valid": true,
"country": "GB",
"line_type": "voip",
"line_type_source": "numbering_plan",
"risk_score": 30,
"risk_level": "medium",
"risk_signals": [
{
"signal": "voip",
"weight": 30,
"dataset": "libphonenumber"
}
],
"ok_to_send": true,
"blocked_by": null
}
```
Related:
- [`premium_rate`](https://spaw.co/docs/signals/phone/premium_rate): a premium-rate number
- [`pager`](https://spaw.co/docs/signals/phone/pager): the numbering plan types the number as a pager
- [`international_network`](https://spaw.co/docs/signals/phone/international_network): an international network or satellite number
- [`shared_cost`](https://spaw.co/docs/signals/phone/shared_cost): a shared-cost number
- [The weights table in the phone guide](https://spaw.co/docs/phone-intelligence#signal-voip)
- [Glossary: line type](https://spaw.co/glossary/line-type)
- [Glossary: SMS policy](https://spaw.co/glossary/sms-policy)
Reference: https://spaw.co/docs/signals/phone/voip
## `pager` (phone): the numbering plan types the number as a pager
The Spaw phone risk signal pager adds 30 points to risk_score when libphonenumber's numbering-plan metadata types the number as a pager, such as parts of the UK 076 range (dataset libphonenumber).
- Weight: +30
- Dataset: `libphonenumber`
- Read from: `line_type`
- Default SMS policy: does not block it on its own: +30 is within the default ceiling of 60, which blocks only a score above it.
A paging number reaches a paging service, not a phone a person can reply from, and few plans still set ranges aside for them; the UK's 076 is one. line_type answers pager with line_type_source numbering_plan, and the dataset is libphonenumber.
**What it cannot tell you.**
- It reads the plan's range: it cannot tell you whether a paging service still answers there.
**What to do.** Do not take it as a contact number for codes; blocked_line_types pager refuses the line type outright. On its own it scores 30 and passes the default SMS policy, which blocks a score above 60; beside other signals the score can pass that.
The SMS policy is set per account with `PUT /api/v1/phone/policy` (https://spaw.co/docs/api/update-phone-policy) or the dashboard's SMS policy tab, and its `block_signals` may list any signal, this one included.
An excerpt of an answer carrying this signal alone, with the number and the other fields left out, because no test number answers it. The score and the decision are worked out as the lookup and the default SMS policy work them out.
```json
{
"valid": true,
"country": "GB",
"line_type": "pager",
"line_type_source": "numbering_plan",
"risk_score": 30,
"risk_level": "medium",
"risk_signals": [
{
"signal": "pager",
"weight": 30,
"dataset": "libphonenumber"
}
],
"ok_to_send": true,
"blocked_by": null
}
```
Related:
- [`premium_rate`](https://spaw.co/docs/signals/phone/premium_rate): a premium-rate number
- [`voip`](https://spaw.co/docs/signals/phone/voip): the numbering plan types the number as VoIP
- [`international_network`](https://spaw.co/docs/signals/phone/international_network): an international network or satellite number
- [`shared_cost`](https://spaw.co/docs/signals/phone/shared_cost): a shared-cost number
- [The weights table in the phone guide](https://spaw.co/docs/phone-intelligence#signal-pager)
- [Glossary: line type](https://spaw.co/glossary/line-type)
- [Glossary: SMS policy](https://spaw.co/glossary/sms-policy)
Reference: https://spaw.co/docs/signals/phone/pager
## `digit_pattern` (phone): repeated or sequential subscriber digits
The Spaw phone risk signal digit_pattern adds 30 points to risk_score when the last seven digits of the number repeat one digit or run straight up or down, such as 555-5555 or 123-4567 (dataset digit-pattern).
- Weight: +30
- Dataset: `digit-pattern`
- Read from: `e164`
- Default SMS policy: does not block it on its own: +30 is within the default ceiling of 60, which blocks only a score above it.
People type these into forms when they do not want to give a number. The check is arithmetic on the number itself: the last seven digits (six when the number is shorter) are one digit repeated, or each is one more or one less than the one before, wrapping from 9 to 0. The dataset is digit-pattern, and because no data is consulted it never appears in sources.
Real numbers with such digits exist, and some businesses pay for them, which is why the weight is small.
**What it cannot tell you.**
- It is a heuristic about what was typed, not a fact about the line: it cannot tell you whether the number is in service or who holds it.
**What to do.** Ask the person to confirm the number when it fires on a form. On its own it scores 30 and passes the default SMS policy, which blocks a score above 60; beside other signals the score can pass that.
The SMS policy is set per account with `PUT /api/v1/phone/policy` (https://spaw.co/docs/api/update-phone-policy) or the dashboard's SMS policy tab, and its `block_signals` may list any signal, this one included.
An excerpt of an answer carrying this signal alone, with the number and the other fields left out, because no test number answers it. The score and the decision are worked out as the lookup and the default SMS policy work them out.
```json
{
"valid": true,
"country": "GB",
"line_type": "mobile",
"line_type_source": "numbering_plan",
"risk_score": 30,
"risk_level": "medium",
"risk_signals": [
{
"signal": "digit_pattern",
"weight": 30,
"dataset": "digit-pattern"
}
],
"ok_to_send": true,
"blocked_by": null
}
```
Related:
- [The weights table in the phone guide](https://spaw.co/docs/phone-intelligence#signal-digit_pattern)
- [Glossary: SMS policy](https://spaw.co/glossary/sms-policy)
Reference: https://spaw.co/docs/signals/phone/digit_pattern
## `numbers_per_client` (phone): one client address tried several numbers within the window
The Spaw phone risk signal numbers_per_client adds 30 points to risk_score when the client address you sent with the lookup, or the browser visitor's own address, tried 5 or more distinct numbers on your account within 60 minutes, this one included (dataset account-velocity).
- Weight: +30
- Dataset: `account-velocity`
- Read from: `client_velocity`
- Default SMS policy: does not block it on its own: +30 is within the default ceiling of 60, which blocks only a score above it.
Send client_ip, the address of the person behind the lookup (your visitor, not your server), and client_velocity counts the distinct numbers tried from it on your account inside the window; an IPv6 address counts by its /64. A browser lookup with a publishable key counts the visitor's own address without being asked. The address is never looked up and never billed, and the dataset is account-velocity.
It does not fire for an address Spaw's own IP data places on a mobile carrier or a corporate proxy, where many people share one address, or for a private or reserved address, which is your own proxy rather than your visitor; those still answer the count.
**What it cannot tell you.**
- It reads only your own account's single and browser lookups: batch, bulk and monitor runs, the test numbers and the guest demo never count, and no other account's traffic is read.
- It catches one script cycling numbers from one address, not an attack rotated through residential proxies. Without a client address the field is null and the signal cannot fire.
**What to do.** Challenge or rate-limit the client rather than the number. On its own it scores 30 and passes the default SMS policy, which blocks a score above 60; beside other signals the score can pass that.
The SMS policy is set per account with `PUT /api/v1/phone/policy` (https://spaw.co/docs/api/update-phone-policy) or the dashboard's SMS policy tab, and its `block_signals` may list any signal, this one included.
An excerpt of an answer carrying this signal alone, with the number and the other fields left out, because no test number answers it. The score and the decision are worked out as the lookup and the default SMS policy work them out.
```json
{
"valid": true,
"country": "GB",
"line_type": "mobile",
"line_type_source": "numbering_plan",
"risk_score": 30,
"risk_level": "medium",
"risk_signals": [
{
"signal": "numbers_per_client",
"weight": 30,
"dataset": "account-velocity"
}
],
"client_velocity": {
"distinct_numbers": 5,
"window_minutes": 60
},
"ok_to_send": true,
"blocked_by": null
}
```
Related:
- [`range_burst`](https://spaw.co/docs/signals/phone/range_burst): your account looked up several numbers in the same range within the window
- [The weights table in the phone guide](https://spaw.co/docs/phone-intelligence#signal-numbers_per_client)
- [Your own traffic, in the phone guide](https://spaw.co/docs/phone-intelligence#own-traffic)
- [Glossary: SMS policy](https://spaw.co/glossary/sms-policy)
Reference: https://spaw.co/docs/signals/phone/numbers_per_client
## `disposable_range` (phone): not listed, but in a range crowded with SMS-receiving numbers
The Spaw phone risk signal disposable_range adds 30 points to risk_score when the number is not on the open SMS-receiving lists itself but sits in a 1,000-number range holding 10 or more listings that still score disposable (dataset disposable-phone-numbers).
- Weight: +30
- Dataset: `disposable-phone-numbers`
- Read from: no field of its own
- Default SMS policy: does not block it on its own: +30 is within the default ceiling of 60, which blocks only a score above it.
SMS-receiving websites rent numbers in runs and the public lists lag the sites, so every daily sync counts, per 1,000-number range (the number without its last three digits, a run of digits rather than a regulator's block), the listings that still score disposable: numbers the lists carry and saw within the last 365 days, and numbers they dropped that were seen within the last 90 days. The dataset is disposable-phone-numbers.
A listed number scores its own listing instead, so the two never add up, and is_disposable stays false on a number this signal flags. GET /api/v1/phone/block/{country}/{prefix} reports the same count under disposable, and Spaw measures the signal weekly against the lists themselves.
**What it cannot tell you.**
- It is an inference about the neighbours, not about the number: a real subscriber can hold a number beside a run of rented ones.
- It fires only where the lists have caught enough of a range; many rented numbers sit in ranges nobody has scraped.
**What to do.** Treat it as a prompt for a second check, not a refusal. On its own it scores 30 and passes the default SMS policy, which blocks a score above 60; beside other signals the score can pass that.
The SMS policy is set per account with `PUT /api/v1/phone/policy` (https://spaw.co/docs/api/update-phone-policy) or the dashboard's SMS policy tab, and its `block_signals` may list any signal, this one included.
The sandbox answer for the test number +1 202-555-0113, cut to the fields that matter here. Test numbers are free on every key and name the dataset test-numbers; a live lookup names disposable-phone-numbers.
```json
{
"valid": true,
"e164": "+12025550113",
"country": "US",
"line_type": "mobile",
"is_disposable": false,
"risk_score": 30,
"risk_level": "medium",
"risk_signals": [
{
"signal": "disposable_range",
"weight": 30,
"dataset": "test-numbers"
}
],
"ok_to_send": true,
"blocked_by": null
}
```
Related:
- [`disposable`](https://spaw.co/docs/signals/phone/disposable): published by an SMS-receiving website
- [`disposable_stale`](https://spaw.co/docs/signals/phone/disposable_stale): published by an SMS-receiving website, but not recently
- [The weights table in the phone guide](https://spaw.co/docs/phone-intelligence#signal-disposable_range)
- [Stop OTP abuse with block data and disposable numbers](https://spaw.co/guides/stop-otp-abuse-with-block-data-and-disposable-numbers)
- [Glossary: SMS policy](https://spaw.co/glossary/sms-policy)
Reference: https://spaw.co/docs/signals/phone/disposable_range
## `international_network` (phone): an international network or satellite number
The Spaw phone risk signal international_network adds 30 points to risk_score when the number's calling code belongs to a network rather than a country: +870 for Inmarsat, +881 for the global mobile satellite systems, or +882 and +883 for the international networks (dataset libphonenumber).
- Weight: +30
- Dataset: `libphonenumber`
- Read from: `calling_code`
- Default SMS policy: does not block it on its own: +30 is within the default ceiling of 60, which blocks only a score above it.
The E.164 plan gives these calling codes to networks that serve no single country, so the answer's country is null. Satellite handsets, maritime and aviation services and international mobile and machine networks number from them. The dataset is libphonenumber.
It is decided from the calling code alone and adds to any line-type signal: the plan types every +883 number as VoIP, so voip adds its own +30 there.
**What it cannot tell you.**
- No regulator's table stands behind these codes. carrier is null unless libphonenumber's prefix data names a network, as it does for every +881 range and a few +882 ranges; carrier_code and carrier_brand stay null.
- A number under these codes that libphonenumber rejects answers invalid_number and carries no score at all.
**What to do.** If you serve nobody on a satellite or international network, set allowed_countries in the SMS policy: a number with no country is outside every allow-list. On its own it scores 30 and passes the default SMS policy, which blocks a score above 60; beside other signals the score can pass that.
The SMS policy is set per account with `PUT /api/v1/phone/policy` (https://spaw.co/docs/api/update-phone-policy) or the dashboard's SMS policy tab, and its `block_signals` may list any signal, this one included.
An excerpt of an answer carrying this signal alone, with the number and the other fields left out, because no test number answers it. The score and the decision are worked out as the lookup and the default SMS policy work them out.
```json
{
"valid": true,
"country": null,
"calling_code": 881,
"line_type": "mobile",
"line_type_source": "numbering_plan",
"risk_score": 30,
"risk_level": "medium",
"risk_signals": [
{
"signal": "international_network",
"weight": 30,
"dataset": "libphonenumber"
}
],
"ok_to_send": true,
"blocked_by": null
}
```
Related:
- [`premium_rate`](https://spaw.co/docs/signals/phone/premium_rate): a premium-rate number
- [`voip`](https://spaw.co/docs/signals/phone/voip): the numbering plan types the number as VoIP
- [`pager`](https://spaw.co/docs/signals/phone/pager): the numbering plan types the number as a pager
- [`shared_cost`](https://spaw.co/docs/signals/phone/shared_cost): a shared-cost number
- [The weights table in the phone guide](https://spaw.co/docs/phone-intelligence#signal-international_network)
- [Glossary: E.164](https://spaw.co/glossary/e164)
- [Glossary: SMS policy](https://spaw.co/glossary/sms-policy)
Reference: https://spaw.co/docs/signals/phone/international_network
## `shared_cost` (phone): a shared-cost number
The Spaw phone risk signal shared_cost adds 20 points to risk_score when the numbering plan types the number as shared cost, including the UK 084 and 087 ranges (dataset libphonenumber).
- Weight: +20
- Dataset: `libphonenumber`
- Read from: `line_type`
- Default SMS policy: does not block it on its own: +20 is within the default ceiling of 60, which blocks only a score above it.
A shared-cost number splits the charge between the caller and the called party and reaches a service rather than a person's handset. line_type answers shared_cost with line_type_source numbering_plan, and the dataset is libphonenumber.
In the United Kingdom Ofcom's numbering plan classes 084 and 087 as service numbers whose charge is capped, and keeps premium rate for 09. libphonenumber's metadata files 0842 to 0845 and 0870 to 0873 as premium rate, so Spaw types them from the plan instead.
**What it cannot tell you.**
- It reads the plan, not the tariff: it cannot tell you what a call or message costs.
- 0871 to 0873 still fall under Ofcom's premium-rate-services rules, and the default SMS policy, which blocks premium_rate, does not block them. Add shared_cost to blocked_line_types to refuse the whole type.
**What to do.** Refuse it as a contact number for codes with blocked_line_types shared_cost if you do not want service numbers. On its own it scores 20 and passes the default SMS policy, which blocks a score above 60; beside other signals the score can pass that.
The SMS policy is set per account with `PUT /api/v1/phone/policy` (https://spaw.co/docs/api/update-phone-policy) or the dashboard's SMS policy tab, and its `block_signals` may list any signal, this one included.
An excerpt of an answer carrying this signal alone, with the number and the other fields left out, because no test number answers it. The score and the decision are worked out as the lookup and the default SMS policy work them out.
```json
{
"valid": true,
"country": "GB",
"line_type": "shared_cost",
"line_type_source": "numbering_plan",
"risk_score": 20,
"risk_level": "low",
"risk_signals": [
{
"signal": "shared_cost",
"weight": 20,
"dataset": "libphonenumber"
}
],
"ok_to_send": true,
"blocked_by": null
}
```
Related:
- [`premium_rate`](https://spaw.co/docs/signals/phone/premium_rate): a premium-rate number
- [`voip`](https://spaw.co/docs/signals/phone/voip): the numbering plan types the number as VoIP
- [`pager`](https://spaw.co/docs/signals/phone/pager): the numbering plan types the number as a pager
- [`international_network`](https://spaw.co/docs/signals/phone/international_network): an international network or satellite number
- [The weights table in the phone guide](https://spaw.co/docs/phone-intelligence#signal-shared_cost)
- [Glossary: line type](https://spaw.co/glossary/line-type)
- [Glossary: SMS policy](https://spaw.co/glossary/sms-policy)
Reference: https://spaw.co/docs/signals/phone/shared_cost
## `reported_unwanted_calls` (phone): consumers reported unwanted calls from the number
The Spaw phone risk signal reported_unwanted_calls adds 20 points to risk_score when consumers filed 3 or more complaints with the FTC naming this North American number as the caller, on at least 2 different days within the last 90 days (dataset us-complaint-reports).
- Weight: +20
- Dataset: `us-complaint-reports`
- Read from: `complaints_90d`, `complaints_last_at`
- Default SMS policy: does not block it on its own: +20 is within the default ceiling of 60, which blocks only a score above it.
Spaw installs the FTC's Do Not Call reported-calls files, published each weekday, once a day, and keeps for every North American number the count of complaints over 90 days and the date of the latest. complaints_90d answers 0 when nobody complained, and null outside the North American plan or before the first sync. The consumer's own location is dropped when the file is read, and the dataset is us-complaint-reports.
It is what consumers told the FTC about the numbers that called them, not the Do Not Call Registry. It says nothing about whether you may call or text the number. And it is not proof of wrongdoing: the reports are unverified and caller ID is often spoofed, so an innocent number can collect complaints, which is why the weight is small.
**What it cannot tell you.**
- It says nothing about who holds the number today, and nothing about a number outside the North American plan.
- It is not a do-not-call scrub: checking a number against the Do Not Call Registry stays your obligation.
**What to do.** Read it beside the rest of the answer; do not refuse a number on it alone. On its own it scores 20 and passes the default SMS policy, which blocks a score above 60; beside other signals the score can pass that.
The SMS policy is set per account with `PUT /api/v1/phone/policy` (https://spaw.co/docs/api/update-phone-policy) or the dashboard's SMS policy tab, and its `block_signals` may list any signal, this one included.
An excerpt of an answer carrying this signal alone, with the number and the other fields left out, because no test number answers it. The score and the decision are worked out as the lookup and the default SMS policy work them out.
```json
{
"valid": true,
"country": "US",
"line_type": "fixed_line_or_mobile",
"line_type_source": "numbering_plan",
"risk_score": 20,
"risk_level": "low",
"risk_signals": [
{
"signal": "reported_unwanted_calls",
"weight": 20,
"dataset": "us-complaint-reports"
}
],
"complaints_90d": 4,
"complaints_last_at": "2026-09-18",
"ok_to_send": true,
"blocked_by": null
}
```
Related:
- [The weights table in the phone guide](https://spaw.co/docs/phone-intelligence#signal-reported_unwanted_calls)
- [Where each fact comes from, in the phone guide](https://spaw.co/docs/phone-intelligence#sources)
- [Glossary: SMS policy](https://spaw.co/glossary/sms-policy)
Reference: https://spaw.co/docs/signals/phone/reported_unwanted_calls
---
# Pricing
Spaw bills per answer. A fresh lookup with a usable answer costs 1 credit, and one credit covers five IP lookups; invalid input, email answers whose mailbox check was uncertain, undeliverable verdicts within fair use, repeats within 7 days, and rejected requests cost nothing. Every account receives 100 free credits at the start of each month with no card on file, plus a one-off bonus of 500 credits when the account confirms its email address, and unused free credits accumulate.
Paid checkout is not open yet. Every account starts with the free credits, and they work on every product today.
## Plans
Every plan is billed monthly, or yearly for the price of ten months; a yearly plan still deposits its credits once a month, not all at once. A pack is the same credits bought once. Plans and packs land in the same balance, and plan credits are spent first. Prices in US dollars.
| Tier | Credits a month | Monthly | Per 1,000, monthly | Yearly | Yearly, per month | Per 1,000, yearly | Pack, once | Support |
| --- | --- | --- | --- | --- | --- | --- | --- | --- |
| Free | 100 | $0 | none | none | none | none | none | Email support |
| Starter | 1,000 | $7 | $7.00 | $70 | $5.83 | $5.83 | $9 | Email support |
| Growth | 5,000 | $29 | $5.80 | $290 | $24.17 | $4.83 | $39 | Email support |
| Scale | 25,000 | $119 | $4.76 | $1,190 | $99.17 | $3.97 | $149 | Priority support |
| Volume | 100,000 | $349 | $3.49 | $3,490 | $290.83 | $2.91 | $449 | Priority support |
| Pro | 250,000 | $749 | $3.00 | $7,490 | $624.17 | $2.50 | $899 | Priority support |
| Business | 500,000 | $1,249 | $2.50 | $12,490 | $1,040.83 | $2.08 | none | Priority support |
| Enterprise | 1,000,000 | $1,999 | $2.00 | $19,990 | $1,665.83 | $1.67 | $2,499 | Priority support |
Plan credits roll over up to three months' allowance and stay usable for 60 days after you cancel; pack credits never expire. Priority support puts a request at the top of the support queue; no tier carries a response-time promise.
## More than 1,000,000 credits a month
There is no published price above the largest tier, and we would rather not print one nobody has agreed to. Tell us the monthly volume, the signals you need and how the rows arrive (API, batch or bulk files), and we will work out what it costs.
Ask about volume pricing: https://spaw.co/support?topic=volume
## Included on every account
Every account, free or paid, gets every product and every response field. The tiers differ in credits, price and support, and in nothing else. One balance and one key cover all of it.
- **Batch and bulk.** Batch requests for email, phone, IP, address and business lookups. Bulk jobs of up to 100,000 rows for email, phone, IP, address and consistency, sent through the API or uploaded as a CSV, with a signed webhook when a job finishes.
- **Monitors.** Saved lists of emails, domains, phone numbers, IP addresses, postal addresses or company identifiers, checked again on a schedule, with an email when an answer gets worse.
- **Suppression lists.** For email, phone and postal addresses. Import your own, and the bounces and failures you report are added for you.
- **Outcome feedback.** Report what happened after an email, SMS, IP or address answer, and later answers take it into account.
- **Sending and shipping policies.** Your own rules behind ok_to_send on email and phone answers and ok_to_ship on address answers.
- **MCP server.** Every product as a tool for AI agents, on the same key and balance. https://spaw.co/docs/mcp
- **API keys.** Per-product scopes, IP allowlists and daily credit caps on secret keys, and publishable keys for browser forms that only work from your own domains.
- **Free repeats.** The same lookup again within 7 days costs nothing.
- **Status page.** Uptime, and the freshness of every dataset behind the answers. https://spaw.co/status
- **Rate limits.** The same on every tier: 50 requests a second for IP, phone, address and business lookups, and 5 for email, domain and consistency.
## What costs a credit
| Outcome | Credits |
| --- | --- |
| Fresh lookup with a usable answer | 1 credit |
| Five fresh IP lookups with a usable answer | 1 credit |
| Email answer whose mailbox check was uncertain (unverified) | 0 |
| Undeliverable email verdict within the fair-use allowance | 0 |
| Undeliverable email verdict past the fair-use allowance | 1 credit |
| Invalid input or no match | 0 |
| Repeat of a recent lookup, within 7 days | 0 |
| Rejected or failed request | 0 |
| Monthly free grant, every account | +100 |
Undeliverable email verdicts that needed a mailbox check are free up to a fair-use allowance of 500 in any 30 days, or the number of deliverable and risky email answers billed in those 30 days when that is more. Past the allowance each one costs 1 credit and its meta.fair_use_billed is true. Verdicts settled without a mailbox check (bad syntax, no working mail server, a username the provider never allows) and repeats are always free and never counted.
IP lookups are billed in blocks of five: the answered IP lookup that opens a block costs 1 credit (`credits_used` 1) and leaves four prepaid on the account, which the next four answered IP lookups use up at `credits_used` 0; `meta.ip_lookups_prepaid` says how many are left. Reserved, private and unallocated ranges and repeats within 7 days are free and never use one, and a ledger row for `ip_lookup` stands for five IP lookups.
### What each product charges for
| Product | Endpoint | Costs a credit | Always free | Page |
| --- | --- | --- | --- | --- |
| Email verification | `POST /v1/email` | 1 credit for a deliverable or risky verdict, or for an undeliverable one past fair use | Unverified answers, invalid syntax, a suppressed address, undeliverable verdicts within fair use | https://spaw.co/products/email-verification |
| Phone intelligence | `POST /v1/phone` | 1 credit for a valid number | Invalid numbers and unassigned blocks | https://spaw.co/products/phone-validation |
| IP intelligence | `POST /v1/ip` | 1 credit per 5 answered lookups | Reserved, private and unallocated ranges | https://spaw.co/products/ip-intelligence |
| Address verification | `POST /v1/address` | 1 credit for a valid address | Invalid addresses, and requests without a country | https://spaw.co/products/address-verification |
| Business verification | `POST /v1/entity` | 1 credit when a register answers | Malformed or unknown identifiers, and registers we do not carry yet | https://spaw.co/products/business-verification |
| Domain intelligence | `GET /v1/email/domain/{domain}` | 1 credit when the domain accepts mail | Dead and invalid domains | https://spaw.co/docs/api#email-domain |
| Cross-signal consistency | `POST /v1/consistency` | The sum of the signals you asked for | The comparison itself, always | https://spaw.co/products/cross-signal-consistency |
## Optional add-ons
| Add-on | Signal | Ask with | Credits in all, when it answers | Status |
| --- | --- | --- | --- | --- |
| Live carrier-network check | Phone intelligence | `hlr: true` | 5 | Off |
| Postal deliverability check | Address verification | `deliverability: true` | 4 | Off |
Both need a licensed partner and neither is enabled on the service; while they are off they add nothing and the response says the check did not run rather than guessing.
## Changing or cancelling a plan
| Change | What happens |
| --- | --- |
| A bigger monthly plan | Charged today. Its credits are deposited at once, and the monthly cycle restarts from that day. |
| A smaller monthly plan | Takes effect at the next renewal, when plan credits above three months of the smaller plan's allowance expire. |
| Monthly to yearly | Any time. The year is charged and starts that day. |
| A bigger yearly plan | The new year starts today and the unused part of the old one is credited from that day. That credit covers the rest of the current month, so the same share of that month's plan credits expires. |
| A smaller yearly plan | Takes over at the renewal. To move up while one is waiting, go back to the plan you paid for first, at no charge, then move up. |
| Yearly to monthly | Not available from the dashboard. |
| Cancelling | Stops future charges at the end of the paid period. Nothing already deposited is taken back, and plan credits stay usable for 60 days after the plan ends. |
## Questions
**Do credits expire?** Pack credits never expire; plan credits roll over up to three months' allowance and stay usable for 60 days after you cancel.
**What happens when the balance reaches zero?** The API answers a typed 402 INSUFFICIENT_CREDITS before the lookup runs, so nothing is billed behind your back. Lookups work again once the balance is topped up or the next monthly grant lands.
**Can the balance top itself up?** Only if you ask it to. Automatic top-ups are off on every account. Switching them on from the Billing page means naming the pack to buy, the balance to act on, and a ceiling in dollars for the calendar month; an account on a plan buys a block the size of its plan at the plan's rate instead, and those credits never expire either. We never charge past the ceiling: once it is reached, lookups answer the same 402 as any empty balance. Every automatic charge is emailed to you and appears on the Usage page.
**Is a plan required?** No. The monthly free grant needs no card, and a pack is a one-time purchase. A plan deposits its credits every month at a lower price per credit than the pack of the same size, billed monthly or yearly, and can be cancelled at any time.
**Do batch, bulk and the MCP server cost differently?** No. Every channel bills the same way: 1 credit per fresh answered lookup, five IP lookups to the credit, with the same free answers and the same fair use. Batch, bulk, monitors, the dashboard, the form widget and the MCP server all follow that rule.
Reference: https://spaw.co/pricing
---
# About Spaw
Spaw is a verification API at spaw.co: email, phone, IP, postal address, business identity and a cross-signal consistency check, on one key and one balance.
Updated: 2026-10-14
Spaw is a verification API, launched in 2026 at spaw.co. You send an identifier someone typed into a form — an email address, a phone number, an IP address, a postal address, a business number — and the API answers what it is, why it says so, and how much to trust the answer. Six signals run off one key and one credit balance. The same engine sits behind a dashboard, bulk uploads, a browser form widget, and an MCP server for AI agents.
## The six signals
- **Email verification** (`POST /v1/email`). Whether mail to an address will arrive. The address is parsed against RFC 5322 and then against the username rules of the big providers, so `a@gmail.com` is rejected the way Gmail itself would reject it. MX records are resolved at query time: a domain with no MX record but an A record is reported as implicit MX, a null MX record as a domain that declines all mail, MX hosts pointing at dead or private addresses as unresolvable. SPF and DMARC records are read and the DMARC policy is reported. The domain is matched against the open disposable-provider list, including domains whose mail is handled by a listed operator, and the local part against the open role-address list. A curated map catches `gmail.con` and friends and suggests the intended domain. Finally an SMTP handshake asks the receiving server whether the mailbox exists; the conversation ends before any message is transmitted, and nothing is ever sent to an address you check. 46 fields, including what the username and the domain look like, whether the mail hosts sit on a threat blocklist, and two signals read from traffic rather than lists.
- **Phone intelligence** (`POST /v1/phone`). Any number normalised to E.164, national, international and `tel:` forms, with its line type and carrier, each naming the source that decided it, and the regulator's own block status read from the regulators' allocation tables for the United States, Canada, the United Kingdom, France, Australia, the Netherlands, New Zealand, Mexico, Poland and Italy. Fictional ranges, disposable numbers and virtual services are flagged, and every answer ends with your own phone policy's `ok_to_send` decision. 49 fields.
- **IP intelligence** (`POST /v1/ip`). Where an address is, who runs it, and whether it is a visitor at all: the network and its ASN, country facts with the sanctions programmes and FATF list naming the country, the time zone down to the nearest place, datacenter and cloud ranges, corporate gateway egress, Tor exits with a thirty-day history, named VPN and relay exits, mobile and satellite operators, verified crawlers named individually, the public blocklists an address appears on, and your own allow and deny lists. 62 fields.
- **Address verification** (`POST /v1/address`). A postal address read against its own country's format, ending in a decision — `ok_to_ship` and `blocked_by` from your own shipping policy — and answering whether the street or town is a placeholder somebody typed to get past the form: the parsed parts, the kind of delivery point it names — street, PO box, private mailbox, general delivery, rural route, military, parcel locker, bulk mail — whether the postcode exists and is live or terminated and whether the town and region written on it belong to that postcode, whether the building appears in a national address register and where it stands, what the register says it is used for, how many companies are registered there and whether the address is a mailbox operator's branch or a registered agent's, and which parts of the address a dataset stood behind and what was normalised. Nothing in the answer says who lives or works at an address. 61 fields.
- **Business verification** (`POST /v1/entity`). A Legal Entity Identifier or a UK company number read against open registers synced onto this server: the legal name, whether the entity is still live in the register's own wording, its country, jurisdiction and incorporation date, and for a LEI its check digit and registration status. Where both registers hold one entity they are compared field by field, with both sides quoted. Spaw is not affiliated with either register. 20 fields, and no registered address, deliberately: an identifier does not say whether a company or a sole trader stands behind it.
- **Cross-signal consistency** (`POST /v1/consistency`). Any subset of the four above in one call. Each signal answers exactly as its own endpoint, and on top of that the agreements and disagreements between them are named with the fields that decided each one, both sides quoted. There is no score, no risk level, no verdict and no recommendation, and there will not be one: a difference between two fields is a fact about the fields, not a judgement about a person.
Domain intelligence asks the same questions about a whole domain rather than one address, at `GET /v1/email/domain/{domain}`. It bills on its own but belongs to the email product.
## What every answer has in common
- **The response names its data sources**, each with a version or a sync date, so you can see exactly what the answer was based on.
- **Null means "not evaluated", never "no".** A field Spaw could not check comes back null and says why, rather than guessing a plausible value.
- **A risk score you can audit.** Four of the six — email, phone, IP and address — carry a 0 to 100 score that is a published sum of weights, one per signal, not a black box. Business verification has no score, and neither does the consistency check.
- **Two premium checks are off, and say so.** A live carrier check on a phone number and a deliverability check on a postal address both need a licensed partner. Until one is configured they add nothing to the bill and the response reports that the check did not run.
## How billing works
You pay for answers, not attempts. What counts as an answer differs by signal — the [pricing page](/pricing) carries the rate card, one row per endpoint — but the rule is the same everywhere:
- A fresh lookup that comes back with a usable answer costs 1 credit, and one credit covers five IP lookups.
- A non-answer is free: an invalid number, a reserved IP range, an address that cannot stand, a malformed business identifier, an email answer whose mailbox check was uncertain. Undeliverable email verdicts are free within a fair-use allowance.
- Repeating a lookup within 7 days is free.
- Every account gets 100 free credits at the start of each month, with no card required, and unused free credits accumulate.
- Pack credits never expire; plan credits roll over up to three months' allowance and stay usable for 60 days after you cancel.
## What we hold ourselves to
- **Full data everywhere.** The free tools show the same fields as the paid API.
- **No data sales.** What you submit is used to answer your query and for nothing else. Only email verification keeps a history row, and it is deleted after 30 days; phone, IP, address and business lookups write no history and no log line carrying the value. The [privacy policy](/privacy) and the [security page](/security) spell this out.
## Channels
- REST API at `https://spaw.co/api/v1`, documented in the [API reference](/docs/api)
- The dashboard, with a playground for every signal, history, bulk runs, monitors, and suppression lists
- Bulk uploads of up to 100,000 email addresses, phone numbers, IP addresses or postal addresses, with signed completion webhooks
- A browser form widget that checks email addresses and phone numbers as people type
- An [MCP server](/docs/mcp) whose twelve tools let AI agents run the same lookups with your API key, one value or a list at a time
## Open data we build on
Spaw credits the open datasets it relies on: Google's libphonenumber, the regulators' number-block tables (NANPA's central-office code, thousands-block and NPA reports, CNAC, Ofcom (Ofcom copyright, reused with acknowledgement), ARCEP under the Licence Ouverte / Open Licence, ACMA, ACM, New Zealand's NAD, Mexico's CRT (formerly the IFT), Poland's UKE — Urząd Komunikacji Elektronicznej, whose tables Spaw retrieves weekly and processes into block answers — and Italy's MIMIT, credited with a link to its numbering page as its legal notes ask), the Federal Trade Commission's Do Not Call reported-calls files (a US Government work), the SMS-receiving number lists of iP.1 Networks and Castle (both MIT), DB-IP Lite, the disposable/disposable-email-domains list, the mixmaxhq role-based address list, and the Kikobeats free-email-domains list. IP lookups also read the Tor Project's exit list and Onionoo relay data, X4BNet's datacenter and VPN lists, the published IP ranges and geofeeds of AWS, Google Cloud, Microsoft Azure, Oracle Cloud, DigitalOcean, Linode, Vultr, Cloudflare and Fastly, GitHub's hosted runner ranges, the server lists of Mullvad, NordVPN, Private Internet Access, IVPN, AirVPN and the VPN Gate project, Apple's iCloud Private Relay egress list, the cloud enforcement node ranges Zscaler publishes, and the threat blocklists Spamhaus DROP and ASN-DROP (© The Spamhaus Project, used with the credit its terms ask for) and abuse.ch's Feodo Tracker (CC0), the attack-source lists of Emerging Threats (from its BSD-licensed open ruleset), CINS and blocklist.de, the ranges Google, Microsoft, OpenAI, Perplexity, DuckDuckGo and Ahrefs publish so sites can verify their crawlers, the RFC 8805 geofeeds network operators publish about their own ranges, and the GeoNames list of populated places (CC BY 4.0) for time zones by nearest place. The sanctions and FATF facts come from a curated file that names the OFAC programme list and the FATF plenary it was checked against.
Address verification reads the country address formats from Google's Address Data Service (© Google LLC, CC BY 4.0), first imported through commerceguys/addressing (MIT), and the national address registers: the US National Address Database and the Census Bureau's TIGER/Line address ranges (both public domain), the Base Adresse Nationale (IGN/DINUM, Licence Ouverte 2.0), the Swiss official directory of building addresses (© swisstopo), the Czech RÚIAN from ČÚZK, the Norwegian Matrikkelen from Kartverket and Latvia's State Address Register from the Valsts zemes dienests (all CC BY 4.0), the Austrian address register (CC BY 4.0, credited as © Österreichisches Adressregister, Stichtagsdaten vom DD.MM.YYYY — the reference date of the installed snapshot, which the status page lists as the at-adressregister version), Belgium's BeST Address registers of Flanders, Wallonia and Brussels republished by FPS BOSA (CC BY 4.0), Luxembourg's BD-Adresses from the Administration du cadastre et de la topographie (CC0), Danmarks Adresseregister (DAR) from Klimadatastyrelsen (CC BY 4.0), New Zealand's street addresses from Land Information New Zealand (CC BY 4.0, where a LINZ key is configured), and — incorporating or developed using G-NAF © Geoscape Australia, licensed by the Commonwealth of Australia under the Open Geo-coded National Address File (G-NAF) End User Licence Agreement — the Australian address file, which is never used to build a mailing list. The Netherlands' Basisregistratie Adressen en Gebouwen (BAG) comes from Kadaster (CC0 1.0), and Brazil's addresses from IBGE's Cadastro Nacional de Endereços para Fins Estatísticos, the 2022 census address list (public data). Three German Länder publish their own building registers: Berlin's addresses from Geoportal Berlin (Amt für Statistik Berlin-Brandenburg) and North Rhine-Westphalia's Gebäudereferenzen from Geobasis NRW, both under Datenlizenz Deutschland – Zero 2.0, and Rhineland-Palatinate's Hauskoordinaten © GeoBasis-DE / LVermGeoRP under Datenlizenz Deutschland – Namensnennung 2.0. US ZIP codes gain the places whose land they cover from the Census Bureau's ZCTA relationship files (public domain). Great Britain's streets and the places its postcodes are written with come from Ordnance Survey's Open Names (Open Government Licence v3.0, containing OS data © Crown copyright and database right 2026). Postcodes come from the ONS Postcode Directory, which contains ONS, OS and Royal Mail data © Crown copyright and database right 2026 under the Open Government Licence v3.0 — Northern Irish postcodes are excluded, because reusing them needs a Land & Property Services licence — and, outside Great Britain, from the GeoNames postal-code dump (CC BY 4.0). Companies registered at an address are counted from the Companies House free company data product (Open Government Licence v3.0), INSEE's Sirene base (Licence Ouverte 2.0), the Brønnøysund Register Centre's Enhetsregisteret (NLOD 2.0) and the Colorado, New York, Oregon, Connecticut and Hawaii business registries (the states' own open data portals); mail drops from the branch lists mailbox operators publish themselves; and registered-agent addresses from the agents' own published addresses and the public filings that name them (US state listings and filings, and Companies House filings). The other names a town or a one-city region is written under come from the GeoNames list of populated places (CC BY 4.0), and the counties an Irish Eircode routing key reaches are derived from the Residential Property Price Register (© Property Services Regulatory Authority, under the PSRA's re-use terms; read 24 September 2026).
Business verification reads the GLEIF Level 1 golden copy of Legal Entity Identifier reference data (CC0 1.0) and the same Companies House free company data product (Open Government Licence v3.0). Both are downloaded and queried here, so no third party sits in the request path — and neither register provides, supports or authorises the answer Spaw gives.
## Contact
Questions, partnership requests, and press: [support@spaw.co](mailto:support@spaw.co).
---
# Security and data handling
How Spaw protects API keys, what each of the six lookups stores and what it does not, and how long data is kept.
Updated: 2026-09-25
Spaw is a verification API: the data you send is mostly other people's — their email addresses, their phone numbers, their IP addresses, where they live. This page states, specifically, what happens to it, one product at a time, because the answer is not the same for all six. It only describes measures that are in place today. Its companion is [/trust](https://spaw.co/trust), which covers the company rather than the lookups: the certifications Spaw does not hold, who processes what and where, the standing data processing addendum — which used to be one line on this page saying it was available on request — the answers a security questionnaire asks for, and where to send a vulnerability.
## Transport
All traffic to spaw.co, including the API, the dashboard, the form widget, and the MCP server, is served over HTTPS. Plain HTTP requests are redirected.
## Calling the API from a browser
The API answers cross-origin requests from any origin, and the browser endpoints authenticate with a publishable key plus an `Origin` check against the list you configure — so the allowlist that matters is the one on your own key, not one held here.
Six response headers are exposed to browser JavaScript, because the documentation tells you to read them: `X-Request-Id` (quote it to support), `X-RateLimit-Limit`, `X-RateLimit-Remaining` and `X-RateLimit-Reset` (what to back off on), `Retry-After` (what a 429 says to wait) and `Idempotent-Replayed` (whether a repeated create was the original answer replayed rather than a second job). Preflight responses are cacheable for 24 hours, so a widget that posts JSON pays one preflight rather than one per lookup.
No cookie ever authenticates the API — it is a bearer token, or a publishable key in the body — so credentialed cross-origin requests are not accepted. Only `/api/*` and Sanctum's CSRF-cookie route are cross-origin readable at all; the dashboard and the marketing pages are not.
## API keys
- **Secret keys** (`sk_live_…`) are shown once at creation and stored hashed. Spaw cannot show a secret key again; if you lose it, create a new one and revoke the old one. Revocation is immediate.
- **Key scopes.** A secret key can be limited to any of the seven scopes — `email`, `phone`, `ip`, `address`, `entity`, `consistency`, `account` — and a call outside them is refused with `403 KEY_SCOPE_DENIED` before any work runs. A key can also carry an allowlist of source addresses or CIDR ranges, and a daily credit cap that stops a batch part-way through rather than letting one request spend past it.
- **Publishable keys** (`pk_…`) are made for the browser and are stored and displayed in plain text by design. They can run the four browser lookups — email, phone, IP and postal address — only from the origins you list, and can be limited with a daily credit cap. Their responses deliberately carry no `meta` block, so a page visitor never sees the account's credit balance. A key can also require a Cloudflare Turnstile token per lookup, which stops scripted abuse without ever blocking a real form submission.
- **Rate limits.** Each product family has its own bucket, so a burst of one never starves another: 5 requests per second per key on the email and consistency endpoints, which do DNS and a mailbox handshake, and 50 per second on the phone, IP, address and business endpoints, which read local data. The browser endpoints allow 20 requests a minute per address, and the free tools have their own per-visitor throttles.
- **Request ids.** Every response carries an `X-Request-Id` header, repeated inside the JSON body, so a support question can be matched to the exact request. The request log behind it records the endpoint, the status, the error code and the cost — never the value you looked up.
## The addresses you verify
- An address is used to answer your query and for nothing else. Spaw does not sell submitted data, build shared datasets from it, or contact the addresses.
- No message is ever sent to an address you check. The mailbox check is an SMTP handshake that ends before any message data is transmitted.
- Only addresses that reach the mailbox step are sent to the mailbox verification partner. Addresses at disposable domains, typo-squat domains, and domains with no working mail server are answered from DNS and open lists alone and never leave Spaw.
- Email verification is the one product that keeps a history row: the address, its verdict, reason and risk score, and what it cost. It is your own usage log, it is deleted after 30 days, and it is what the dashboard's history, exports and bounce-feedback matching read.
- Bounce and complaint outcomes you report are your own account's record, with one deliberate exception: the share of reported sends that bounced is counted per **domain** across every account, and a domain says nothing until it has at least five outcomes. What crosses accounts is that ratio for a domain, never an address and never who reported it.
- The six `@spaw.test` test addresses answer fixed verdicts and are never logged.
## Phone, address and business lookups
- **Nothing about the lookup is stored.** No history row, no log line carrying the number, the address or the identifier. What a lookup leaves is keyed and never holds the value: a seven-day marker that makes a repeat of the same value free (a phone number whose live check you paid for gets a second one, for the check); for a phone number, the traffic counts in the next point; and, when a live carrier check runs, the network's answer, held for a day under a keyed hash of the number so a repeat does not ask again.
- **Your own phone traffic is counted, not recorded.** For single and browser phone lookups, each number is kept as a truncated keyed hash under the account's own key, filed under keyed hashes of its 1,000-number range and of the client address the caller sent, for 60 minutes; and the account's lookups per country and hour are kept as counts, with no number, for 30 days. They answer the account's own `range_seen_by_you`, `client_velocity` and `destination_new_for_you` and its owner's pumping and enumeration alerts, and are never read for another account. Erasing a number takes it out of every range count.
- **They are answered on the server.** A phone number is answered from libphonenumber and the regulators' allocation tables held here, except the opt-in live carrier-network check, which sends that one number to a carrier-network partner; a postal address from the format table, the postcode directory, the national address registers and the open company registers synced here; a business identifier from register copies downloaded and queried here. Apart from that one opt-in check, no third-party processor sees any of the three, and the `sources` block of every response names each dataset and the date it was synced.
- **The premium checks are off.** A live carrier check on a phone number, a deliverability check on a postal address, and listing the addresses at a postcode would each send the value to a licensed partner. None is configured, so none runs: the response says the check did not run rather than guessing, and nothing is sent anywhere.
- **Two public datasets name phone numbers, and they are Spaw's own.** The complaints the US Federal Trade Commission publishes about unwanted calls are downloaded each weekday and kept for 90 days: the number named as the caller, the day, whether it was a recorded message and the subject, never anything about the consumer who complained. Three on two different days raise that number's risk for every account, although caller ID is easily spoofed. And the numbers public SMS-receiving websites publish are downloaded daily; one the sites stop showing is kept for up to 730 days after it was last seen, scoring as disposable for the first 90 of them (a number the lists still carry scores as disposable for 365 days after it was last seen), and as a weaker stale listing after that. Erasing a number deletes its complaints and any listing the sites no longer publish.
- **Bulk runs** keep the list you uploaded and the result file written from it as the only copies of what you sent; both are deleted with the job after 30 days.
- **Monitored lists** are the one place a value is stored on purpose, because re-checking it later is the whole point: a saved list holds its numbers, addresses or identifiers and each one's most recent answer. A list nobody runs any more is deleted along with everything it stored, 365 days after its last run; a list in active use is never touched, because every run resets that clock. Every monitor payload reports its own `prunes_after_days`.
- **Outcome reports** you send about a phone number or a postal address are stored per account, hold the value you reported, and are pruned after 180 days. They shape your own later lookups. One exception, narrow and a count only: once three or more separate accounts report abuse for the same number inside 30 days, that number's risk rises for everyone, and what other accounts read is the number of accounts, never the reports. Address feedback has no such exception and never will: a phone number is a handset and an IP is a network endpoint, but a postal address is where somebody lives, and a shared "post fails here" database would be a database about residences.
- **Business verification returns no registered address**, deliberately. An identifier does not say whether a company or a sole trader stands behind it, so the product answers what the register says about the entity and nothing about where it can be found.
## IP lookups
- An IP address is personal data and is handled as such. Nothing about an IP lookup is stored: no history row, no log line with the address. The traces are a keyed seven-day marker that makes a repeat of the same address free, and, under the same hash, the count behind `seen_by_you`.
- `privacy: true` on a lookup, or `?privacy=1` on a GET, skips both. Every such call is a fresh, billable lookup with no trace kept.
- IP lookups never leave the server. Every signal is a local database or a compiled feed, so no third-party processor sees the address. The one exception is opt-in: asking for `abuse_contact` sends the address to the Internet registry's public RDAP service (via rdap.org) to read the network's abuse mailbox, and that answer is cached per allocation. The `sources` block of every response names each dataset consulted and the date it was synced.
- Bulk IP runs keep the input list and the result file as the only copies of what you sent; both are deleted with the job after 30 days.
- Outcome reports you send to `POST /api/v1/ip/feedback` are the one thing stored on purpose, and even they never hold the address: each is a salted SHA-256 hash of the canonical address with the outcome and a timestamp, kept for 90 days and then pruned. Your reports shape only your own later lookups, except that once three or more accounts report the same address its risk rises for everyone; what other accounts read is that count alone, never the reports.
## Cross-signal consistency
A consistency call stores exactly what its legs store and nothing more: the email leg writes its history row, the phone, address and IP legs write nothing. The comparison itself touches no dataset, no database and no network — it is arithmetic over fields the four pipelines already computed, which is why it is free — and its result is returned to you and not retained.
## Retention
- **Email lookup history** is kept for 30 days, then deleted automatically. Bulk job files, for every product, follow the same window, and so does the API request log.
- **Repeat-lookup markers** last 7 days; they are what makes a repeat free.
- **Phone velocity counts** last 60 minutes and the per-country counts of phone lookups 30 days; neither holds a number.
- **The FTC complaint data** is kept for 90 days from the day each complaint was filed, and **a disposable-number listing** for up to 730 days after a public list last showed the number. Both are public data about numbers, not anything you sent, and both go with an erasure of the number.
- **Monitored lists** are deleted 365 days after their last run, with everything they stored. A list still being run is never pruned.
- **Suppression lists and feedback** are stored per account. A suppression entry you imported, or one recording a spam complaint, an opt-out or an abuse report, stays until you remove it (an erasure of the number also deletes an abuse entry you did not import); one a lookup or your reports of failed deliveries added also leaves when a later answer or report clears it. Suppression entries a lookup or your reports of failed deliveries added are re-verified rather than trusted once they are older than 90 days; imports, complaints, opt-outs and abuse reports never go stale. Outcome reports are pruned after 180 days, except IP reports, which are hashed and pruned after 90.
- **The IP addresses your own account has been seen from** — a signed-in browser or an API key — are kept for 180 days from when each was last seen, and are the one address store held in the clear rather than hashed. They are a security record read by staff when somebody writes in about their account, never shown to another customer and never joined to the addresses you verify. The privacy policy carries the full entry.
- **The credit ledger** is kept for the life of the account as your billing record.
- **Deleting your account** from the settings page removes the account and everything attached to it: keys, history, jobs, monitors, suppression lists and feedback. Two parts of that were only true on paper until 2026-09-09. Each bulk run's uploaded list and result file is now deleted from disk with the account — a database cascade fires none of the application's own cleanup, so those files were being orphaned with no row left pointing at them, and the files are the personal data, not the rows. And secret API keys are now destroyed with the account too: their table is polymorphic rather than a foreign key, so it did not cascade, and rows holding a key's name, its token hash, its scopes and the allowlist of a customer's own server addresses were surviving the account indefinitely. Any live subscription is cancelled at the same time, so a closed account stops being charged.
## What the API promises, and what it does not
The OpenAPI document at [/openapi.yaml](https://spaw.co/openapi.yaml) is the contract. Not a description of it — the contract itself: a test fails the build if a route exists that the document does not describe, if an operation describes a route that does not exist, or if a typed error code appears in one and not the other. What you read there is what runs.
**Additive changes ship without notice, and your integration has to tolerate them.** New endpoints, new fields on an existing response, new values in an enum, new typed error codes, and new optional request parameters are all additive, and all of them will happen. A client that rejects an unknown field, or that treats an unrecognised `reason` or `error.code` as a crash rather than as a value it has not seen yet, will break on a change that breaks nobody else. Match on the codes you handle and let the rest fall through.
**Breaking changes get a new path.** Removing a field, changing the type or meaning of one, renaming an enum value, tightening validation, or changing what an endpoint costs is a breaking change, and it does not happen to `/api/v1`. It happens at a new version prefix, with both live at the same time.
**Nothing is deprecated today.** When something is, it will be marked deprecated in the OpenAPI document, keep working, carry a `Sunset` header naming the date it stops, and be announced in the [changelog](https://spaw.co/changelog) — which has a permalink per release and an [Atom feed](https://spaw.co/changelog.atom), so you can follow it without watching the page.
## Availability
There is no numeric uptime commitment, and we would rather say so than publish a figure we cannot stand behind.
What there is, is the measurement. [/status](https://spaw.co/status) publishes, live and without authentication: the state of every component behind an answer, ninety days of measured availability, the age and version of every dataset the answers are read from, and the median and 95th-percentile latency per endpoint group over the last hour and the last day, with the success rate beside them. Below those, dated rather than live, it publishes what the weekly quality measurements found. `GET /api/v1/status/data` returns everything in the first list as JSON, unauthenticated and free, so you can watch it from your own monitoring rather than trusting a page; the quality measurements are page copy and are deliberately not in that payload, because they are a weekly figure and a monitor polling for one would be polling for last Sunday.
Three things are worth knowing about how that is reported. The percentiles cover requests we actually answered: a refusal that never reached a lookup — an empty balance, a rate limit, a validation failure — returns in a millisecond or two and is not evidence that anything is fast, so those are counted in the success rate instead. And a component nobody has switched on reads as not configured rather than degraded, so an optional check that is off does not pretend to be an outage.
The third is the ninety-day availability figure, and it is the one to read carefully. It is counted from our own timings — one anonymous row per lookup request, kept a week and rolled up into a daily count before it is deleted — and for each UTC day it reports the share of the requests we recorded that were not answered with a fault on our side. Refusals stay out of it on the same grounds as the percentiles. The limits are worth stating plainly, because they are the limits of any measurement taken from inside. It only counts requests that reached this server and were recorded. An outage that took the service off the network records nothing at all: if it lasted a whole day, that day is blank and a blank day is never counted as a good one — but if it lasted an hour, the other twenty-three are all that is left, and the day can read as a clean 100%. A partial outage is the common case, and this figure is close to blind to it. A fault that also stops the recording write — a database incident, which is the likeliest cause of a real burst of 5xx — takes those failures out of the figure along with everything else, because the recorder is allowed to fail silently rather than break a request. So read it as a floor on the bad days rather than as proof of a good one. It is a measurement of the requests we answered, not a claim about the time we were reachable, it is not an SLA, and the incident log is where an outage is actually described, because a person writes that.
The quality measurements are a different kind of number again, and the same care applies. Two of them live only on that page. The first pools the delivery outcomes customers reported back to us against the verdicts we had given those addresses, and it is not an accuracy rate — we do not have one. It counts what customers chose to send us, and a bounce breaks a send and gets filed while a delivery is silence, so it is biased upwards by an amount nobody can measure; it is blind in the other direction too, because an address we called undeliverable that would have been fine is never sent to and never reported. So it is published beside the thing that shows how small a slice it is: how many addresses were verified in the same window, and how many of those same addresses anyone came back about. No share is published at all until at least a hundred reported outcomes stand behind that verdict, from at least three accounts, with no single account more than half of them — a pooled figure drawn from one customer's rows is that customer's mail performance on our website, whatever we call it. The second measurement takes identifiers out of each installed business register's own file and asks for them back through the ordinary lookup: it is a check on our own plumbing, it says nothing about whether a register is complete or right, and a register whose file measures nothing is named as unchecked rather than scored. The other three products' measurements are published in full on their own product pages, so each figure lives in one place.
Incidents are written up by hand at [/incidents](https://spaw.co/incidents), with an [Atom feed](https://spaw.co/status.rss) if you would rather subscribe than watch a page. That history carries what we found and recorded, which is the same limit as above said another way.
If a dependency we do not run is down, the component that uses it says so on that page rather than the answer quietly getting worse.
## Reporting
Report a security issue, or ask anything about how your data is handled, at [support@spaw.co](mailto:support@spaw.co). A report is acknowledged by a person within five business days; the disclosure policy is on [/trust](https://spaw.co/trust#reporting-a-vulnerability) and machine-readable at [/.well-known/security.txt](https://spaw.co/.well-known/security.txt).
If Spaw holds something about **you** and you never signed up here — because a customer verified your address, or reported that a message to it bounced — the erasure form at [/privacy/data-request](https://spaw.co/privacy/data-request) deletes it, and names beside the form the few things it leaves alone, such as a customer's own import of the address. It confirms by mail to the address and runs immediately.
---
# Press and brand kit
How to describe Spaw, the facts that stay true, and the logo files. For directories, listings, articles, and anyone writing about the verification API.
Updated: 2026-10-02
Everything on this page may be quoted as is. If a fact here disagrees with an older mention elsewhere, this page is the current one.
## One-line description
Spaw is a verification API for email, phone, IP, postal address and business identity: one key, one credit balance, and answers that name their reasons and their data sources.
## Short description
Spaw verifies the identifiers people type into forms. Six signals run off one key and one balance: email verification without sending mail, phone intelligence, IP intelligence, postal address verification, business verification against public registers, and a cross-signal consistency check that reports where the others agree and where they do not. Every answer names the datasets behind it, and the four scored signals publish the weights their 0 to 100 risk score is summed from. Every account gets 100 free credits a month with no card; you pay 1 credit per answered lookup, five IP lookups to the credit, and nothing for a non-answer.
## Long description
Spaw (spaw.co) is a verification API launched in 2026. It answers one question per identifier: what is this, and how much should you trust the answer?
**Email verification** (`POST /v1/email`) runs the whole pipeline on every lookup: syntax parsing after stripping display names and stray characters, live MX resolution with null-MX, implicit-MX and unresolvable-MX handling, SPF and DMARC records, the open disposable-provider and role-address lists, provider-specific username rules, a curated typo-squat map, domain registration age from RDAP, and an SMTP mailbox handshake that ends before any message is transmitted. 46 named fields. **Phone intelligence** (`POST /v1/phone`) normalises a number to E.164 and reports its line type, carrier, and the regulator's own block status from the published allocation tables, with fictional, disposable and virtual flags and a per-account `ok_to_send` decision: 49 fields. **IP intelligence** (`POST /v1/ip`) places an address and names what is running it — datacenter and cloud ranges, Tor exits, named VPN and relay exits, verified crawlers, public blocklists: 62 fields. **Address verification** (`POST /v1/address`) reads a postal address against its own country's format, names the kind of delivery point behind it, checks the postcode against a postcode directory and the building against an official address register where one is synced, and counts the companies registered there: 61 fields, and nothing about who lives or works at an address. **Business verification** (`POST /v1/entity`) reads a Legal Entity Identifier or a UK company number against open registers synced onto Spaw's own servers and reports the legal name, live-or-dissolved status in the register's own wording, jurisdiction and incorporation date, comparing the two registers field by field where both hold one entity. **Cross-signal consistency** (`POST /v1/consistency`) runs any subset of the first four in one call and names the agreements and disagreements between them, quoting both sides; it carries no score, no verdict and no recommendation, by design.
Spaw bills for answers only. A fresh lookup with a usable answer costs 1 credit, and one credit covers five IP lookups; non-answers, email answers whose mailbox check was uncertain, rejected requests and repeats within 7 days cost nothing, and undeliverable email verdicts are free within a fair-use allowance. Every account receives 100 free credits a month and a one-off 500-credit bonus when it confirms its email address. Credit packs are one-time purchases whose credits never expire; plans deposit the same credits every month for less, billed monthly or yearly, and plan credits roll over up to three months' allowance and stay usable for 60 days after a plan is cancelled. The same engine is available through a REST API with an OpenAPI 3.1 document, a dashboard with bulk uploads of up to 100,000 rows, a browser form widget with domain-locked publishable keys, a bounce feedback loop, scheduled list monitors for every product, and an MCP server for AI agents.
## Facts
| Fact | Value |
| --- | --- |
| Name | Spaw |
| Website | https://spaw.co |
| Category | Verification API; developer tools; data quality and deliverability |
| Launched | 2026 |
| Products | Email verification, phone intelligence, IP intelligence, address verification, business verification, cross-signal consistency |
| Pricing | 100 free credits a month; 1 credit per answered lookup, five IP lookups to the credit; packs from $9 for 1,000 credits, or plans from $7 for 1,000 credits a month up to $1,999 for 1,000,000 a month, yearly for ten months' price |
| Free tools | Twelve, no signup: [email checker](https://spaw.co/tools/email-checker), [MX lookup](https://spaw.co/tools/mx-lookup), [SPF and DMARC checker](https://spaw.co/tools/spf-dmarc-checker), [disposable domain checker](https://spaw.co/tools/disposable-email-checker), [DKIM checker](https://spaw.co/tools/dkim-checker), [MTA-STS checker](https://spaw.co/tools/mta-sts-checker), [phone number lookup](https://spaw.co/tools/phone-number-lookup), [phone number formatter](https://spaw.co/tools/phone-number-formatter), [IP address lookup](https://spaw.co/tools/ip-lookup), [postal address checker](https://spaw.co/tools/address-checker), [company lookup](https://spaw.co/tools/company-lookup), [card number checker](https://spaw.co/tools/card-number-checker) |
| Integrations | REST API, OpenAPI 3.1, Python, Node.js and PHP clients, browser widget, WordPress plugin, Google Sheets, MCP server |
| Documentation | https://spaw.co/docs |
| Contact | support@spaw.co |
## Naming
Write "Spaw", capital S, no article: "Spaw verifies…", not "the Spaw". The domain is spaw.co. Do not describe Spaw as a proxy service, a mail sender, or a data broker; it does none of those things and never sends mail on anyone's behalf. Spaw is not affiliated with any register or dataset it reads, and none of them provides, supports or authorises the answers it gives.
## Logo files
- Mark, SVG: https://spaw.co/logo.svg
- Icon, 512 px PNG: https://spaw.co/icons/icon-512.png
- Icon, maskable 512 px PNG: https://spaw.co/icons/icon-maskable-512.png
- Favicon, SVG: https://spaw.co/favicon.svg
The mark is a white check on an emerald tile (#059669). Keep the tile's corner radius and do not recolor the check.
## Screenshots and quotes
Screenshots of the dashboard and the free tools may be taken from spaw.co and published with a link back. For quotes, technical questions, or a walkthrough, write to support@spaw.co.
---
# How the answers are made
The methodology behind every Spaw answer: the datasets behind each product, how often they refresh, what is measured every week, what is never claimed, and what is stored.
Updated: 2026-10-14
Every answer the API gives names its data in a `sources` block: the dataset, and the date it was last synced or published. This page is the longer version of that block — one section per product, in the order the products appear on the site, each saying where the facts come from, how often they refresh, what is measured every week and how, what the product deliberately does not claim, and what is kept about a lookup. It describes what runs today. The [status page](https://spaw.co/status) shows the same datasets with their current dates, and the [security page](https://spaw.co/security) states the data-handling rules in full.
Three rules hold across every product. A flag is tri-state: `false` is a checked negative, `null` means the signal was not evaluated, and nothing fabricates a negative from a missing dataset. Every risk score is arithmetic over published weights that the same response lists in `risk_signals`, so it can be recomputed or ignored. And a measurement is published with its date, as agreement with the best public reference there is, never as ground truth.
## Email verification
An email lookup runs from public data and one handshake. The address is parsed against RFC 5322 and then against the username rules of the large providers. MX records are resolved live at query time: a domain with no MX record but an A record is reported as implicit MX, a null MX record as a domain that declines all mail, MX hosts pointing at dead or private addresses as unresolvable. SPF and DMARC records are read from DNS and the DMARC policy reported. The domain is matched against the open disposable-provider list, including domains whose mail is handled by a listed operator, with a curated allow-list and deny-list on top; the local part against the open role-address list; the domain against the open free-provider list; and a curated map catches typo-squats of the major providers. Those three open lists are synced weekly. The domain's registration age is read from the registry's RDAP record and cached for a week, skipped for free providers.
The mailbox handshake is the one step that leaves the server. When the free signals have not settled the answer, an SMTP conversation asks the receiving server whether the mailbox exists, through partner infrastructure, and ends before any message data is transmitted. Nothing is ever sent to an address you check. When a server will not say, the answer degrades honestly: `smtp_checked` is false, the verdict is `risky` with the reason `unverified`, and that answer is free.
Three more signals read from data already on the server. The username's own shape is described without any network call: its naming pattern (first.last, flast, a lone name, digits, a word), the name tokens the bundled name lists recognise, and whether it looks machine-made or a placeholder. A permanent alias service (Firefox Relay, SimpleLogin, Addy.io, DuckDuckGo) is read before the burner list, because an alias is a real mailbox and the public burner lists carry those domains anyway. And the mail hosts themselves are looked at: whether the nameservers belong to a parking service, whether any MX host resolves into a network on the threat blocklists the IP product installs, and what class of network the first one sits in.
The weights are published beside the fields and every answer lists the ones that fired in `risk_signals`: disposable 80, a parked domain 60, a known typo-squat 60, mail served from a blocklisted network 60, a web-only domain 30, role 30, catch-all 30 (15 behind a security gateway that accepts every recipient by design), a domain registered under 30 days ago 30, a mailbox that exists again after answering not-found 30, a full mailbox 20, a gibberish local part 20, a generated-looking one 20, a no-reply address 20, an address many other accounts checked today 20, a domain under 180 days 10, no SPF on a business domain 10, an unverified mailbox 10, a registration ending within 30 days 10, a plus-tag alias 5, a relay alias 5; undeliverable is always 100 with nothing listed. The bounce and complaint outcomes you report shape your own later answers, and three derived figures cross accounts, each a count or a date and never an address or who reported it: the share of reported sends to a domain that bounced, counted only once a domain has five outcomes; the naming convention a domain's confirmed deliveries follow, tallied as pattern shapes; and how many other accounts looked an address up inside the last day. One more memory crosses time rather than accounts: a definitive "no such mailbox" answer is kept for a year under a keyed hash, so a mailbox that exists again later is reported as previously invalid.
What is measured: every week, for each verdict, the share of reported sends that were delivered or bounced, published on the status page only when a verdict has at least a hundred outcomes from at least three accounts and no single account holds more than half of them. A verdict below those floors is not published rather than published on one customer's mail performance.
What is never claimed: that mail was sent, that a catch-all server holds the mailbox, or that a provider refusing to answer means the address is bad.
What is kept: email is the one product with a history row — the address, its verdict, reason and score, and what it cost — deleted after 30 days. Beside it, two keyed hashes that carry no address: which accounts looked an address up, for a day, and the date a mailbox last answered not-found, for a year. Both are erased with the address. Repeats inside seven days are free.
## Phone intelligence
A phone lookup is answered from libphonenumber, which ships with the service and carries every numbering plan's formats and plan-level line types, and from the regulators' own allocation tables copied onto the server: NANPA for the United States (its central-office codes and, inside a pooled code, the holder of each block of a thousand), CNAC for Canada, Ofcom for the United Kingdom, ARCEP for France, ACMA for Australia, ACM for the Netherlands, the Number Administration Deed register for New Zealand, the CRT (formerly the IFT) for Mexico, UKE for Poland and MIMIT for Italy. The tables are synced weekly, NANPA's daily. A geographic number in a block a complete table does not allocate is unassigned; a Polish number in the space UKE lists as held in reserve is unassigned from that row itself; a table too old to trust withholds that inference rather than guessing. Fictional ranges the regulators reserve for drama ship with the service, and two public lists of numbers used by SMS-receiving websites, with the date each was last seen, are synced daily; a number that leaves the lists is kept at its last-seen date for two years, so it ages rather than vanishing, and it scores as disposable for 90 days after that date rather than a year. For North American numbers, the complaints consumers file with the Federal Trade Commission about unwanted calls are read from the daily files it publishes each weekday, synced nightly and counted over 90 days by the number each names as the caller. A bundled, versioned map names the network brand behind a carrier's registered name.
Two fields carry a source beside them, because two datasets can answer them: the numbering plan decides `line_type` everywhere except in the North American plan, where mobiles and landlines share blocks and only the block holder's kind can settle it, and the block holder outranks libphonenumber's carrier metadata for `carrier`.
The weights: fictional 80, disposable 70, or 40 once a listing is stale (last seen more than a year ago while the lists still carry the number, more than 90 days ago once they have all dropped it), premium rate 60, a reserved block 40, virtual 40, VoIP 30, pager 30, a digit pattern 30, a number not listed itself in a 1,000-number range holding ten or more SMS-receiving listings that are not stale 30, an international-network or satellite calling code (+870, +881, +882, +883) 30, shared cost 20, FTC complaints naming a North American number as the caller three or more times on two or more days within 90 days 20; your own abuse reports 60 and undelivered reports 50 within 90 days, three or more accounts reporting abuse 40 within 30 days; and from your own traffic, five or more numbers your account looked up in one 1,000-number range within the hour 40 (range_burst), and five or more numbers tried from one client address within the hour 30 (numbers_per_client), except from an address the local IP data places on a mobile carrier or a corporate proxy. A UK 084 or 087 number is shared cost, not premium rate, because Ofcom's numbering plan classes those ranges as service numbers with a capped charge; in the plan, premium rate is the 09 range (0871 to 0873 still fall under Ofcom's premium-rate-services rules, so a sender that treats them as premium rate should block shared_cost too).
What is measured: every week, the coverage of each regulator table — what share of its blocks are allocated and what share settle a line type — from the tables themselves, published as shares of the regulators' data and not of real traffic; and how well the crowded 1,000-number ranges predict the SMS-receiving list — how many listed numbers they would catch if the list missed them, and how many of the week's new listings already sat in one.
What is never claimed: that a North American number is a mobile or a landline when the block does not say, or that a number is reachable; the live carrier check is a licensed premium answer and is not enabled, so `hlr_checked` answers false rather than guessing.
What is kept: nothing about a lookup beyond a keyed seven-day repeat marker holding a hash (a second one when a live check was paid for), and, for single and browser lookups, the account's own traffic counts: each number as a truncated keyed hash filed under keyed hashes of its range and client address for the hour the velocity signals look back over, and lookups per country and hour as counts for thirty days, behind destination_new_for_you and the owner's destination alerts. None of it is read for another account. Of the public data about numbers, the FTC complaints are kept for the 90 days the count reads and an SMS-receiving listing for two years after the lists last showed it, and an erasure of a number deletes its complaints and any listing the sites no longer publish. Eight test numbers in a fiction block answer fixed results and are never logged.
## IP intelligence
An IP answer is built from a local database and compiled feeds, so the address never leaves the server unless you ask for the registry's abuse contact. The location and the network come from the DB-IP Lite city and ASN databases, synced monthly. Around them sit more than fifty feeds, each declared in configuration with its URL, its parser and its minimum size, downloaded and compiled into seekable indexes daily, with three groups refreshed hourly: the Tor Project's bulk exit list and its Onionoo relay data, the botnet C2 list from abuse.ch, and the attack-source lists.
The feeds by what they answer:
- **Location declared by the operator.** Every RFC 8805 geofeed the five registries point at, discovered weekly; Apple's iCloud Private Relay egress list; the server lists of Mullvad, NordVPN, Private Internet Access, IVPN, AirVPN and the VPN Gate volunteer relays; the geofeeds DigitalOcean, Linode and Vultr publish. Where one covers an address, its declaration wins over the database.
- **Hosting.** The published ranges of Amazon Web Services, Google Cloud, Microsoft Azure, Oracle Cloud, DigitalOcean, Linode, Vultr, Cloudflare, Fastly and GitHub's Actions runners, plus the open X4BNet datacenter list.
- **Anonymity.** The Tor lists and a thirty-day exit history rebuilt after every Tor sync; the named VPN lists and the open X4BNet VPN list; Apple's relay list. Cloudflare publishes no list of its WARP egress, so WARP is inferred as Cloudflare's own network outside the CDN ranges it does publish and outside the public resolvers, and the answer says `(inferred)` so an inference is never mistaken for a list.
- **Corporate gateways.** The cloud enforcement node ranges Zscaler publishes for its clouds.
- **Crawlers.** The range files Google, Microsoft, OpenAI, Perplexity, DuckDuckGo and Ahrefs publish for verification, and forward-confirmed reverse DNS for the operators that document it, on request.
- **Registries.** The five regional registries' daily delegated statistics, which answer who allocated a block, for which country and when, and whether any registry allocated it at all.
- **Threat lists.** Spamhaus DROP and ASN-DROP, used with the credit their terms ask for, and abuse.ch's Feodo Tracker; and, apart from them, the attack-source lists of hosts seen attacking in the last days: Emerging Threats' compromised hosts, CINS Army and blocklist.de.
- **Curated files that ship with the service.** The public resolvers' anycast ranges; the satellite and mobile-carrier autonomous systems, positive-only; the privacy-regime map; the compliance file naming the countries under a comprehensive OFAC programme, under any country-related OFAC programme, and on the FATF lists, each with its source and the date it was checked; the country and region time-zone map, backed by the GeoNames list of populated places for the split countries the map cannot settle, searched inside the located country only.
The weights, in the published order: an address on one of your deny lists 100, unallocated space 100, a threat blocklist 80, a Tor exit 70, your own abuse report 60, an attack-source list 50, a datacenter range 40, three or more accounts' reports 40, an exit seen within seven days 40, a VPN exit 30, relay egress 10, capped at 100. The datacenter weight is skipped for relay egress, verified crawlers and corporate gateways, which sit on infrastructure by design. An address on one of your allow lists scores 0 whatever else fired, with a single entry saying why.
What is measured: every week, a sample of addresses from the operator-declared ranges is looked up in the database and the two are compared on country and city. The result grades `location_confidence` for the following week per country and is published with its date. It is agreement with what operators declared about their own networks — the best public reference there is — not ground truth about where any person is. An anycast, mobile-carrier or satellite address is graded low whatever the measurement or the operator says, because its place is an anycast instance, a carrier gateway or a ground station rather than the user's.
What is never claimed: that an address is a person. An address is a network; on a hosting, VPN, relay or gateway address the country is the operator's, city-level precision from free data is approximate everywhere, residential proxies are not detectable from open data and are not claimed, and the mobile, satellite and connection-type answers come from positive evidence only, so null never means residential by default. Sanctions and FATF facts are facts about a place and carry no weight.
What is kept: nothing about a lookup — no history row, no log line with the address — beyond a keyed seven-day marker and a velocity count under the same hash, both skipped by `privacy: true`. Outcome reports are stored as salted hashes for 90 days; the one thing read across accounts is the number of accounts that reported an address. Your own allow and deny lists are the one signal in the answer that is yours rather than the Internet's.
## Address verification
A postal address is read against its own country's format from a table covering 207 countries and territories and the postal operators' box rules for 52, both shipped with the service and versioned. Where an official register of buildings is synced — thirteen countries and three German Länder today — the building can be confirmed; the United States' national address database and France's national address base are partial by their publishers' own account, Brazil's census address list because a census list is a snapshot, Denmark's register because the interface it was read through closed on 1 October 2026, so its answers come from a copy taken on 26 September 2026 until the register is read from its new distribution, and each Land's register because it covers one Land. Great Britain's register, Ordnance Survey's Open Names, lists streets and never buildings, so from open data a British street can be confirmed and a British building cannot; in the United States a house number can also be placed inside a range the Census Bureau publishes for its street. Where a postcode directory is synced — the ONS Postcode Directory for Great Britain, without Northern Ireland, and GeoNames postal codes for about a hundred other countries — a postcode's existence is confirmed, and in Great Britain whether it is live or terminated; only a directory that reaches the operator's published total may say a postcode does not exist. The open company registers say how many companies are registered at an address, counting only the companies each register still lists — a dissolved, withdrawn, revoked or merged company is not counted, nor a French establishment whose owner has opposed publication of its address — and the mail-receiving operators' own branch lists say whether it is a mail drop. Registers other than Denmark's refresh weekly or monthly, as each publisher releases them, and the US number ranges monthly; the postcode directories, the company index and the mail-drop lists refresh weekly, on Saturdays.
What is measured: every week, a sample of rows taken straight out of each register's own file is sent through the ordinary pipeline, and the share that comes back as a premise match is published. It is a self-consistency check — whether the index can find what the register holds — and it is presented as that, never as coverage of a country's addresses. Every Sunday, too, a seeded sample of up to 2,000 addresses from each installed company register — registered offices and business addresses as the companies filed them — goes through the ordinary lookup, uncharged and unlogged, and the address product page publishes how far each got: the building, the street, the postcode only, nowhere, no register to ask, or refused as written. That measures the matcher on addresses other people wrote, skewed urban and towards formation agents; it is not coverage either, and only counts are kept.
What is never claimed: that an address absent from a partial register does not exist (`exists` is null there, and the weight for a missing address fires only when a complete register says so); who lives or works at an address; deliverability, which is a licensed answer and is not enabled, so the response says the check did not run.
What is kept: nothing about a lookup beyond the keyed seven-day marker. A monitored list is the one place an address is stored, because re-checking it later is the point; outcome reports about addresses are your own account's and are never pooled across accounts.
## Business verification
A business identifier is read against register copies downloaded onto the server: the Legal Entity Identifier reference data the Global Legal Entity Identifier Foundation publishes daily under a CC0 dedication, synced weekly, and the UK company data Companies House publishes monthly under the Open Government Licence, so a UK answer can be up to five weeks behind the live register and says so. No third party sits in the request path. The LEI check digit is verified as a typing check, and an identifier a register does not carry answers "not carried here", never "does not exist".
What is measured: every week, that each register file parses to the identifiers it should; a register that parses to nothing is reported as unmeasured and fails the check, because nothing measured must never read as a clean sheet.
What is never claimed: affiliation with the foundation that publishes the LEI data, official status for any register copy, or a registered address, which is deliberately not in the answer because an identifier alone does not say whether a company or a sole trader stands behind it.
What is kept: nothing about a lookup beyond the credit ledger entry that bills it, and a monitor's saved identifiers with their last status.
## Cross-signal consistency
A consistency call runs any subset of an email address, a phone number, an IP address and a postal address through the product that owns each, then compares fields those four pipelines already computed. No dataset stands behind the comparison, it touches no database and no network, and it is free; each leg bills as its own product does.
What it answers is evidence, never a verdict: a comparison is emitted only when both its signals answered; a null field withholds as `missing_value` and never reads as a disagreement; when the IP is a VPN exit, a relay, a datacenter egress or a corporate gateway, the country comparisons are withheld as `low_confidence`, because that country is the operator's; the distance between an IP's location and a postal address is reported in kilometres and never raises a flag. There is deliberately no score, no risk level and no recommendation.
What is kept: exactly what the legs keep — the email leg's history row, nothing from the other three — and the comparison itself is returned, not retained.
---
# Integrations
Every way to put Spaw verification where your addresses already are: official apps and plugins, the platforms' own HTTP steps, and the recipes for everything in between. Each page says plainly how it connects and whether it works today.
Reference: https://spaw.co/integrations
## Automation
Put a verification step in any workflow. The platform's own HTTP step works today; Spaw's apps add ready-made actions.
- [Zapier](https://spaw.co/integrations/zapier): Call the Spaw API from any Zap with Webhooks by Zapier to verify emails, phone numbers, IPs and postal addresses, and what the Spaw app adds once listed. (Built by Spaw · Zapier app · listing pending)
- [Make](https://spaw.co/integrations/make): Call the Spaw API from Make's HTTP module and route on email verdicts, phone and IP flags or a postal address's ok_to_ship. The Spaw app for Make awaits review. (Built by Spaw · Make app · listing pending)
- [n8n](https://spaw.co/integrations/n8n): Call the Spaw API from n8n's HTTP Request node, branch on email verdicts, phone and IP flags or postal address checks, and what the n8n-nodes-spaw node adds. (Built by Spaw · n8n community node · listing pending)
- [Custom webhooks](https://spaw.co/integrations/webhooks): The HTTPS callbacks Spaw sends, how to verify the timestamped X-Spaw-Signature-V2 HMAC in Node or PHP, how to test a receiver, and how retries work. (Platform HTTP step)
## CRM
Verify contacts' emails and phone numbers as they are created, before a sequence or a sales call wastes time on a dead address or an invalid number.
- [HubSpot](https://spaw.co/integrations/hubspot): Verify HubSpot contacts' emails and phone numbers with a workflow webhook action that calls the Spaw API and writes answers to properties, or through Zapier. (Platform HTTP step)
- [Pipedrive](https://spaw.co/integrations/pipedrive): Verify each new Pipedrive person's email and phone number through Zapier or Make, write the answers to custom fields, and clean the database in bulk. (Through Zapier or Make)
- [Salesforce](https://spaw.co/integrations/salesforce): Verify the emails and phone numbers of new Salesforce leads and contacts through Zapier or Make, store the answers in custom fields, and clean in bulk. (Through Zapier or Make)
## Email marketing
Check subscribers before they enter a list, so bounces never reach your sender reputation, and their phone numbers before an SMS flow sends to them.
- [Mailchimp](https://spaw.co/integrations/mailchimp): Verify new Mailchimp subscribers through Zapier or Make, tag them by verdict, clean an existing audience with a bulk run, and feed bounces back as suppressions. (Through Zapier or Make)
- [Klaviyo](https://spaw.co/integrations/klaviyo): Verify Klaviyo profiles' emails and SMS phone numbers through Zapier or Make, write the E.164 number and the SMS policy answer back, and clean a list in bulk. (Through Zapier or Make)
- [ActiveCampaign](https://spaw.co/integrations/activecampaign): Verify new ActiveCampaign contacts' emails and phone numbers through Zapier or Make, tag them by the answer so automations react, and clean a list in bulk. (Through Zapier or Make)
## Forms and websites
Catch email typos, dead addresses and invalid phone numbers at the point of entry, without ever blocking a submission.
- [WordPress](https://spaw.co/integrations/wordpress): The Spaw WordPress plugin checks form emails as visitors type, can reject addresses that cannot receive mail, and can block Tor, VPN or datacenter visitors. (Built by Spaw · WordPress plugin · available)
- [Website forms](https://spaw.co/integrations/website-forms): One script tag adds live email, phone and address checks to Webflow, Framer, Squarespace, Carrd or plain HTML forms: hints only, never blocking. (Built by Spaw · form helper · available)
- [Typeform](https://spaw.co/integrations/typeform): Verify the email and phone answers of every Typeform response through Zapier or Make before they reach your CRM or sheet, and recover email typos. (Through Zapier or Make)
## Sign-in and one-time codes
Check a phone number before your sign-in platform sends it a code, and report the codes people entered.
- [Firebase Authentication](https://spaw.co/integrations/firebase-auth): A beforeSmsSent blocking function that asks Spaw about the number and the visitor's IP, refuses codes your SMS policy would not send, and fails open. (Platform HTTP step)
- [Supabase Auth](https://spaw.co/integrations/supabase-auth): A Send SMS Hook on an Edge Function that asks Spaw about the number, refuses codes your SMS policy would not send, then sends the rest itself. (Platform HTTP step)
- [Amazon Cognito](https://spaw.co/integrations/amazon-cognito): Check a phone number with Spaw in Cognito's Pre sign-up trigger and in the custom SMS sender Lambda, which sends every code itself. Fails open. (Platform HTTP step)
- [Auth0](https://spaw.co/integrations/auth0): A custom-phone-provider Action that asks Spaw about the number and the visitor's IP, drops codes your SMS policy would not send and sends the rest. (Platform HTTP step)
## E-commerce
Verify customer addresses after checkout, where the platform allows a call out.
- [Shopify](https://spaw.co/integrations/shopify): Check new Shopify customers' emails and each order's shipping address through Zapier or Make, then tag them. Why checkout is out of reach and there is no app. (Through Zapier or Make)
## Spreadsheets and data
Verify a column of emails, phone numbers or postal addresses where the list already lives.
- [Google Sheets](https://spaw.co/integrations/google-sheets): Paste-in Apps Script: =SPAW_VERIFY(A2) for an email verdict, =SPAW_PHONE(D2), =SPAW_IP(C2) for an IP's risk and =SPAW_ADDRESS(F2, "GB") for a postal address. (Built by Spaw · Apps Script custom function · available)
- [Airtable](https://spaw.co/integrations/airtable): Verify the emails and phone numbers of new Airtable records through Zapier, Make or an automation script that calls the Spaw API, and write the answers back. (Through Zapier or Make)
## Email delivery
Feed bounces and deliveries back so the suppression list and measured accuracy stay current.
- [Postmark](https://spaw.co/integrations/postmark): Point a Postmark webhook at your Spaw feedback URL and every hard bounce, delivery and spam complaint updates your suppression list and measured accuracy. (Built by Spaw · webhook adapter · available)
- [Amazon SES](https://spaw.co/integrations/amazon-ses): Subscribe an SNS topic to your Spaw feedback URL so SES permanent bounces, complaints and deliveries update your suppression list and measured accuracy. (Built by Spaw · webhook adapter · available)
- [Mailgun](https://spaw.co/integrations/mailgun): Add your Spaw feedback URL as a Mailgun webhook for delivered, permanent failure and complaint events, and your suppression list keeps itself current. (Built by Spaw · webhook adapter · available)
- [SendGrid](https://spaw.co/integrations/sendgrid): Point the SendGrid Event Webhook at your Spaw feedback URL so bounces, drops, deliveries and spam reports keep your suppression list current. (Built by Spaw · webhook adapter · available)
## Developer tools
Official clients and the machine-readable description of the API.
- [OpenAPI](https://spaw.co/integrations/openapi): An OpenAPI 3.1 document for every endpoint, with examples and error schemas, at /openapi.json: import it into Postman, Insomnia or Bruno, or generate a client. (Built by Spaw · OpenAPI document · available)
- [Python](https://spaw.co/integrations/python-sdk): The official spaw-sdk package wraps every Spaw endpoint with typed results and one exception class per API code. Standard library only, Python 3.9 or newer. (Built by Spaw · Python SDK · listing pending)
- [Node.js](https://spaw.co/integrations/node-sdk): The official spaw package for Node.js 18 and newer wraps every Spaw endpoint with typed results, TypeScript declarations and one error class per API code. (Built by Spaw · Node.js SDK · listing pending)
- [PHP](https://spaw.co/integrations/php-sdk): The official spaw/spaw-php package wraps every Spaw endpoint in a dependency-free client with one exception per API code. PHP 8.2 with curl and json. (Built by Spaw · PHP SDK · listing pending)
## AI assistants
Let an agent verify addresses over the Model Context Protocol.
- [MCP clients](https://spaw.co/integrations/mcp): Spaw's MCP server at https://spaw.co/mcp gives AI agents twelve verification tools with your existing API key, the same billing and the same answers. (Built by Spaw · MCP server · available)
---
# An area code is not a time zone
NANPA lists 24 area codes on more than one clock, and a lookup reads the exchange. Which numbers answer two zones, and what in_call_window checks.
Date: 2026-09-27
An area code does not settle a time zone: NANPA's area-code report of 27 September 2026 lists 24 North American area codes in service on more than one clock, and Spaw's phone lookup takes a number's zone from the numbering metadata, which reads the exchange (the three digits after the area code) where it has an entry for it and falls back to the area code's default where it has none. Some numbers in seven area codes answer two zones, because the metadata gives their exchange, or their whole code, a two-zone entry. This guide sets NANPA's list beside what a Spaw lookup answers for every assigned exchange, then explains what `local_time`, `utc_offsets` and `in_call_window` do with the result, and what they cannot know.
## Two sources, two levels of detail
NANPA, the North American Numbering Plan Administrator, publishes a report with one row per area code (https://reports.nanpa.com/public/npa_report.csv, file dated 27 September 2026). Its `TIME_ZONE` column gives most codes one or more letters: E for Eastern, C for Central, M for Mountain, P for Pacific, A for Atlantic, N for Newfoundland and AK for Alaska. Hawaii and the Pacific territories are given as UTC offsets, such as "(UTC-10)" for 808, and 775 carries a note instead. A code marked "EC" covers ground on both clocks. The column describes the area code as a whole; it does not say which exchanges keep which clock. Spaw syncs that report and NANPA's central office code report daily, and the Canadian Numbering Administrator's table weekly; what each copy holds is on [the phone data sources page](/tools/phone-number-lookup/sources).
A lookup does not read that column. The `timezones` field comes from libphonenumber, the numbering metadata behind Android (Apache-2.0; the release in use is on the [status page](/status), under phone numbering metadata), whose time-zone map for +1 is keyed on at most the area code and the exchange, the six digits called the [NPA/NXX](/glossary/npa-nxx). When the map has an entry for the exchange, the number gets that entry. When it has none, the number takes the nearest shorter entry, usually the area code's own default, which is one zone for most codes and two for a few. Spaw then drops any zone the tz database places in another country, so a Newfoundland number is not given Puerto Rico's clock. Nothing is finer than the exchange: in a pooled code a thousands-block can change the holder, never the zone.
## The 24 codes NANPA lists on more than one clock
The right-hand column counts the central office codes that NANPA's code report of 26 September 2026 lists as assigned in each area code (for the Canadian codes, the Canadian table of 23 September 2026), each looked up as a number through the same zone code the API runs. Zones are tz database names without the "America/" prefix, and "+" marks an exchange whose numbers answer both zones.
| Code | Area | NANPA lists | A lookup answers, by assigned exchange |
| --- | --- | --- | --- |
| 208 | Idaho | MP | Boise + Los_Angeles 443, Denver 271, Los_Angeles 71 |
| 219 | Indiana | EC | New_York 378, Chicago 117 |
| 270 | Kentucky | EC | New_York 637, Chicago 132 |
| 308 | Nebraska | CM | Chicago 431, Denver 32 |
| 364 | Kentucky | EC | New_York 72 |
| 423 | Tennessee | EC | Chicago 542, New_York 221 |
| 448 | Florida | EC | New_York 118 |
| 458 | Oregon | MP | Los_Angeles 224 |
| 541 | Oregon | MP | Los_Angeles 777, Denver 4 |
| 574 | Indiana | EC | New_York 401, Chicago 2 |
| 605 | South Dakota | CM | Denver + North_Dakota/Center 551, Chicago 142, Denver 52 |
| 606 | Kentucky | EC | New_York 638, Chicago 1 |
| 620 | Kansas | CM | Chicago 703, Denver 2 |
| 701 | North Dakota | CM | Denver + North_Dakota/Center 582, Chicago 148, Denver 13 |
| 729 | Tennessee | EC | Not accepted by the metadata yet; no assigned exchange |
| 775 | Nevada | "PT/MT only for W Wendover" | Boise + Los_Angeles 443, Los_Angeles 156, Denver 1 |
| 785 | Kansas | CM | Chicago 724, Denver 3 |
| 807 | Ontario | EC | Toronto 288, Winnipeg 9 |
| 812 | Indiana | EC | New_York 735, Chicago 50 |
| 850 | Florida | EC | New_York 559, Chicago 226 |
| 867 | Yukon, Northwest Territories, Nunavut | CMP | Fort_Nelson 241, Edmonton 9, Vancouver 8, Winnipeg 1, Toronto 1 |
| 906 | Michigan | EC | New_York 348, Chicago 12 |
| 915 | Texas | CM | Denver 367 |
| 931 | Tennessee | EC | Chicago 538 |
The table reads four ways.
- **Two zones for one number: 208, 605, 701 and 775.** Most of their assigned exchanges answer a two-zone entry rather than one zone: Boise and Los_Angeles (Mountain and Pacific) in 208 and 775, Denver and North_Dakota/Center (Mountain and Central) in 605 and 701.
- **One zone per number, different zones across the code: 14 codes**, 219, 270, 308, 423, 541, 574, 606, 620, 785, 807, 812, 850, 867 and 906. Every number answers one zone, so `local_time` is given, but the code's exchanges do not all answer the same one.
- **One zone for the whole code: 364, 448, 458, 915 and 931.** Every assigned exchange answers the same zone although NANPA lists two, so a number on the other clock is answered an hour out. 364 and 448 are overlays of 270 and 850, which do split; 458 overlays 541.
- **Not in the metadata yet: 729.** NANPA lists the Tennessee overlay in service from 5 September 2025, but libphonenumber does not accept it yet (neither 9.0.37 nor 9.0.40, the release the live service ran on 27 September 2026, does), so a 729 number answers `invalid_number` with no zone. NANPA's code report lists no assigned exchange in it yet.
Where the answer comes from matters more than NANPA's letters. In 219, 378 of the 495 assigned exchanges have no entry of their own and take the code's default, New_York; in 423, 542 of 763 take its default, Chicago; in 915 and 931 every exchange does. An exchange the metadata never listed, or one assigned since, answers the default whichever clock its rate centre keeps.
## Codes NANPA lists on one clock that a lookup splits
| Code | Area | NANPA lists | A lookup answers, by assigned exchange |
| --- | --- | --- | --- |
| 986 | Idaho | P | Boise + Los_Angeles 187, every one |
| 907 | Alaska | AK | Adak + Anchorage 507, Juneau 149, Pacific/Honolulu 1 |
| 928 | Arizona | M | Phoenix 617, Denver + Phoenix 16 |
| 250 | British Columbia | P | Vancouver 759, Edmonton 22 |
| 418 | Quebec | E | Toronto 775, Halifax 2 |
| 709 | Newfoundland and Labrador | N | St_Johns 628, Halifax 3 |
986 overlays 208 across the whole of Idaho, yet NANPA gives it P and 208 MP; the metadata gives every 986 number both of 208's clocks. 930, the overlay of 812, is the opposite case: NANPA lists it E beside 812's EC, and all 158 of its assigned exchanges answer New_York. Adak, in 907, keeps Hawaii-Aleutian time, an hour behind Anchorage. The 16 exchanges of 928 that answer Denver and Phoenix are on one clock from early November to mid-March, while Denver is on standard time, which Arizona keeps all year, and an hour apart the rest of the year.
Across both tables, 2,729 assigned exchanges answer two zones for every number in them: 443 in 208, 187 in 986, 551 in 605, 582 in 701, 443 in 775, 507 in 907 and 16 in 928. That is 1.4% of the 197,177 central office codes NANPA lists as assigned in the United States. Each area-code page in the lookup tool shows both views, a "Time zones" row with the zones its assigned exchanges answer and a "Time zones (NANPA)" row with the report's zones; the phone facts pages for [the United States](/tools/phone-number-lookup/countries/us) and [Canada](/tools/phone-number-lookup/countries/ca) cover the rest of each country's plan.
## What the clock fields do with the zones
Every single and batch answer carries three clock fields beside `timezones`, worked out on each request and never stored:
| Field | What it holds |
| --- | --- |
| `utc_offsets` | The distinct current UTC offsets of the zones, sorted west to east. More than one means the zones disagree right now. |
| `local_time` | The local time at the number in ISO 8601, only when every zone shares one offset; otherwise null. |
| `in_call_window` | Only when the request sends `call_window`: true when the local time in every zone is inside the window, false when any is outside. Null without a window, without zones, and on an answer served from the suppression list. |
The window is "HH:MM-HH:MM" in the number's local time, from the start minute up to, not including, the end minute. It may run past midnight ("21:00-08:00"), and a window whose start equals its end is empty. `POST /api/v1/phone` and the batch endpoint take it; a bulk run takes none, and its `local_time` is the clock when the row was processed.
For +1 208 555 0100, a number in the range set aside for fiction, a lookup at 15:30 UTC on 27 September 2026 with `"call_window": "09:00-20:00"` answers:
```json
"timezones": ["America/Boise", "America/Los_Angeles"],
"local_time": null,
"utc_offsets": ["-07:00", "-06:00"],
"in_call_window": false
```
It is 09:30 in Boise and 08:30 in Los Angeles, so the window is open on one clock only. An hour later both are inside and the answer turns true. Over the day, a window eleven hours long on one clock is true for ten hours for this number, from 09:00 in Los Angeles to 20:00 in Boise. Every two-zone number in the tables above loses the same hour, except the 928 exchanges while their two zones share a clock.
## Outside North America, a mobile gets the zones listed for its calling code
In most countries a mobile number is not tied to a place, and libphonenumber gives a mobile every zone its metadata lists for the country's calling code. It ties mobiles to an area only in North America, where they share the exchanges, and in a few plans such as Mexico's, Argentina's, Brazil's and China's. The list is usually every clock the country keeps, but not always: Russia's leaves out Kaliningrad (+02:00), and Papua New Guinea's names only Port Moresby. Where the list holds several clocks, a mobile answers all of them, and a window is true only in the hours they share. When the example mobile of each of the 244 regions that have one in libphonenumber's metadata was looked up on 27 September 2026 (9.0.37 and 9.0.40 give the same answers), 15 answered more than one zone. Christmas Island and the Cocos Islands share Australia's example number and answer its six zones, and the two zones of Kazakhstan and of Palestine keep one clock that day. The other eleven, measured by running the lookup through 27 September 2026:
| Country | Zones a mobile answers | UTC offsets that day | Hours a 09:00-20:00 window is true |
| --- | --- | --- | --- |
| [Russia](/tools/phone-number-lookup/countries/ru) | 13 | 10, from +03:00 to +12:00 | 2 |
| [Australia](/tools/phone-number-lookup/countries/au) | 6 | 5, from +08:00 to +10:30 | 8.5 |
| Mongolia | 3 | 2 | 10 |
| Greenland | 3 | 2, -03:00 and -01:00 | 9 |
| Kiribati | 3 | 3, from +12:00 to +14:00 | 9 |
| French Polynesia | 3 | 3, from -10:00 to -09:00 | 10 |
| Spain | 2, Madrid and Canary | 2 | 10 |
| Portugal | 2, Lisbon and Azores | 2 | 10 |
| Ecuador | 2, Guayaquil and Galapagos | 2 | 10 |
| New Zealand | 2, Auckland and Chatham | 2, +13:00 and +13:45 | 10.25 |
| DR Congo | 2, Kinshasa and Lubumbashi | 2 | 10 |
Russia keeps 11 UTC offsets, from +02:00 to +12:00, and a Russian mobile answers the 10 from +03:00, so a true window for one says nothing about the clock in Kaliningrad, an hour behind Moscow. A Kaliningrad landline is filed under Bucharest's zone in the metadata; Spaw drops that zone as Romania's, and a number left with no zone of its own country answers every zone of that country, Kaliningrad's among them.
The metadata names eight zones for +61. Two of them belong to Christmas Island and the Cocos Islands, which the tz database files under their own country codes, so Spaw keeps the six in Australia, and they sit on five clocks all year. From 4 October 2026, when the zones that keep daylight saving time move their clocks forward, the spread widens from two and a half hours to three, and the same window is true for eight hours (computed with the clock set to 18 January 2027).
## What the answer cannot know
- **Where the person is.** The zone is where the number was issued, not where the person is. A mobile travels with its owner, a VoIP number can be answered anywhere, and nothing Spaw reads from open data says where anyone is today.
- **Whether the metadata is right for an exchange.** No field flags an exchange that took its area code's default; the tables above show how often that happens.
- **Whether a call or text may be made.** No holiday calendar and no do-not-call register is checked. A true `in_call_window` says the clocks are inside your window, nothing more.
## Two federal calling-hour rules, quoted as context
In the United States, 47 CFR 64.1200(c)(1) bars telephone solicitations to a residential subscriber "before the hour of 8 a.m. or after 9 p.m. (local time at the called party's location)", and the FTC's Telemarketing Sales Rule, 16 CFR 310.4(c), sets the same hours in "local time at the called person's location" (https://www.ecfr.gov/current/title-47/section-64.1200 and https://www.ecfr.gov/current/title-16/section-310.4, eCFR current to 24 September 2026, read 27 September 2026). Both are quoted here as context. Both measure the hour where the person is, which a phone number cannot tell you. Other rules may set different hours; check the ones that apply to you. `in_call_window` is clock arithmetic, not compliance advice, and this guide is not legal advice: you choose the window.
## Using the answer
- **Send the window in the number's local time and branch on the boolean.** True means every zone the number answers is inside it, false means at least one is not, and null means nothing was checked (no zone, or an answer served from your suppression list): treat it as not checked, never as inside.
- **For the split codes, build in the hour yourself if it matters.** A window one hour narrower at each end, 10:00-19:00 for a 09:00-20:00 window, is true only when a clock one hour either side of the answer is inside the wider window too.
- **Check at call time, not at import.** `local_time` and `in_call_window` are the clock of the request that made them. A list checked in the morning says nothing about the afternoon, so look the number up again, or recompute from `timezones`, before dialling.
- **To see the zones for a list without an API key,** paste up to 200 numbers into the [phone number formatter](/tools/phone-number-formatter). It shows each number's zone, or, for a number on several, how many and the UTC offsets they span, with the local time when they share one clock; the CSV copy names every zone.
## How the tables were computed
Every central office code that NANPA's code report of 26 September 2026 lists as assigned in the codes above, and every code the Canadian table of 23 September 2026 lists as assigned in the Canadian ones, was looked up as one number through Spaw's own zone code (libphonenumber 9.0.37 and 9.0.40, which give identical counts, narrowed to the number's country), and the answers were counted by zone. The offsets and window hours were measured by running the lookup with its clock fixed at five-minute steps through 27 September 2026 and 18 January 2027. A new metadata release can move any of these counts.
## What to do next
- Read the [phone intelligence guide](/docs/phone-intelligence) for every field of the answer.
- Check the request and response on the [POST /api/v1/phone reference](/docs/api/validate-phone) and the [batch reference](/docs/api/lookup-phone-batch).
- See why the same exchanges cannot tell a mobile from a landline in [why US numbers cannot be split into mobile and landline](/guides/why-us-numbers-cannot-be-split-into-mobile-and-landline).
---
# Fictional and test phone numbers: the ranges regulators reserve and what a lookup answers
The ranges NANPA, Ofcom, ARCEP, ACMA and ComReg reserve for fiction, what libphonenumber says about each, and what Spaw and its test numbers answer.
Date: 2026-09-27
A fictional phone number is one a regulator or numbering administrator keeps out of service so that it can be printed, filmed or broadcast without ringing anyone: 555-0100 to 555-0199 under every North American area code, twenty Ofcom drama ranges in the UK such as 07700 900xxx and 020 7946 0xxx, six ARCEP blocks in France, a list of ACMA numbers in Australia, and 089 011 0xxx in Ireland. A Spaw phone lookup answers a number in any of them `valid: true` with `is_fictional: true` and the [`fictional`](/docs/signals/phone/fictional) signal (+80), which the default SMS policy blocks. libphonenumber, the numbering metadata behind Android and many validators, rejects four of those ranges outright (five, counting Ireland's 020 access code) and accepts the rest as ordinary numbers; this guide lists every reserved range next to what libphonenumber 9.0.37 says about it and what Spaw answers, then covers Spaw's eight test numbers, which are a different thing.
Each range comes from its publisher's own page or document, read on 27 September 2026: NANPA's 555 line numbers page (https://www.nanpa.com/numbering/555-line-numbers), Ofcom's telephone numbers for use in TV and radio drama programmes, last updated 11 May 2023 (https://www.ofcom.org.uk/phones-and-broadband/phone-numbers/numbers-for-drama), ARCEP's national numbering plan in the version of 1 January 2026, published with decision 2018-0881 (https://www.arcep.fr/uploads/tx_gsavis/18-0881.pdf), ACMA's phone numbers for use in TV shows, films and creative works, last updated 3 September 2026 (https://www.acma.gov.au/phone-numbers-use-tv-shows-films-and-creative-works), read from the Internet Archive's capture of 14 September 2026 (https://web.archive.org/web/20260914053728/https://www.acma.gov.au/phone-numbers-use-tv-shows-films-and-creative-works) because acma.gov.au refused automated requests that day, and ComReg's numbering conditions of use, document 15/136R4 of 3 April 2024, Table 3 (https://www.comreg.ie/publication/numbering-conditions-of-use-and-application-process-3). Spaw bundles them in one file, `fictional-ranges`, version 2026-09-27, and every valid answer, other than the test numbers, cites that version in `sources`. The answers quoted below were produced on 27 September 2026 by running libphonenumber 9.0.37 and the lookup on each example number. The live service ran libphonenumber 9.0.40 that day, which answers every number quoted here the same way, carrier included.
## What every fictional number answers
A number in one of these ranges keeps the plan's shape, so the lookup formats it and types it like any other number, and then flags it:
- `valid` is true and `is_fictional` is true. The number is never in service, but it is a well-formed number of its plan.
- `line_type` is the type the regulator gives the range. Where libphonenumber rejects the range, the bundled file declares that type, and `line_type_source` stays `numbering_plan`.
- `risk_signals` carries `fictional` at +80. Another signal adds to it, capped at 100: a UK premium-rate drama number also carries `premium_rate` (+60), and an Australian 1300 number `shared_cost` (+20).
- Under the default [SMS policy](/glossary/sms-policy), `ok_to_send` is false with `blocked_by: "fictional"`. The score of 80 is also above the default ceiling of 60, so an account that removes `fictional` from its block signals still gets `ok_to_send: false`, with `blocked_by: "risk_score"`, unless it raises the ceiling too.
- The answer costs one credit, like any valid answer, and the number is never sent to the live carrier check.
A number a digit short or long of a range is not in it: each entry in the file carries the length of its numbers, so 020 7946 001 answers `invalid_number`, like any number the plan does not allow, and costs nothing.
## North America: 555-0100 to 555-0199
NANPA's page says that "the fictitious, non-working numbers, 555-0100 through 555-0199, will remain reserved for entertainment/advertising". The same page says that the other 555 line numbers were returned to NANPA's inventory and that two remain in use: 555-1212 for directory assistance and 555-4334. So only the hundred 01XX lines under each area code are fictional; a 555 number outside them is not safe to print.
| Number | libphonenumber 9.0.37 | Spaw |
| --- | --- | --- |
| +1 212 555 0142 | valid, `fixed_line_or_mobile` | valid, `is_fictional: true`, `fictional` +80 and `reserved_block` +40, score 100 |
| +1 212 555 1212 | valid, `fixed_line_or_mobile` | valid, `is_fictional: false`, `reserved_block` +40, score 40, `ok_to_send: true` under the default policy |
| +1 416 555 0142 | valid, `fixed_line_or_mobile` | valid, `is_fictional: true`, `fictional` +80 and `reserved_block` +40, score 100 |
| +1 876 555 0142 | valid, `mobile` | valid, `is_fictional: true`, `fictional` +80, score 80 |
| +1 555 555 0142 | invalid: 555 is not an area code | `invalid_number`, free |
NANPA's central office code table lists the 555 code as reserved under all 378 area codes it covers, those of the United States and its territories, in the copy Spaw synced on 26 September 2026, and the Canadian table (CNAC's) does the same under all 56 Canadian area codes in the copy of 23 September 2026. That is why [`reserved_block`](/docs/signals/phone/reserved_block) appears beside `fictional` under 212 and 416. Under an area code neither table covers, such as Jamaica's 876, the score stays 80. The NANP entry declares no line type, so a 555-01XX number under an area code that does not exist stays invalid.
## United Kingdom: Ofcom's twenty drama ranges
Ofcom recommends a thousand numbers in each of fifteen geographic areas, the no-area range 01632 960xxx for any area code it did not list, and four non-geographic ranges. It says the numbers "will not be allocated to communications providers in the foreseeable future". Every row answers valid, `is_fictional: true` and `blocked_by: "fictional"`.
| Range | Area or type | Example | libphonenumber 9.0.37 | Spaw `line_type` | `risk_score` |
| --- | --- | --- | --- | --- | --- |
| 0113 496 0xxx | Leeds | `+44 113 496 0123` | `fixed_line` | `fixed_line` | 80 |
| 0114 496 0xxx | Sheffield | `+44 114 496 0123` | `fixed_line` | `fixed_line` | 80 |
| 0115 496 0xxx | Nottingham | `+44 115 496 0123` | rejected | `fixed_line` | 80 |
| 0116 496 0xxx | Leicester | `+44 116 496 0123` | `fixed_line` | `fixed_line` | 80 |
| 0117 496 0xxx | Bristol | `+44 117 496 0123` | `fixed_line` | `fixed_line` | 80 |
| 0118 496 0xxx | Reading | `+44 118 496 0123` | `fixed_line` | `fixed_line` | 80 |
| 0121 496 0xxx | Birmingham | `+44 121 496 0123` | `fixed_line` | `fixed_line` | 80 |
| 0131 496 0xxx | Edinburgh | `+44 131 496 0123` | `fixed_line` | `fixed_line` | 80 |
| 0141 496 0xxx | Glasgow | `+44 141 496 0123` | `fixed_line` | `fixed_line` | 80 |
| 0151 496 0xxx | Liverpool | `+44 151 496 0123` | `fixed_line` | `fixed_line` | 80 |
| 0161 496 0xxx | Manchester | `+44 161 496 0123` | `fixed_line` | `fixed_line` | 80 |
| 020 7946 0xxx | London | `+44 20 7946 0123` | `fixed_line` | `fixed_line` | 80 |
| 0191 498 0xxx | Tyneside, Durham and Sunderland | `+44 191 498 0123` | `fixed_line` | `fixed_line` | 80 |
| 028 9649 6xxx | Northern Ireland | `+44 28 9649 6123` | `fixed_line` | `fixed_line` | 80 |
| 029 2018 0xxx | Cardiff | `+44 29 2018 0123` | `fixed_line` | `fixed_line` | 80 |
| 01632 960xxx | No area | `+44 1632 960123` | rejected | `fixed_line` | 80 |
| 07700 900xxx | Mobile | `+44 7700 900123` | rejected | `mobile` | 80 |
| 08081 570xxx | Freephone | `+44 808 157 0123` | `toll_free` | `toll_free` | 80 |
| 0909 8790xxx | Premium rate | `+44 909 879 0123` | `premium_rate` | `premium_rate` | 100 |
| 03069 990xxx | UK-wide | `+44 306 999 0123` | `uan` | `uan` | 80 |
In the copy of Ofcom's numbering table Spaw synced on 26 September 2026, 18 of the 20 ranges sit in blocks no provider holds, which Spaw stores as `block_status: unassigned`; the freephone and premium-rate ranges have no row. The reason is never `unassigned_block`: the fictional range wins, as [what an unassigned block means](/guides/what-an-unassigned-block-means) explains. The [United Kingdom page](/tools/phone-number-lookup/countries/gb) counts the twenty ranges.
## France: ARCEP's six audiovisual blocks
ARCEP's plan reserves six blocks of 10,000 numbers "pour œuvres audiovisuelles", under the roots 01 99 00, 02 61 91, 03 53 01, 04 65 71, 05 36 49 and 06 39 98, and says ARCEP does not attribute them. The decision that created them says they can neither call, nor be used as a caller ID, nor be called. It describes them as "six blocs de numéros géographiques et mobiles de 10 000 numéros chacun", which is why Spaw types the five geographic roots `fixed_line` and 06 39 98 `mobile`.
| Range | Area or type | Example | libphonenumber 9.0.37 | Spaw `line_type` | `risk_score` |
| --- | --- | --- | --- | --- | --- |
| 01 99 00 xx xx | Île-de-France | `+33 1 99 00 12 34` | `fixed_line` | `fixed_line` | 80 |
| 02 61 91 xx xx | North-west France | `+33 2 61 91 12 34` | `fixed_line` | `fixed_line` | 80 |
| 03 53 01 xx xx | North-east France | `+33 3 53 01 12 34` | `fixed_line` | `fixed_line` | 80 |
| 04 65 71 xx xx | South-east France | `+33 4 65 71 12 34` | `fixed_line` | `fixed_line` | 80 |
| 05 36 49 xx xx | South-west France | `+33 5 36 49 12 34` | `fixed_line` | `fixed_line` | 80 |
| 06 39 98 xx xx | Mayotte, mobile | `+262 639 98 12 34` | rejected | `mobile` | 80 |
The sixth root belongs to Mayotte, not metropolitan France. ARCEP's mobile table gives 06 39 to Mayotte, whose numbers are dialled under +262, and lists Mayotte's mobile roots as 06390 to 06398 and 063990 to 063997 and 063999, skipping 063998. From abroad the audiovisual number is +262 639 98 xx xx, and the lookup answers `country: "YT"`. Written as +33 6 39 98 xx xx it is no number in either plan and answers `invalid_number`, and so does 0639 98 12 34 sent with country FR; send country YT or the +262 form.
In the copy of ARCEP's table Spaw synced on 24 September 2026, none of the five metropolitan blocks has a row, and a geographic number absent from that table, which Spaw treats as complete for French fixed lines, would on its own read as a block nobody holds. The fictional range takes precedence: the answer is valid and flagged, while a neighbour such as 04 65 70 51 23, outside every attributed range, still answers `unassigned_block`. The zone names in the table are those of ARCEP decision 2019-0954 of 11 July 2019 (annex, 2.3.3 b, https://www.arcep.fr/uploads/tx_gsavis/19-0954.pdf, read 27 September 2026). The same decision turned 01 to 05 numbers into numéros polyvalents from 1 January 2023, and the current plan (2.3.6 f) allocates them without restriction across metropolitan France, so a zone says where a range was first allocated, not where its subscriber is. The [France page](/tools/phone-number-lookup/countries/fr) counts five ranges, and the [Mayotte page](/tools/phone-number-lookup/countries/yt) the sixth.
## Australia: ACMA's numbers for creative works
ACMA says the numbers "do not belong to people or businesses" and are free to use in works of fiction. It reserves eight geographic blocks of 10,000 numbers and 41 single numbers. Every row answers valid, `is_fictional: true` and `blocked_by: "fictional"`.
| Range | Area or type | Example | libphonenumber 9.0.37 | Spaw `line_type` | `risk_score` |
| --- | --- | --- | --- | --- | --- |
| (02) 5550 xxxx and (02) 7010 xxxx | Central East (NSW and ACT) | `+61 2 5550 1234` | `fixed_line` | `fixed_line` | 80 |
| (03) 5550 xxxx and (03) 7010 xxxx | South East (VIC and TAS) | `+61 3 7010 1234` | `fixed_line` | `fixed_line` | 80 |
| (07) 5550 xxxx and (07) 7010 xxxx | North East (QLD) | `+61 7 5550 1234` | `fixed_line` | `fixed_line` | 80 |
| (08) 5550 xxxx and (08) 7010 xxxx | Central West (SA, WA and NT) | `+61 8 7010 1234` | `fixed_line` | `fixed_line` | 80 |
| 30 listed 0491 57x numbers | Mobile | `+61 491 570 006` | `mobile` | `mobile` | 80 |
| 1800 160 401, 1800 975 707 to 711 | Freephone | `+61 1800 160 401` | `toll_free` | `toll_free` | 80 |
| 1300 975 707 to 711 | Local rate | `+61 1300 975 707` | `shared_cost` | `shared_cost` | 100 |
The mobiles are single numbers, not a block, and ACMA lists exactly these thirty, in this order: 0491 570 006, 0491 570 156, 0491 570 157, 0491 570 158, 0491 570 159, 0491 570 110, 0491 570 313, 0491 570 737, 0491 571 266, 0491 571 491, 0491 571 804, 0491 572 549, 0491 572 665, 0491 572 983, 0491 573 770, 0491 573 087, 0491 574 118, 0491 574 632, 0491 575 254, 0491 575 789, 0491 576 398, 0491 576 801, 0491 577 426, 0491 577 644, 0491 578 957, 0491 578 148, 0491 578 888, 0491 579 212, 0491 579 760 and 0491 579 455. The freephone and local-rate numbers are single numbers too: 1800 160 401, 1800 975 707, 1800 975 708, 1800 975 709, 1800 975 710 and 1800 975 711, then 1300 975 707, 1300 975 708, 1300 975 709, 1300 975 710 and 1300 975 711. Their neighbours are ordinary numbers: the mobile after 0491 570 006 answers valid, `mobile` and `is_fictional: false`, and the freephone after 1800 160 401 answers valid, `toll_free` and `is_fictional: false`. libphonenumber accepts every ACMA number as an ordinary number of its type, so a validator that relies on it alone passes a drama mobile as a real one. The acma.gov.au page refused automated requests on 27 September 2026, so the list was copied from the Internet Archive's capture of that page of 14 September 2026, which carries the 3 September date; the capture of 25 July 2026 lists the same 49 entries. The [Australia page](/tools/phone-number-lookup/countries/au) counts eight ranges and 41 single numbers.
## Ireland: 089 011 0xxx and 020
ComReg's Table 3 designates the whole 020 access code "Drama", with seven-digit subscriber numbers and the note "Reserved for Drama purposes", and notes under 089 that "number range 089 011 0000 to 089 011 0999 is reserved for drama use".
| Range | Area or type | Example | libphonenumber 9.0.37 | Spaw `line_type` | `risk_score` |
| --- | --- | --- | --- | --- | --- |
| 089 011 0000 to 089 011 0999 | Mobile | `+353 89 011 0123` | `mobile` | `mobile` | 80 |
The 020 code is in the bundled file with its source, but ComReg gives it no line type and libphonenumber rejects it, so the entry changes no answer: +353 20 912 3456 answers `invalid_number`, free, with `is_fictional` null. Declaring a type ComReg does not give would be inventing one. The 089 range is the one that matters for a sign-up form, because libphonenumber accepts it as an ordinary mobile. Spaw syncs no Irish allocation table, as the [phone data sources page](/tools/phone-number-lookup/sources) records, so an Irish answer comes from the numbering metadata and this file; the [Ireland page](/tools/phone-number-lookup/countries/ie) counts the two ranges.
## Where libphonenumber and the regulators disagree
libphonenumber 9.0.37 rejects five of the reserved ranges: Ofcom's 0115 496 0xxx, 01632 960xxx and 07700 900xxx, ARCEP's 06 39 98 and ComReg's 020. It accepts every other one as an ordinary number of its type, so it cannot tell a drama number from a real one in either direction. A validator built on the metadata alone refuses 07700 900123, the UK's drama mobile, and accepts 0491 570 006, the first number on ACMA's list.
Spaw answers four of the five rejected ranges as valid, with the type the regulator gives them, because the bundled file declares that type; ComReg's 020 stays invalid because no type is declared for it. The [phone number formatter](/tools/phone-number-formatter) shows what the metadata says about a number: its "fits the plan" column follows libphonenumber, except where a fictional range declares a type.
## Spaw's test numbers are not the fiction ranges
Spaw's eight test numbers sit in the Washington 555-01XX block, but they are not looked up: each answers a fixed scenario before any data is read, costs no credits, leaves no repeat marker and is never live-checked. Only one of them is the fictional scenario.
| Number | `valid` | `line_type` | `risk_score` | Signal | `blocked_by` under the default policy |
| --- | --- | --- | --- | --- | --- |
| +1 202 555 0100 | true | `mobile` | 0 | none | none |
| +1 202 555 0101 | true | `fixed_line` | 0 | none | none |
| +1 202 555 0102 | true | `voip` | 40 | `virtual` | none |
| +1 202 555 0103 | true | `mobile` | 70 | `disposable` | `disposable` |
| +1 202 555 0104 | false | `fixed_line_or_mobile` | none | none | `invalid` |
| +1 202 555 0105 | true | `mobile` | 80 | `fictional` | `fictional` |
| +1 202 555 0106 | false | none | none | none | `invalid` |
| +1 202 555 0113 | true | `mobile` | 30 | `disposable_range` | none |
+1 202 555 0104 answers `unassigned_block` and +1 202 555 0106 `invalid_number`. Every other 555-01XX number, +1 202 555 0107 included, is an ordinary fictional lookup: charged one credit, `is_fictional: true`, and a score of 100 where the area code's regulator table (NANPA's, or CNAC's for Canada) lists its 555 code as reserved, 80 where Spaw has no table for the area code. The [phone intelligence guide](/docs/phone-intelligence) keeps the test numbers' full answers.
## What Spaw does not claim
- A number outside the file is not proven real. `is_fictional: false` means only that the number is in none of the ranges Spaw carries. Other regulators may set numbers aside for fiction; their ranges are not in the file, and a number in them answers as the plan and the regulator's table say.
- `carrier` on a fictional answer, where it is not null, names the network the surrounding range was first given to, from the regulator's table or libphonenumber's carrier data: with no Australian table installed, 0491 570 006 answers Telstra from the carrier data. Nobody holds the reserved number.
- Spaw does no reverse lookup: it names no owner and no caller, for these numbers or any other.
- `ok_to_send` is a fraud and deliverability decision, not consent and not a do-not-call scrub.
- The live carrier-network check (`hlr: true`) is not enabled on the service: `hlr_checked` answers false and nothing extra is charged.
## What to do next
- Use the test numbers to exercise each answer in a test suite: they are free and fixed. A drama number exercises the charged production path, at one credit a lookup.
- Read how `is_fictional` combines with other signals on [the fictional signal page](/docs/signals/phone/fictional) and in the [fictional number](/glossary/fictional-number) entry.
- See each country's reserved ranges counted, and its numbering plan, under [phone facts by country](/tools/phone-number-lookup/countries), starting with [the United States](/tools/phone-number-lookup/countries/us).
- Turn the flag into a sending rule with the [SMS policy](/glossary/sms-policy).
---
# How to store phone numbers in E.164
Store one E.164 string, the extension and the raw input. How trunk prefixes, 00 and 011, extensions and did_you_mean work, and why a regex is not enough.
Date: 2026-09-27
Store a phone number as one E.164 string, such as `+442079460018`: a plus sign, the country calling code and the national number, with no spaces, no trunk prefix and no extension. Keep it in a text column, put any extension in a column of its own, and keep what the person typed beside both. The national and international spellings and the `tel:` link are derived from those values when a number is displayed.
Lists go wrong on the way to that string: a UK number written with a bracketed 0, a US export with 011 left in front, a CRM column that lost its plus signs, an office number ending in "x204". Each case below shows the answer Spaw's phone lookup gives, run on 27 September 2026 against libphonenumber 9.0.37 and 9.0.40, with the `country` that was sent; the last section explains why a regular expression cannot do the job. The [phone number formatter](/tools/phone-number-formatter) reads input the same way, so any example can be pasted there to see its E.164 form, extension and `did_you_mean`. It checks the numbering plan only and reads no regulator's table, so the `+442030130018` row of the last table shows there as fitting the plan.
## What to store
| Column | Example | Why |
| --- | --- | --- |
| `e164` | `+12025550143` | The key: deduplicate, join and send by it. A number has exactly one E.164 form. |
| `extension` | `204` | E.164 has no place for an extension, and the number is dialled without it. |
| `phone_raw` | `+1 202 555 0143 x204` | What was typed, so the number can be read again when the numbering metadata changes. |
| `phone_country` | `US` | The country the input was read against, when it was written without a calling code. |
+1 202 555 0143 sits in the North American 555-0100 to 555-0199 range, which NANPA keeps reserved for entertainment and advertising (https://www.nanpa.com/numbering/555-line-numbers, accessed 27 September 2026), so the lookup answers it valid with `is_fictional: true`; it stands in for an office number throughout this guide.
Use a text column, never an integer. libphonenumber's list of falsehoods programmers believe about phone numbers (https://github.com/google/libphonenumber/blob/master/FALSEHOODS.md, accessed 27 September 2026) puts it plainly: 007, 07 and 7 are the same number but not necessarily the same phone number, because in some countries a leading 0 is part of the number. The same page notes that ITU-T limits a phone number to fifteen digits and that Germany has assigned longer ones. libphonenumber's metadata allows German fixed-line numbers of up to 17 digits with the calling code, and Indonesian toll-free numbers of up to 19. Spaw's own suppression and feedback tables hold E.164 values in 20-character columns.
Do not store `valid` or the line type as permanent facts about a row. The falsehoods page advises not storing them at all and checking again when they are needed: a number that connects today may be disconnected tomorrow, a number of one type can be reassigned to another, and a plan can insert a digit into existing numbers. If you keep an answer, keep the date it was given, and look the number up again when a decision depends on it.
## A national number needs a country
`07400 123456` sent with no country answers `valid: false` with the reason [`missing_country`](/docs/reasons/missing_country), free: without a calling code, a national number could belong to any plan. Sent with `country: "GB"`, the same input answers `+447400123456`, and so do `7400123456` and `447400123456` sent with GB, because libphonenumber recognises a leading calling code that matches the country sent. Sent with US, `2025550143`, `1 202 555 0143` and `12025550143` all answer `+12025550143`.
The country is a hint, and the wrong hint can produce a valid answer for the wrong country. `447400123456` sent with DE answers valid as a German landline number, because those twelve digits fit Germany's plan; nothing in the digits shows that they were a UK mobile number that lost its plus. When a form collects a country, send it. When a CSV has no country column, send the country the list came from, and prefer values that start with a plus. An API key can carry a default country, applied only when a request names none.
## Trunk prefixes: dialled at home, dropped after the calling code
A trunk prefix is what is dialled before a number inside its own country and left out after the calling code: the 0 of `020` in London, the 1 in front of a North American long-distance number. libphonenumber's metadata (the same in 9.0.37 and 9.0.40) describes 245 numbering regions: 113 use 0 as their trunk prefix, 101 have none, and 31 use another one, which is 1 in the North American regions and the Marshall Islands, 8 in Russia, Kazakhstan, Belarus and Turkmenistan, and 06 in Hungary. The [formatter](/tools/phone-number-formatter) lists every region whose trunk prefix is something other than 0, and the 17 without one whose numbers keep a leading 0; each region's page under [phone facts by country](/tools/phone-number-lookup/countries) states its trunk and international prefixes.
Getting the trunk prefix wrong produces two mistakes.
**Keeping the trunk 0.** `+44 (0)7400 123456` is a common way to write a UK number for readers at home and abroad at once. ITU-T Recommendation E.123 (02/2001), the standard for writing numbers down (https://www.itu.int/rec/T-REC-E.123, accessed 27 September 2026), uses parentheses for digits that are not always dialled and says they should not be used in an international number (clause 7.2). The lookup answers it valid, `+447400123456`, and does the same for `+4407400123456`, where the 0 was kept without brackets. A database is less forgiving: `+4407400123456` and `+447400123456` are different strings, so a table keyed on what was typed holds the same person twice.
**Dropping a 0 that belongs to the number.** In 17 of the regions with no trunk prefix, a number can itself start with 0, and the 0 stays after the calling code. Italy is the usual example: the falsehoods page records that since 1998 the old prefix has been part of the number. libphonenumber's example Italian landline `02 1234 5678` is `+39 02 1234 5678`, E.164 `+390212345678`. `+39 2 1234 5678`, with the 0 removed as if it were a UK trunk prefix, answers `invalid_number`. The [Italy](/tools/phone-number-lookup/countries/it) and [United Kingdom](/tools/phone-number-lookup/countries/gb) pages show both plans.
North America is the third case. Its trunk prefix is 1, the same digit as its calling code, so `1 202 555 0143` sent with US and `+1 202 555 0143` are the same number, and the [United States](/tools/phone-number-lookup/countries/us) page describes the plan behind the 1.
## International prefixes: 00, 011 and the rest
The plus sign stands for whatever is dialled to leave the caller's country, which differs from country to country (E.123, clause 7.1). In the same metadata, 00 is the international prefix libphonenumber gives for 185 of the 245 regions, and the patterns of seven more, Ukraine's among them, also accept it. Twenty-six dial only 011: the 25 North American regions and the Marshall Islands. Palau's pattern accepts 011 or 012, and Singapore's any three digits from 000 to 039, so both accept 011 too. The rest dial something else, such as 0011 in Australia, 010 in Japan, or 8~10 (dial 8, wait for a tone, dial 10) in Russia, Kazakhstan, Belarus, Tajikistan and Turkmenistan. People type the prefix they dial, so lists carry all of these.
Spaw reads the two common prefixes differently, on purpose.
| Input | Country sent | Answer |
| --- | --- | --- |
| `00447400123456` | US | valid, `+447400123456` |
| `0044 7400 123456` | none | valid, `+447400123456` |
| `011 44 7400 123456` | US | valid, `+447400123456` |
| `011447400123456` | GB | invalid, `too_long`, `did_you_mean: "+447400123456"` |
A leading 00 is read as a plus sign whatever country is sent, whenever the country's own reading of the digits is not a valid number, and the result is kept only if it is valid. Most of the world dials 00. A number read this way is an ordinary valid answer, charged like any other. Where 011 is one of the country's own international prefixes, libphonenumber reads it directly, as with `011 44 7400 123456` sent with US. Anywhere else, 011 is never removed on the lookup's own authority. In the UK the area codes of Leeds (0113), Sheffield (0114), Leicester (0116), Bristol (0117) and Reading (0118) all begin with those three digits. The lookup answers such input invalid and offers the other reading as `did_you_mean` when it is the only valid one.
## did_you_mean is a suggestion, never a correction
`did_you_mean` appears on invalid answers only, and only when a country was sent. It carries the E.164 form of the one valid reading of what was typed: the digits with a plus added, when they land in another country, or the digits without a foreign international prefix that was left in front of them, such as 011 sent with a country that does not dial it.
```bash
curl https://spaw.co/api/v1/phone \
-H "Authorization: Bearer sk_live_…" \
-H "Content-Type: application/json" \
-d '{"phone": "447400123456", "country": "US"}'
```
```json
{
"success": true,
"data": {
"valid": false,
"reason": "too_long",
"e164": null,
"did_you_mean": "+447400123456",
"// 45 more fields, then sources": "see the phone intelligence guide"
},
"meta": { "credits_used": 0, "credits_remaining": 100, "cache_hit": false }
}
```
Those are four of the 49 fields under `data`. The answer stays invalid and free, and nothing is applied. The field is null when no reading is valid, when more than one is, when the reading lands in a block the regulator's table says nobody holds (unless it sits in a range reserved for fiction), and when the input could be a national number of the country sent with one digit too many or too few. `33612345678` sent with US shows the last case. With a plus, those digits are libphonenumber's example French mobile number, `+33 6 12 34 56 78`, but eleven digits is also what a US number with one extra keystroke looks like, so the answer is [`too_long`](/docs/reasons/too_long) with no suggestion.
Show the suggestion to the person who typed the number and let them confirm it. On an imported list, write it to a review column and never over the stored value: a corrected number can belong to someone who never gave it to you.
## Extensions stay out of E.164
`+1 202 555 0143 x204` answers `e164: "+12025550143"` and `extension: "204"`. The display forms keep the extension: `national` reads `(202) 555-0143 ext. 204`, and `rfc3966` reads `tel:+1-202-555-0143;ext=204`. The `;ext=` parameter is the one RFC 3966, the `tel` URI specification (December 2004, https://www.rfc-editor.org/rfc/rfc3966), defines. The spellings `ext. 204`, `extension 204`, `#204` and `;ext=204` all parse to the same two values. E.123 draws the same line on paper (clause 2.7): the extension is written after the number, so it stays apart from the digits to be dialled.
Because the extension is not part of E.164, two extensions of one switchboard share an `e164`. Spaw's bulk runs recognise repeats the same way: a row with `x205` after a row with `x204` on the same number is marked `is_duplicate`, answered free, and left out of the `deduped` download, although its own answer still carries `extension: "205"`. If a contacts table needs both, make its unique key `(e164, extension)` rather than `e164` alone, and store an empty string rather than NULL when there is no extension: PostgreSQL (unless the index is declared `NULLS NOT DISTINCT`), MySQL and SQLite let a unique index hold any number of rows whose extension is NULL.
## Why a regex is not enough
The widely copied pattern `^\+[1-9]\d{1,14}$` describes the shape of an E.164 string. It is a reasonable check on a column that is already normalised, widened if the column must admit the longer German and Indonesian numbers above. It cannot validate what people type: it rejects `+44 (0)7400 123456`, which is a valid number, and it accepts every row below.
| Input | Pattern | libphonenumber 9.0.37 and 9.0.40 | Spaw |
| --- | --- | --- | --- |
| `+39212345678` | accepts | invalid: Italy's leading 0 was dropped | `invalid_number` |
| `+12025550143` | accepts | valid | valid, `is_fictional: true` (555-01XX) |
| `+447700900123` | accepts | invalid: Ofcom's drama range is not in the metadata | valid, `is_fictional: true` |
| `+442030130018` | accepts | valid | invalid, `unassigned_block`: Ofcom lists 020 3013 as protected, with no holder |
| `+13575550143` | accepts | invalid: area code 357 is not in the metadata yet | `invalid_number` |
A pattern knows lengths. The numbering plan, which libphonenumber encodes, knows which ranges exist and what type each range is. The regulators' allocation tables know which blocks somebody holds: of the 343,860 number blocks in Ofcom's table as synced on 26 September 2026, 218,840 are free, protected, quarantined or reserved in Ofcom's terms, which Spaw reads as [unassigned](/guides/what-an-unassigned-block-means). And the plan cannot say whether a North American number is a mobile; [the carrier the block was originally allocated to sometimes can, until the number is ported](/guides/why-us-numbers-cannot-be-split-into-mobile-and-landline).
The last row is a limit Spaw shares. NANPA's central office code report of 26 September 2026 (https://www.nanpa.com/reports/co-code-reports) lists 33 assigned central office codes in four overlay area codes, 357 and 837 in California, 483 in Alabama and 861 in Illinois, all assigned between 7 August and 25 September 2026 and none yet marked in service. libphonenumber 9.0.37 and 9.0.40 do not include those area codes, and Spaw decides validity from the metadata, and its bundled list of fiction ranges, before it reads any regulator table, so a number in them answers `invalid_number` until a metadata update adds them. That is one more reason to keep the raw input: a number rejected today can be read again later.
## Checklist
- Parse on the server, with the country the input was written for, and store `e164`, `extension` and the raw input.
- Derive `national`, `international` and the `rfc3966` link when displaying a number, and never key on them.
- Make `e164` unique, or `(e164, extension)` for switchboard contacts, with an empty extension rather than NULL when there is none.
- Offer `did_you_mean` to the person who typed the number, and never apply it to a list.
- Treat validity and line type as dated answers, not permanent columns.
- Paste a column into the [formatter](/tools/phone-number-formatter) to see how each row reads, and see the [E.164 glossary entry](/glossary/e164) and the [phone intelligence guide](/docs/phone-intelligence) for every field.
---
# Number pooling: why a US code's holder is often not your block's holder
In a pooled US code each block of a thousand can have its own holder. How often it differs from the code's, and what carrier and line type then describe.
Date: 2026-09-27
In a pooled US code, the holder NANPA lists for the six-digit code (NPA-NXX) is often not the holder of the number's own block of a thousand (NPA-NXX-X): in NANPA's files of 26 September 2026, 141,810 of the 197,177 assigned US codes (71.9%) were pooled, 663,918 of the 1,303,876 allocated thousands-blocks (50.9%) sat under a different [operating company number](/glossary/operating-company-number) (OCN) than their code, and at least 468,207 (35.9%) under a different network brand.
This guide explains how pooling splits a code, what each of NANPA's two reports says, how often the holders differ by OCN, by brand and by line type, and which fields of Spaw's answer describe the block. Unless a figure says otherwise it comes from the NANPA files Spaw synced on 26 September 2026 and counts the United States without its territories.
## How pooling splits a code
A central office code covers 10,000 numbers in one [rate centre](/glossary/rate-centre). Before pooling, a carrier that needed numbers in a rate centre was given a whole code of 10,000, whatever its actual need, which the FCC named as one of the major factors behind numbering exhaust in its Numbering Resource Optimization order of March 2000 (FCC 00-104, CC Docket 99-200, paragraph 4, https://docs.fcc.gov/public/attachments/FCC-00-104A1.pdf, accessed 27 September 2026). [Thousands-block number pooling](/glossary/number-pooling) separates a code's 10,000 numbers into ten blocks of 1,000 and allocates them separately within that rate centre (47 CFR § 52.20(a), https://www.law.cornell.edu/cfr/text/47/52.20, accessed 27 September 2026). The same order adopted pooling nationwide, pointing to the Illinois trial in which pooling had extended the life of area code 847 by 24 months beyond its projected exhaust date (paragraph 8).
When a code joins the pool, its holder must donate every block with ten percent or less contamination to the pool for that rate centre, and may keep at least one block per rate centre as a footprint (§ 52.20(c)(1) and (c)(2)). A block is contaminated when at least one of its numbers is not available for assignment (47 CFR § 52.7(h), https://www.law.cornell.edu/cfr/text/47/52.7, accessed 27 September 2026). NANPA assigns the pooled blocks to the carriers that apply for numbers in that rate centre (https://www.nanpa.com/numbering/co-codesthousands-blocks, accessed 27 September 2026).
All ten blocks stay in the code's rate centre, so pooling changes who holds a number, not where it is. Spaw takes `region` from the code whichever level answers.
## NANPA's code report and thousands-block report
NANPA publishes two tables that matter here. The central office code report lists every assigned code with one holder, its OCN and whether the code is pooled (https://www.nanpa.com/reports/co-code-reports, accessed 27 September 2026). The thousands-block report lists every block of a thousand in every pooled code with its status and, for an allocated block, its own holder and OCN (https://www.nanpa.com/reports/thousands-block-reports/region, accessed 27 September 2026). Spaw syncs both daily.
A reading that stops at the six digits can only return the code's holder, because that is all the first report holds. The block's holder is in the second. It uses five statuses; the counts below are from the report NANPA published on 27 September 2026, across every region including the territories.
| Status | Meaning | Blocks | What Spaw answers for a number in the block |
| --- | --- | --- | --- |
| AS | Assigned by NANPA to a service provider | 990,929 | The block's holder, with `block_assigned_at` as the date NANPA assigned the block (null for the 526 US blocks the report leaves undated) |
| RT | Retained: not assigned by NANPA (typically a block the code holder kept when the code was pooled) | 318,979 | The block's holder; `block_assigned_at` is null because the report gives no date |
| AV | Available for assignment | 108,447, of which 23,284 contaminated | Not contaminated: `valid: false` with [`unassigned_block`](/docs/reasons/unassigned_block). Contaminated: the code holder's answer |
| AP | Available, pending verification of its activation in the network | 4,006, of which 133 contaminated | As AV |
| AF | Available from a future effective date | 2,025 | The code holder's answer |
A free block answers `unassigned_block` only while Spaw's copy of the report is under 45 days old. Contaminated blocks are the reason for the exception in the table. The numbers that made a block contaminated stay with the carrier that donated it, and the report gives how many of them there are, not which. Reading such a block as free could call live numbers unassigned, so Spaw lets the code's own row answer for it, and does the same for a block available only from a future date. In pooled US codes, 25,662 of the 1,418,100 block positions (1.8%) have no row Spaw reads, because the block is contaminated, future-dated or missing from the report, and 88,562 (6.2%) answer `unassigned_block`.
## How often the holders differ
| United States, NANPA files of 26 September 2026 | Count | Share |
| --- | --- | --- |
| Assigned codes that are pooled | 141,810 of 197,177 | 71.9% |
| Pooled codes whose allocated blocks sit under two or more OCNs | 122,286 of 141,810 | 86.2% |
| Allocated blocks under a different OCN than their code | 663,918 of 1,303,876 | 50.9% |
| of which dated blocks, all assigned by NANPA (AS) | 647,684 of 985,981 | 65.7% |
| of which undated blocks, nearly all retained (RT) | 16,234 of 317,895 | 5.1% |
The split by status is the informative one. A retained block almost always stays with the code's holder. A block NANPA assigned from the pool belongs to a different OCN two times in three, and 26 pooled codes had ten blocks under ten different OCNs.
## An OCN is not a carrier brand
An OCN identifies one operating entity, and one company often holds several. Verizon's wireless and wireline companies in New Jersey, for example, hold different OCNs. A different OCN is therefore not always a different company. Spaw's brand map groups OCNs and names under today's network brand, reported as `carrier_brand` (map of 24 September 2026), and with it the 663,918 blocks under another OCN divide three ways:
| Block holder compared with the code's holder | Blocks | Share of the 663,918 |
| --- | --- | --- |
| A different brand | 468,207 | 70.5% |
| The same brand under a sibling OCN | 43,326 | 6.5% |
| One or both holders not in the brand map | 152,385 | 23.0% |
So at least 35.9% of all allocated US blocks sit under another brand than their code's holder, and the unmapped remainder can only add to that. The commonest pairs are an AT&T code with a block held by Inteliquent (23,082 blocks), T-Mobile (20,524) or Verizon (16,784), and a Verizon code with a block held by Inteliquent (17,446) or T-Mobile (16,344).
## What it does to line type
libphonenumber, the numbering metadata behind Spaw, answers `fixed_line_or_mobile` for every geographic North American number, so Spaw settles `line_type` from the holder when the holder's name marks a wireless carrier or a VoIP wholesaler ([why US numbers cannot be split into mobile and landline](/guides/why-us-numbers-cannot-be-split-into-mobile-and-landline) explains the rule). Pooling decides whose name that is. The counts below cover the same 1,303,876 allocated blocks. A holder that is not on Spaw's classification list settles nothing, so counts of blocks held by a listed wireless or VoIP holder are lower bounds, and counts of blocks whose holder is not on the list may include wireless or VoIP carriers the list does not know.
| United States, NANPA files of 26 September 2026 | Blocks | Share |
| --- | --- | --- |
| Line type settled, reading each block's own holder | 873,008 | 67.0% |
| Line type settled, reading only the code's holder | 673,803 | 51.7% |
| Block holder and code holder give different answers (mobile, VoIP or unsettled) | 434,076 | 33.3% |
| Blocks held by a holder on the wireless list | 514,532 | 39.5% |
| of which inside a code whose holder is not on that list | 177,240 | 34.4% of them |
| Blocks inside a wireless holder's code, held by a holder not on that list | 98,289 | 22.6% of 435,581 |
Of the 177,240 wireless blocks inside other holders' codes, 124,999 sit in codes whose holder settles nothing and 52,241 in codes held by a holder on the VoIP list. Read at code level, the first group would stay `fixed_line_or_mobile`, and the second would answer `voip` and raise the [`virtual`](/docs/signals/phone/virtual) signal for a number whose block a wireless carrier holds.
## One code, block by block
Code 201-544 in Jersey City is held by Verizon New Jersey, Inc. (OCN 9206) and is pooled. The table shows its ten blocks as NANPA listed them on 26 September 2026 and what Spaw answered for a number in each. VoIP here is a line type read from the holder's name, not a judgement about the carrier or the person using the number.
| Block | NANPA status | Holder as NANPA lists it (OCN) | What Spaw answers |
| --- | --- | --- | --- |
| 201-544-0, -1, -2 | Assigned 12 Nov 2025 | HD Carrier LLC (321J) | `voip`, `block_assigned_at` 2025-11-12 |
| 201-544-3, -8 | Assigned 13 Aug 2012 | Verizon New Jersey, Inc. (9206) | `fixed_line_or_mobile`, brand Verizon, `block_assigned_at` 2012-08-13 |
| 201-544-4 | Assigned 19 Mar 2026 | Boost SubscriberCo L.L.C. (490J) | `mobile`, brand Boost Mobile, `block_assigned_at` 2026-03-19 |
| 201-544-5 | Available, contaminated | none | The code holder, Verizon New Jersey; `block_assigned_at` null |
| 201-544-6, -7 | Available | none | `valid: false`, `unassigned_block` |
| 201-544-9 | Assigned 15 Jun 2026 | Bandwidth.com CLEC, LLC - NJ (118F) | `voip`, brand Bandwidth, `block_assigned_at` 2026-06-15 |
The six digits alone say Verizon New Jersey, a holder whose name settles no line type. Seven of the ten blocks answer differently: five are held by other carriers and two are free. In the report of 27 September 2026, block 201-544-5 still held five numbers that were not available for assignment. A number in block 201-544-4 answers:
```json
"line_type": "mobile",
"line_type_source": "block_allocation",
"carrier": "BOOST SUBSCRIBERCO L.L.C.",
"carrier_source": "block_allocation",
"carrier_code": "490J",
"carrier_brand": "Boost Mobile",
"block_status": "allocated",
"block_pooled": true,
"block_assigned_at": "2026-03-19"
```
## What the answer says about the level
- `block_pooled: true` means the number's code is pooled, whichever level answered. `false` means a North American code that is not pooled. `null` means no allocated block answered, or the number is outside North America.
- `carrier`, `carrier_code`, `carrier_brand`, a settled `line_type` and `is_virtual` describe whichever holder answered: the block's where NANPA's report lists an allocated block, otherwise the code's.
- A date in `block_assigned_at` on a pooled code means NANPA assigned the number's own block on that date, so the block answered.
- A null `block_assigned_at` on a pooled code does not tell you the level. A retained block has no date in the report, nor do 526 assigned US blocks, and where the code holder answers (a contaminated or future-dated block, or a pooled Canadian code) the field stays null on purpose, because the code's own date says nothing about the block. In 201-544 above, blocks 3 and 5 both answer Verizon New Jersey, and only block 3 carries a date.
- Undated does not mean new. No assigned US code carries a date before 2 January 2001, and 112,611 of the 197,177 (57.1%) carry none; block dates start on 19 June 1998, and 317,895 allocated blocks, nearly all of them retained, have no date.
No field names the level directly.
## Canada is read at code level
The CRTC ordered thousands-block pooling in Telecom Regulatory Policy CRTC 2024-26 of 5 February 2024, and the Canadian Numbering Administrator is rolling it out in phases, from a three-exchange launch trial planned to complete on 28 July 2026 to a last phase planned for 28 July 2027 (https://www.cnac.ca/co_codes/TBP/TBP_Implementation.htm, accessed 27 September 2026). CNAC's code table of 23 September 2026 (https://www.cnac.ca/co_codes/co_code_status.htm, accessed 27 September 2026) marked 2 of the 28,710 assigned Canadian codes as pooled. Spaw reads no Canadian block table, so a Canadian number answers with its code's holder, and `block_pooled` is true for those two codes only. What the Canadian table holds is on [the Canada page](/tools/phone-number-lookup/countries/ca).
## Porting: the block's holder is not today's carrier
A block's holder is the original allocation. A number [ported](/glossary/number-portability) to another carrier stays in its block, and NANPA's reports keep listing the original holders: the code report the code's, the thousands-block report the block's. Pooling moves the question from the code to the block; porting moves it again, from the block to whoever serves the number today, which no open table records. Only a query into the carrier networks can answer that, and the API keeps it apart in `ported` and `current_carrier`. The live carrier-network check (`hlr: true`) is not enabled on the service: `hlr_checked` answers false and nothing extra is charged.
## Where Spaw counts codes and where it counts blocks
The area-code pages and [`GET /api/v1/phone/block`](/docs/api/phone-block-report) count six-digit codes, so their holder shares describe code holders. The [carrier pages](/tools/phone-number-lookup/carriers) count numbers and credit each thousands-block to its own holder, not to the code's. The [United States page](/tools/phone-number-lookup/countries/us) and [the phone data sources page](/tools/phone-number-lookup/sources) say which NANPA files are installed and when they were synced.
## Using the answer
- Do not infer a number's carrier from another number with the same first six digits. In 86.2% of pooled US codes the blocks sit under two or more OCNs.
- Group by `carrier_brand` when you report by company and by `carrier_code` when you need the legal entity. One brand spans many OCNs: 43,326 blocks sit under a sibling OCN of their code holder's brand.
- Re-read numbers whose carrier or line type you stored from a code-level source. A third of allocated US blocks give a different line type from their code's holder.
- Treat `fixed_line_or_mobile` as unknown, not as a landline, at either level.
## What to do next
- Read the [phone intelligence guide](/docs/phone-intelligence) for every field and where it comes from.
- See how the six digits are named in the [NPA/NXX](/glossary/npa-nxx) glossary entry, and what the line types mean in [line type](/glossary/line-type).
- See how the tables name holders in [operating company number](/glossary/operating-company-number) and [number pooling](/glossary/number-pooling).
- See what a free block answers in [what an unassigned block means](/guides/what-an-unassigned-block-means) and the [unassigned block](/glossary/unassigned-block) entry.
- Check the request and response on the [POST /api/v1/phone reference](/docs/api/validate-phone).
---
# SMS delivery error codes: retry, remove the number, or fix your sender
Twilio, Sinch and MessageBird delivery error codes sorted by what to do next, and which of them a phone lookup can warn you about before you send.
Date: 2026-09-27
An SMS delivery error code says why a message failed: the number is not an active line, the line cannot take text messages, the handset was out of reach at that moment, the recipient opted out, or your own sender was refused. Sinch 150 and MessageBird 1 mean the number is not an active line, so remove it; Twilio 30006, Sinch 152 and MessageBird 11 mean the line cannot take text messages, so reach the person another way; Twilio 30003 and MessageBird 29 mean the handset was off or out of reach, so retry later; Twilio 21610 and MessageBird 103 mean the recipient opted out, so stop; Twilio 30007 and 30034 mean your sender or your content was refused, and removing the number would only hide that. Twilio publishes no delivery code that proves a number is gone.
This guide sorts the codes of three SMS providers by what to do next, from each provider's own documentation, and says which failures a phone lookup can warn you about before you send. The code lists are the ones Spaw's status webhooks read, so every code in the tables below is recorded the way this page describes.
## Five things a failed receipt can mean
| What failed | What to do | Twilio | Sinch | MessageBird | Can a lookup warn you first? |
| --- | --- | --- | --- | --- | --- |
| The number is not an active line, or its line is barred (`permanent`) | Remove the number; for a barred line, stop until the recipient has the bar lifted | none | `150`, `151` | `1` | Only when the regulator's table shows no carrier holding the number's block, which the lookup refuses as invalid |
| The line cannot take SMS (`not_sms_capable`) | Stop texting it and reach the person another way; for `30006` from a short code, Twilio suggests a long-code fallback first, and Sinch `154` can pass once the recipient is off the roaming network | `30006` | `152`, `154`, `204`, `212` | `10`, `11`, `21` | A hint where the plan tells landlines from mobiles (`line_type: fixed_line`), which the plans of the US, Canada, several other +1 areas and Denmark do not; nothing for a plan, SIM or network without SMS |
| The recipient opted out (the `opted_out` outcome) | Stop, and keep it stopped | `21610` | none in Spaw's table | `103` | Only from your own records (`meta.opted_out`) |
| The handset or its network, this time (`transient`) | Retry later, and give up after repeats | 4 codes: `30003`, `30004`, `30005`, `30008` | 20 codes, such as `153`, `159`, `201` | 21 codes, such as `27`, `28`, `29` | No: these describe the handset or its network when the message was sent, or are too vague to pin on the number (MessageBird marks `2` and `9`, both about the handset's registration, permanent) |
| Your sender, content or sending rate (`sender_side`) | Fix your registration, content or rate | 12 codes, such as `30007`, `30034`, `30032`, `30018` | 40 codes, such as `60`, `62`, `64`, `310` | 11 codes, such as `104`, `105`, `110` | No lookup of the recipient can; MessageBird `131`, a destination its pumping guard forbids, is covered below |
The words in brackets are the `failure_class` Spaw records for a failed delivery, and `opted_out` is recorded as an outcome of its own, never as a failure. The providers' own labels differ: Sinch and MessageBird mark most codes permanent or temporary ("intermediate" in MessageBird's table), and Twilio marks none. Spaw's classes answer a narrower question than the providers' labels do: is the failure about the number? A code the provider calls permanent because the sender is unregistered, such as MessageBird `104`, is `sender_side` here, because the number is fine.
## The codes that are about the number
Thirteen codes across the three providers say something about the recipient's number rather than the moment or the sender (MessageBird `131`, a destination its pumping guard forbids, is kept with the sender-side codes, for the reason given further down). The descriptions paraphrase each provider's documentation: [Twilio's error dictionary](https://www.twilio.com/docs/api/errors), [Sinch's SMPP error codes](https://developers.sinch.com/docs/sms/smpp/error-codes), which Spaw reads from the `code` field of its delivery reports, and [MessageBird's SMS API reference](https://developers.messagebird.com/api/sms-messaging/), all checked 27 September 2026.
| Provider | Code | Provider's name | Spaw records | What the provider's documentation says | Before sending |
| --- | --- | --- | --- | --- | --- |
| Twilio | `30006` | Landline or unreachable carrier | `not_sms_capable` | The destination is a landline, or the carrier cannot be reached from a short code | A hint from `fixed_line`, where the plan tells landlines from mobiles |
| Twilio | `21610` | Attempt to send to unsubscribed recipient | `opted_out` | The recipient sent STOP or another opt-out keyword to your number, sender or Messaging Service; Twilio also lists a recently reassigned number as a cause | Your own suppression list |
| Sinch | `150` | Unknown subscriber | `permanent` | The number is not active with the carrier | Only if the regulator's table shows no holder for its block |
| Sinch | `151` | Call barred | `permanent` | The recipient's line is barred; Sinch says it might be best to stop sending to it for now, until the recipient has the bar lifted | No |
| Sinch | `152` | Teleservice not provisioned | `not_sms_capable` | The line has no SMS subscription | A hint from `fixed_line`, where the plan tells landlines from mobiles |
| Sinch | `154` | Facility not supported | `not_sms_capable` | SMS is not provisioned in the network the recipient is visiting, so it fails while they roam there and can pass once they are back on their home network | No |
| Sinch | `204` | Facility not supported | `not_sms_capable` | SMS is not provisioned on the recipient's SIM | No |
| Sinch | `212` | Device not short message equipped | `not_sms_capable` | The device cannot receive texts, such as a data-only SIM | No |
| MessageBird | `1` | EC_UNKNOWN_SUBSCRIBER | `permanent` | The number is not associated with an active line | Only if the regulator's table shows no holder for its block |
| MessageBird | `10` | EC_BEARERSERVICE_NOT_PROVISIONED | `not_sms_capable` | The subscriber has two numbers and this one is not enabled for SMS | No |
| MessageBird | `11` | EC_TELESERVICE_NOT_PROVISIONED | `not_sms_capable` | Not a mobile, such as an IoT number, or a plan without SMS | A hint from `fixed_line`, where the plan tells landlines from mobiles |
| MessageBird | `21` | EC_FACILITY_NOT_SUPPORTED | `not_sms_capable` | The receiving network does not support SMS | No |
| MessageBird | `103` | EC_SUBSCRIBER_OPTEDOUT | `opted_out` | The recipient opted out; only an opt-in keyword such as START from them reverses it | Your own suppression list |
Two labels in the providers' tables read differently from Spaw's. Sinch marks `212` temporary, and MessageBird marks `10`, `11` and `21` intermediate; Spaw keeps all four with the not-SMS-capable codes because each describes the line, the SIM or the network rather than the handset's state at that moment. MessageBird also files `21` under the status reason "received network error", which it calls temporary. Not-SMS-capable failures are never scored, so reading these four that way never counts them against the number.
Sinch has no opt-out row. Its SMPP table describes `255` and `257`, both personal service barring, as an end user who opted out of messages, but gives `255` a second meaning for toll-free senders: a message the carrier blocked for spam filtering. Its [10DLC and toll-free error page](https://developers.sinch.com/docs/sms/smpp/10dlc-errors) (checked 27 September 2026) maps toll-free `255` to spam and only `257` to a STOP. Spaw's table reads neither, so a receipt carrying either is recorded as an unclassified failure. When you know the recipient opted out, report the number yourself with the outcome `opted_out`.
## Twilio has no code that proves a number is gone
Spaw reads eighteen of Twilio's codes. The two about the number are in the table above; these are the other sixteen, and the four that can be about the handset are all read as `transient`, including 30005, whose name reads like a number that is gone.
| Code | Twilio's name | Spaw records |
| --- | --- | --- |
| `30003` | Unreachable destination handset | `transient` |
| `30004` | Message blocked | `transient` |
| `30005` | Unknown destination handset | `transient` |
| `30008` | Unknown error | `transient` |
| `30001` | Queue overflow | `sender_side` |
| `30002` | Account suspended | `sender_side` |
| `30007` | Message filtered | `sender_side` |
| `30017` | Carrier network congestion | `sender_side` |
| `30018` | Destination carrier requires sender ID pre-registration | `sender_side` |
| `30019` | Content size exceeds carrier limit | `sender_side` |
| `30022` | US A2P 10DLC - Rate Limits Exceeded | `sender_side` |
| `30023` | US A2P 10DLC - Daily Message Cap Reached | `sender_side` |
| `30024` | Numeric Sender ID Not Provisioned on Carrier | `sender_side` |
| `30027` | US A2P 10DLC - T-Mobile Daily Message Limit Reached (deprecated; Twilio says it no longer generates it) | `sender_side` |
| `30032` | Toll-Free Number Has Not Been Verified | `sender_side` |
| `30034` | US A2P 10DLC - Message from an Unregistered Number | `sender_side` |
Twilio's page for 30005 says the destination number "is unknown and may no longer exist", and then lists a handset that is switched off or out of signal, a number that cannot receive SMS and a problem at the carrier among the possible causes. A code with four explanations, two of them passing, cannot justify removing a number on one sighting, so Spaw counts it with the other temporary failures: two of them with no delivery after them put the number on your suppression list. The same goes for 30004, whose causes include a blocked destination and filtering of your content.
Twilio documents two more codes about the number: 21211, an invalid `To` number, and 21614, "'To' number is not a valid mobile number". Both describe the `To` number of the API request that creates the message, and neither page says whether the error comes back on that request or on a status callback. Spaw's Twilio table does not read them, so a callback carrying either is recorded as an unclassified failure. 21614, which Twilio says may be a landline, is the one a `fixed_line` answer can hint at. For messages sent through a Messaging Service, 21610 arrives on the status callback: Twilio's [Advanced Opt-Out documentation](https://www.twilio.com/docs/messaging/tutorials/advanced-opt-out) (a Messaging Service feature, checked 27 September 2026) says later messages to a recipient who opted out fail with 21610 asynchronously.
The twelve sender-side codes are about 10DLC and toll-free registration, sender IDs, content, sending rate, daily caps, carrier congestion and the account. Almost none of them says anything about the recipient: Twilio's 30024 page also lists a destination that recently ported, or is not provisioned for SMS, among its causes, which is one more reason Spaw never scores these codes against the number. The same failure meets the next person on your list until the registration, the content, the sending rate or the account is fixed, or until the day's cap resets (30023 resets at midnight Pacific time) or the carrier's congestion clears (30017, congestion in MMS traffic, which Twilio retries before it returns the code). Sinch's equivalents include `60`, an originator whose campaign is not active, `62`, a keyword that triggered a filter, and `64`, a quota exceeded; its [10DLC and toll-free error page](https://developers.sinch.com/docs/sms/smpp/10dlc-errors) (checked 27 September 2026) maps the carriers' reasons to them. MessageBird's include `104` and `105`, an unregistered sender or content, `130`, which its surge guard returns when it sees a traffic-pumping pattern in your sending, and `131`, which it returns when it has marked the destination itself as forbidden for suspected traffic pumping. Spaw files `131` as sender-side so that it is never scored against the number; one lookup cannot say a number is pumped, and [what the default SMS policy lets through](/guides/sms-pumping-what-the-default-sms-policy-lets-through) covers the rules that watch your own traffic for the pattern.
## What a lookup can warn you about before you send
**A block nobody holds.** A number answers `valid: false` with the reason `unassigned_block` when the regulator's table lists its block as unassigned, whether the block was never handed out or was returned and is resting before reuse, or when its block is missing from a table that lists every assigned block and was synced within the last 45 days. The SMS policy blocks every invalid number, so a flow that checks `ok_to_send` never sends to it. The test number +1 202 555 0104 answers exactly that, `ok_to_send: false` with `blocked_by: "invalid"`, and like every test number it costs nothing. This is the only part of the "remove the number" row a lookup can see: a number that was assigned and later disconnected usually sits in a block its carrier still holds, and passes. Which regulators publish such a table, and whether each is complete, is on [the phone data sources page](/tools/phone-number-lookup/sources).
The live carrier-network check (`hlr: true`) is not enabled on the service: `hlr_checked` answers false and nothing extra is charged.
**A fixed line, where the plan can tell.** Where the numbering plan tells landlines from mobiles, the answer carries `line_type: fixed_line` from the plan; a London 020 number answers that way, and the page for [the United Kingdom](/tools/phone-number-lookup/countries/gb) gives the digits its mobile numbers start with. It is a hint rather than a verdict, because some fixed lines are enabled for text. The default SMS policy blocks no line type; add `fixed_line` to `blocked_line_types` if you would rather not try. In the United States and Canada no real number answers `fixed_line`: the plan answers `fixed_line_or_mobile`, and the holder of the number's block can settle it only to `mobile` or `voip`, for the reasons in [why US numbers cannot be split into mobile and landline](/guides/why-us-numbers-cannot-be-split-into-mobile-and-landline). The same holds for Puerto Rico, the Dominican Republic, Jamaica, Guam, the Northern Mariana Islands, the US Virgin Islands and Denmark, and for part of India's plan; the other +1 areas, such as the Bahamas and Trinidad and Tobago, do answer `fixed_line`. A `fixed_line_or_mobile` answer predicts nothing about 30006.
**Not toll-free.** `line_type: toll_free` is not a sign that a number cannot take SMS. In the United States and Canada toll-free numbers can be enabled for texting, which is why Twilio has a code, 30032, for sending from one that has not been verified.
**Not the plan, the SIM or the network.** MessageBird `10`, `11` and `21` and Sinch `154`, `204` and `212` describe a subscriber's second number, a plan or SIM without SMS, a data-only device or a network that does not carry SMS. No public data shows any of these before a message is sent; only where `11` means the number is not a mobile at all can the `fixed_line` hint above flag a landline.
**An opt-out, from your own list only.** Once a recipient has opted out and you or your provider reported it, the number is on your suppression list with the reason `opted_out`, a single lookup answers `meta.opted_out: true`, and under the default policy that answer is `ok_to_send: false` with `blocked_by: "opted_out"`. No public data says who opted out of whose messages. `ok_to_send` is a fraud and deliverability decision: it is not consent to message anyone and not a do-not-call scrub, and Spaw knows only the opt-outs you or your provider report to it.
**A number that delivers but should not get the code.** A number published on an SMS-receiving website takes messages, so the trouble with it is not a failed delivery and no error code flags it. Spaw can flag it before sending when one of the lists it syncs carries the number: the test number +1 202 555 0103 answers `is_disposable: true` and, under the default policy, `blocked_by: "disposable"`. When one slips through, report it as `abused`, not as undelivered.
## Sending the receipts back
Point each provider's status callback at `https://spaw.co/api/v1/phone/feedback/{provider}/{key}`, with `twilio`, `sinch` or `messagebird` as the provider and your account's feedback key, and the codes above are read from `ErrorCode` (Twilio), `code` (Sinch) or `statusErrorCode` (MessageBird). Spaw reads Sinch's SMS API delivery reports; Sinch's SMS API reference (checked 27 September 2026) says that API is available to its existing customers and that new customers send through its Conversation API, whose callbacks this webhook does not read. A MessageBird report without an error code falls back to its status reason, and `unknown subscriber` without the code stays `transient`, because MessageBird files eight codes under that reason and only `1` is read as permanent. The [webhook reference](/docs/api/sms-status-webhook) lists the fields each provider sends. If your provider is not one of these, [`POST /api/v1/phone/feedback`](/docs/api/report-phone-feedback) takes the same outcomes with a `failure_class` of your own reading.
What happens next depends on the class:
- **`permanent`, `transient` and unclassified failures count against the number.** While the latest report about it in the last 90 days is a failure, your later lookups of the number carry [`reported_undelivered`](/docs/signals/phone/reported_undelivered), +50. One permanent failure, or two of the others with no delivery or verification after them, puts it on your suppression list, which batch, bulk and monitored runs answer as `suppressed`, free. A later delivery or verified code takes it off.
- **`not_sms_capable` and `sender_side` failures are kept and never scored.** They appear in `undelivered_by_class` on your [feedback summary](/docs/api/phone-feedback-summary), which is where a jump in `sender_side` shows up as a registration or content problem rather than a list problem.
- **An opt-out is recorded as `opted_out`.** It never scores, and it keeps the number on your suppression list until you remove the entry.
A code none of the tables lists is recorded as an unclassified failure, which counts against the number like a `transient` one: it adds `reported_undelivered` (+50) while it is the latest report, and two unclassified or transient failures with no delivery or verification after them put the number on your suppression list. Delivered, undelivered and opted-out reports only ever affect your own account's lookups; no other account reads them.
## What to do next
- Set the status callback URL in each provider's console to the [SMS status webhook](/docs/api/sms-status-webhook) for that provider.
- Check `undelivered_by_class` on the [feedback summary](/docs/api/phone-feedback-summary) after a campaign: sender-side failures are yours to fix, not your list's.
- Read [what an unassigned block means](/guides/what-an-unassigned-block-means) for the one "remove the number" case a lookup refuses before you send: a block nobody holds.
- Set the line types and signals you refuse once, as [gating SMS verification codes with the SMS policy](/guides/gate-sms-verification-codes-with-the-sms-policy) describes.
- For the email side of the same question, read [SMTP bounce codes explained](/guides/smtp-bounce-codes-explained).
---
# SMS pumping (artificially inflated traffic): what the default SMS policy lets through
SMS pumping bills you for codes sent where someone earns a share of each message. What the default SMS policy passes, and the rules that refuse it.
Date: 2026-09-27
SMS pumping, also called artificially inflated traffic (AIT), is fraud in which bots make a business send text messages, such as one-time codes, to numbers where a party in the delivery chain is paid a share of every message: the business pays for each message and nobody reads it. Spaw's default SMS policy is not tuned against it. The defaults refuse the numbers that almost always waste or abuse a code: fictional, disposable, reported for abuse and premium-rate. They let through the ordinary mobiles, the +1 numbers outside the United States and Canada and the international-network numbers that pumping can use. This guide shows what the defaults pass, the one request that tightens them for a code sender, and what your own traffic can and cannot flag. The rules themselves, and the order they run in, are in [gate SMS verification codes with the SMS policy](/guides/gate-sms-verification-codes-with-the-sms-policy).
## How the fraud makes money
AWS describes AIT as bots generating large volumes of fake SMS traffic through the forms that trigger a message: one-time passwords, app download links, promotion signups. The profit, in its words, comes from "colluding with parties in the SMS delivery chain to receive a portion of the revenue generated from each fraudulent message sent" (AWS Messaging blog, "Lower Your Risk of SMS Fraud with Country Level Blocking and AWS End User Messaging", 6 June 2024, https://aws.amazon.com/blogs/messaging-and-targeting/lower-your-risk-of-sms-fraud-with-country-level-blocking-and-amazon-pinpoint/, accessed 27 September 2026). A later post names insecure public web forms as the vector most often used, and puts blocking every country a business does not send to first among the defences (AWS Messaging blog, "Defending Against SMS Pumping: New AWS Features to Help Combat Artificially Inflated Traffic", 29 April 2025, https://aws.amazon.com/blogs/messaging-and-targeting/defending-against-sms-pumping-new-aws-features-to-help-combat-artificially-inflated-traffic/, accessed 27 September 2026).
Two things follow for a phone lookup. Nothing in that arrangement needs a special kind of number: the share comes from delivering the message, so the destination can be an ordinary mobile that is valid, sits in a carrier's block and scores 0. And the pattern shows only across many lookups: number after number submitted by one script, in one range or from one source or to a country the business rarely sends to. One answer cannot say a number is pumped. The controls are the destination rules and the counts of your own traffic, and both live in the SMS policy.
Spaw publishes no figure for how common pumping is. The counts that could show it, of ranges, client addresses and destinations, are kept per account and never read for another account, so there is no cross-account pumping count to publish.
## SMS pumping and IRSF
SMS pumping is often confused with International Revenue Share Fraud (IRSF). Microsoft's documentation for its multifactor authentication describes IRSF as unauthorized actors who "divert telephony traffic and generate profits through a technique called traffic pumping" (Microsoft Learn, "Understanding telephony fraud risk for Microsoft Entra multifactor authentication", dated 4 March 2025, https://learn.microsoft.com/en-us/entra/identity/authentication/concept-mfa-telephony-fraud, accessed 27 September 2026). Microsoft uses "traffic pumping" for IRSF too; what separates the two here is what is abused and who pays. SMS pumping abuses a business's own message sending, usually to ordinary mobile numbers, and bills that business per message. IRSF mostly pushes calls to premium-rate or international numbers whose termination fee is shared, often through a hijacked phone system. The default `premium_rate` block refuses only ranges the numbering plan types as premium rate, such as UK 09, and says nothing about where a revenue share is paid. For a lookup the useful split is by kind of destination, because each kind meets a different rule:
| Destination | What the lookup answers | Under the defaults | The rule that refuses it |
| --- | --- | --- | --- |
| An ordinary mobile in a country you do not serve | `line_type: "mobile"`, no signals, score 0 | Sent | `allowed_countries`, as `country` |
| A +1 number outside the United States and Canada, such as +1 876 | The region's own `country`, `JM` for Jamaica | Sent | `allowed_countries` without that region |
| An international network or satellite number: +870, +881, +882, +883 | `country: null`, `international_network` 30, plus `voip` 30 where the plan types it VoIP | Sent, at 30 or 60 | Any `allowed_countries`, or `international_network` in `block_signals` |
| A premium-rate number, such as UK 09 | `premium_rate` 60 | Refused, as `premium_rate` | The default |
| A UK 084 or 087 shared-cost number | `shared_cost` 20 | Sent | `shared_cost` in `blocked_line_types` |
## What the defaults let through, scored
The defaults block five signals, `fictional`, `disposable`, `reported_abuse`, `reported_abuse_widely` and `premium_rate`, and any score above 60. They send to every country. That leaves the pumping cases below passing, each as the API answers it on 27 September 2026:
| Answer | Signals | Score | `ok_to_send` under the defaults |
| --- | --- | --- | --- |
| A +881 number, or a +870 or +882 number the plan types as mobile or voicemail | international_network 30 | 30, medium | true |
| A +883 number, or a +870 or +882 number the plan types as VoIP | voip 30, international_network 30 | 60, high | true |
| The fifth distinct number your account looked up in one 1,000-number range within the hour, nothing else firing | range_burst 40 | 40, medium | true |
| The fifth distinct number one client address tried within the hour | numbers_per_client 30 | 30, medium | true |
| Both of the last two at once | range_burst 40, numbers_per_client 30 | 70, high | false, `blocked_by: "risk_score"` |
The ceiling refuses only a score above 60, so an answer can read `high` and still be sent. libphonenumber, the numbering metadata Spaw starts from (9.0.37 and 9.0.40 carry the same ranges for these codes on 27 September 2026; the release in use is on the [status page](/status), under phone numbering metadata), types every +883 range it knows as VoIP, +881 as mobile, and +870 and +882 ranges as mobile or VoIP, with some +882 voicemail ranges. A +883 answer, trimmed:
```json
{
"valid": true,
"country": null,
"line_type": "voip",
"risk_signals": [
{ "signal": "voip", "weight": 30, "dataset": "libphonenumber" },
{ "signal": "international_network", "weight": 30, "dataset": "libphonenumber" }
],
"risk_score": 60,
"risk_level": "high",
"ok_to_send": true,
"blocked_by": null
}
```
## Tightening the policy for a code sender
One request adds a country list and three signals: the two your own traffic raises during a pumping run, and the one that marks international-network numbers. This example is for a service whose customers are in the United States, Canada and Puerto Rico:
```bash
curl -X PUT https://spaw.co/api/v1/phone/policy \
-H "Authorization: Bearer sk_live_…" \
-H "Content-Type: application/json" \
-d '{"allowed_countries": ["US", "CA", "PR"], "block_signals": ["fictional", "disposable", "reported_abuse", "reported_abuse_widely", "premium_rate", "range_burst", "numbers_per_client", "international_network"]}'
```
`block_signals` replaces the list rather than adding to it, which is why the example names the five defaults again. A body naming only the three new signals would let through a premium-rate number, a number your account reported as abused and one that three or more accounts reported, whenever nothing else fires, because they score 60, 60 and 40 and the ceiling refuses only a score above 60. Fictional (80) and disposable (70) numbers would still be refused, but by the score, as `risk_score`. The answer's `data` is the policy as stored, with the signals in the weights' order and the countries upper-case and sorted (`updated_at` left out here):
```json
{
"max_risk_score": 60,
"block_signals": ["fictional", "disposable", "reported_abuse", "reported_abuse_widely", "premium_rate", "range_burst", "numbers_per_client", "international_network"],
"allowed_countries": ["CA", "PR", "US"],
"blocked_countries": null,
"blocked_line_types": [],
"require_reachable": false,
"block_opted_out": true,
"is_default": false
}
```
Under that policy the cases above change:
| Answer | `blocked_by` |
| --- | --- |
| An international-network number | `country`: a number with no country is outside every allow-list, and the country rule runs before the signals |
| The same number, for an account with `allowed_countries` left null | `international_network` |
| A +1 876 number | `country` |
| The fifth number in one range within the hour | `range_burst` |
| The fifth number from one client address | `numbers_per_client` |
| Both at once | `range_burst`, the first blocked signal in the weights' order |
Send `client_ip` with every server-side lookup, the address of the person asking for the code; without it `numbers_per_client` cannot fire. Blocking `range_burst` refuses every further number in that range while five distinct numbers of it sit in the last 60 minutes, including a real customer's number that happens to be there. A sender who would rather slow the flow than refuse it can leave both signals out of `block_signals` and add a delay or a challenge whenever an answer carries one. Naming the signals is also narrower than lowering `max_risk_score` below 40, which would refuse every number in a virtual-number wholesaler's block as well.
## The +1 regions outside the United States and Canada
The calling code +1 is shared by 25 regions. libphonenumber 9.0.37 and NANPA's area-code report of 26 September 2026 agree on the 23 outside the United States and Canada and the 27 geographic area codes they use. The answer's `country` is the region's own code, so an allow-list of `US` and `CA` refuses every row below, the five US territories included, because each has a region code of its own. This is a map of the numbering plan, not a list of places to avoid: a service with customers in Puerto Rico names `PR`, and one with no customers in the other regions loses nothing by leaving them out.
| Region | Code | Area codes | US territory |
| --- | --- | --- | --- |
| [American Samoa](/tools/phone-number-lookup/countries/as) | AS | 684 | Yes |
| [Anguilla](/tools/phone-number-lookup/countries/ai) | AI | 264 | No |
| [Antigua & Barbuda](/tools/phone-number-lookup/countries/ag) | AG | 268 | No |
| [Bahamas](/tools/phone-number-lookup/countries/bs) | BS | 242 | No |
| [Barbados](/tools/phone-number-lookup/countries/bb) | BB | 246 | No |
| [Bermuda](/tools/phone-number-lookup/countries/bm) | BM | 441 | No |
| [British Virgin Islands](/tools/phone-number-lookup/countries/vg) | VG | 284 | No |
| [Cayman Islands](/tools/phone-number-lookup/countries/ky) | KY | 345 | No |
| [Dominica](/tools/phone-number-lookup/countries/dm) | DM | 767 | No |
| [Dominican Republic](/tools/phone-number-lookup/countries/do) | DO | 809, 829, 849 | No |
| [Grenada](/tools/phone-number-lookup/countries/gd) | GD | 473 | No |
| [Guam](/tools/phone-number-lookup/countries/gu) | GU | 671 | Yes |
| [Jamaica](/tools/phone-number-lookup/countries/jm) | JM | 658, 876 | No |
| [Montserrat](/tools/phone-number-lookup/countries/ms) | MS | 664 | No |
| [Northern Mariana Islands](/tools/phone-number-lookup/countries/mp) | MP | 670 | Yes |
| [Puerto Rico](/tools/phone-number-lookup/countries/pr) | PR | 787, 939 | Yes |
| [Sint Maarten](/tools/phone-number-lookup/countries/sx) | SX | 721 | No |
| [St. Kitts & Nevis](/tools/phone-number-lookup/countries/kn) | KN | 869 | No |
| [St. Lucia](/tools/phone-number-lookup/countries/lc) | LC | 758 | No |
| [St. Vincent & Grenadines](/tools/phone-number-lookup/countries/vc) | VC | 784 | No |
| [Trinidad & Tobago](/tools/phone-number-lookup/countries/tt) | TT | 868 | No |
| [Turks & Caicos Islands](/tools/phone-number-lookup/countries/tc) | TC | 649 | No |
| [U.S. Virgin Islands](/tools/phone-number-lookup/countries/vi) | VI | 340 | Yes |
libphonenumber also answers `DO` for the valid toll-free range +1 800 14x, so an allow-list of `US` and `CA` refuses those numbers too.
Spaw reads NANPA's central-office code and thousands-block reports for the five territories as it does for the states; in NANPA's files of 26 September 2026 the territories' thousands-block rows are all Puerto Rico's. It holds no regulator table for the other eighteen, so their line type, and any carrier, come from the numbering plan alone and `block_status` answers null. The SMS-receiving lists, the [unwanted-call complaint counts](/docs/signals/phone/reported_unwanted_calls) and your own delivery reports still apply to them. The [data sources page](/tools/phone-number-lookup/sources) dates every table the lookup reads.
## What your own traffic can flag, and what it cannot
Three fields and two signals come from nothing but your own account's recent phone lookups, and they are the only part of an answer that can see a script at work.
- [`range_burst`](/docs/signals/phone/range_burst), +40: your account looked up five or more distinct numbers in the same 1,000-number range within 60 minutes, this one included; `range_seen_by_you` answers the count as `{"distinct_numbers": 5, "window_minutes": 60}`. The range is the E.164 number without its last three digits, a run of digits rather than a regulator's block, so the signal is a heuristic. It says a range was walked, not why: someone testing numbers against a form, or opening accounts in bulk, walks ranges too.
- [`numbers_per_client`](/docs/signals/phone/numbers_per_client), +30: one client address tried five or more distinct numbers on your account within 60 minutes; `client_velocity` answers the count in the same shape as `range_seen_by_you`, and is null when no client address was sent; an IPv6 address counts by its /64. It stays quiet for an address Spaw's own IP data places on a mobile carrier or a corporate proxy, where many people share one address, and for a private or reserved address. It flags one script cycling numbers from one source, not an attack rotated through residential proxies.
- `destination_new_for_you`: true when your account has not looked up a valid number in that country in the last 30 days. It carries no weight and cannot be named in `block_signals`, which answers `422` for it, and it is null until a week of the account's own lookups has been counted.
They are counted for single lookups (`POST /api/v1/phone`, the dashboard, the MCP verify-phone tool, the consistency check) and for browser lookups with a publishable key. Batch, bulk and monitor runs are never counted, and neither are the test numbers or the guest demo. A browser answer returns the three fields null, because it reaches your visitor, while the two signals still score there. No other account's lookups are read. What is kept is a keyed hash of each number and client address for the 60-minute window, and per country and hour, counts of lookups and of invalid answers for 30 days.
Three emails go to the account owner from the same counts:
| Alert | When | How often |
| --- | --- | --- |
| Range burst | A range reaches five distinct numbers inside the window | Once per range per window, at most five a day |
| Destination spike | An hour with at least 20 lookups to one country and ten times that country's hourly average over the previous 14 days, or over the days counted so far when fewer; not sent until a week of the account's lookups has been counted | At most once per country a day |
| Enumeration | An hour with at least 50 lookups to one country, half or more of them invalid or in blocks nobody holds. Input that names no country, the international-network numbers included, counts as one destination of its own | At most once per country a day |
The destination alerts are checked every five minutes. The spike alert does not watch the no-country destination, so a surge of valid international-network numbers sends no email; an allow-list or `international_network` in `block_signals` is the control for them.
None of the eight test numbers can show these signals. Test numbers are never counted as your traffic, and none of them is an international-network number. What they can show is the country rule: with a stored `allowed_countries` of `["GB"]`, a secret-key lookup of `+1 202 555 0100` answers `ok_to_send: false` with `blocked_by: "country"`. The guest demo and the sandbox key answer under the defaults, so try the rule with a secret key.
## After the send: delivery reports and verified_share
Pumped messages are delivered and never used; AWS lists "reduced deliverability rates due to the messages never being interacted with" among the challenges AIT poses, in the June 2024 post above. Report outcomes to [`POST /api/v1/phone/feedback`](/docs/api/report-phone-feedback), `delivered` from your SMS provider's receipts and `verified` when a code is entered, and the [feedback summary](/docs/api/phone-feedback-summary) answers `verified_share` over 90 days: of the numbers reported delivered or verified, the share reported verified. It is your one-time-code conversion, and a drop is worth reading beside the destination counts on the Usage page. It only works if you report `verified`. The summary's block list is ranked by failed deliveries and holds only blocks with at least one, so a pumped range whose messages all arrive may never appear in it.
## What the policy is not
`ok_to_send` is a fraud and deliverability decision. It is not consent to message anyone and not a do-not-call scrub, and both stay your obligation. A country list is a business choice about where you send, not a verdict that a country's numbers are fraudulent. The own-traffic signals describe the pattern of your lookups, not a person: Spaw does no reverse lookup and names nobody behind a number.
## What to do next
- Read the [SMS policy section of the phone docs](/docs/phone-intelligence#policy) and the [own-traffic section](/docs/phone-intelligence#own-traffic) for the full contract.
- Change your policy with [`PUT /api/v1/phone/policy`](/docs/api/update-phone-policy); the [SMS policy](/glossary/sms-policy) glossary term defines it in one page.
- Read [the international_network signal](/docs/signals/phone/international_network) before you decide whether you send to satellite and network numbers.
- Check each region an allow-list names on [phone facts by country](/tools/phone-number-lookup/countries).
- See the signals for rented and SMS-receiving numbers in [stop OTP abuse with block data and disposable numbers](/guides/stop-otp-abuse-with-block-data-and-disposable-numbers).
---
# UK number ranges in a phone lookup, from 01 to 09
What a phone lookup answers for each UK range from 01 to 09, which ranges carry risk weight, and how much of each range Ofcom lists as allocated.
Date: 2026-09-27
In a Spaw phone lookup, the digits after a UK number's leading 0 decide its line type: 01 and 02 answer `fixed_line`, 071 to 075 and 077 to 079 answer `mobile`, 070 `personal_number`, 076 `pager`, 03 and 055 `uan`, 056 `voip`, 080 `toll_free`, 084 and 087 `shared_cost`, and 090, 091 and 098 `premium_rate`. Three exceptions matter to code that reads the answer: 07624 is an Isle of Man mobile range, some 01 and 07 blocks belong to Guernsey, Jersey and the Isle of Man and answer those countries, and Ofcom's drama ranges, such as 07700 900xxx, answer `is_fictional: true`. This guide sets out each range as Ofcom's numbering plan designates it, what the API returns for it, what the default SMS policy does with it, and how much of each range Ofcom's own table lists as allocated.
The designations come from Ofcom's National Telephone Numbering Plan, the version with effect from 22 April 2025 (https://www.ofcom.org.uk/siteassets/resources/documents/phones-telecoms-and-internet/information-for-industry/numbering/other/national-numbering-plan.pdf, accessed 27 September 2026). The block counts come from Ofcom's telephone number files S1, S3, S5, S7, S8 and S9, published on 23 September 2026 (https://www.ofcom.org.uk/phones-and-broadband/phone-numbers/numbering-data, accessed 27 September 2026). Both documents are Ofcom copyright. Every answer quoted was produced by running the lookup against those files on 27 September 2026.
## What each range answers
| Range | Ofcom's designation | `line_type` | Risk weight | Default SMS policy |
| --- | --- | --- | --- | --- |
| 01, 02 | Geographic numbers | `fixed_line` | none | sends |
| 030, 033, 034, 037 | Non-geographic numbers charged at up to the geographic rate | `uan` | none | sends |
| 055 | Corporate numbers | `uan` | none | sends |
| 056 | Location-independent electronic communications services | `voip` | [`voip`](/docs/signals/phone/voip) +30 | sends |
| 070 | Personal numbers | `personal_number` | none | sends |
| 071 to 075, 077 to 079 | Mobile services | `mobile` | none | sends |
| 076, except 07624 | Radiopaging | `pager` | [`pager`](/docs/signals/phone/pager) +30 | sends |
| 07624 | Radiopaging and mobile services on the Isle of Man, closed to new allocation | `mobile`, country `IM` | none | sends |
| 080 | Free to caller | `toll_free` | none | sends |
| 0843 to 0845, 0870 to 0873 | Non-geographic numbers with a capped service charge | `shared_cost` | [`shared_cost`](/docs/signals/phone/shared_cost) +20 | sends |
| 090, 091, 098 | Non-geographic numbers with a service charge capped at 300p a minute or 500p a call (098 for sexual entertainment services; 0908 and 0909 closed to new allocation) | `premium_rate` | [`premium_rate`](/docs/signals/phone/premium_rate) +60 | blocks, `blocked_by: premium_rate` |
`line_type_source` is `numbering_plan` for every UK answer that has a line type. A block holder settles the line type only in North America, where mobiles and landlines share blocks, as [why US numbers cannot be split into mobile and landline](/guides/why-us-numbers-cannot-be-split-into-mobile-and-landline) explains; in the UK the plan already separates them. `uan` is libphonenumber's type for universal access or company numbers, one number a business routes to whichever office it chooses, and `personal_number` its type for a number tied to a person and forwarded to a mobile or a fixed line. The weights are the published ones behind `risk_score`. Until an account sets its own [SMS policy](/glossary/sms-policy), the defaults block the `fictional`, `disposable`, `reported_abuse`, `reported_abuse_widely` and `premium_rate` signals and any score over 60, and allow every line type.
The block, not the range, decides two signals. A valid number in a block Ofcom has designated rather than allocated scores [`reserved_block`](/docs/signals/phone/reserved_block) +40, and a number in a block held by a VoIP wholesaler scores [`virtual`](/docs/signals/phone/virtual) +40, which the section on wholesalers below counts.
## How much of each range Ofcom lists as allocated
Ofcom's files list number blocks, and a block is not a fixed size. Most 01 rows are blocks of 1,000 numbers, the 07 mobile rows are blocks of 100,000, the 084, 087 and 09 rows are blocks of 10,000, and most 0800 rows are blocks of 1,000 or 100, so a count of rows compares unlike things. The table below counts numbers: each row covers 10 to the power of the number length minus the digits of its prefix, with the number length taken from the file's own column (most UK numbers have ten digits after the 0; a few 01 areas and the closed 0800 plus six digits range have nine). Ofcom's table does not list every number in every range (0843's rows cover 6.1 million of its 10 million numbers), so the share divides the numbers in allocated blocks by the numbers in all of the range's rows, whatever their status, not by the whole range.
| Range | Blocks in Ofcom's table | Usual block size | Allocated blocks | Numbers in allocated blocks | Share of the numbers the table lists |
| --- | --- | --- | --- | --- | --- |
| 01 | 269,646 | 1,000 | 91,464 | 317.2 million | 36.6% |
| 02 | 29,438 | 10,000 or 1,000 | 7,389 | 62.1 million | 8.5% |
| 030 | 3,178 | 10,000 | 305 | 3.0 million | 3.0% |
| 033 | 2,305 | 10,000 | 613 | 6.1 million | 6.1% |
| 034 | 3,871 | 10,000 | 1,370 | 13.2 million | 13.2% |
| 037 | 3,772 | 10,000 | 1,166 | 11.6 million | 11.6% |
| 055 | 400 | 10,000 | 26 | 0.26 million | 6.5% |
| 056 | 595 | 10,000 | 130 | 1.3 million | 1.3% |
| 070 | 3,061 | 10,000 or 100,000 | 733 | 43.1 million | 43.1% |
| 071 to 075, 077 to 079 | 6,029 | 100,000 | 5,515 | 551.5 million | 68.9% |
| 076, except 07624 | 800 | 100,000 | 117 | 11.7 million | 14.6% |
| 07624 | 19 | 100,000 or 10,000 | 10 | 0.82 million | 82.0% |
| 0800 | 7,505 | 1,000 or 100 | 3,909 | 2.5 million | 40.6% |
| 0808 | 410 | 10,000 | 403 | 3.9 million | 98.2% |
| 084 | 2,117 | 10,000 | 2,055 | 20.1 million | 97.0% |
| 087 | 1,957 | 10,000 | 1,825 | 18.1 million | 93.2% |
| 090 | 5,283 | 10,000 | 5,089 | 50.9 million | 96.3% |
| 091 | 1,115 | 10,000 | 997 | 10.0 million | 89.4% |
| 098 | 1,580 | 10,000 | 1,424 | 14.2 million | 90.1% |
Most of what is not allocated is not free either. Of the 269,646 rows under 01, 171,587 are Protected, withheld from allocation, and only 5,357 are Free; under 070, 2,268 of 3,061 rows are Protected. Ofcom's six status words (Allocated, Free, Protected, Quarantined, Reserved and Designated, with closed ranges marked as such) fold into Spaw's three values of `block_status` as [what an unassigned block means](/guides/what-an-unassigned-block-means) describes. A number the metadata accepts in a Free, Protected, Quarantined or Reserved block answers `valid: false` with the reason `unassigned_block`, free of charge, unless it sits in a drama range. The [United Kingdom page](/tools/phone-number-lookup/countries/gb) splits the allocated mobile numbers by range and by brand from the copy of these files Spaw has installed, and lists the area codes and non-geographic ranges one by one; the [phone data sources page](/tools/phone-number-lookup/sources) names the files, their publisher and the date of the installed copy.
Read every figure in this section with these limits. Allocated means allocated to a communications provider, not in service: a provider may hold a block of 100,000 numbers and have issued a fraction of them. The shares are shares of Ofcom's table, not of subscribers or of traffic. And the table records the original allocation: a number ported to another provider keeps its block, so the holder Spaw reports as `carrier` is the provider Ofcom allocated the block to, not the one serving the number today.
## Where the plan and the metadata disagree
Spaw starts from libphonenumber, the numbering metadata behind Android (9.0.37 and 9.0.40 carry the same UK ranges on 27 September 2026; the release in use is on the [status page](/status), under phone numbering metadata), and reads the plan where the two part ways.
- **084 and 087.** The metadata types both ranges as premium rate. The plan designates 0843 to 0845 and 0870 to 0873 as non-geographic numbers whose service charge may not exceed 5.833p and 10.83p a minute or a call before VAT, and caps the service charge on 090, 091 and 098 at 300p a minute or 500p a call. Spaw answers `shared_cost` with a weight of +20, applied only while the metadata still says premium rate, so a later metadata change cannot turn into a wrong answer. Ofcom's premium-rate-services rules still cover 0871 to 0873, as the [line type](/glossary/line-type) entry notes, and Ofcom's quick guide to premium rate services says premium rate numbers "generally begin with 09, 118, 087 and some 084" (https://www.ofcom.org.uk/make-a-complaint/complain-about-premium-rate-services/quick-guide-to-premium-rate-services-prs, last updated 23 June 2025, accessed 27 September 2026). A sender that treats those numbers as premium rate blocks `shared_cost` too.
- **Drama ranges.** Ofcom sets aside twenty ranges of a thousand numbers for television and radio, among them 020 7946 0xxx, 07700 900xxx and 03069 990xxx, and says they will not be allocated to providers in the foreseeable future (https://www.ofcom.org.uk/phones-and-broadband/phone-numbers/numbers-for-drama, accessed 27 September 2026). The metadata rejects some of them, 07700 900xxx among them. Spaw answers each one valid, with the type Ofcom gives the range, `is_fictional: true` and the [`fictional`](/docs/signals/phone/fictional) signal at +80, which the default policy blocks.
- **Network codes inside 08 and 07.** The 179 rows under 08979 and 08998 to 08999 are codes the plan lists for calling line identification and inbound routing, not numbers anyone dials, and the lookup answers `invalid_number`. The 391 Designated rows under 07 are, all but one, the blocks under 07603 to 07622, 07682 to 07692 and 07991 to 07998, which the plan lists among its mobile number portability codes: the metadata rejects the first two groups (`invalid_number`), and accepts the third, which answers `mobile` with `block_status: reserved` and `reserved_block` +40.
## 070 looks like a mobile and is not
The plan defines a personal numbering service as one that enables an end-user "to be called or otherwise contacted at a single Personal Number, and to receive those calls or other communications at almost any Telephone Number, including a Mobile Number". A person reading 070 sees a mobile prefix; the network sees a forwarding service. In the 23 September files, 733 blocks under 070 were allocated to 78 providers, 43.1 million numbers. The plan does not mention text messages, so whether a one-time code reaches a handset depends on the service behind the number.
Spaw answers `personal_number` with no risk weight, and the default policy lets it through. An account that sends codes only to handsets can say so once, on the server, through `PUT /api/v1/phone/policy` ([reference](/docs/api/update-phone-policy)):
```json
{ "blocked_line_types": ["premium_rate", "shared_cost", "pager", "personal_number"] }
```
With that policy a 070, 076, 084, 087 or 09 number answers `ok_to_send: false` with `blocked_by: line_type`, while 07 mobiles, 03 numbers and 056 numbers still pass on their scores. `ok_to_send` is a fraud and deliverability decision computed from the answer and the account's rules. It is not consent to message anyone and not a do-not-call scrub.
## Guernsey, Jersey and the Isle of Man
The three Crown dependencies' numbers sit in the UK +44 plan and appear in Ofcom's files, and libphonenumber assigns them their own region codes, so the lookup answers `country` as `GG`, `JE` or `IM` rather than `GB`. In the 23 September files, 93 allocated geographic blocks sit under 01481 (Guernsey), 01534 (Jersey) and 01624 (Isle of Man), and 54 allocated 07 blocks, 5.22 million numbers, resolve to the islands: 07781 and 07839 in Guernsey, 07509, 07797, 07829 and 07937 in Jersey, and 07624 on the Isle of Man, plus three of the allocated blocks under 07700 (Jersey), two under 07911 (Guernsey) and one under 07457 (Isle of Man). The United Kingdom page's island rows count only the whole stems, 4.62 million numbers. A country allow-list of `GB` alone refuses all of them. The pages for [Guernsey](/tools/phone-number-lookup/countries/gg), [Jersey](/tools/phone-number-lookup/countries/je) and [the Isle of Man](/tools/phone-number-lookup/countries/im) name the regions that share +44 and the table each is read from.
## Blocks held by VoIP wholesalers
A number's range says what the plan designated; the block holder says who the numbers went to. When the holder Ofcom lists for a block is on Spaw's classification list of VoIP wholesalers, the answer keeps the range's line type and adds `is_virtual: true` with `virtual` +40. In the 23 September files, 3,586 allocated blocks had such a holder: 3,229 under 01, 258 under 02 and 99 across 03, 056, 070, 07 and 08. That is a lower bound, since only holders on the list are counted. The weights add up: a 01 number in such a block scores 40 and is sent to under the default policy, while a 056 number in one scores 70 (`virtual` +40 and `voip` +30) and is refused with `blocked_by: risk_score`.
## What a call to each range costs, and who may share the revenue
For a service that places calls to numbers people type in, the plan's tariff column caps the service charge, the part of the price the organisation being called sets; the caller's own provider adds an access charge on top (Ofcom, UK Calling, https://www.ofcom.org.uk/phones-and-broadband/phone-numbers/uk-calling, published 31 March 2022, accessed 27 September 2026). Calls to 03 numbers are charged at up to the rate of a call to a geographic number and count towards a caller's inclusive minutes; 080 is free to the caller; the service charge on 0843 to 0845 is capped at 5.833p, on 0870 to 0873 at 10.83p, and on 090, 091 and 098 at 300p a minute or 500p a call, all before VAT.
Part B3 of the plan says where those who use a number may not share, directly or indirectly, the revenue from providing a service on it: geographic numbers, 03, corporate numbers (055), 056, personal numbers (070) and radiopaging (076, except 07624) with an end-user or a calling party, and 084 and 087 with the calling party. Ofcom's list of amendments dates the geographic, corporate and radiopaging prohibitions and the 084 and 087 calling-party prohibition to 11 April 2025, notified in its statement on revenue sharing of 11 October 2024 (https://www.ofcom.org.uk/phones-and-broadband/phone-numbers/numbering, accessed 27 September 2026). Part B3 places no such restriction on 09.
## What Spaw does not claim
- The range and the table describe the allocation. Neither says who uses a number today, and Spaw does no reverse lookup: it names no owner and no caller.
- `carrier` is the provider Ofcom allocated the block to. Neither it nor `line_type` is port-aware. The live carrier-network check (`hlr: true`) is not enabled on the service: `hlr_checked` answers false and nothing extra is charged.
- A number outside every row of the table answers `block_status: null`: the table says nothing about it, and nothing is inferred. Numbers in the freephone and premium-rate drama ranges, 0808 157 0xxx and 0909 879 0xxx, are among them.
## What to do next
- See the 07 table, the area codes and the regions sharing +44 on [the United Kingdom page](/tools/phone-number-lookup/countries/gb), and other countries under [phone facts by country](/tools/phone-number-lookup/countries).
- Read every field and where it comes from in the [phone intelligence guide](/docs/phone-intelligence).
- Turn the line types into a sending rule with [gate SMS verification codes with the SMS policy](/guides/gate-sms-verification-codes-with-the-sms-policy).
- See how the regulators' statuses map to `block_status` in [what an unassigned block means](/guides/what-an-unassigned-block-means).
---
# What FTC unwanted-call complaints can and cannot say about a number
Complaints to the FTC name the number shown on caller ID. What 90 days of them show, what they cannot prove, and how Spaw's complaint fields read them.
Date: 2026-09-27
An FTC unwanted-call complaint records that a consumer received a call they did not want and reported the number their caller ID showed; it does not say who placed the call, whether the number's holder did anything wrong, or whether you may call or text that number. The complaints are not the Do Not Call Registry, the FTC does not verify them, and caller ID is often spoofed. Among the complaints filed between 26 June and 23 September 2026, 691,715 named one of 588,360 valid North American numbers as the caller, and 91.8% of those numbers were named exactly once. Only 9,301 of them (1.6%) were named three or more times on at least two different days, the rule behind Spaw's `reported_unwanted_calls` signal.
This guide explains what the Federal Trade Commission publishes, what 90 days of it look like when counted by number, where the numbers sit in the US numbering plan, and what the phone lookup's `complaints_90d`, `complaints_last_at` and `reported_unwanted_calls` do with it. Every figure was counted on 27 September 2026 from the FTC files Spaw had installed, the newest published on 24 September 2026, and from NANPA's tables of 26 September 2026.
## What the FTC publishes, and what Spaw keeps
Consumers report unwanted calls to the FTC, and the FTC publishes the number each report names as the caller. Its Do Not Call reported-calls data (https://www.ftc.gov/policy-notices/open-government/data-sets/do-not-call-data, accessed 27 September 2026) lists, for each complaint, "the telephone number originating the unwanted call, the date the complaint was created, the time the call was made, the consumer's city and state locations reported, the subject of the call, and whether the call was a robocall". A file appears each weekday, weekend complaints arrive in Monday's file, and the page says plainly: "None of the information about the reported calls is verified."
Spaw downloads each file once, nightly at 04:50 UTC, and asks again for any file missing from the last seven days. Of each complaint it keeps four things for 90 days: the number named as the caller, the day the complaint was filed, the recorded-message-or-robocall answer and the subject. The consumer's city and state are dropped when the file is read, and a row whose number is not a valid North American number is skipped. Counts are by filing day, not by the call date.
Three statements hold for every figure in this guide and for the complaint fields in every answer:
- These are complaints consumers filed with the FTC about calls they received, naming the number shown to them as the caller ID. They are not the Do Not Call Registry, which lists consumers' own numbers.
- They say nothing about whether you may call or text a number.
- They are unverified, and caller ID is often spoofed, so a number can be named without its holder having placed any call.
The FTC's recorded-message-or-robocall field was yes on 73.6% of the complaints that filled it in (475,811 of 646,612), and consumers chose these subjects:
| Subject the consumer chose | Complaints | Share |
| --- | --- | --- |
| Reducing your debt (credit cards, mortgage, student loans) | 166,840 | 24.1% |
| Calls pretending to be government, businesses, or family and friends | 76,370 | 11.0% |
| Dropped call or no message | 54,730 | 7.9% |
| Medical & prescriptions | 42,104 | 6.1% |
| The ten other named subjects | 37,435 | 5.4% |
| Other | 267,618 | 38.7% |
| No subject given | 46,618 | 6.7% |
The lookup answers neither the subject nor the robocall answer. It carries a count and a date, described below.
## Most numbers are named once
Counted by number over the 90 days of filings, the complaints fall like this.
| Complaints naming the number | Numbers | Share of numbers |
| --- | --- | --- |
| One | 540,075 | 91.8% |
| Two | 36,801 | 6.3% |
| Three or more, all filed on one day | 2,183 | 0.4% |
| Three or more, filed on two or more days | 9,301 | 1.6% |
| All numbers | 588,360 | 100% |
The 9,301 numbers in the last row drew 69,713 complaints between them, 10.1% of the total. The two-day condition is what separates the last two rows: 2,183 numbers collected three or more complaints on a single day and none on any other, which fits one afternoon of calls, or one person filing several times, as well as it fits a pattern.
A 90-day snapshot understates repeats for numbers first named near its end, so the fairer test is the numbers first named in the window's first 30 days. Of the 184,238 numbers whose first complaint in the window fell between 26 June and 25 July 2026, 12,069 (6.6%) were named again on a later day by 23 September, 3,666 (2.0%) were named again at least 30 days after their first complaint, and 3,946 (2.1%) went on to meet the three-on-two-days rule.
The data cannot say why a number is named only once. A number used briefly and dropped, a number displayed by a caller who never held it, and a number whose other recipients did not file a complaint all look the same: one row. What it does support is practical. A single complaint is thin evidence about a number, and a `complaints_90d` of 0 is not a clean record, only the absence of a filed complaint.
## Toll-free numbers: one in eight reported numbers, and half of the numbers that meet the rule
Placing each number by its area code in NANPA's area-code report gives the split below. The last two columns count the numbers that met the rule.
| Where the number sits | Numbers | Share | Meeting the rule | Share of those |
| --- | --- | --- | --- | --- |
| US area code | 498,437 | 84.7% | 4,698 | 50.5% |
| Toll-free code (800, 833, 844, 855, 866, 877, 888) | 72,715 | 12.4% | 4,584 | 49.3% |
| Canadian area code | 16,885 | 2.9% | fewer than 20 | |
| Other North American code (Caribbean, US territories, non-geographic) | 323 | 0.1% | fewer than 20 | |
Toll-free numbers were named once in 78.0% of cases, against 93.7% for every other number. They also sit outside the tables that describe the rest: toll-free numbers are not in NANPA's central-office code or thousands-block reports, so for one the lookup answers `line_type: "toll_free"` from the numbering plan, `block_status: null` and no `carrier`, while `complaints_90d` is counted like any other North American number's. The data does not say why toll-free numbers repeat more often.
The mix also moves quickly. Numbers in Canadian area codes named in complaints went from 202 in July 2026 to 5,985 in August.
## Where the US numbers sit in the numbering plan
For the 498,437 numbers in US area codes, the block each one sits in says which carrier NANPA assigned it to: the holder of the number's thousands-block where NANPA lists one, else the holder of its central-office code (NANPA's code and thousands-block reports, https://www.nanpa.com/reports/co-code-reports and https://www.nanpa.com/reports/thousands-block-reports/region, files of 26 September 2026). Spaw classifies holders as VoIP providers or wireless carriers from their FCC filing type and their own description, matched on the holder name the regulator lists; a holder not on that list stays unclassified. The same classification applied to all 1,883,208,000 allocated US numbers gives the baseline in the last column, counted as numbers: each allocated thousands-block is 1,000 numbers with its own holder, and what is left of each allocated code, once its thousands-blocks (held or free) are taken out, belongs to the code's holder.
| Block holder, as NANPA assigned it | Reported numbers | Share of reported | Share of those meeting the rule | Share of allocated US numbers |
| --- | --- | --- | --- | --- |
| A VoIP provider (the blocks that score `virtual`) | 302,850 | 60.8% | 73.9% | 19.7% |
| A wireless carrier | 22,379 | 4.5% | 5.9% | 35.0% |
| A holder not on the classification list | 171,701 | 34.4% | 20.1% | 45.3% |
| Nobody (free pool block, reserved or vacant code) | 1,507 | 0.3% | fewer than 20 | |
VoIP providers' blocks hold 60.8% of the reported US numbers against 19.7% of the numbering, and 73.9% of the 4,698 US numbers that met the rule. Read that with its limits. It describes the caller ID shown to the consumer, not the network that carried the call, and a spoofed caller ID can sit in anyone's block. It describes the original allocation: a number ported to another carrier keeps its block. Both the VoIP and the wireless shares are lower bounds, because they count only holders on Spaw's classification list. And it is a share of the regulator's table and of the reported numbers, never of call traffic. The [phone statistics page](/statistics/phone) publishes the same breakdown every week, with monthly counts and a CSV, so its figures move as its 90-day window moves.
## Numbers nobody holds, and the spoofing a table cannot see
Of the US numbers, 1,507 (0.3%) sat where NANPA lists no holder: 932 in central-office codes its report does not list, 396 in thousands-blocks it lists as available, and 179 in codes it reserves. No subscriber line can carry a number in a block nobody holds, so a caller ID there was most likely not the caller's own line. The tables are dated 26 September, so a block returned to the pool after the call is counted here too, and a block assigned since the call hides an invented number, which makes the count approximate.
It is roughly a floor for invented caller IDs, because one that lands in an assigned block looks like any other, and it is not a measure of spoofing. In what the FTC calls "neighbor spoofing", the caller ID carries the recipient's own area code and often the first six digits of their number (https://consumer.ftc.gov/articles/how-block-unwanted-calls, accessed 27 September 2026). A number displayed that way sits in the same central-office code as the recipient's own, which is assigned, so an allocation table usually sees nothing wrong with it, and a complaint count cannot separate it from its holder's own calls.
The lookup answers a number in a code NANPA does not list, or in an available thousands-block, with `valid: false` and the reason `unassigned_block`, free, whatever its complaints; `complaints_90d` is still reported beside it. A number in a reserved code stays valid and gains the `reserved_block` signal. [What an unassigned block means](/guides/what-an-unassigned-block-means) explains the tables behind both, and the [glossary entry](/glossary/unassigned-block) gives the short version.
## What the lookup returns
For a North American number, every answer carries two complaint fields, and one risk signal reads them.
| Field or signal | What it holds |
| --- | --- |
| `complaints_90d` | Complaints filed in the last 90 days that name the number as the caller. 0 when nobody complained; null outside the North American plan, for the documented test numbers and for input that is not a number, or before the first file is installed. |
| `complaints_last_at` | The filing date of the latest of them, or null. |
| `reported_unwanted_calls` | A risk signal of weight 20, when there are three or more complaints on at least two different days within the 90 days. |
| `sources` | Includes `us-complaint-reports`, dated by the newest FTC file installed, whenever `complaints_90d` is not null, 0 included. |
An illustrative excerpt of an answer, with the number left out:
```json
"complaints_90d": 3,
"complaints_last_at": "2026-09-18",
"risk_score": 20,
"risk_level": "low",
"risk_signals": [
{ "signal": "reported_unwanted_calls", "weight": 20, "dataset": "us-complaint-reports" }
],
"ok_to_send": true,
"blocked_by": null
```
What the counts do to an answer, from running the lookup on 27 September 2026 with each count set in turn:
| The number, and its complaints | Signals | `risk_score` | `risk_level` | `ok_to_send` under the default policy |
| --- | --- | --- | --- | --- |
| Wireless carrier's block, 2 complaints on 2 days | none | 0 | low | true |
| Wireless carrier's block, 3 complaints on 1 day | none | 0 | low | true |
| Wireless carrier's block, 3 complaints on 2 days | `reported_unwanted_calls` | 20 | low | true |
| Wireless carrier's block, 40 complaints on 12 days | `reported_unwanted_calls` | 20 | low | true |
| Toll-free, 5 complaints on 3 days | `reported_unwanted_calls` | 20 | low | true |
| VoIP provider's block (the [`virtual`](/docs/signals/phone/virtual) signal), 3 complaints on 2 days | `virtual`, `reported_unwanted_calls` | 60 | high | true |
| Available thousands-block, 5 complaints on 3 days | none: `valid: false`, `unassigned_block` | null | null | false, `blocked_by: "invalid"` |
The count does not raise the weight: forty complaints score what three do. Under the published defaults the signal never blocks on its own, because the default [SMS policy](/glossary/sms-policy) refuses five other signals and any score above 60, and a number in a VoIP provider's block with complaints lands exactly on 60 and passes. Beside other signals it can still tip a number over the ceiling: a digit pattern (30) and a crowded SMS-receiving range (30) score 60 and pass, and with 20 for complaints they score 80 and are refused with `blocked_by: "risk_score"`. The weight is small on purpose: the reports are unverified, caller ID is often spoofed, and the number typed into your form may belong to someone whose number another caller displayed. Every weight is listed on the [methodology page](/methodology), and the [signal's own page](/docs/signals/phone/reported_unwanted_calls) gives its wording and an example answer.
## What the count cannot tell you
- **Who placed the call.** A complaint names the caller ID the consumer saw. Spaw does no reverse lookup and names no owner or caller for any number.
- **Whether the holder did anything.** An innocent person's number can collect complaints when others display it.
- **Who serves the number today.** The block records the original allocation, and porting moves numbers between carriers without moving the block. Only a query into the carrier networks can say who serves a number now. The live carrier-network check (`hlr: true`) is not enabled on the service: `hlr_checked` answers false and nothing extra is charged.
- **Whether you may call or text the number.** The complaints are not the National Do Not Call Registry, which the FTC describes as "designed to stop unwanted sales calls from real companies that follow the law" (https://consumer.ftc.gov/articles/national-do-not-call-registry-faqs, accessed 27 September 2026). No field in the answer is a do-not-call scrub, and none records consent.
- **Anything outside the North American plan.** Spaw reads only North American numbers from the files, so a number with another calling code answers null.
## Using it in a product
- **At sign-up or before a one-time code**, read the signal beside the rest of the answer rather than refusing a number on it alone: the person entering the number may hold a number someone else displayed. If complaints should stop a code in your product, send the whole list with [`PUT /api/v1/phone/policy`](/docs/api/update-phone-policy), because `block_signals` replaces the list rather than adding to it: `{"block_signals": ["fictional", "disposable", "reported_abuse", "reported_abuse_widely", "premium_rate", "reported_unwanted_calls"]}`. An answer carrying the signal then has `ok_to_send: false` and `blocked_by: "reported_unwanted_calls"`. Sending those numbers to a review step instead keeps a spoofing victim out of a hard refusal.
- **Before calling or texting a list**, treat `ok_to_send` as what it is: a fraud and deliverability decision, not consent and not a do-not-call scrub. The complaint count changes nothing about what the Registry or your consent records say, and Spaw checks neither.
- **In a CRM clean-up**, the count is one more column. A contact whose number collected complaints is not thereby a bad contact, so sort by it rather than deleting on it.
- **If your own number is named**, an erasure request, checked by a person, deletes the complaints Spaw holds for it. The [trust page](/trust) explains the process, and why a complaint the FTC publishes after the erasure is counted again.
- **Check the date.** `complaints_last_at` and the `sources` entry show how current the count is, and the [status page](/status) lists the complaint dataset beside every other phone table with the date of its newest file.
## How the figures were computed
Everything above was counted on 27 September 2026 from the complaints Spaw had installed: 65 weekday FTC files published from 26 June to 24 September 2026, holding 691,715 complaints filed between 26 June and 23 September that name a valid North American number. Each number was counted once, with its complaints and its distinct filing days, as the lookup counts them. Area codes come from NANPA's area-code report, and US numbers were placed in NANPA's central-office code and thousands-block reports of 26 September 2026 by the longest prefix listed, the thousands-block first. Nothing below the area code is published, a count under 20 is left out, no holder is named, and no number appears in this guide. The [phone statistics page](/statistics/phone) repeats the placement every week over its own 90-day window, and the [phone data sources page](/tools/phone-number-lookup/sources) lists each table with its terms and the date of the installed copy.
## What to do next
- Read the [phone intelligence guide](/docs/phone-intelligence) for every field and where it comes from.
- See the weekly counts on the [phone statistics page](/statistics/phone).
- See what the regulator tables say about US numbering on the [phone facts page for the United States](/tools/phone-number-lookup/countries/us).
- See what the block holder can and cannot prove in [why US numbers cannot be split into mobile and landline](/guides/why-us-numbers-cannot-be-split-into-mobile-and-landline).
- Turn signals into one decision with [gate SMS verification codes with the SMS policy](/guides/gate-sms-verification-codes-with-the-sms-policy).
---
# Gate SMS verification codes with the SMS policy
One boolean per phone answer, decided by rules you set once: the countries you send to, the line types and signals you refuse. How to use ok_to_send.
Date: 2026-09-24
Updated: 2026-09-27
An SMS policy is the set of rules an account keeps about which phone numbers it will send a code or a message to. Every phone lookup ends with `ok_to_send`, the boolean those rules produce for that answer, and `blocked_by`, the name of the rule that decided a refusal. The score stays arithmetic and the flags stay facts; the policy is where the decision is made, and it is made once, on the server, so a signup form, an import script and a bulk run all branch the same way.
## Why a boolean, when the answer has 49 fields
Because a verification step needs a decision, and every code it sends costs money. SMS pumping works on that cost: a script submits numbers in ranges where someone earns a share of every message delivered, usually in countries the business has no customers in, and the bill arrives with the carrier invoice. The defence is not one field. It is the country the number belongs to, the kind of line it is, whether an SMS-receiving website publishes it, what your own delivery reports said about it, and how many numbers your account has tried in the same range lately. A team that reads those fields one by one ends up with an if-chain that drifts between the signup form, the password reset and the marketing sender. A policy puts the chain in one place and leaves `blocked_by` in the answer, so the reasoning stays visible.
Seven rules make up a policy, all optional.
| Rule | What it does |
| -------------------- | ---------------------------------------------------------------------------------------------------------------- |
| `allowed_countries` | Send only to numbers of these regions. Null sends to any. |
| `blocked_countries` | Never send to numbers of these regions. |
| `blocked_line_types` | Refuse these line types, such as `premium_rate`, `pager` or `voip`. |
| `block_signals` | A list of risk-signal names. Any answer whose `risk_signals` carries one of them is blocked, whatever its score. |
| `max_risk_score` | A ceiling. An answer whose `risk_score` is above it is blocked. |
| `require_reachable` | Pass only a number a live carrier check reached. |
| `block_opted_out` | Refuse a number your suppression list holds as opted out. On by default. |
They are applied in a fixed order: an invalid number is blocked first and always, then an opt-out, then the country rules, then the line types, then the first blocked signal in the published weights' order, then the score ceiling, and last the live-check requirement. `blocked_by` names the first rule that fired: `invalid`, `opted_out`, `country`, `line_type`, the signal's own name, `risk_score`, `unreachable` or `unconfirmed`. A batch, bulk or monitored run answers `suppressed` for a number served from your suppression list.
## The defaults
Until an account sets a policy, the published defaults apply. They block the five signals that almost always mean a code is wasted or abused, `fictional`, `disposable`, `reported_abuse`, `reported_abuse_widely` and `premium_rate`, and any score over 60, and they refuse a number your suppression list holds as opted out. They send to every country and every line type, and they require no live check. A number in a block held by a virtual-number wholesaler scores 40 and passes; so does a VoIP number at 30.
The test numbers show the defaults at work without spending a credit. `+1 202 555 0103` is the disposable scenario and answers `ok_to_send: false` with `blocked_by: "disposable"`; `+1 202 555 0105` is fictional and answers `blocked_by: "fictional"`; `+1 202 555 0102`, the virtual number, answers `ok_to_send: true` with a score of 40. The guest demo and the sandbox key answer under the defaults, because neither has an account to keep a policy. Every lookup a secret key makes, the test numbers included, answers under the account's stored policy, which makes them a way to try a policy before a customer meets it.
```json
{
"e164": "+12025550103",
"valid": true,
"is_disposable": true,
"risk_score": 70,
"ok_to_send": false,
"blocked_by": "disposable"
}
```
## Countries: the first control against pumping
SMS providers tell their customers to switch off every destination they do not serve, and a country allow-list is the same control one step earlier, before the code is requested. It is a business choice about where you send, not a verdict that a country's numbers are fraudulent: a bank in Ireland that only ever onboards Irish and British customers loses nothing by refusing a code to anywhere else, and a travel app would lose customers doing the same.
The country is the number's own, read from its calling code and prefix, which matters in two places people get wrong. The North American plan puts the United States, Canada and much of the Caribbean under +1, so `+1 876` is Jamaica and not the United States; an allow-list of `US` and `CA` refuses it. The United Kingdom's +44 also carries Jersey, Guernsey and the Isle of Man, which libphonenumber reports as `JE`, `GG` and `IM`, so a British service that serves the Crown dependencies names them too. A number with no country at all, such as the satellite and international-network codes, is outside every allow-list. Each region's page under [phone facts by country](/tools/phone-number-lookup/countries) names the regions that share its calling code, as the pages for [the United States](/tools/phone-number-lookup/countries/us) and [the United Kingdom](/tools/phone-number-lookup/countries/gb) show, so an allow-list can be checked against them before it goes live.
```bash
curl -X PUT https://spaw.co/api/v1/phone/policy \
-H "Authorization: Bearer sk_live_…" \
-H "Content-Type: application/json" \
-d '{"allowed_countries": ["GB", "IE", "JE", "GG", "IM"], "blocked_line_types": ["premium_rate", "shared_cost"]}'
```
Send only the keys you are changing. A `null` sends to any country again or clears the ceiling, an empty signal list blocks on no signal, and a body naming none of the seven rules answers `422`, because that is the shape a misspelled key takes. An empty allow-list is refused rather than read as "nowhere". The SMS policy tab on the phone verification page edits the same seven rules.
## Line types, and the one you cannot block
Blocking a line type is for numbers that cannot receive what you send: a premium-rate or shared-cost number costs the caller extra, a pager cannot show a code, and a toll-free number is rarely anybody's own handset. In the United Kingdom the 084 and 087 ranges answer `shared_cost`, because Ofcom's numbering plan classes them as service numbers with a capped charge and keeps premium rate for 09; that is why the example above lists `shared_cost` beside `premium_rate`, and why the default `premium_rate` signal alone lets them through. Blocking `voip` is a stronger choice than it looks, because a great many real people use VoIP numbers from their carrier or their employer; prefer the `virtual` signal, which names the block holder, and the score.
`fixed_line_or_mobile` can never be blocked. It is what the numbering plan answers when it cannot tell a mobile from a landline, which is most of North America, so a rule against it would refuse most American customers rather than any kind of line. Where the regulator's block table names a wireless carrier or a virtual-number wholesaler as the holder, the answer settles the type to `mobile` or `voip` and a line-type rule applies to it.
## Opt-outs, and when to switch the rule off
When a recipient replies STOP, or your SMS provider reports an opt-out code, the report puts the number on your suppression list with reason `opted_out`. A batch, bulk or monitored run then serves it from the list as `suppressed`. A single lookup still answers the number in full, with `meta.opted_out: true`, and under the defaults `block_opted_out` makes that answer `ok_to_send: false` with `blocked_by: "opted_out"`, so a marketing send never reaches someone who asked it to stop.
A sender of one-time codes or other transactional messages may want the rule off. An opt-out from marketing does not always forbid a verification code, and the person typing the number into your form may be its owner asking for a code again. Switch it off with `{"block_opted_out": false}` and the number is decided like any other while `meta.opted_out` still tells you it opted out; keep it on for the flows that send anything else. Either way the rule reads your own list, not a consent register, and honouring an opt-out stays your obligation.
## What the policy does not do
It does not change the verdict, the score or any field; it adds two. It is not consent: a number that passes is one you may send to as far as fraud and deliverability go, and whether the person agreed to hear from you is still yours to record. It is not a do-not-call scrub, and no field in the answer is one. It does not know your product either: a policy is a floor under your judgement, not a replacement for the segment-level decisions the fields support.
The live-check rule deserves care. It passes only a lookup made with `hlr: true` that reached the handset, answers `unreachable` when the network could not reach it, and answers `unconfirmed` when no live answer came back. While the live check is not enabled on the service, that is every lookup, so leave it off until you ask for the check on every lookup you gate.
## What to do next
- Read the SMS policy section of the [phone intelligence docs](/docs/phone-intelligence#policy) for the contract and the decision order.
- Read yours with [`GET /api/v1/phone/policy`](/docs/api/get-phone-policy) and change it with [`PUT`](/docs/api/update-phone-policy).
- See which signals catch OTP abuse in [stop OTP abuse with block data and disposable numbers](/guides/stop-otp-abuse-with-block-data-and-disposable-numbers).
- Put the check in front of a sign-in platform's code with the recipes for [Firebase Authentication](/integrations/firebase-auth), [Supabase Auth](/integrations/supabase-auth), [Amazon Cognito](/integrations/amazon-cognito) and [Auth0](/integrations/auth0).
- See what the defaults let through against SMS pumping, and the request that tightens them, in [SMS pumping: what the default SMS policy lets through](/guides/sms-pumping-what-the-default-sms-policy-lets-through).
- Read [why US numbers cannot be split into mobile and landline](/guides/why-us-numbers-cannot-be-split-into-mobile-and-landline) before you write a line-type rule for North America.
---
# How to tell a UK virtual-office or formation address from a trading address
The 2024 appropriate-address rule, 28,936 companies at the Companies House default address on 1 September 2026, and what open UK sources can and cannot say.
Date: 2026-09-22
Updated: 2026-09-23
A UK company's registered office is the address the law makes it keep for legal post. It is often not where the company works. Formation agents, virtual-office providers, accountants and solicitors all supply registered offices, and the free Companies House data product has no column for a trading address at all. So when a supplier, customer or applicant hands you "the company address", you are usually holding a service address, and the useful question is what kind.
The general picture — a mailbox branch, an agent's office and a crowded building are three different things, and `mail_drop_source` says which one Spaw found — is in [a registered agent's address is not a mail drop, and not a home](/guides/a-registered-agents-address-is-not-a-mail-drop-and-not-a-home). This guide covers what the UK adds to it: a legal test for registered offices since March 2024, an address the registrar keeps for companies that fail that test, and counts from the register itself.
Every count below comes from the Companies House free company data product, the BasicCompanyData snapshot dated 1 September 2026, read in full on 22 September 2026. The snapshot lists 5,689,367 companies. None of them is dissolved: a struck-off company leaves the file rather than being marked.
## The rule since 4 March 2024: an "appropriate address"
The Economic Crime and Corporate Transparency Act 2023 rewrote section 86 of the Companies Act 2006, with effect from 4 March 2024. A company must keep its registered office "at all times at an appropriate address", and an address qualifies if, in the ordinary course of events, "a document addressed to the company, and delivered there by hand or by post, would be expected to come to the attention of a person acting on behalf of the company" and "the delivery of documents there is capable of being recorded by the obtaining of an acknowledgement of delivery" (https://www.legislation.gov.uk/ukpga/2006/46/section/86, accessed 22 September 2026).
The government's guidance puts it more bluntly: "You can no longer use a Royal Mail PO Box as your registered office address. This includes similar services from other companies." It adds that a service provider's address, such as an accountant's or solicitor's, "must meet all of these requirements" (https://www.gov.uk/limited-company-formation/company-address, accessed 22 September 2026).
Notice what the rule does not say. It does not ban formation agents or virtual offices. An agent's building where somebody signs for the post and acts for the company can be an appropriate address, and a company registered there has done what the law asks. What the rule rules out is a box on its own.
## The Companies House default address
Anyone who believes a company's registered office is not appropriate can ask the registrar to change it, on form RP07. If satisfied, the registrar may move the registered office to "what is known as a default address. This is an address held and maintained at Companies House (the default address used will depend on the company's jurisdiction)" (Registrar of Companies: rules and powers, https://www.gov.uk/government/publications/company-registrars-rules-and-powers/company-registrars-rules-and-powers, updated 4 March 2024, accessed 22 September 2026). The same page says what happens next. The company has 28 days to file a new address, with evidence that it may use the property. After that the registrar may publish a notice of intention to strike it off, and after two more months strike it off. Meanwhile the registrar is not required to open the post that arrives, and may destroy anything uncollected after six months.
The guidance cited here does not print the default addresses; the register does, in every record moved there. Each is written as a Companies House PO box, then the company number and the words "Companies House Default Address". In the 1 September 2026 snapshot, 28,936 companies have a registered office written that way:
| Default address | Company numbers there | Companies |
| --- | --- | --- |
| PO Box 4385, Cardiff CF14 8LH | England and Wales | 27,456 |
| PO Box 24238, Edinburgh EH7 9HR | Scotland | 934 |
| PO Box 2381, Belfast BT1 9DY | Northern Ireland | 546 |
How it was counted: each row's care-of, PO box, first and second address lines and post town were joined and searched for the words "DEFAULT ADDRESS"; the postcode is the row's own RegAddress.PostCode; the jurisdiction is read from the company number prefix: SC and SO numbers are Scottish, NI numbers Northern Irish, and the Cardiff box holds the rest — 27,295 plain numbers, 152 OC and 7 LP, the England and Wales series, plus one FC and one GE. Three more rows carry the Cardiff postcode without those words, and they are not counted.
Of the 28,936, 22,640 (78 percent) have the status "Active - Proposal to Strike off", 820 are in liquidation and 5,410 are plainly "Active". That is the rule working as the registrar's page describes it: a company that does not replace the default address is heading off the register. [How a UK dissolution shows up in open data](/guides/how-a-uk-dissolution-shows-up-in-open-data) covers what that looks like afterwards.
Written the way the register writes it, each default address is a post-office box with no street, and Spaw reads it as one: `address_type: po_box`. A box has no building behind it, so no register of buildings is asked and `companies_registered` stays null. What else the answer carries depends on the postcode:
- **Edinburgh, EH7 9HR** is a live postcode, so the box is the only signal: the `po_box` weight of 30, and a risk score of 30.
- **Cardiff, CF14 8LH** also carries `terminated_postcode`, another 30, for a score of 60. The ONS Postcode Directory's August 2026 release records CF14 8LH as terminated in March 2016; the register still prints it for 27,456 companies.
- **Belfast, BT1 9DY** answers `postcode_status: null`: not evaluated, rather than live or terminated. Northern Irish postcodes are left out of the directory installed here, because their reuse needs a Land & Property Services licence, so the code is neither confirmed nor refused and carries no centroid. The box is again the only signal, for a risk score of 30. [Can you look up UK addresses from a postcode for free?](/guides/a-uk-postcode-is-not-an-address) explains the gap.
## The PO box column
The snapshot also has a RegAddress.POBox column, and 31,145 companies have a value in it. Read carelessly, that looks like thirty-one thousand companies breaking the box rule. It is not. 28,921 of those values are the registrar's own box numbers at the three default addresses. The other 2,224 hold some other box, and all but one of them also give a first address line. A box number next to a street address does not tell you whether the street address is appropriate, so the column cannot settle that question.
## What each open source can and cannot say
Spaw's `is_mail_drop` flag for a UK address comes from three sources, reported in `mail_drop_source` in this order of precedence:
| Source | What it can say | What it cannot say |
| --- | --- | --- |
| `operator_list`: Mail Boxes Etc's own centre pages | The address is a Mail Boxes Etc centre, where anyone can rent a mailbox. The pages crawled on 16 September 2026 list 124 UK centres (115 in England, 6 in Scotland, 2 in Northern Ireland, 1 in Wales) and 4 in Ireland; 121 of the 124 key to a building. | Which companies or people rent a box there; other operators' centres; a centre opened since the crawl. |
| `registered_agent`: the bundled agent list | The address is one a formation or registered-office service publishes, or one that Companies House filings naming the service carry, and `mail_drop_provider` names the service. In the list's 16 September 2026 version, 8 of its 17 agents have a British address: 2 checked against the agent's own site, 6 against Companies House filings that name the service. | Which companies use the agent; whether any of them trades there; agents the list does not hold. |
| `company_cluster`: Companies House counts | At least 25 companies — the published threshold — name the building as their registered office, and `companies_registered` carries the count. | Why: a formation agent the list does not know, an accountant's office, a serviced office and a large landlord all look the same. A building below the threshold is not flagged. |
Two more counts from the same snapshot, keyed on 23 September 2026 the way the lookup keys an address (a house number, a street and a postcode or town, folded to one key per building), show why the threshold matters. 4,109,723 companies have a registered office that keys to a building, across 1,838,998 buildings. 10,460 of those buildings carry 25 companies or more, and between them they hold 1,541,045 companies: 37 percent of every company that keyed. A hundred and one buildings carry a thousand or more. These are counts per building across the whole register; the [British postcode district pages](/tools/address-checker/gb/districts) tally the same snapshot district by district, keyed by district rather than by building, so a sum of their counts need not match these.
The lists and the count overlap. Of the 121 Mail Boxes Etc centres that key, 78 stand at a building with 25 or more companies registered, so the count alone would flag them. The list is what names the operator, and what catches the other 43.
## Telling the trading address apart
- **Treat the registered office as a service address.** The register records a registered office, not a trading address, and a formation agent's office is a lawful registered office. Ask for the trading address separately, and say why.
- **Read the source, not just the flag.** `registered_agent` is the most ordinary answer a registered office can give. `operator_list` on a registered office invites a question: is post going to a rented box? `company_cluster` is a count to read.
- **A trading address should answer none of the three.** It is also no box: an address that reads as `po_box` or `private_mailbox` is a place post is collected, not a place business is done. [What a PMB (private mailbox) is, and why it cannot prove where you live](/guides/what-a-private-mailbox-is-and-why-a-box-cannot-prove-where-you-live) covers the second.
- **A default address is a warning about the record, not the company's trade.** The registrar moved the registered office there, and most such companies are on their way off the register.
## A mail drop is not a "drop address"
The two phrases sound alike and mean different things. A mail drop, in Spaw's answer, is a fact about a place: the address takes post for companies and people who are not there. In parcel-fraud slang, a "drop address" is how one order uses an address. Goods bought with stolen payment details go to an address where someone — a mule, in fraud teams' vocabulary — collects or reships them. It can be anyone's home, and nothing about the building marks it: no branch list or company count would flag it.
Spaw holds no list of reshippers or parcel mules, and `is_mail_drop` is not built to detect that fraud and does not. A checkout that wants to act on the delivery-point facts Spaw does hold can do so through [the shipping policy](/guides/gate-a-checkout-with-the-shipping-policy).
## None of this says anything about a person
Every number in this guide counts companies on a public register. Every flag is a fact about a building: who publishes it as a branch, whose office it is, how many companies name it. None of it says who lives or works at an address, whether a company registered at an agent's office is honest, or why a company ended up at the default address. The response carries no field that says who is anywhere. For the fields themselves, see the [address verification guide](/docs/address-verification), and try any address on the [free postal address checker](/tools/address-checker).
---
# A suite at a single-family home
What the flat or suite written on an address says against the units a register lists at the building: unit_check, its two signals, and which registers can tell.
Date: 2026-09-17
"Suite 300" at a house on a cul-de-sac is one of the oldest tells in address fraud, and one of the cheapest to check once a register says how many units a building has. An apartment number invented to make a residential address look like an office, or to make a parcel addressed to a name the household does not know look deliberate, shows up as a unit at a building the register lists as a single residence. The reverse — a block of forty flats with no flat number — is not fraud but it is the delivery-point problem the postal services sell an answer to, and a register that lists the units can answer it for free. Since 17 September 2026 both are in the address answer as `unit_check`, and two of its values are risk signals.
## The four answers
`unit_check` is read only where a national register found the building itself (`match_level: premise`) and says something about its units. It answers one of four things.
| Value | Meaning | Signal |
| --- | --- | --- |
| `ok` | The unit written agrees with the register: a unit at a building that lists several, or none at a single one. | none |
| `missing` | The register lists several units and none was written. | `unit_missing`, weight 20 |
| `unexpected` | A flat or suite was written at a building the register lists as a single residence. | `unit_unexpected`, weight 30 |
| `null` | The register did not find the building, does not say how many units it has, or lists one unit at a building whose use is not residential. | none |
Two rules keep the check honest. A private-mailbox number is a box, not a flat: "2093 Philadelphia Pike PMB 1234" is a rented box at a mail-receiving agency, which the `private_mailbox` signal already scores at 60, and the PMB never counts as a unit here, so the same address is not scored twice. And `unexpected` fires only at a residential building. An office block that the register counts as a single unit — the Austrian register counts dwellings, and an office has none — will carry "Suite 5" on every letter it receives, and a suite there is ordinary; the check answers null rather than pretend to know.
## Which registers can tell
A register can only say what it publishes, and registers differ in whether they publish units at all.
The Austrian register counts the dwellings in each building, so a Viennese address at a building with two or more dwellings and no door number answers `missing`, and one with a door number at a single dwelling answers `unexpected` where the building is residential. The Belgian BeST register carries a box number on the rows that have one — 621,000 of Brussels' 848,000 rows — so a Brussels address is judged the same way. The Australian G-NAF lists flats as rows of their own, with a flat type and number, and a building with two or more such rows is multi-unit. The Dutch BAG carries no separate unit: a toevoeging is part of the house number in the Netherlands, and the check stays null there.
The United States National Address Database is the register that must not be over-read. It carries a unit column, but only about fourteen percent of its rows fill it and whole contributing authorities list none, so a building with no unit rows there is not a single-family home; it is a building whose authority did not say. That register therefore never calls a building single, `is_multi_unit` answers null at one or zero unit rows, and `unit_check` answers null with it. Only a register that declares its units — the Austrian one does — lets zero listed units mean one.
The rule generalises: `is_multi_unit` is true at two or more listed units, false only where the register declares its units and lists none, and null otherwise, and `unit_check` inherits exactly that caution. A null is "the register cannot say", never "there is no unit".
## Reading it with the other fields
The check sits beside `building_use` and `is_multi_unit`, and the three read together.
```json
{
"house_number": "104",
"unit": "Apt 3",
"match_level": "premise",
"building_use": "residential",
"is_multi_unit": false,
"unit_check": "unexpected",
"risk_signals": [{ "signal": "unit_unexpected", "weight": 30, "dataset": "at-adressregister" }]
}
```
The dataset in the signal is the register's own, because the fact is the register's. A `missing` at a building the register lists as multi-unit reads:
```json
{
"house_number": "100",
"unit": null,
"is_multi_unit": true,
"unit_check": "missing",
"risk_signals": [{ "signal": "unit_missing", "weight": 20, "dataset": "at-adressregister" }]
}
```
At 30, `unit_unexpected` alone is `medium`; with a town that matches nowhere in its postcode area it reaches 60 and `high`. The default shipping policy blocks neither; the guide on gating a checkout suggests adding `unit_unexpected` for a shop shipping physical goods, and leaving `unit_missing` to the carrier, which will ask the customer for the flat number as a matter of routine.
## What it does not claim
It does not say that the unit exists. A register that lists flats 1 to 12 at a building and receives "Flat 7" answers `ok` because a unit was written at a multi-unit building, not because flat 7 was found; the unit rows are counted, never matched, because a register's unit spellings ("Apt 7", "7", "Wohnung 7", "bus 7") and a customer's rarely agree and a mismatch there would be noise. That is a deliberate limit and a possible next step, register by register.
It does not run where the building was not found. An address matched to its street or its postcode only has no building to compare with, and `unit_check` is null however many units were written. And it does not run for a post-office box, general delivery or a parcel locker, which stand in for the street line and carry no building at all. Where the register is silent the answer is silent, which is the same rule every other field in the answer follows.
---
# Gate a checkout with the shipping policy
One boolean per address, decided by rules you set once: which signals block, how high the score may go, whether a register must confirm the building.
Date: 2026-09-17
Updated: 2026-10-14
A shipping policy is the set of rules an account keeps about which address answers it will ship to. Every address lookup ends with `ok_to_ship`, the boolean those rules produce for that answer, and `blocked_by`, the name of the rule that decided a refusal. The score stays arithmetic and the flags stay facts; the policy is where a decision is made, and it is made once, on the server, so a checkout, a batch job, a bulk run and a monitored list all branch the same way.
## Why a boolean, when the answer has 61 fields
Because the fields are evidence and a checkout needs a decision. A mail drop at 50 is a fact about a place; whether a parcel may go to a mailbox shop is a decision only the merchant can make, and a software vendor selling gift cards will make it differently from one selling furniture. A team that reads the fields ends up with an if-chain that grows with every new signal and drifts between the checkout, the import script and the fulfilment filter. A policy puts the chain in one place, applies it to every channel, and leaves `blocked_by` in the answer so the reasoning is visible to the person who has to explain a refusal.
Three rules make up a policy, all optional.
| Rule | What it does |
| --- | --- |
| `block_signals` | A list of risk-signal names. Any answer whose `risk_signals` carries one of them is blocked, whatever its score. |
| `max_risk_score` | A ceiling. An answer whose `risk_score` is above it is blocked. |
| `require_exists` | Whether a national register must have confirmed the building. When true, an answer whose `exists` is anything but true is blocked. |
They are applied in a fixed order after the answer is assembled: an address that cannot stand as written is blocked first and always, then the first blocked signal in the published weights' order, then the score ceiling, then the register requirement. `blocked_by` names the first rule that fired: `invalid`, the signal's own name, `risk_score` or `unconfirmed`. An address on your suppression list, served from the list without a lookup, answers `suppressed`.
## The defaults
Until an account sets a policy, the published defaults apply. They block three signals — `undeliverable`, which only a licensed partner can raise; `placeholder`, the "asdf" and "no address" of a form filled to get past the form; and `vacant`, again from a partner — and any score over 60, and they require no register. A post-office box at 30, a terminated postcode at 30, a town that matches nowhere in its postcode area at 30 and a mail drop at 50 all pass by default; a private mailbox at 60 passes too, because the ceiling is exclusive, and a private mailbox with a town mismatch, at 90, does not.
Two of the defaults deserve a word. `mail_drop` is not blocked by default because a mailbox shop is a legitimate delivery point for a great many people — expatriates, people who travel, small businesses without an office — and a shop that sells physical goods to them would lose customers for nothing. And nothing about the register is required by default because `exists` is true only where an installed register of buildings carries the building; a policy that required it would refuse every British address, where the open register lists streets and never buildings, every address in a country with no register at all, and every address a register whose coverage is partial does not carry.
The defaults are what the guest demo and the sandbox key answer with, because neither has an account to keep a policy. The five test addresses answer under the account's stored policy, like every other lookup a key makes, which makes them a way to try a policy before a customer meets it: the mail-drop test address, `2 Spaw Test Street, Springfield, IL 99999`, answers `ok_to_ship: true` under the defaults and `blocked_by: "mail_drop"` on an account that blocks `mail_drop`. Under the defaults, "1 asdf, Los Angeles, CA 90028" answers `ok_to_ship: false` with `blocked_by: "placeholder"`.
```json
{
"valid": true,
"looks_placeholder": true,
"risk_score": 60,
"risk_level": "high",
"ok_to_ship": false,
"blocked_by": "placeholder"
}
```
## Setting a policy
The policy belongs to the account and applies to every channel the account is billed for: the single and batch endpoints, bulk runs, monitored lists, the browser endpoint and the dashboard. Read it with `GET /api/v1/address/policy`, change it with `PUT`, and restore the defaults with `DELETE`. All three are free and none is logged; a key without the address scope is refused with `KEY_SCOPE_DENIED`.
```bash
curl -X PUT https://spaw.co/api/v1/address/policy \
-H "Authorization: Bearer sk_live_…" \
-H "Content-Type: application/json" \
-d '{"max_risk_score": 40, "block_signals": ["undeliverable", "placeholder", "vacant", "mail_drop", "unit_unexpected"], "require_exists": false}'
```
Send only the keys you are changing. A `null` clears the ceiling, an empty list blocks on no signal, a signal name the weights table does not carry is dropped, and a body naming none of the three rules answers `422`, because that is the shape a misspelled key takes. The answer carries the policy in force, `is_default`, and when it was last changed. The Shipping policy tab on the verification page edits the same three rules with checkboxes, listing every signal beside its weight.
## What a checkout should block
A checkout is the gate where a false refusal costs a sale and a false acceptance costs a parcel. Three policies cover most cases.
**A shop shipping physical goods to consumers.** Keep the defaults and add `unit_unexpected`: a suite number written at a building the register lists as a single residence is the invented-flat tell, and a parcel addressed to "Apt 3" at a house with no flats comes back. Leave `mail_drop` alone unless your fraud losses say otherwise, and leave `unit_missing` alone — a block of flats with no flat number is a delivery problem your carrier will raise with the customer, not a fraud signal.
**A service with a free trial or a physical welcome kit.** Add `mail_drop` and `private_mailbox` to the blocked signals and lower the ceiling to 40. A registered-agent address answers `mail_drop_source: registered_agent` and is blocked by the same signal; a company whose registered office is an agent's is ordinary, but nobody lives there, and a welcome kit sent there is a welcome kit sent to nobody.
**A regulated onboarding that needs a residential address.** Set `require_exists` to true — but only for the countries where a complete register of buildings is installed, which the country pages list, and only if you are prepared to hold addresses from anywhere else for a manual look, because `unconfirmed` is what each of them answers unless a register finds the building: every address in a country with no register of buildings, and every one a partial register does not carry. The honest way to do this is a policy per market, and the policy is per account, so most onboarding flows are better served by reading `exists` and `register_coverage` themselves and keeping the account policy for the signals.
None of these should block `not_in_register` without thought. It fires only under a register whose coverage is complete, and those registers are good, but a new building takes a quarter to reach a quarterly register, and a customer in a house finished last month is exactly who you would be refusing.
## Using it in the flow
The browser endpoint verifies as the visitor types, with a publishable key and, if you turn it on, Cloudflare Turnstile in front of the lookup so bots cannot spend your credits. It answers the same `ok_to_ship` and `blocked_by` as the secret-key endpoint, under the same account policy, and it never blocks a submit on its own; the decision is yours to wire, and `blocked_by` is the word to show the customer or the fraud desk. Bulk runs carry both fields as CSV columns and count the blocked rows in the run's summary, and a monitored list re-applies the policy in force on the day it runs, so a policy tightened on Monday is what Saturday's run uses.
---
# Placeholder addresses: what people type instead of an address
asdf, n/a, no address, 123 Fake Street: what a form gets when nobody means to give an address, how Spaw spots it by whole name, why Test Valley Road is safe.
Date: 2026-09-17
Updated: 2026-09-23
Every address field that is required and not validated fills up with the same things. "asdf". "test". "n/a". "none". "no address". "same as above". "123 Fake Street, Anytown". A row of x's. A keyboard run. They are not typos and they are not fraud in the usual sense; they are what a person types when the form insists on an address they do not want to give, and they are what a script types when it needs the field to be non-empty. Until 17 September 2026 Spaw read them as addresses, because structurally they are: "asdf" is a street name of four letters, "Anytown" is a town, and a postcode beside them can be perfectly real. Every such answer now carries `looks_placeholder`, and when it is true, a `placeholder` signal of 60.
## The whole-name rule
The single most important thing about the detector is what it does not do: it never looks inside a name. "Test Valley Road" is a real road in Hampshire, "Sample Road" exists in a dozen American towns, "Nowhere Road" runs through Georgia, and a detector that flagged a street for containing the word "test" would flag every one of them. So the comparison is made on the whole folded name — the street with its suffix folded the way a register's is, so "Fake Street" and "Fake St" are one key, and the town as one folded word — against a bundled list of the things people type instead. "Test" alone is on the list; "Test Valley Road" folds to a key the list does not carry, and is not.
The list is short and deliberately dull: test words (test, testing, tester), keyboard runs (asdf, qwerty, zxcv), refusals (n/a, none, null, unknown, no address, not applicable, not provided, not telling), deferrals (same as above, see above, tbd, tba, pending, will provide), fillers (xxx, aaa, abc, blah, foo, lorem ipsum), fictions (fake street, 123 fake street, dummy, sample, example, placeholder), the non-places (anywhere, nowhere, somewhere, anytown, no street, my address, home, my house), and the honest ones (no fixed abode, homeless, refused). A word goes on the list only when nobody would name a place that way. "Main" and "Park" would flag real streets and are not there; "Sample" is, because "Sample Road" folds to a different key than "Sample" and stays safe.
## The four shapes
Beside the list, four shapes are judged as placeholders whatever the letters, applied to whole names only and, for a street, without the suffix and directional at its end, so "Sdfgh Road" reads as the no-vowel run it is rather than as a word that happens to end in a real one, and "MG Road" and "MLK Blvd" read as the initials they are.
| Shape | Example | Why |
| --- | --- | --- |
| No word with a vowel, and one word of four or more letters | "sdfgh", "hgfdk" | Every word lacks a vowel and one runs to four letters, so initials such as MLK Jr pass; y counts everywhere, w in Great Britain (Bwlch, Pwll) and r and l in Czechia and Slovakia (Tvrz, Srch). |
| One character repeated three or more times | "xxxx", "aaa", "1111" | A key held down. |
| Digits alone | "12345" as a town | A postcode or a phone number in the wrong field. |
| A run along a keyboard row of four or more | "asdfg", "qwer", "1234" as a street | The oldest placeholder there is. |
Short names are left alone. Rye, Ely and Ayr are towns, and so is Cwm, in Wales. The vowel rule and the keyboard run start at four letters and stay there; a single character repeated counts from three, and digits alone at any length.
## A part a dataset confirmed is never judged
Neither the list nor the shapes are applied to a street or a town that a dataset has already found. A street counts as found when a register matches it, at a building, a number range or the street itself, or when a company register, the mail-drop list or the registered-agent list holds the address; a town counts as found when the postcode directory lists it for the code or somewhere in the code's area, or when it names the code's region and that region is one city, as CDMX does for a Mexico City postcode. So "1 Test Road, Southampton, SO14 3JU" passes, though "test road" is on the list, because Open Names files a Test Road under SO14; and so do None, one of the places GeoNames lists for the Italian postcode 10060, and Jbphh, the place it lists for 96853, Joint Base Pearl Harbor–Hickam in Hawaii. A shape is a guess about what somebody typed, and a dataset that lists the name is evidence that it is a place.
## What the answer carries
`looks_placeholder` is true, false or null. True when the street or the town matched; false when the address had a street or a town and neither did; null when the address had neither to judge — a bare postcode, a box with no street line. A true answer adds `placeholder` to `risk_signals` at 60, which is `high` on its own, cites `address-placeholders` in `sources` with the list's version, and lands the address in the default shipping policy's block list: `ok_to_ship: false`, `blocked_by: placeholder`, without anyone configuring anything.
```json
{
"street": "asdf",
"locality": "Los Angeles",
"looks_placeholder": true,
"risk_signals": [{ "signal": "placeholder", "weight": 60, "dataset": "address-placeholders" }],
"risk_score": 60,
"risk_level": "high",
"ok_to_ship": false,
"blocked_by": "placeholder"
}
```
The weight is deliberately the same as a private mailbox's and higher than a mail drop's. A mail drop is a real place where post arrives; a placeholder is not a place, and the only reason it is not scored at 100 is that the other signals still have something to say about the postcode and the town beside it — a placeholder street with a terminated postcode is two facts, and the score should say so.
## What it is not
It is not a typo detector. "Amphitheater Pkwy" for "Amphitheatre Parkway" is a misspelt real street, and the register answers `street_suggestion` with the spelling it holds; the placeholder flag stays false. It is not a completeness check either: an address with no street at all is refused with `missing_street` before any of this runs, for free, and a placeholder only arises when something was written. And it does not read the organisation or the building line. "Test Ltd" is a company that exists, "The Test House" could be a building, and a customer who writes their company as "N/A" has written the truth.
It is also honest about its limits. A placeholder in a language the list does not carry, or a plausible-looking fiction — "42 Elm Street, Springfield" — passes, because it is a real-looking address that a register may or may not carry, and that is the register's question, answered through `exists`, `match_level` and the confirmation map, not this flag's. The list grows when a customer's outcome reports or a bulk file show a word it lacks, and its version travels in every answer that used it.
## What to do with it
For a signup form, block it: the default policy already does, and a form that lets "asdf" through will pay to verify it again on every later step. For a bulk file, the run summary counts `placeholders` beside the verdicts, which is the quickest measure of how much of an old list was never an address to begin with. For a monitored list it is a flag on the day a row first carries one; a row that was a real address and became "asdf" is a row somebody edited. And for a customer-facing message, `blocked_by: placeholder` is the one refusal that can be worded plainly: the address field needs an address.
---
# Reading a British postcode district page
One page per outward code from installed data: live and terminated postcodes by sector, the roads Open Names files there, nearest districts, a company count.
Date: 2026-09-17
Updated: 2026-09-23
A British postcode has two halves. The outward code — SW1A, RG5, B91 — is the district Royal Mail sorts to; the inward code narrows the post to a street or a handful of buildings. The August 2026 ONS Postcode Directory holds 3,016 districts in Great Britain's 120 postcode areas. District pages have been published under the address checker since 17 September 2026, and each of the 3,016 now has one. A page is built from nothing but what this server already holds: the national postcode directory, the places Ordnance Survey's Open Names publishes for each postcode, the roads the same file lists under each district, and the Companies House company snapshot. This guide says what is on such a page, where each number comes from, and what a district page will never tell you.
## What a page carries
Every district page opens with the postcode area and its conventional name — SW is South West London, RG is Reading, B is Birmingham — from a hand-curated table of area names that is not read from any dataset. Then it carries what the weekly count measured for the district, and what is read when the page is rendered:
- **The places.** Open Names publishes, for every full postcode, the populated place, the district or borough, the county or unitary authority and the region it sits in. A district page lists the distinct places its postcodes carry, most frequent first, up to twenty-four. SW1A lists Greater London, London, the City of Westminster and Wandsworth; RG5 lists the South East, Woodley, Wokingham and Reading. The page's title names one of them: the place Open Names most often lists first for the district's postcodes, which is the populated place where Open Names names one and the borough where it does not — the City of Westminster for SW1A, Woodley for RG5, Solihull for B91. It is never called a post town. These are the names the checker matches a written town against, lenient to the borough and the region, so a page is also a preview of which towns an address in that district may honestly carry.
- **Live and terminated postcodes.** The directory keeps every code ever issued with its status, and the page counts both. SW1A has 145 live postcodes of 355; RG5 has 504 of 675. A terminated code is a real address that has moved on, not a typo, and the count of them says something about how much the district has been redeveloped.
- **Sectors.** A sector is the postcode less its last two characters: SW1A 2 of SW1A 2AA. The page tabulates each sector's live and terminated codes; SW1A's 145 live postcodes sit in SW1A 0 (3), SW1A 1 (67) and SW1A 2 (75).
- **The roads.** Open Names files every road under one district. The page counts the roads filed under this one and lists all of them, named roads first and numbered roads (A40, B4632, M4) after, folding everything past the first sixty into a disclosure: 33 for SW1A, beginning with Arlington Street, Bennet Street and Blue Ball Yard; 57 for WC2H; 285 for RG5, ending with the A3290. In the listing installed on 22 September 2026, 2,411 of the 2,752 districts with a road filed under them carry more than sixty, and BL1 carries the most, 1,540. The list is read from the listing when the page is rendered and cached for a day under the listing's version.
- **A centroid.** The mean position of the district's live postcodes, from the directory's coordinates. SW1A's is 51.50455, −0.13223, which is Whitehall. It locates the district, never a building.
- **The nearest districts.** The twelve districts whose centroids lie closest, by great-circle distance, among the indexable districts — those with a live postcode and a listed road — whichever postcode area they are in. WC2H's nearest are WC2E at 0.3 km and W1D at 0.4 km, and nine of SW1A's twelve are W1 and WC2 districts rather than SW ones. Every district has a page, but a district that is not indexable is never listed as anybody's neighbour. A district without a centroid, such as one with no live postcode, links the first twelve indexable districts of its own area instead.
- **Companies.** How many companies the Companies House snapshot lists with a registered office in the district and, where there are at least a hundred, how many of them are at addresses that 25 or more companies share; 25 is the count at which the address lookup reports a company cluster. WC2H has 91,283, of which 89,741 are at such addresses, in the 1 September 2026 snapshot as read on 23 September 2026; SW1A has 1,307, of which 674. These are counts only: the page never names an address or a company.
- **The datasets.** Which copy of the directory, which copy of Open Names and which Companies House snapshot the page was read from, with their versions and Open Government Licence v3.0 attributions, and a "Data synced on" line giving the newest of those versions. The versions are those of the copies installed on the server that rendered the page, so they move as each dataset is re-synced.
- **A hand-off.** A link into the address checker with the district prefilled, so a visitor who has found their district can check an address inside it, and the same road list as a call to the streets endpoint.
On 22 September 2026, counting the August 2026 directory, the warm command measured 3,042 districts: 3,016 in Great Britain, of which 2,752 are indexable, and 26 in the Channel Islands and the Isle of Man.
## What makes a page indexable
A district page exists for every outward code the directory holds a postcode for, but only a page with at least one live postcode and at least one listed road is offered to search engines and listed in the sitemap; the others render with a noindex directive. On 22 September 2026 that left 264 British districts unindexed: 144 whose codes are all terminated, and 120 with live codes but no road filed under them. The rule is the same one the country pages follow: a page earns its place with data of its own, and a district of terminated codes and no roads has nothing to say that the index page does not.
Districts are keyed exactly, by the code with its last three characters removed, never by prefix: SW1 does not swallow SW10, and B9 does not swallow B91. The index page lists every district grouped by area and says how many of them carry both, so the 2,752 are browsable rather than one long list.
## What a district page will never tell you
Three things, each stated on the page itself.
A directory says whether a code is in use, never who is there. Nothing on a district page, and nothing in the address product, names a resident, a household or an occupant, and no field ever will; the counts are of codes, places, roads and companies.
A road is filed under one district. Open Names files each named road under a single outward code, while a long road runs through several, so a street missing from a district's list may be filed under its neighbour's. That is why the checker never scores a British street it cannot find — on 16 September 2026, 87.3 percent of 49,106 street lines taken from Companies House registered offices matched by district, a share of those street lines and not of every filing — and why the road count on a page is a count of what is filed there, not of every road a resident would say is in the district.
There is no open register of buildings in Britain. Every full British address list derives from the Royal Mail Postcode Address File, which is licensed, so a district page can list roads and places but never say whether number 12 stands on one of them; `exists` stays null for a British address until a licensed partner is configured. [Can you look up UK addresses from a postcode for free?](/guides/a-uk-postcode-is-not-an-address) says the rest.
## Why Northern Ireland is missing
The directory excludes BT postcodes by design: their reuse needs a licence from Land and Property Services that the open directory does not carry, so no Northern Irish district has a page, and none is counted in the 3,016. That is a licensing fact, not a coverage claim, and the directory's attribution, printed on every district page, says so.
## The Channel Islands and the Isle of Man
The directory does carry the Channel Islands and the Isle of Man: the GY, JE and IM areas, 26 districts between them. They are Crown Dependencies, not part of Great Britain, so the index lists them apart from the British areas and leaves them out of its counts. OS Open Names does not cover them, so their pages count postcodes and sectors but list no roads, are not indexed and carry no Companies House count. The copy of the August 2026 directory installed on 22 September 2026 carries no coordinate for any of their 13,173 postcodes either, so their pages have no centroid and name no nearest districts.
## Using the pages
For a person, the pages are the quickest answer to "which towns does RG5 cover" and "is SW1A 2AA still a live code", and the hand-off turns the answer into a check. For a developer, they are the human-readable face of what the streets endpoint answers for a British postcode — the same listing, the same versions — and a reminder that the API's British answers are about streets and codes, with the building deliberately left to the licensed list. For a search engine they are what the product knows about a district, dated, attributed and refreshed every Saturday after the places sync.
---
# What a postcode directory learns from a register
A register of buildings adds the towns it writes under each postcode to the directory, beside its rows and never in their place. Why, and what the US gets.
Date: 2026-09-17
A postcode directory and an address register answer different questions. The directory says whether a code is in use, roughly where it is, and which towns it is written with; the register says whether a particular building stands on a particular street. They are compiled from different publishers on different days, and on 17 September 2026 the two started talking to each other: a register of buildings now contributes, as it compiles, the towns its buildings are written with and the mean of their positions to the country's postcode directory. This guide is about what that adds, what it deliberately does not do, and why the United States gets its extra place names from somewhere else.
## What the register adds
Ten registers declare it: Switzerland, Austria, Czechia, Norway, the three Belgian regional files, Luxembourg, Australia and Latvia. As each compiles, every postcode its rows carry is aggregated once — the distinct towns written under it, capped at two hundred and deduplicated by their folded form, and a centroid averaged over the buildings that carry a position — and after the register's indexes are installed the country's directory is augmented: a code the directory lacks is inserted under the register's own dataset name with a `-postcodes` suffix, and a code the directory holds gains the register's towns after its own and a centroid only where it had none. The directory's dataset name, version, coverage claim and prefix length stay what they were.
Two things improve for a lookup. `locality_matches_postcode` now knows every town a register writes under a code, not only the operator's default one — a Czech code that the postal-code dump lists with one obec now lists every settlement the state register files under it — and `postcode_status: live` answers for codes the dump never carried at all. Measured on the Czech register on 17 September: 38 codes added to the dump's 2,694, and 2,639 codes given the register's places, for 2,732 in the directory.
The Saturday places sync, which already re-applies Open Names' places to the British directory after the weekly dump replaces its rows, re-applies the registers' contributions the same way, so a fresh copy of the dump never loses them.
## Why augment, and not replace
The obvious design was to let a complete register replace the directory outright: if the register lists every building in the country, its postcodes are the country's postcodes, the directory could be recorded complete, and a code the register lacks could answer `postcode_not_found`. It would have been wrong, and the reason is worth stating because it recurs everywhere in this product.
A register of buildings never lists the postcodes that have no buildings. Swiss and Austrian Postfach codes, Australian GPO box codes, the Czech special codes for large recipients, the Norwegian codes reserved for box customers — these are real codes that real post is addressed to, and none of them stands at a building. A directory rebuilt from a register would have turned every one of those addresses into `postcode_not_found`, which is not a mismatch but a refusal: the address is invalid and free, and the customer behind it is real. So the register adds and never subtracts, and coverage does not move to complete by itself. It moves only when the operator's published total of codes is configured for the country and the union of the dump and the registers reaches it, and on 17 September no country's total is configured, because the totals the operators publish were not ones anyone in the session could state with confidence. A directory that is partial denies nothing; that is the honest state until the number is known.
The three Belgian registers illustrate the other half of the design. They are three files for one country, and each augments the same Belgian directory in turn; nothing is overwritten, so the last file to compile does not decide what the first two contributed.
## The United States, and why it stays unscored
The American directory has the opposite problem. The postal-code dump carries exactly one place per ZIP — the postal service's default city — while the postal service accepts several names for many codes, and no open source publishes that list. A customer who writes "Moffett Field, CA 94043" or "Hollywood, CA 90028" is writing what their mail is delivered to, and the directory used to answer `locality_matches_postcode: false`.
The United States has no complete open register to augment from, so the directory learns from a different publisher: the Census Bureau's ZCTA-to-place relationship file, public domain, which lists for every ZIP code tabulation area the incorporated places and census-designated places whose land it touches. Applied on 17 September to the 41,488 installed ZIPs — a 9.8-megabyte file last modified in December 2021 — it gave 28,668 of them additional places in 3.5 seconds. ZIP 94043 now lists Mountain View, Palo Alto and Sunnyvale, and "1 Main Street, Palo Alto, CA 94043" answers `locality_matches_postcode: true`.
It does not list Moffett Field, and it does not list Hollywood. The file describes the places whose land a ZIP touches, and a neighbourhood of Los Angeles is not a place in the census sense, while a postal name for a military field is not one either. The Census list is therefore a better approximation of the postal service's acceptable names than the default city alone, and still not that list, which is why the American directory keeps its `primary` places declaration and a town mismatch there is still reported in the field and never scored. That is a deliberate asymmetry: a false `locality_mismatch` on a legitimate American address would put thirty points on the most common address in the product for a reason the customer cannot see, and a missing thirty points on a wrong town is the cheaper mistake.
## Reading the answer
Nothing new appears in the fields; what changes is how often the existing ones say true. `confirmation.locality` answers `postcode_directory` when the town is one the directory lists under the code — including the towns a register added — and `postcode_area` when it is one listed somewhere in the code's area, and `unconfirmed` otherwise; `locality_mismatch` fires only in the last case and only in a country whose directory lists every place, which the United States does not. `sources` names the directory dataset and, for a code a register contributed, the register's `-postcodes` dataset with the register's version, so a customer can see which publisher the town came from.
For Australia the contribution lands on the register's next sync — the G-NAF compile takes the better part of an hour and was not re-run for this — and Denmark's and New Zealand's registers are not compiled on the reference machine at all. Each register's status row on the data page says whether its postcodes have been applied.
---
# A registered agent's address is not a mail drop, and not a home
A mailbox branch, a formation agent's office and a building full of companies all collect other people's post. What mail_drop_source distinguishes.
Date: 2026-09-16
Updated: 2026-09-23
Some addresses exist to receive post for people and companies who are not there. A mail-forwarding branch rents boxes to anyone. A company-formation agent lets thousands of companies name its office as their registered address. A building that carries a crowd of registered companies is, whatever its sign says, one of the two. Spaw has always answered all three with one flag, `is_mail_drop`, and since 16 September 2026 it says which of the three it found, because the three mean different things about the person in front of you.
## Three sources, one flag
`mail_drop_source` takes one of three values, tried in that order:
`operator_list` means the address is on the published branch list of a mailbox operator — The UPS Store and PostNet in the United States, and the operators added since. Anyone can rent a box there, and a customer who writes one as a home address is writing a place where nobody lives.
`registered_agent` means the address is on the bundled list of the offices registered-agent and formation services stand at: nine American registered agents, five of them in Delaware, where a large share of American companies are registered, and eight London formation and registered-office services, the busiest of whose addresses, 71-75 Shelton Street, is the registered office of 86,789 companies in the 1 September 2026 Companies House snapshot as the lookup counts them. On 16 September 2026 the list named seventeen operators. `mail_drop_provider` carries the agent's name, because that is a fact about the agent's own business.
`company_cluster` means neither list knows the address, but an open company register lists more companies at it than the published threshold. It is a heuristic about a building, and `companies_registered` carries the count so the evidence sits beside the flag.
All three carry the same `mail_drop` weight of +50, because the question the flag answers — does somebody live here? — has the same answer for all three: no.
## Why the distinction matters anyway
The weight is the same; the decision you make with it is not.
A business onboarding check that meets a registered-agent address has met the most ordinary thing in company formation. In Delaware the agent's office *is* the registered office of a great many companies, by design, and a company that names one has complied with the law rather than evaded it. The right reading is "the registered office is an agent's; ask for the trading address", and `registered_agent` is what lets a rule say exactly that instead of "mail drop, decline".
A residence check that meets the same address has met something else entirely. Nobody lives at 251 Little Falls Drive, and a proof-of-address form that names it is naming a place that receives the company's legal post. That is what the +50 is for.
A `company_cluster` sits between the two. The building might be a formation agent the list does not know, a serviced office, a large landlord's registered address, or a genuinely busy commercial building; the count tells you how busy, and the threshold that turned the count into a flag is published in the documentation so the arithmetic can be checked.
## What the answer never says
It never says a company is suspicious for being registered at an agent's address, and the documentation is written so nobody can quote it that way. It never names a person, and it carries no register of who is registered where: `companies_registered` is a count, and the agent list holds the agents' own addresses and nothing about their clients. And it never treats a private mailbox — a PMB number at a real street address — as the same thing as an agent's office: that is `address_type: private_mailbox` with its own weight of +60, decided from the way the postal service requires such an address to be written, not from any list.
## Where the list comes from
The agent list ships with the product as a versioned data file, and every entry is an address the agent publishes as its own, or that public filings naming the agent's service carry, and the file says which for each. In the 16 September 2026 version, four of the seventeen were checked on the agent's own site — the registered-office address on a formation agent's order page, the "our address" line on a registered agent's contact page — two American and two British. The other thirteen were checked against the public record instead: three against the Delaware Division of Corporations' registered-agent listing, four against Secretary of State filings in Wyoming, Washington, Nevada and Texas, and six against Companies House filings that name the service. A filing is read for the service's own address and nothing else: nothing in the list comes from a review site or a customer's report, and nothing in it names a client. The file's version appears in the answer's `sources` as `registered-agents` whenever it decided the answer, so a change to the list is visible in the response that used it.
Each entry is keyed the way a national register is: the street name folded to its comparison form, the postcode or town, and the house number with both ends of a range tried, so "71-75 Shelton Street", "71 Shelton St" and "75 SHELTON STREET" all find the same agent. An agent's address the list does not yet hold falls through to the company register, where the cluster usually catches it anyway; the count is published beside the flag either way.
## Reading it in a workflow
For a residence claim, treat every `mail_drop_source` alike: the address cannot prove where somebody lives, and the score already says so. For a company's registered office, treat `registered_agent` as normal, `operator_list` as a question ("why is the registered office a rented box?") and `company_cluster` as a count to read. For a trading address, ask for one that answers none of the three. The flag is a fact about a place; which of your decisions it should change is yours to write.
---
# How an open register locates a building
National registers publish where each building stands, and some say what it is for. Which carry what, how grids are projected, what location_source means.
Date: 2026-09-16
An address that a national register carries can be placed on the map by the register itself. Since 16 September 2026 the premise index Spaw compiles from each register keeps, beside every building's key, what the register publishes about it: where it stands, and where the register says so, what it is used for and how many flats or suites it holds. A `premise` match now answers coordinates from the building rather than from the postcode, and two new fields, `building_use` and `is_multi_unit`, say what the register declares.
## What location_source tells you
`latitude` and `longitude` were always there; `location_source` says what they describe. `register` means the register located the building and the coordinates are its. `postcode_centroid` means no register located it, and the coordinates are the centre of the postcode from the national postcode directory — which for a large rural postcode can be kilometres from the door. `null` means neither was available.
The difference matters in any use that measures distance. The cross-signal consistency check reports the distance between an IP's location and the address as evidence; with a register position that distance is about the building, with a centroid it is about the postcode. The field says which.
## Which registers carry what
Every register publishes a position for the buildings it lists. Use and units are rarer, and the answer never infers them from the address text.
| Register | Position | Building use | Units |
| --- | --- | --- | --- |
| United States, National Address Database | yes | where the contributing authority filled its address type in | the register's unit rows |
| France, Base Adresse Nationale | yes | — | — |
| Switzerland, building register | yes | residential, mixed, other from the building category | — |
| Austria, address register | yes | from the building attributes | dwellings counted per building |
| Czechia, RÚIAN address points | yes | — | — |
| Norway, Matrikkelen | yes | — | — |
| Belgium, BeST (three regions) | yes | — | box numbers |
| Luxembourg, national addresses | yes | — | — |
| Australia, G-NAF | yes | — | flat numbers |
| Latvia, State Address Register | yes | — | — |
| Denmark, Danmarks Adresseregister | yes | — | floor and door |
Great Britain's Open Names is a register of streets and locates none of them as buildings, which is why a British `premise` match does not occur and a British answer's coordinates stay the postcode centroid.
## How is_multi_unit is decided
`is_multi_unit` is `true` when the register lists two or more flats, suites or dwellings at the building. It is `false` only when the register declares the units at each building and lists none — on 16 September 2026 only the Austrian register counts dwellings per building, so only Austria answers `false`. Everywhere else a building with no unit rows answers `null`, because a register that carries no units has said nothing about the building's shape, and the American database in particular lists units for a minority of its rows and for none in whole states. A register that lists exactly one unit row answers `null` too: one row is not a count.
`building_use` follows the same rule. It is one of `residential`, `commercial`, `mixed`, `industrial`, `institutional`, `agricultural` or `other`, read from the register's own category codes, and `null` wherever the register publishes no category. It is not the licensed residential delivery indicator: `is_residential` is the postal service's answer about the delivery point and stays `null` until a partner is enabled.
## How the coordinates are projected
Registers publish positions in their national grids, not in latitude and longitude, and each has to be converted. The Swiss register uses LV95, the Austrian one three Gauss-Krüger zones on the MGI datum, the Czech one the Křovák projection of S-JTSK, the Norwegian one UTM zone 33, and the British Code-Point Open the OSGB36 National Grid. Each conversion was checked against proj4js on 16 September 2026: the transverse Mercator and Helmert paths agree with it to between 0.02 and 0.06 metres, and the Swiss approximate formula to between 0.4 and 2.3 metres, the larger figure at Geneva on the edge of the country. The index stores each position to about 1.2 metres.
Known buildings were then looked up after the registers were recompiled. The Federal Palace in Bern, Oslo City Hall and the Storting each answered within about twenty metres of a reference position taken from memory; the residuals in those checks say more about the reference than about the maths. Recompiling the four European registers grew each premise index from eight to fifteen bytes a key — the Swiss one from 25.8 to 48.1 megabytes — which is what a building's position costs.
## What a centroid is good for
The postcode centroid is not the building, and the answer never claims it is. It is good for the questions a postcode can answer: which town, which region, roughly how far from an IP's location, whether a written state agrees with the code. For anything that needs the door — a route, a geofence, a distance to a branch — read `location_source` first and use the coordinates only when it says `register`.
---
# How to read the address confirmation map
Every address answer says which dataset stood behind each part and what the parser changed. What each source means, and why unconfirmed is not wrong.
Date: 2026-09-16
Updated: 2026-10-14
An address answer used to tell you how far the building could be followed into a register and leave the rest to inference: if `match_level` was `street`, the street was fine and the number was not, and the postcode must have been all right or the address would have been invalid. That inference was usually correct and occasionally wrong, and it never said which dataset had done the work. Since 16 September 2026 every valid answer carries two fields that make it explicit: `confirmation`, a map from each part of the address to the source that stood behind it, and `changes`, the list of things the parser did to your input before anything was checked.
## The five parts and their sources
`confirmation` has exactly five keys — `postal_code`, `street`, `house_number`, `locality` and `administrative_area` — and each carries a source name, the word `unconfirmed`, or `null`.
| Source | Meaning |
| --- | --- |
| `postcode_directory` | The national postcode directory lists the postcode, or lists the town or region under it. |
| `postcode_area` | The town is not one the directory lists under the postcode itself, but it is one the directory lists somewhere in the postcode's area — or, where the postcode's region is a single city (Mexico City, Delhi, Jakarta, Bangkok, Moscow, Seoul and Tokyo's wards among them), it is that region's own name, such as Distrito Federal for 06000 or Tokyo for 100-0005. |
| `register` | A national address register followed the address at least this far: the postcode, the street, or the building itself. |
| `range` | The house number sits inside a range the register publishes for the street, without the building being listed. |
| `company_register` | An open company register lists companies at exactly this building, which proves the street and number exist. |
| `mail_drop_list` | A mailbox operator or a registered agent publishes this exact address as its own. |
| `pattern` | The postcode matched only the country's published shape; no directory or register was there to check it. A code the installed directory leaves out, such as a Northern Irish BT postcode, is never put to it, so an address in Belfast reads `pattern`, a box address or a street address alike: Open Names covers Great Britain only and is not asked about a BT code either. |
| `unconfirmed` | A dataset that could have confirmed this part did not. |
| `null` | Nothing on this server could check the part, or the part was not sent. |
Read the map top to bottom and it tells a story. A Swiss address that answers `postcode_directory, register, register, postcode_directory, null` was found in the postcode directory, followed to the building in the Swiss register, and has its town confirmed, while the region column is `null` because a Swiss address carries no region to check. A British address that answers `postcode_directory, register, unconfirmed, postcode_directory, null` had its street confirmed by Open Names, while its number was looked for only among the buildings Companies House lists companies at, and none was there: Britain's open street register lists no buildings.
## Why unconfirmed is not wrong
The word was chosen with care. `unconfirmed` says that a dataset in a position to confirm the part did not, and nothing more. Under a register whose coverage is partial — the United States National Address Database, the French Base Adresse Nationale, Britain's Open Names — a street that is not carried has not been called absent, because the register cannot vouch that it carries every street: the American and French registers are partial by their publishers' own account, and Spaw marks Open Names partial because it files each road under one postcode district, though a long road runs through several. The verdicts live in other fields: `exists` for the building, which is `false` only under a register whose coverage is complete; `locality_matches_postcode` and `administrative_area_matches_postcode` for the town and the region. The map is the evidence beside those verdicts, and evidence of a check that could not settle the question is still evidence.
Two consequences follow. First, the risk signals are never derived from the map: `not_in_register`, `street_not_found` and `number_out_of_range` fire on the verdicts and on the register's declared coverage, so an `unconfirmed` street under a partial register costs nothing. Second, a `null` is a fact about this server, not about the address — a country with no directory and no register installed answers `null` everywhere, and the honest reading is "not checked", never "not found".
## What the changes list says
`changes` records what the parser moved or set aside on the way from your input to the parts the datasets were asked about. It is empty for a cleanly separated address and carries one or more of these names otherwise:
- `country_line_removed` — the last line repeated the country ("United Kingdom", "USA", "Deutschland"); it was set aside so the town could be read from the line above it.
- `care_of_removed` — a line opening with c/o, attn, FAO, z.Hd. or their kin named an addressee rather than a place, and was dropped. The product is about places, so the name never reaches the answer.
- `building_separated` — a line without a number above the street carried a building word ("Rose Cottage", "The Old Rectory") and became `building`.
- `organization_separated` — a line without a number above a numbered street named a company or an addressee, and became `organization` because none was sent.
- `administrative_area_folded` — a state or province written in full was folded onto its code ("California" to CA).
- `box_line_moved` — a box line stood where the street line goes, and moved into `po_box` under the name the answer reports it by: Canada's `GD` reads `GENERAL DELIVERY`.
- `private_mailbox_moved_to_unit` — a PMB number on the street line became the `unit`. A bare `#` number is read as a flat or suite and becomes the `unit` without this change: `2261 Market St #4321` answers `address_type: street`.
What to do with them depends on where the address came from. From a form your own users filled in, `country_line_removed` and `care_of_removed` are worth feeding back into the form's design: a separate country field and an addressee field stop them recurring. From a file someone else exported, they are a record of what the pipeline judged, so a reviewer can see that "c/o Jane Smith" was read as a person and not as a street. `building_separated` is the one to act on: a building name is a real part of a British address and, if your own storage has no place for it, the `building` field is where the answer keeps it.
## An example, end to end
Sent as one line with the country set to GB:
```text
c/o Reception, Rose Cottage, High Street, Oxford, OX1 1AA, United Kingdom
```
The answer reads `building: "Rose Cottage"`, `street: "High Street"`, `locality: "Oxford"`, `changes: ["country_line_removed", "care_of_removed", "building_separated"]`, and — on a server with the British postcode directory and Open Names installed — a confirmation map of `postcode_directory` for the postcode, `register` for the street, `null` for the house number (there is none), `postcode_directory` or `postcode_area` for the town, and `null` for the region. Nothing in that answer had to be inferred, which is the whole point.
---
# Number ranges: what the US range match can and cannot say
The Census publishes every US street's house-number ranges. What match_level range and number_out_of_range mean, and why the table is declared partial.
Date: 2026-09-16
The United States has no complete open list of buildings. The National Address Database is public domain and enormous, but it is fed by the states and counties that choose to contribute, and whole states are absent, which is why it is declared `partial` and why an American address it does not carry answers `exists: null`. What the country does publish completely is its streets, with the house-number ranges along them: the Census Bureau's TIGER/Line address-range tables, public domain, one pair of files for each of the 3,227 counties. Since 16 September 2026 Spaw compiles them into a range table that the address lookup consults after a premise register has missed.
## The three answers a range can give
A range register answers at two levels of its own, and the lookup reports whichever it reaches.
`match_level: range` means the street is in the table for that ZIP and the house number falls inside a range published for it. `confirmation.house_number` reads `range`, and `exists` stays `null`: a range says the number is plausible on that street, not that a building stands there. TIGER ranges are potential ranges — the numbers a segment could carry — so a number inside one might still be a gap between two houses.
`match_level: street` with the `number_out_of_range` weight of 20 means the street is in the table for that ZIP and the number falls outside every range published for it. That is the solid negative: 9999 North King Street in Wilmington is not a place, and the table says so.
`match_level: none` with no weight means the table does not know the street under that ZIP. This is not a denial, and the reason is measured rather than assumed: TIGER assigns ranges to most edges but not all, and a street on a ZIP boundary can carry only its neighbour's ZIP, so a real street can be missing from the ZIP a customer correctly wrote. The table is therefore declared `partial`, and a street it lacks answers `confirmation.street: unconfirmed` and costs nothing. Had it been declared complete, a real street would have scored `street_not_found` one time in some unknown number, and nobody had measured that number.
## Where it sits in the order
A range never outranks a building. The National Address Database is consulted first; a building it lists answers `premise` with the database's coordinates and building facts, and the range table is not asked. Only a NAD miss falls through to TIGER, so an address in a state that contributes to the database gets the stronger answer and an address in one that does not still gets placed on its street.
## What was verified
Delaware's three counties were compiled on 16 September 2026: a 4.0-megabyte download, 139,683 ranges on 24,424 streets, a 2.37-megabyte table, 23 seconds to compile, and 2,000 lookups in 0.30 seconds. "500 N King St, Wilmington, DE 19801" answered `range`, as did "500 North King Street" and "500 King Street"; "9999 N King St" answered `street` with `number_out_of_range`; "1209 Orange Street, Wilmington 19801" answered `range` through the name without its directional; "12 Nonexistent Boulevard" answered `none` with no signal. Ranges are filed under every name the Census gives an edge, and for a name with a directional also without it, so "Orange St" finds "N Orange St".
Extrapolated from Delaware, the whole country is about 1.6 gigabytes of downloads, some 55 million ranges and a table of about a gigabyte, compiled in a few hours; each county's files are cached so a later run downloads nothing until the Census publishes a new vintage.
## How the table is built
Each county publishes two files: the address ranges, one row per side of a street segment with a from-number, a to-number and a ZIP, and the feature names, one row per name a segment carries. Both are database tables inside zip archives, read without unpacking them to disk, and joined on the segment identifier the Census uses for both. A segment with several names — a state route that is also Main Street — is filed under each, and a name with a directional prefix is filed both with and without it, which is how "Orange Street" finds the segment the Census calls "N Orange St". Ranges published backwards are normalised, and the small share of ranges written with hyphens or letters, about six in a thousand in Delaware, are skipped rather than guessed at.
Two limits follow from the source. The weekly round-trip report that checks every premise register against its own file does not measure the range table, because a range is not a row that can be looked up as an address; and a county whose files fail to download fails the whole run, so a partial national table is never installed by accident.
## What it is not
A range is not a delivery-point check. Whether the postal service delivers to 500 North King Street is licensed data and stays behind `is_deliverable`, which answers `null` until a partner is enabled. A range is the public map's idea of which numbers a street segment could hold, and the answer says exactly that much.
---
# What a street match means in Great Britain
Britain has no open list of buildings, but it has one of streets. What Open Names confirms, the 87 percent of street lines it matched, why exists stays null.
Date: 2026-09-16
Updated: 2026-10-02
Every full list of British addresses derives from Royal Mail's Postcode Address File, and reselling it needs a licence, which is why a British address has never been able to answer `exists` from open data and still cannot. What Britain does publish openly is its streets. Ordnance Survey's Open Names, released under the Open Government Licence, lists every named and numbered road in Great Britain with the postcode district it runs through, and since 16 September 2026 Spaw installs it as a register of streets.
## What it confirms
A British address whose street Open Names lists under the address's postcode district answers `match_level: street` and `confirmation.street: register`. That is a real check: the street exists, in the part of the country the postcode names. A street Open Names does not list there answers `match_level: none` and `confirmation.street: unconfirmed`, and — this is the part to read carefully — carries no weight.
`exists` stays `null` either way. A register of streets has nothing to say about a building, and the answer will not pretend otherwise. The building question in Great Britain has exactly one honest source, the licensed file, and until a partner is enabled it is unanswered.
## How a street is keyed
Open Names files each road under one postcode district — the outward half of a postcode, the SW1A of SW1A 2AA — so a written address is keyed the same way: the street name folded to its comparison form, under the district of the postcode it was written with. Bilingual roads are indexed under both names, so Castle Street and Heol y Castell in Cardiff both match.
The district is also the register's limit. A long road runs through several districts, and Open Names files it under one of them: Baker Street sits under W1U, so 221B Baker Street written with its real postcode NW1 6XE does not match. Measured on 16 September 2026, district keying matched 87.3 percent of 49,106 street lines taken from Companies House registered offices. Filing every road under every district of its settlement or borough recovered three more points at thirteen times the index size, and was not worth it.
That is a share of street lines, not of filings. The recall table on the address product page measures something wider: every Sunday it draws whole filings, whatever their lines hold and whether or not their postcode stands, and sends each through the ordinary lookup. In a run of 23 September 2026, 1,441 of 2,000 filings drawn from the 1 September 2026 snapshot, 72 percent, matched a street. Neither number is the share of British addresses Open Names knows; both are what the matcher makes of addresses other people wrote.
The 87.3 percent is why the register is declared `partial` and why `street_not_found` never fires for a British address. Under complete coverage a miss would be a denial and would cost 20 points; at 87 percent it would be a false denial one time in eight, on the biggest market the product has. So a miss is reported as exactly what it is — a street the register did not confirm — and scored as nothing.
## The town, at last
The same release names, for every postcode, the populated place, the district or borough, the county or unitary authority and the region it lies in. Until now a British answer could not say whether the town matched the postcode, because the postcode directory publishes no post town; on 16 September 2026, 1,742,466 of 1,749,109 installed postcodes gained those places.
The check is lenient on purpose, because British towns are written several ways. "London" and "Westminster" both confirm SW1A 2AA, the first as the region and the second as the borough. "Reading" for a Woodley postcode is not one of that postcode's places but is a place the directory lists elsewhere in the RG area, so it reads `confirmation.locality: postcode_area` with `locality_matches_postcode: false` and no weight: a post town written for a settlement is how envelopes are addressed. "Manchester" for a London postcode is found nowhere in the area, and that one carries the `locality_mismatch` weight of 30.
## What is installed, and how often
Open Names is republished quarterly, and the register syncs on the first of every month with a freshness window of 33 days, so a single missed monthly run is flagged on the status page within days. The release is about a hundred megabytes; on 16 September 2026 the 999,065 road rows compiled to 876,830 street keys in a 7.3-megabyte index in about thirty seconds from the local file, and applying the places to the postcode directory took another thirty-five. Street names are folded before they are keyed, so "Rd" and "Road", "St" and "Street" and the two spellings of a Welsh road all land on one key, and a written address is folded the same way before it is looked up.
The file covers Great Britain — England, Scotland and Wales. Northern Ireland's postcodes are excluded from the directory in the first place, because reusing them needs a Land & Property Services licence, and Open Names carries no Northern Irish roads, so a BT address answers `postcode_status: null` — the directory was not consulted — rather than as a code that does not exist, and it never gets a street match from Open Names, which carries no weight either way.
## Reading a British answer
Put together, a British answer on a server with the postcode directory and Open Names installed can now say: the postcode is live or terminated, the town belongs to it, the street exists in that district, and — from Companies House — how many companies are registered at the building. What it cannot say is whether the building stands, and the fields that would say so, `exists` and the licensed `is_deliverable`, answer `null` rather than guess.
---
# A catch-all behind a security gateway is the gateway's design, not a squat
Proofpoint, Mimecast and Barracuda typically accept every recipient at the edge. What mx_provider_type says, why the weight drops to 15, how to send.
Date: 2026-09-13
A security gateway is a mail filtering service that a company puts in front of its real mail system: every inbound message goes to the gateway's servers first, is scanned, and is then passed on to the mailbox platform behind it. Proofpoint, Mimecast and Barracuda are the three Spaw recognises from a domain's MX records. A domain behind one of them typically answers `250 OK` to every recipient at the gateway, because the gateway does not always hold the company's directory, and that makes the domain a catch-all in the eyes of any handshake. Spaw reports it as such, but with a lighter weight, because the accept-everything behaviour here is the product working as designed rather than a sign that nobody runs the domain.
## What a gateway changes about the handshake
A mailbox handshake asks the receiving server, at the `RCPT TO` step, whether it will take mail for one recipient. A server that knows its users answers `550 5.1.1` for an unknown one, and that answer is what lets a verifier report `mailbox_not_found`.
A gateway sits between the world and that server. Some gateways verify recipients against a directory the customer synchronises to them, and those answer like an ordinary server. Many do not, or are configured not to: they accept the message, pass it inward, and let the real mail system reject unknown users afterwards, generating a bounce message back to the sender rather than a rejection during the handshake. From the outside, every recipient at the domain is accepted, which is the definition of a catch-all.
The difference from a squat is the domain's reason for being catch-all. A parked domain or a harvesting domain accepts everything because collecting mail is the point. A domain behind Proofpoint accepts everything because a filtering company is answering on its behalf. The mailbox question is equally unanswerable in both cases, but the two domains are not equally suspicious, and a score that treats them the same is wrong about one of them.
## What Spaw answers
The domain's mail provider is classified from its MX hostnames into `mx_provider` and, since September 2026, into `mx_provider_type`, which for these three providers is `security_gateway`. When a catch-all answer arrives for a domain whose provider type is a gateway, the score carries `catch_all_gateway` at 15 instead of `catch_all` at 30. The reason stays `catch_all`, because that is still what the handshake found.
```json
{
"email": "mia.kowalski@acme.com",
"deliverable": "risky",
"reason": "catch_all",
"risk_score": 15,
"risk_level": "low",
"mx_provider": "proofpoint",
"mx_provider_type": "security_gateway",
"catch_all": true,
"mailbox_exists": null,
"mailbox_confidence": 90,
"risk_signals": [{ "signal": "catch_all_gateway", "weight": 15, "dataset": "mailbox-smtp" }]
}
```
The two catch-all rows are exclusive: an answer carries one or the other, never both. Fifteen points leaves the address in the low band on its own, which matches how such addresses behave in practice: a name-shaped address at a company that pays for mail security is usually a real employee.
`mailbox_confidence` moves the same way. The estimate starts at 50 and, among other things, adds 10 for a domain whose mail is hosted at an established provider, and the three gateways count as established alongside Google, Microsoft, Zoho and Fastmail. A gateway domain with DMARC at reject, SPF published, a registration older than two years and a `first.last` username lands in the high 80s or 90s. The confidence score is the number to branch on when the reason says `catch_all`.
## How to tell which case you are in
The domain endpoint answers the question for a whole domain in one call, for one credit when the domain accepts mail.
```bash
curl https://spaw.co/api/v1/email/domain/acme.com \
-H "Authorization: Bearer sk_live_…"
```
Read three fields together. `mx_provider_type` says `security_gateway`, `mailbox_provider`, `transactional`, `hosting`, `self_hosted` or `other`. `catch_all` is `true` once any address at the domain has answered catch-all in the last week, and `null` when that is not known. And `mx_provider` names the provider, which links to Spaw's provider page for it: [Proofpoint](/email-providers/proofpoint), [Mimecast](/email-providers/mimecast) and [Barracuda](/email-providers/barracuda) each describe how the provider typically answers a handshake.
A catch-all whose provider type is `other` or `self_hosted` is the case to look at more carefully. It may be an anti-harvesting policy at a well-run company, which the confidence score will reflect through DMARC, SPF and registration age, or it may be a domain that collects whatever arrives. `parked` and `mx_blocklisted` settle the worst of those on their own, with their own reasons and weights.
## How to send to a gateway catch-all
The advice is the same as for any catch-all, with the threshold moved.
Send to gateway domains at a normal confidence threshold rather than a cautious one. A common policy holds catch-all addresses below 60 and sends above it; behind a gateway, the same address typically scores higher for the reasons above, so more of the list goes through, which is the intended effect.
Report the outcomes. A delivery reported through the feedback endpoint turns later lookups of the address into a confirmed mailbox for your account, and a bounce moves the domain's bounce share, which feeds every other address's confidence at that domain. Both are counts per domain, never addresses shared between accounts.
Expect the bounce to arrive later than usual. Because the gateway accepted the message, a wrong address bounces after delivery rather than during it, sometimes hours later, so the report from your sending provider is the only way to learn about it. Point the provider's bounce webhook at Spaw and the address lands on your suppression list without a credit spent.
Do not use the lighter weight as proof of existence. Fifteen points is a statement that the domain is a real, filtered company, not that `mia.kowalski` works there. `mailbox_exists` stays `null` behind a gateway for exactly that reason.
## What the sending policy does with it
The default policy blocks nothing on a catch-all: it blocks the five signals that are almost certainly wrong (disposable, typo-squat, parked, blocklisted, no-reply) and any score over 60, and a gateway catch-all sits well under that. If your list needs a firmer rule, `min_mailbox_confidence` in the policy holds back any unconfirmed mailbox whose estimate is below the floor you set, and `blocked_by` then reads `mailbox_confidence`. Adding `catch_all_gateway` to `block_signals` blocks the lighter case by name, and `catch_all` blocks the heavier one, so the two can be treated differently without reading the provider fields at all.
## What to do next
- Read the [catch-all guide](/guides/catch-all-email-addresses) for the confidence table and the sending advice in full.
- Look up a domain's provider type and catch-all memory with the [domain intelligence endpoint](/docs/api/domain-intelligence).
- Set a confidence floor or a per-signal rule in your [sending policy](/docs/api/update-email-policy).
- Read the provider pages for [Proofpoint](/email-providers/proofpoint), [Mimecast](/email-providers/mimecast) and [Barracuda](/email-providers/barracuda).
---
# DKIM selectors explained, and why 'none found' is not 'no DKIM'
A DKIM key lives under a selector only the sender knows. What a selector is, which names the common platforms use, and what dkim_selectors answers.
Date: 2026-09-13
A DKIM selector is the label that tells a receiving mail server where to find the public key for a signed message. The signature header of the message names it, the receiver builds the hostname `selector._domainkey.example.com`, reads the TXT record there, and uses the key it finds to check the signature. Because the selector is chosen by whoever set up the signing, usually the sending platform, a domain can publish any number of keys under any names, and nothing in DNS lists them. That one fact explains both what a DKIM check can tell you and what it cannot.
## What a selector does
DKIM, DomainKeys Identified Mail, is defined in RFC 6376 (2011). The sender signs selected headers and the body of a message with a private key and adds a `DKIM-Signature` header that carries, among other things, `d=` for the signing domain and `s=` for the selector. The receiver looks up `._domainkey.` in DNS, expects a TXT record of the form `v=DKIM1; k=rsa; p=…`, and verifies the signature with the public key in `p=`.
The selector exists so that a domain can hold several keys at once. One platform signs the newsletter, another signs the invoices, and the corporate mail system signs everything else; each has its own key under its own selector, and each can rotate its key without touching the others. Rotation is the second reason: publishing a new key under a new selector, switching signing to it, and retiring the old one later is how a key is replaced without a gap.
A record with an empty `p=` is a revoked key, which is itself a valid statement under RFC 6376: this selector existed and signs nothing now.
## Which selectors the common platforms use
Sending platforms typically use fixed selector names, and because a receiving server never needs the list, the names are simply conventions you can read from their setup instructions.
| Platform | Selectors typically used |
| --- | --- |
| Google Workspace | `google` |
| Microsoft 365 | `selector1`, `selector2` |
| Mailchimp | `k1`, `k2`, `k3` |
| SendGrid | `s1`, `s2` |
| Fastmail | `fm1`, `fm2`, `fm3` |
| Proton | `protonmail`, `protonmail2`, `protonmail3` |
| Mandrill | `mandrill` |
| HubSpot | `hs1`, `hs2` |
| Mailjet | `mailjet` |
| Generic self-hosted setups | `default`, `dkim`, `mail`, `smtp` |
Two platforms in the same table can point to the same name, and a domain that uses a platform is not obliged to keep the platform's default, so the table is a guide to what is likely, never a guarantee.
## What Spaw answers
The domain endpoint carries `dkim_selectors`, the list of common selectors that publish a key for the domain, read by querying a fixed set of 33 names, the table above plus a dozen more that platforms commonly use. A selector counts when its TXT record carries `v=DKIM1` or a `p=` tag.
```bash
curl https://spaw.co/api/v1/email/domain/acme.com \
-H "Authorization: Bearer sk_live_…"
```
```json
{
"domain": "acme.com",
"dkim_selectors": ["google", "k1"],
"mta_sts": true,
"tls_rpt": true,
"bimi": false,
"has_spf": true,
"spf_all": "-all",
"dmarc_policy": "reject"
}
```
Three answers are possible, and each means something different. A list of names means those selectors publish keys. An empty list means none of the 33 common names does. `null` means the lookups themselves failed, typically a nameserver that did not answer, so nothing can be said. The answer is cached for a day, the same as the other policy records the endpoint reads, since keys change slowly and the check is over thirty DNS queries.
The [free DKIM checker](/tools/dkim-checker) runs the same check for one domain without a key, and the domain monitor raises `dkim_lost` when a domain that had selectors in the previous run has none in this one.
## Why an empty list is not "no DKIM"
This is the caveat the tool page and the endpoint both carry, and it follows directly from how selectors work. A domain can sign every message with a key under a selector Spaw never asks about, `20250901`, say, or `acme-prod`, and the check reports an empty list for a domain with perfect DKIM. The only way to see such a key is to receive a signed message and read `s=` from its header.
So the field answers a narrower question than "does this domain use DKIM": it answers "does this domain publish a key under any of the names the big platforms use". That is still useful, in two ways. A listed selector says which platform signs for the domain, which is a fact about who runs its mail. And an empty list at a domain that sends business mail is a prompt to check the headers of a real message rather than a verdict.
Spaw never turns the answer into a risk weight for the same reason. `dkim_selectors` is information on the domain endpoint; it does not touch the per-address score.
## Reading a selector against the rest of the record
DKIM only proves that a message was signed by whoever holds the key. Whether that signer was allowed to speak for the domain is DMARC's question, defined in RFC 7489, and DMARC answers it by checking that the `d=` of a valid signature aligns with the domain in the visible From address. A domain with a DKIM key under `k1` but no DMARC record has a signature nobody is required to act on. The [SPF, DKIM and DMARC guide](/guides/spf-dkim-dmarc-for-developers) walks through the three together, and the [SPF and DMARC checker](/tools/spf-dmarc-checker) reads the other two records for the same domain.
A rotation shows up as a change in the list between two monitor runs: the old selector disappears, a new one appears. Only the disappearance of every selector raises `dkim_lost`, because a rotation that replaces one name with another is healthy.
## What to do with the answer
For your own sending domains, use the check to confirm that the key your platform told you to publish is actually there under the name it uses, and keep a domain monitor on the list so a key that is removed during a DNS migration is noticed the same day rather than when deliverability drops.
For a domain you are evaluating, treat a listed selector as a sign of a maintained mail setup and an empty list as an unknown, never as a fault. Combine it with the fields that do decide something: `has_spf` and `spf_all`, `dmarc_policy` and `dmarc_pct`, `mta_sts`, `parked` and `mx_blocklisted`.
## What to do next
- Run a domain through the [free DKIM checker](/tools/dkim-checker).
- Read `dkim_selectors` beside the SPF and DMARC details on the [domain intelligence endpoint](/docs/api/domain-intelligence).
- Watch a list of sending domains for a lost key with a [domain monitor](/docs/api/create-domain-monitor).
---
# Gate a signup form with the sending policy
One boolean per answer, decided by rules you set once: which signals block, how high the score may go, how sure a catch-all must be. How to use ok_to_send.
Date: 2026-09-13
Updated: 2026-10-08
A sending policy is the set of rules an account keeps about which email answers it will act on. Every verification ends with `ok_to_send`, the boolean those rules produce for that answer, and `blocked_by`, the name of the rule that decided a refusal. The score stays arithmetic and the verdict stays a verdict; the policy is where a decision is made, and it is made once, on the server, so a signup form, a batch job and a bulk run all branch the same way.
## Why a boolean, when the answer has 46 fields
Because the fields are facts and a form needs a decision. `risky` is a verdict that covers a burner and a catch-all at a well-run company, and those deserve opposite treatment on a signup form. A team that reads the fields ends up with an if-chain that grows with every new signal and drifts between the form, the import script and the campaign filter. A policy puts the chain in one place, applies it to every channel, and leaves `blocked_by` in the answer so the reasoning is visible.
Three rules make up a policy, all optional.
| Rule | What it does |
| --- | --- |
| `block_signals` | A list of risk-signal names. Any answer whose `risk_signals` carries one of them is blocked, whatever its score. |
| `max_risk_score` | A ceiling. An answer whose `risk_score` is above it is blocked. |
| `min_mailbox_confidence` | A floor for a mailbox the handshake could not confirm. An answer with a `mailbox_confidence` below it is blocked; a confirmed mailbox has no confidence figure and never fails this rule. |
They are applied in a fixed order after the verdict: an undeliverable verdict is blocked first and always, then the first blocked signal in the published weights' order, then the score ceiling, then the confidence floor. `blocked_by` names the first rule that fired: `undeliverable`, the signal's own name, `risk_score` or `mailbox_confidence`.
## The defaults
Until an account sets a policy, the published defaults apply. They block the five signals that are almost certainly wrong, `disposable`, `likely_typo`, `parked_domain`, `mx_blocklisted` and `no_reply`, and any score over 60, and they set no confidence floor. A role address at 30, a plus-tag at 5, a relay alias at 5 and a catch-all at 30 all pass by default; a gibberish username on a catch-all domain with no SPF, at 60, passes too, and at 70 with a plus-tag it would not.
The defaults are what the guest demo and the sandbox key answer with, because neither has an account to keep a policy: there `disposable@spaw.test` answers `ok_to_send: false` and `blocked_by: "disposable"`. The test addresses answer under the account's stored policy, like every other lookup a key makes, which makes them a way to try a policy before a customer meets it: `role@spaw.test` answers `ok_to_send: true` under the defaults and `blocked_by: "role"` on an account that blocks `role`.
```json
{
"email": "signup2847@mailinator.com",
"deliverable": "risky",
"reason": "disposable",
"risk_score": 90,
"ok_to_send": false,
"blocked_by": "disposable"
}
```
## Setting a policy
The policy belongs to the account and applies to every channel the account is billed for: the single and batch endpoints, bulk runs, monitors, the form widget and the dashboard. Read it with `GET /api/v1/email/policy`, change it with `PUT`, and restore the defaults with `DELETE`. All three are free.
```bash
curl -X PUT https://spaw.co/api/v1/email/policy \
-H "Authorization: Bearer sk_live_…" \
-H "Content-Type: application/json" \
-d '{"max_risk_score": 40, "block_signals": ["disposable", "likely_typo", "parked_domain", "mx_blocklisted", "no_reply", "recycled_mailbox"], "min_mailbox_confidence": 60}'
```
Send only the keys you are changing. A `null` clears the ceiling or the floor, an empty list blocks on no signal, and a body naming none of the three rules answers `422`, because that is the shape a misspelled key takes. The answer carries the policy in force, `is_default`, and when it was last changed. The Sending policy tab on the verification page edits the same three rules with checkboxes.
## What a signup form should block
A signup form is the strictest gate most products need, and also the one where a false refusal costs a customer. Three policies cover most cases.
**A consumer product that mails receipts and product updates.** Keep the defaults. They stop the addresses that would never receive anything and let everything human through, including role inboxes and relay aliases, which are real people's mail.
**A B2B trial with a free credit.** Add `role` to the blocked signals, since `info@` cannot start a trial, and lower the ceiling to 40 so a catch-all at a young domain with no SPF is held for a manual look rather than granted the credit. Keep relays allowed; a relay user is a person.
**A newsletter or a lead list.** Set a confidence floor of 60 so a catch-all address is only accepted when the domain looks administered and the username looks like a person, and add `recycled_mailbox` to the blocked signals so a mailbox that came back from the dead is held until the person re-engages. That floor also holds relay aliases on servers that accept every address, because their `mailbox_confidence` stays at 50; if your list welcomes them, keep the floor at 50.
None of these should block `unverified`. A handshake that timed out is a fact about the mail server's mood, not about the person, and a form that refuses an address because a server was greylisting will refuse real customers at random. Let the address in and, if the mailbox matters, verify it again later or send the confirmation mail and let the outcome decide.
## Using it in the form
The form widget verifies as the visitor types, with a publishable key and, if you turn it on, Cloudflare Turnstile in front of the lookup so bots cannot spend your credits. It never blocks a submit on its own; it fires a `spaw:result` event whose detail is the full answer, and the decision is one listener away.
```html
```
The browser endpoint answers the same fields under the account's policy, so the widget and your server agree without any duplicated logic. A server-side check on submit, with the secret key, is still worth doing, because a browser can be told to ignore its own JavaScript; the answer will be the same, and repeats inside seven days are free.
Say why. `blocked_by` exists so the form can tell the person what to change: a typo-squat gets the `did_you_mean` suggestion, a burner gets a request for a lasting address, a no-reply address gets a request for one that is read. A refusal with no reason reads as a broken form.
## What the policy does not do
It does not change the verdict, the score or any field; it adds two. It does not block a relay alias unless you tell it to, with the `relay` signal or with a confidence floor above 50, which holds every relay alias on a server that accepts every address; this guide's advice is that you should not. It cannot let an undeliverable answer through, because there is no mailbox to send to. And it does not know your product: a policy is a floor under your judgement, not a replacement for the segment-level decisions the fields support, such as keeping role addresses out of drip campaigns while accepting them at the door.
## What to do next
- Read the sending policy section of the [email verification docs](/docs/email-verification) for the contract and the decision order.
- Read yours with [`GET /api/v1/email/policy`](/docs/api/get-email-policy) and change it with [`PUT`](/docs/api/update-email-policy).
- Put the widget on the form through the [browser endpoint](/docs/api/verify-email-public) with a publishable key.
- See what each weight means in [how Spaw computes the risk score](/guides/how-spaw-computes-the-risk-score).
---
# MTA-STS, TLS-RPT and BIMI for senders
The three DNS records beyond SPF, DKIM and DMARC: what each promises, what receivers do with it, what Spaw reads, and the order to add them in.
Date: 2026-09-13
MTA-STS, TLS-RPT and BIMI are the three published policies a sending domain adds once SPF, DKIM and DMARC are in place. The first two are about the transport: MTA-STS tells other mail servers that mail to your domain must travel over authenticated TLS, and TLS-RPT tells them where to report when it could not. BIMI is about the inbox: it lets a receiver show your logo beside your messages, and it only works on a domain whose DMARC policy enforces. Spaw reads the DNS record for each and answers three booleans on the domain endpoint, beside the SPF and DMARC details that decide whether BIMI can apply at all.
## MTA-STS: no more opportunistic downgrade
SMTP between mail servers upgrades to TLS opportunistically, through the `STARTTLS` command, and an attacker who sits on the path can simply strip the offer, after which the mail travels in the clear. MTA-STS, defined in RFC 8461 (2018), lets a domain say that this must not happen.
The policy has two parts. A TXT record at `_mta-sts.example.com` carries `v=STSv1; id=…`, where the id changes whenever the policy does, and a policy file served over HTTPS at `https://mta-sts.example.com/.well-known/mta-sts.txt` names the mode (`enforce`, `testing` or `none`), the MX hostnames the policy covers, and how long a sender may cache it. A sending server that supports MTA-STS fetches the policy, and in enforce mode refuses to deliver to any MX not on the list or to any connection without a valid certificate for the MX name.
Spaw reads the TXT record and answers `mta_sts: true` when it carries `v=STSv1`. It does not fetch the policy file, so the field says "this domain has declared a policy", not "the policy is valid and enforcing". A domain monitor raises `mta_sts_lost` when the record disappears between runs.
The cost of getting it wrong is real mail refused, which is why the mode exists: publish in `testing` first, read the reports, then switch to `enforce`.
## TLS-RPT: the reports that make testing possible
TLS-RPT, RFC 8460 (2018), is the reporting channel for MTA-STS and for DANE. A TXT record at `_smtp._tls.example.com` with `v=TLSRPTv1; rua=mailto:…` tells sending servers where to send a daily report of connections to your domain that failed to meet the policy: certificate mismatches, expired certificates, STARTTLS that was offered but did not complete, MX hosts outside the policy.
Without it, an MTA-STS policy in testing mode is a promise nobody reports back on. Spaw answers `tls_rpt: true` when the record exists with `v=TLSRPTv1`; a domain with `mta_sts: true` and `tls_rpt: false` has skipped the step that would tell it whether enforcing is safe.
## BIMI: the logo, and what it requires
BIMI, Brand Indicators for Message Identification, is a draft specification maintained by the AuthIndicators Working Group rather than an RFC. A TXT record at `default._bimi.example.com` carries `v=BIMI1; l=…` with the URL of an SVG logo, and optionally `a=` with the URL of a certificate that proves the trademark.
Two requirements sit underneath it. The domain's DMARC policy must be at enforcement, quarantine or reject, and applied to all mail, so `pct` at 100 rather than a partial rollout, because a logo shown beside a message is only meaningful when unauthenticated mail from the domain is being rejected. And the large mailbox providers typically require a Verified Mark Certificate before they display the logo, which means a registered trademark and a paid certificate.
Spaw answers `bimi: true` when the record exists with `v=BIMI1`, and beside it the two fields that say whether the record can matter: `dmarc_policy` and `dmarc_pct`. A domain with a BIMI record and `dmarc_policy: "none"` has published a logo no receiver will show.
## The SPF and DMARC details that go with them
The same domain answer carries the details of the two older records that the newer ones lean on.
| Field | What it says |
| --- | --- |
| `spf_all` | The qualifier on the policy's `all` term: `-all` fails everything else, `~all` soft-fails it, `?all` says nothing, `+all` allows everyone, which is no policy at all. |
| `spf_lookup_count` | How many DNS-querying terms the policy needs once `include:` and `redirect=` are followed. RFC 7208 allows ten; a policy over that is a permanent error at every receiver, and it happens quietly when one more platform is added to an old record. |
| `dmarc_policy` | `none`, `quarantine` or `reject`. |
| `dmarc_pct` | The share of mail the policy applies to, 100 when the record omits it. |
| `dmarc_rua` | Whether an aggregate-report address is published, which is the only way to know what a policy is doing. |
`spf_lookup_count` is the one that surprises people. Each `include:` counts, and so does every mechanism inside the included record, so a domain that includes Google, Microsoft, a marketing platform, a support tool and a CRM can pass ten without anyone noticing, and from that day some receivers treat its SPF as broken.
## The order to add them in
Each record depends on the one before it, so the order is not a matter of taste.
1. **SPF with `-all` or `~all`, under ten lookups.** Every later record assumes mail from your domain can be authenticated.
2. **DKIM on every platform that sends as you.** Check the selectors are published under the names each platform uses; the [DKIM guide](/guides/dkim-selectors-explained) covers the names and the caveat.
3. **DMARC at `p=none` with `rua=`.** Read the aggregate reports for a few weeks; they show every source that sends as your domain, including the ones you forgot.
4. **DMARC at `quarantine`, then `reject`, then `pct=100`.** Move when the reports show only your own sources passing.
5. **TLS-RPT.** Publish the reporting address before any transport policy, so the testing phase has somewhere to report.
6. **MTA-STS in `testing`, then `enforce`.** Cover every MX host, keep the certificate on each valid for its MX name, and only enforce once the TLS reports are clean.
7. **BIMI**, if the brand has a trademark and the budget for a certificate. It needs step 4 complete.
## What Spaw does with the answer
Nothing on the per-address score. These are facts about a domain's sending posture, answered on the domain endpoint and the free checker, and read by the domain monitor, which flags a lost MTA-STS record and a lost DKIM key beside the SPF and DMARC changes. They describe how well a domain sends, which is the question a sender asks about itself and a security team asks about a partner, not whether a mailbox at the domain exists, which is the question a verification answers.
## What to do next
- Check a domain with the [free MTA-STS, TLS-RPT and BIMI checker](/tools/mta-sts-checker).
- Read all three beside the SPF and DMARC details on the [domain intelligence endpoint](/docs/api/domain-intelligence).
- Put your sending domains on a [domain monitor](/docs/api/create-domain-monitor) so a record that disappears is reported the day it does.
- Start from the beginning with the [SPF, DKIM and DMARC guide](/guides/spf-dkim-dmarc-for-developers).
---
# Reading a domain monitor alert
A domain monitor alerts when a domain goes dead, gets parked, lands on a blocklist, loses a policy or changes provider. Every status and flag, and what to do.
Date: 2026-09-13
A domain monitor is a saved list of up to 500 domains that Spaw re-checks daily, weekly or monthly through the same domain lookup the API sells, and reports on when something about a domain's mail setup changes for the worse. The alert names each domain and the reason, and the run's summary counts how many domains are fine, flagged, dead or invalid. This guide explains every status and flag, the rule that decides when an alert goes out, and what each one is asking you to do.
## What a run reads for each domain
Every run asks the domain endpoint the same question for every domain on the list, and keeps a compact entry per domain: its status, the flags that fired, whether it changed since the previous run, and the facts the next run will compare against. Those facts are the provider and its type, whether SPF is published and its `all` qualifier, the DMARC policy, the DKIM selectors found, MTA-STS, whether the domain is parked, whether its mail hosts sit on a threat blocklist, and the registration's expiry date.
Billing is the domain lookup's: one credit when a domain accepts mail, nothing for a dead or invalid domain, and repeats inside the seven-day window free. That last rule is why a daily cadence is allowed on domain lists and not on address lists. A run is DNS and a registry record with no mailbox handshake, and a domain checked daily is still billed at most once a week.
## The four statuses
| Status | Meaning |
| --- | --- |
| `ok` | The domain accepts mail and no flag fired. |
| `flagged` | The domain accepts mail and at least one flag fired. |
| `dead` | The domain has no usable mail servers. Mail sent there will not arrive. |
| `invalid` | The entry is not a hostname at all, typically a typo in the list. |
The statuses are ordered: `invalid` wins over `dead`, which wins over `flagged`, which wins over `ok`. A dead domain shows no flags, because the flags are read from a mail setup the domain no longer has.
## The ten flags
Five are standing flags, read from the domain as it is on the day of the run.
| Flag | It fires when |
| --- | --- |
| `parked` | The nameservers belong to a parking or for-sale service, and the domain still accepts mail. Whoever collects that mail is not the company you knew. |
| `blocklisted` | At least one mail host resolves into a network the Spamhaus DROP lists or the abuse.ch botnet list name. |
| `expiring` | The registration ends within the next 30 days, read from the registry's RDAP record. |
| `no_spf` | The domain publishes no SPF record. |
| `no_dmarc` | The domain publishes no DMARC record. |
Five compare the domain with what the previous run recorded, and can only fire from the second run on.
| Flag | It fires when |
| --- | --- |
| `provider_changed` | The mail provider read from the MX records is not the one recorded last time. |
| `spf_changed` | SPF appeared, disappeared, or changed its `all` qualifier. |
| `dmarc_changed` | The DMARC policy is not what it was: `reject` loosened to `none`, or a record removed. |
| `dkim_lost` | The previous run found DKIM selectors and this one found none of the common ones. |
| `mta_sts_lost` | The MTA-STS record was there and is gone. |
Every flag that fires is listed, in that order, so an entry can carry `["no_dmarc", "provider_changed", "dmarc_changed"]` after a migration that moved the domain to a new provider and dropped its DMARC record on the way.
## When an alert goes out
The first run only sets the baseline. It records every domain's entry and sends nothing, because there is nothing yet to compare against; a domain that is already parked on the first run is flagged in the results but not announced.
From the second run on, an alert email goes out for a domain that was `ok` and no longer is, that went dead or invalid from any other status, that fired one of the five comparison flags, or that gained a standing flag it did not have before. A domain that stays flagged for the same reason run after run is not announced again; the flag stays visible in the results, but the alert is for change.
A new domain added to the list behaves like a first run for itself: it is baselined, not announced.
The summary of each run counts `total`, `ok`, `flagged`, `dead`, `invalid`, `changed`, `newly_flagged` and `credits_used`, with `stopped_reason` set when the run could not finish: out of credits, a key's daily cap, or a run that died and is re-checked by the hourly sweep once 45 minutes have passed. `newly_flagged` is the number of domains the alert names.
The same run can be posted to a webhook as `{ "event": "domain_monitor.run", "monitor": …, "summary": …, "changes": [ … ] }`, signed like every other delivery, with `changes` naming the domains the email names. By default only runs that found something post; `every_run` posts each one, which doubles as a heartbeat.
## What each flag is asking you to do
**`dead`.** The domain has lost its mail servers. If it is one of your own sending domains, your DNS or your provider has changed under you and nothing addressed there arrives. If it is a customer's domain, every address at it is now undeliverable; a bulk run of those addresses will confirm it and add them to your suppression list for free.
**`parked`.** The domain has been given up and is sitting on a parking service, yet its mail servers still answer. Whatever you send there is collected by whoever runs the page. Stop sending to the domain's addresses; the per-address answers carry the same fact as the `parked_domain` reason.
**`blocklisted`.** The domain's mail now lives on a network the blocklists name. For a domain you own, that is a hosting problem to raise with the provider today. For a domain on your list, it is the strongest available sign that the addresses at it are not worth mailing.
**`expiring`.** Thirty days from the end of the registration. For your own domain, renew it. For a customer's, expect the domain to go dead or parked within the month if nobody acts, and treat mail to it as time-limited.
**`no_spf`, `no_dmarc`.** The domain sends mail without the records receivers use to authenticate it. For your own domains that is a deliverability problem to fix in the order the [MTA-STS, TLS-RPT and BIMI guide](/guides/mta-sts-tls-rpt-and-bimi-for-senders) describes. For a domain you mail, it is information about how carefully it is run, not a reason to stop.
**`provider_changed`.** The domain moved its mail. Often that is a planned migration, but it is also what a domain that changed hands looks like, and what a takeover looks like. Check the new provider in the results; a move from Google or Microsoft to `other` deserves a look.
**`spf_changed`, `dmarc_changed`.** A policy was loosened or removed. A DMARC record dropping from `reject` to `none` is the change attackers benefit from most, since mail spoofing the domain stops being rejected.
**`dkim_lost`, `mta_sts_lost`.** A key or a transport policy disappeared, typically during a DNS migration that copied some records and not others. Both are worth fixing the same day for a sending domain.
## Keeping the baseline
The baseline is the whole value of a monitor, and two things protect it. Editing the list through the dashboard or `PATCH` keeps what was recorded about every domain that stays, so a rename or a cadence change never resets the comparison; deleting a monitor and creating it again throws the baseline away and buys a full cycle of silence. And a run that stops early, out of credits or after a failure, carries the previous answers forward for the domains it never reached, so those still compare correctly on the next run.
A list nobody runs for a year is deleted with everything it stored; a list in use never is, because every run moves the clock.
## What to do next
- Read the domain monitor section of the [email verification docs](/docs/email-verification) for the API contract.
- Create a monitor for your sending domains with [`POST /api/v1/email/domain-monitors`](/docs/api/create-domain-monitor), or from the Domains tab.
- Look up any single domain with the [domain intelligence endpoint](/docs/api/domain-intelligence) before adding it to a list.
---
# Recycled spam traps, and the mailbox that came back
A mailbox that bounced for months and then accepts mail again is the shape of a recycled spam trap. What recycled_mailbox means, and what to do with one.
Date: 2026-09-13
A recycled spam trap is an email address that once belonged to a person, was abandoned, bounced as unknown for a long time, and was then quietly reactivated by the mailbox provider or a blocklist operator as a trap. It receives mail again, but nobody reads it, and every message that arrives is evidence that the sender is mailing a list nobody has cleaned. Spaw reports the same shape from its own evidence: a mailbox that answered "no such mailbox" and later answers "exists" is returned as `risky` with the reason `recycled_mailbox`.
## What is a spam trap?
A spam trap is an address whose only purpose is to catch senders who should not have it. There are two kinds, and they catch different mistakes.
A pristine trap has never belonged to anyone. It is seeded on web pages, in code comments and in places only a scraper would look, so any mail to it proves the sender harvested addresses rather than collected them. A verifier cannot see a pristine trap: it is a real mailbox at a real domain, it accepts mail, and nothing in DNS or the handshake distinguishes it from a colleague's address.
A recycled trap did belong to someone. The mailbox was abandoned, the provider closed it, and for a period, typically a year or more, it rejected every message with a permanent error. Then the provider turned it back on. A sender who still has it on a list did not act on the bounces, and the trap records exactly that. M3AAWG's Sender Best Common Practices (version 3, 2015) describes the mechanism and the remedy in the same breath: process every bounce, and stop mailing an address after it has hard-bounced.
The recycled kind is the one a verifier can see coming, because the death of the mailbox left a trace.
## How does Spaw remember a dead mailbox?
The mailbox handshake ends with the receiving server's answer at `RCPT TO`. A definitive rejection, typically `550 5.1.1` in the terms of RFC 5321 and RFC 3463, is the answer Spaw reports as `undeliverable` with the reason `mailbox_not_found`. Since September 2026 that answer is also remembered: the date it was seen is kept for a year under a keyed hash of the address. The address itself is not stored with it, and the memory is erased along with everything else when an address is erased on request.
A bounce you report feeds the same memory. When a delivery outcome arrives through the feedback endpoint or one of the provider webhooks with a reason that reads as no such mailbox, the enhanced code `5.1.1` or the wording that mail servers use for it, the date of that bounce is recorded the same way. So a list that bounced through your own sending provider months ago is already known to Spaw as dead, whether or not anyone verified it at the time.
When a later handshake says the mailbox exists, the two facts are compared. The answer carries `mailbox_exists: true`, because it is true, and beside it `previously_invalid: true` with `previously_invalid_at` naming the date of the rejection. The verdict is `risky`, the reason is `recycled_mailbox`, and the weight is 30, the medium band on its own.
```json
{
"email": "former.employee@acme.com",
"deliverable": "risky",
"reason": "recycled_mailbox",
"risk_score": 30,
"risk_level": "medium",
"mailbox_exists": true,
"previously_invalid": true,
"previously_invalid_at": "2026-03-02",
"risk_signals": [{ "signal": "recycled_mailbox", "weight": 30, "dataset": "mailbox-history" }]
}
```
The field is tri-state. `previously_invalid` is `false` for a confirmed mailbox with no such history, which is the ordinary case, and `null` whenever the mailbox was not confirmed, because there is nothing to compare a rejection against.
## What the signal does and does not mean
It means the mailbox was rejected once and accepts now. That is the whole claim, and it has innocent explanations: a person left a company and came back, an alias was removed and recreated, a mailbox was suspended for non-payment and restored. It also has the explanation a sender should fear, which is that the address was turned into a trap. Spaw cannot tell those apart from the outside, and says so by keeping the verdict at `risky` rather than `undeliverable`.
It does not mean the address is on any blocklist operator's trap network. Trap lists are secret by design, nobody sells a legitimate one, and a verifier that claims to detect traps directly is either guessing from the same shape Spaw reports or selling a list that would stop being a trap list the moment it was sold.
The memory also decides nothing about the domain. A recycled mailbox at a domain says nothing about the mailbox next to it; the signal is per address.
## What to do with a recycled mailbox
Treat it as you would treat a catch-all address: a real mailbox that you should not mail from a list-cleaning run.
Hold it out of any campaign that is sending to a list rather than to a person who just asked for something. A trap does damage in proportion to the volume that reaches it, and a cleaning run is precisely the moment a recycled trap is designed to catch.
Let the person re-engage first. If the address belongs to a returning customer, they will sign in, reply, or submit a form, and that action is the evidence the bounce history cannot give you. Once it happens, verify the address again: a fresh single lookup runs the handshake anew, and if you report the delivery of a transactional message through the feedback endpoint, later lookups of the address answer from your own evidence (`smtp_reason: "delivered_recently"`).
Fix the list hygiene that let it happen. A recycled trap only catches senders who ignored a hard bounce, so the durable fix is to process bounces as they come: point your sending provider's bounce webhook at Spaw, and every hard bounce joins your suppression list without a credit spent, while the verdict-level feedback improves the answers for everyone.
The sending policy can enforce the hold for you. Adding `recycled_mailbox` to the `block_signals` list makes every answer carrying it come back with `ok_to_send: false` and `blocked_by: "recycled_mailbox"`, so a form or a campaign filter branches on one boolean instead of reading the history fields.
## How this interacts with the rest of the answer
Precedence decides the reason when several apply. Undeliverable reasons win over everything; among risky ones a typo-squat, a parked domain, a disposable domain, a blocklisted mail network, an implicit MX, a full mailbox, a no-reply address and a role address all rank ahead of `recycled_mailbox`, and it in turn ranks ahead of `catch_all` and `unverified`. Whatever the reason names, every flag that fired is in `risk_signals` with its weight, so a recycled role mailbox scores 60 even though `reason` says `role`.
The memory is read only when a handshake confirmed the mailbox. A catch-all domain never confirms one, so a recycled address at a catch-all domain keeps its `catch_all` reason and its `mailbox_confidence` estimate, and `previously_invalid` stays `null`.
Repeats inside the seven-day window answer from the earlier answer, so the flag does not flicker between calls. A `refresh: true` lookup runs a fresh handshake and re-reads the memory.
## Where the numbers come from
Two things are worth knowing before you build a policy on this signal. First, the memory starts from the day it was introduced, and from the bounces already on file; an address that died two years ago and revived last year carries no history here, because nobody told Spaw about the death. Second, the pooled measurement on the status page will, once enough outcomes have been reported, show the share of sends to `recycled_mailbox` answers that bounced or were delivered, under the same floors as every other published figure. Until then the signal is a well-founded shape, not a measured rate, and the docs say so.
## What to do next
- Read the [`recycled_mailbox` reason page](/docs/reasons/recycled_mailbox) for the exact contract and precedence.
- Wire your bounce webhook into the [feedback endpoint](/docs/api/report-delivery-feedback) so hard bounces reach the memory and your suppression list on the day they happen.
- Add the signal to your [sending policy](/docs/api/update-email-policy) if a hold should be automatic.
- Work through the [email list cleaning checklist](/guides/email-list-cleaning-checklist) before the next campaign.
---
# Relay addresses are not burners
Firefox Relay, SimpleLogin, Addy.io and DuckDuckGo aliases sit on disposable lists but deliver to a real mailbox. What Spaw answers, and how to treat them.
Date: 2026-09-13
Updated: 2026-10-08
A relay address is an alias handed out by a forwarding service such as Firefox Relay, SimpleLogin, Addy.io or DuckDuckGo Email Protection. Mail sent to the alias is forwarded to the person's real mailbox, which the alias hides, and the person can turn the alias off if it starts receiving mail they did not ask for. It is the opposite of a burner: a burner exists for ten minutes so its owner can get past a form, a relay alias is kept for years so its owner can keep using your product without giving out their real address.
## Why relays end up on disposable lists
The open disposable-domain lists that every verifier uses, Spaw included, collect domains that hand out addresses on demand. A relay service does exactly that. In August 2026 the upstream list carried `relay.firefox.com`, `addy.io` and `33mail.com` next to the ten-minute mailbox providers, and on 7 October 2026 it carried `mailer.me`, one of Addy.io's alias domains. A list maintained by volunteers answers the marketer's question, "is this a throwaway?", and not the question a signup form is asking, which is "will this person still read this in a year?"
Until September 2026 Spaw answered those domains from the list and reported them as disposable, with the 80-point weight and the `disposable` reason that a burner earns. That was wrong in a way that hurt exactly the users a privacy-conscious product wants: the person who reads the privacy policy, takes it seriously, and uses an alias so that a breach at one company cannot follow them to the next.
## What Spaw answers for a relay address
The relay check now runs before the disposable list, and a relay is never disposable. A Firefox Relay alias typically answers like this:
```json
{
"email": "quiet.fox.42@mozmail.com",
"deliverable": "risky",
"reason": "catch_all",
"risk_score": 35,
"risk_level": "medium",
"disposable": false,
"is_relay": true,
"relay_service": "Firefox Relay",
"domain_category": "relay",
"mailbox_exists": null,
"catch_all": true,
"mailbox_confidence": 50,
"risk_signals": [
{ "signal": "catch_all", "weight": 30, "dataset": "mailbox-smtp" },
{ "signal": "relay", "weight": 5, "dataset": "forwarding-services" }
]
}
```
Four fields carry the relay answer. `is_relay` is `true`, `relay_service` names the service, `domain_category` is `relay`, and `disposable` is forced to `false` even when the domain sits on the public list. The mailbox handshake still runs, as it does for any domain that is not a burner. Firefox Relay receives its mail on Amazon SES and decides whether an alias exists only after it has accepted the message, so the handshake is accepted for any alias, live or made up. That is why the answer is `risky` with `reason: catch_all`, `catch_all: true` and `mailbox_exists: null`: no handshake can confirm one alias on a server that accepts them all. `mailbox_confidence` stays at 50 for the same reason. The domain's DMARC, SPF and age describe the relay operator, and an alias's name, whether the relay generated it or its owner chose it, says nothing about whether it exists, so only evidence against the alias lowers it: most reported sends to the domain bouncing, or a plus-tag. Where a relay's servers refuse an unknown alias during the handshake, a live alias can answer `deliverable` with `mailbox_exists: true`.
The relay itself adds 5 to the score, for the withheld identity; the other 30 in this answer is the catch-all weight. Five points is a statement of fact rather than a penalty: you do not know who the person is, and if a downstream fraud check depends on matching the address to a name, it will not match. It is deliberately far from the disposable weight, and it never moves the address out of the low band on its own.
The bundled list of relay services is curated by hand and carries the well-known forwarding services with their per-account subdomains, so an alias at a subdomain a service hands out to one user is matched by the service's root. A service Spaw does not know reads as whatever the disposable list says about it, which is why the list is worth extending when a customer names one.
Since 7 October 2026 the list also carries Brave Email Aliases (`bravealias.com`), the alias feature built into the Brave browser, and ExpressMailGuard (`clivaro.io`), ExpressVPN's alias service, along with newer alias domains of Addy.io (`mailer.me` and `addymail.com`) and SimpleLogin (`silomails.com` and `slmails.com`). Since 8 October it also carries Surfshark Alternative ID's alias domains, IronVest Masked Email (`opayq.com`, with Abine's older Blur and MaskMe domains), Erine.email, Alias Email (`bumpmail.io`) and Guard Email (`m.guardemail.com`), and more shared domains of Addy.io (`4wrd.cc`, `addy.to`) and 33Mail (`33m.co`). Cloaked's identity domains are not on it: a Cloaked identity keeps its mail in Cloaked's own inbox unless its owner turns forwarding on. Brave receives its alias mail on Amazon SES too and accepts every address at `bravealias.com`, so a Brave alias typically answers like the Firefox Relay example above: `risky`, `reason: catch_all`, a score of 35.
## Mailbox providers that also hand out aliases
The list holds only domains where every address is an alias. A mailbox provider that also offers aliases stays off it, because a domain-level flag would mislabel every real mailbox on that domain. Apple and Proton are the two that matter:
- **Proton Pass** hands out aliases on its own domains, `passmail.net`, `passmail.com`, `passinbox.com` and `passfwd.com`. Spaw flags those and `pass2mail.com`, which Proton registered for Pass too, so they answer `is_relay: true` with `relay_service: "Proton Pass"`. Proton Mail's mailbox domains (proton.me, protonmail.com, protonmail.ch, pm.me) answer `is_relay: false`, `free_provider: true` and `domain_category: "webmail"`.
- **Sign in with Apple** gives an app a random address when the user hides their own, and Apple forwards only mail from the domains and addresses the app registered with Apple. Those addresses have been issued on `privaterelay.appleid.com`, and Apple said in August 2026 that new ones will be issued on `private.icloud.com` from later that year, while existing ones keep working. Both domains answer `is_relay: true` with `relay_service: "Sign in with Apple private relay"`. The flag belongs to the name `private.icloud.com` alone, so it never reaches icloud.com.
- **Hide My Email in iCloud+** puts its aliases on icloud.com, next to ordinary iCloud mailboxes, and nothing in the address marks one. Apple's August 2026 announcement keeps them there. Spaw cannot tell them apart, so every icloud.com, me.com and mac.com address answers `is_relay: false` and `domain_category: "webmail"`, alias or not.
Until 2 October 2026 the list carried icloud.com, me.com, mac.com and Proton Mail's four domains, so every ordinary iCloud and Proton Mail address answered as a relay and carried the relay weight. They now answer as the free mailboxes they are.
## Why a relay deserves to be let in
Three practical reasons, all about outcomes.
**The mailbox is real and read.** A relay forwards to a mailbox the person checks daily. Bounce rates on relay addresses are ordinary, and engagement is ordinary, because the person asked for the mail.
**The person is telling you something.** Someone who used an alias for your form is someone who intends to keep the account and cares about their data. Refusing them at the door selects against your most careful users. Firefox Relay, SimpleLogin and DuckDuckGo all publish the same explanation of the product: the alias exists so that the person can keep using services without exposing the real address, and can block a sender that misbehaves.
**Blocking them does not stop abuse.** A fraud ring does not need a relay; it has burner domains, which Spaw does report as disposable, and it has the same address at many providers, which the `widely_seen` signal catches when three or more accounts see it in a day. The relay flag is a fact about privacy, not about intent.
## When the withheld identity matters
There are contexts where the alias is a genuine limitation, and the field exists so you can handle them without treating the person as a burner.
| Context | Treatment |
| --- | --- |
| Newsletter, product updates, account mail | Accept. Nothing changes. |
| Signup with a free trial or credit | Accept, and count the trial against the account rather than the address; a relay user can make aliases as easily as anyone can make Gmail accounts. |
| Identity or age checks that match the address to a name | Treat `is_relay: true` as "no match possible" and fall back to the check you would use for any unmatched address. |
| Abuse investigations | Read `relay_service`: the service's own abuse process can reach the person, which is more than a burner offers. |
| Deduplication | Do not merge on the relay domain; every alias is a different person's choice, and one person may hold several. |
If your policy has to treat aliases differently, the sending policy does it without code. The `relay` signal can be added to `block_signals`, and every relay answer then carries `ok_to_send: false` with `blocked_by: "relay"`. The default policy does not block it, and this guide's advice is that it should not. A confidence floor above 50 holds relay aliases too, on any server that accepts every address, because their `mailbox_confidence` stays at 50; keep a floor at 50 or below if relays must pass.
## How this differs from a plus-tag alias
A plus-tag (`mia+shop@acme.com`) is a label on a mailbox you can already see. Spaw folds it away in `normalized_email`, reports `is_alias: true`, and adds 5 points for it, because a plus-tag is the favourite tool of people farming signup bonuses. A relay alias is the reverse: you cannot see the mailbox at all. The two look alike on a list, since both are one person under several addresses, but the plus-tag hides nothing and the relay hides everything, and the two fields let you tell them apart.
Gmail's dot-insensitivity is a third thing again: `m.ia@gmail.com` and `mia@gmail.com` are one mailbox, and Spaw folds the dots away in `normalized_email` too, with no weight at all.
## What to do next
- Read the [relay address](/glossary/relay-address) definition and the [disposable email domains guide](/guides/disposable-email-domains) for where the two lists come from.
- Check a domain's category, relay service and disposable flag with the [domain intelligence endpoint](/docs/api/domain-intelligence).
- If your form has been rejecting relay users, look at the [free email checker](/tools/email-checker) with one of their addresses and update the rule that blocked them.
---
# What email verification keeps under GDPR
An email address is personal data. What Spaw stores about one and for how long, what is read across accounts, who sees it, and how it is erased.
Date: 2026-09-13
An email address identifies a person, so under the General Data Protection Regulation it is personal data whatever else is known about its owner, and a verification is processing of it. This guide sets out what that processing consists of when the address goes through Spaw: what is stored, for how long, what is derived from it and read across accounts, who else sees it, and how it is erased. Every figure here is the one the code keeps, and the [privacy policy](/privacy) publishes the same table with the tests that pin it.
## Who is the controller of the addresses you verify
You are. You collected the address, you decide why it is being verified, and Spaw processes it on your behalf to answer your query. That makes you the controller and Spaw the processor for the verification itself, in the terms of Article 4 of the regulation, and the written terms Article 28 requires between the two are the standing data processing addendum on the [trust page](/trust), which applies to every account without a signature.
Two things Spaw does with an address are its own processing rather than yours, and the privacy policy says so: the pooled readings described below, which are counts derived from many customers' lookups and reported to all of them. For those Spaw is the controller, on the legitimate-interest basis of keeping the verdicts accurate, and they are designed so that nothing about an identifiable person leaves them.
## What a verification stores
A single email lookup writes one history row for your account: the address, the verdict, the reasons and score behind it, the signals that fired, and the request id the answer was returned under. It is your own verification history, the CSV export is these rows, and it is deleted after 30 days by a nightly prune, or earlier when you delete your account or the address is erased on request.
Beside the row, a lookup leaves a repeat marker: a hash of the input under your account's own key, with the time it was written, so a repeat inside seven days is free. It is never the input, and it cannot be turned back into the address.
Everything else is something you asked Spaw to keep, and each has its own window.
| What | Window | What ends it |
| --- | --- | --- |
| A bulk run: the list you uploaded and the result file written from it | 30 days | The nightly prune, which deletes the run's whole directory; deleting your account deletes the files explicitly |
| A bulk draft, parsed and not yet started | 24 hours | Starting it, discarding it, or the sweep |
| A monitored list: the addresses you saved and the last run's answer for each | 365 days after its last run | Deleting the monitor; a list still being run is never pruned |
| A delivery outcome you reported: the address as written, what happened, when | 180 days | The nightly prune, deleting your account, or an erasure confirmed from that address |
| A suppression entry: the address and the verdict that earned it | Until you remove it | You, deleting your account, or an erasure. One a verdict or a reported bounce added also leaves when a later verdict or a reported delivery clears it; an import or a spam complaint leaves only when you remove it, and an erasure keeps both. An entry a verdict or a reported bounce added is re-verified rather than trusted once it is older than 90 days; an import or a complaint never goes stale |
| The request log: which endpoint was called and what it answered, never the value looked up | 30 days | The nightly prune |
Nothing is ever sent to an address you verify. The mailbox handshake is an SMTP conversation that ends before any message data is transmitted, and the partner that performs it sees only an address that reached the mailbox step.
## What is read across accounts
Four readings cross accounts, and each is a count or a date under a keyed hash, never the address and never who.
**How many other accounts looked an address up inside a day.** The `widely_seen` field says whether three or more did. It is kept for that day as a keyed hash of the address with the account ids that saw it and when, then discarded.
**Whether a mail server once said the mailbox did not exist.** A definitive "no such mailbox" answer is remembered for a year under a keyed hash, so a mailbox that exists again is reported as `previously_invalid`. A bounce you report with that reason feeds the same memory.
**Two figures per domain, never per address.** The share of reported sends to a domain that bounced, counted only once the domain has five or more outcomes, and the naming convention the domain's confirmed deliveries follow, tallied as pattern shapes such as `first.last`. Both feed the confidence estimate for catch-all mailboxes, and both are read as counts.
Recital 26 of the regulation is the relevant line: pseudonymised data that can still be attributed to a person by using additional information remains personal data, while data rendered anonymous is out of scope. Spaw treats the keyed hashes as the former, which is why they are erased with the address rather than kept as if they were nothing.
## Who else sees the address
The trust page publishes the subprocessors by role and region: application hosting and the database, content delivery and bot protection, transactional email, payments, the mailbox verification partner, and the public registry lookup that an opt-in request makes. For a verification the partner sees the address that reached the mailbox step and nothing else about you or your customer. Domain-level checks such as SPF, DMARC, DKIM, MTA-STS and the registration record are DNS and registry reads and carry no address at all.
Spaw never sells submitted data and never contacts the addresses you verify.
## How an address is erased
Article 17 gives the person the right to erasure, and a verification service has to be able to honour it for an address the person never gave it. Two routes exist.
The self-serve route is the form at [/privacy/data-request](/privacy/data-request). It takes an email address only, mails a confirmation link to it, and erases nothing until the link is opened, so a mail gateway that prefetches links cannot trigger a deletion and nobody can erase an address they do not control. The pending request lives in the cache for the 60 minutes the link is valid, a second marker remembers for 15 minutes that a link has gone to that address so asking twice does not send two mails, and the record kept afterwards carries a salted digest of what was erased rather than the address itself.
The other route is support. An operator can run the same erasure for an address, and it is the only route for a phone number or a postal address, where a person checks first.
What the erasure removes, for an email address: the history rows, the reported outcomes and the suppression entries a verdict or a reported bounce added, across every account that held them, matched on the address as typed and on its normalised form, plus the derived memories, the shared handshake answer, the sightings hash, the not-found date and the domain-level counts the address contributed to. What it deliberately leaves alone: an entry a customer imported into their own suppression list, and a customer's record that the address complained about its mail, because deleting either would put the address back in front of the next campaign; a customer's monitored list and uploaded bulk file, which are their records under their own retention; the seven-day repeat marker, a hash and a timestamp that expires by itself; and the one-way hash of an address that claimed the new-account credit bonus, which exists so the bonus cannot be claimed twice. The erasure page names all five beside the form.
## What this means for your own compliance
Your records of processing can describe the verification precisely: an address is sent to a processor, answered, and held by that processor in your own history for 30 days, in a bulk file for 30 days if you uploaded a list, and in a monitor for as long as you keep the monitor. Your privacy notice can tell your users that addresses are verified without any message being sent. And your erasure procedure can point users at the self-serve form for the part of their data that sits with Spaw, or run the deletion through your own account: deleting a history row, a suppression entry or a monitor entry is available to you in the dashboard and the API.
The [methodology page](/methodology) states the same facts in the product's own terms, and the [trust page](/trust) carries the security controls and the subprocessor list.
## What to do next
- Read the retention table in the [privacy policy](/privacy); every window in it is pinned by a test to the code that deletes.
- Send a person who asks about their address to the [erasure form](/privacy/data-request).
- Read [how the product works](/methodology) for what each check reads and what it never claims.
---
# How Tor exit rotation defeats a static blocklist
Exits join and leave the Tor list through the day, so a copied list is wrong both ways. What the hourly sync, the thirty-day history and recently_tor do.
Date: 2026-09-12
The Tor exit list is one of the few network facts that is exact rather than inferred: the Tor Project publishes which relays allow traffic to leave the network, and a request from one of them arrived through Tor. It is also a list that changes through the day, and most of the blocking built on it treats it as if it did not. This guide is about that gap and what Spaw does with it.
## What the list is
A Tor circuit ends at an exit relay, the machine that makes the request to the destination on the user's behalf. Which relays are exits depends on each operator's exit policy, and the set changes as relays come online, go offline, change policy or are reconfigured. The Tor Project publishes two views of it. The bulk exit list at https://check.torproject.org/torbulkexitlist is the plain list of exit addresses, regenerated many times an hour. Onionoo, the project's relay-details API at https://onionoo.torproject.org/, describes every relay with its addresses, its flags and, usefully, the time it was last seen — and it covers IPv6, which the bulk list does not.
## How rotation breaks a static copy
A blocklist is only as current as its last download, and Tor exits rotate faster than most download schedules. Two failures follow, in opposite directions.
The first is the miss. A relay that became an exit this morning is not in a file copied last week, so the traffic it carries is not recognised as Tor at all. The list is right; the copy is stale.
The second is the false positive, and it is the one that hurts real people. A relay stops being an exit — its operator changes the policy, the machine is decommissioned, a volunteer's home connection is reassigned — and the address goes back to ordinary use. A copy that still lists it keeps blocking whoever holds it next: a broadband customer whose ISP handed them the address a week after the relay left. Weeks-old copies are common, and every one of them is blocking addresses that are no longer exits.
There is a subtler version too. An address that left the list yesterday is very likely still the same machine, run by the same operator, and treating it as clean the moment it drops off is as wrong as treating it as an exit forever. The truth is in between, and a single boolean cannot say it.
## What Spaw does
Three things, each aimed at one of those failures.
**The list is synced hourly.** Both Tor feeds — the bulk exit list and the Onionoo relay data — are small files, and the service re-downloads and recompiles them every hour, alongside the daily run that refreshes everything else. `is_tor` answers from the current copy, so a miss is at most an hour old, and a false answer is a checked negative: "not on the list as of this hour".
**A thirty-day history is kept.** After every Tor sync the service rebuilds an exit history from both lists (today's count and sync date are on the [exit list's source page](/tools/ip-lookup/sources/tor-exits)): every address either list has named in the last thirty days, with the date it was last seen exiting — Onionoo's own last-seen time where it gives one, the sync date otherwise. The history is compiled into a feed of its own and consulted for every public address. On one sync on 12 September 2026 it held 2,257 exits.
**Recency is a signal with its own weight.** `tor_last_seen` answers the date: the current list's date while the address is on it, else the history's date, else null. And `recently_tor` is a separate risk signal, at +40, for an address the history saw exiting within the last seven days that is not on the current list. A live exit scores +70; a machine that was an exit this week scores 40; one that left the list three weeks ago scores nothing and still carries its date, so the reader can see it. The two signals never stack: while an address is on the current list, `recently_tor` is false, because the +70 already counts it.
Seven days and forty points are choices, and they are published so they can be recomputed or ignored. A week is about how long an exit's address plausibly stays with the same machine; forty is the weight of a strong hint rather than a fact.
## What to do with it
- **Block nothing on `is_tor` alone.** Tor is used by people who need it. The [VPN and relay policy guide](/guides/a-vpn-and-relay-policy-that-does-not-punish-real-people) argues for a step-up — a confirmed email, a second factor — rather than a refusal, and the same applies here.
- **Read the date.** `tor_last_seen` two days ago on an address that is not an exit now is the same machine; two months ago is an address that has moved on. Branch on the date rather than on the flag when the flag is false.
- **Watch your own addresses.** A monitored list re-checks the addresses you care about on a cadence and alerts you the day one becomes an exit — the case where a volunteer's relay on your network gets your mail server blocked elsewhere.
- **Never keep your own copy.** If the list you block on is downloaded weekly, it is wrong in both directions by Wednesday. Ask per request, or if you must cache, cache the answer with its date and expire it within the hour.
## Sources
- The Tor Project's bulk exit list: https://check.torproject.org/torbulkexitlist
- Onionoo, the Tor relay details API: https://onionoo.torproject.org/
- The history size above is one sync on 12 September 2026 on one deployment; the current figures for every feed, with their dates, are on the [status page](/status).
The [IP intelligence guide](/docs/ip-intelligence) documents `is_tor`, `tor_last_seen` and the weights in full.
---
# How accurate is free IP geolocation?
Country holds, city does not: what the weekly measurement against operator-declared locations found, how location_confidence grades it, and what to build on.
Date: 2026-09-12
Updated: 2026-10-02
Every IP geolocation answer is a claim about a network, and the honest question is not "is it right" but "right at what level, and how would anyone know". This guide gives the numbers Spaw measures every week, explains what they do and do not mean, and says which fields to build a decision on.
## Where a location comes from
Two kinds of source answer the `country`, `region`, `city`, `latitude` and `longitude` fields.
The first is a database: DB-IP Lite, a monthly release that maps every prefix to a place, built by the publisher from routing data, registry records, measurements and whatever else its method uses. It is the answer for most addresses, and `location_source` reads `database` when it is.
The second is the operator's own declaration. Some networks publish where their prefixes are: a geofeed in the format RFC 8805 fixed in 2020, which the registries let an operator link from its registration record; Apple's list of iCloud Private Relay egress addresses with the city each stands for; the server lists the VPN providers publish; the RFC 8805 files DigitalOcean, Linode and Vultr publish for their clouds. Where one of these covers an address, its declaration wins over the database and `location_source` reads `operator`. Once a week the service discovers every geofeed the five registries point at and compiles them into one feed.
## What is measured, and how
Every Sunday a measurement samples addresses from the operator-declared ranges and asks the database the same question: does the database put this address in the country the operator declared, and in the city? The result is published on the [status page](/status) and on the product page, dated, and it grades the `location_confidence` field for the following week.
It is important to be precise about what that measures. It is agreement between the database and an operator's statement, not agreement with ground truth. An operator's file says where a prefix is deployed, as the operator understands it; it is the best public evidence there is, and it is still a statement. The sample is also skewed by construction: only networks that publish declarations are in it, which means clouds, relay egress and VPN exits are over-represented and ordinary consumer ISPs, most of which publish nothing, are under-represented. Where a country's sample is dominated by CDN and relay ranges whose declared city the database does not know, the grade is harsher than a home connection in that country deserves. The measurement says so rather than hiding it.
## The numbers
Measured on 5 September 2026, over the geofeed ranges:
- Country agreement: 84.6 %.
- City agreement: 28.6 %, on an exact city-name match, which undercounts — "Frankfurt am Main" and "Frankfurt" are one city and count as a miss.
- Per-country city agreement, for the countries with enough compared samples: United States 69.9 %, United Kingdom 65.8 %, France 61.4 %, Netherlands 46.6 %, Germany 8.4 %, Japan 9.5 %. The current figure for every measured country is on its [country page](/tools/ip-lookup/countries).
The gap between the first two lines is the whole story of free geolocation. Country is the level that holds. City is approximate everywhere, sometimes to the point of being wrong more often than right, and the German and Japanese figures are the reason the product page says city-level precision from free data should never carry a compliance or identity decision on its own.
## How location_confidence reads it
`location_confidence` turns the measurement into one word per answer. An operator-declared location is `high`, because the network said so. A database-located address is graded by its country's measured city agreement: 75 % and above is `high`, 50 % and above `medium`, anything below `low`. A country with fewer than 20 compared samples, or a deployment where the measurement has not run yet, answers null rather than a guess.
One rule comes before both sources. When `is_anycast`, `is_mobile` or `is_satellite` is true, the grade is `low` whoever placed the address. An anycast range such as 1.1.1.1 is announced from hundreds of sites, so any city given for it is one instance among many. A mobile carrier puts its subscribers behind carrier-grade NAT at a gateway, and a satellite operator lands them at a ground station; an operator's geofeed for such a range truthfully names the gateway's city, and the people behind it can be hundreds of kilometres away.
Two more rules protect the coordinates. When an operator's declaration disagrees with the database on the country or the city, `region`, `latitude` and `longitude` answer null instead of a point in the wrong place — a coordinate for the wrong city is worse than none. And where `is_anycast`, `is_datacenter`, `is_relay` or `is_vpn` is true, the place in the answer is the operator's infrastructure, not anyone's home; the consistency endpoint withholds its country comparisons for exactly those addresses.
## Time zones, and the border rule
`timezone` is settled first by a curated map: a country that keeps one clock, or a region known to lie in one zone, answers directly and `timezone_source` reads `country` or `region`. Split countries and regions — the United States, Canada, Australia, Brazil, Russia, Indonesia — used to answer null. Since September 2026 the nearest listed populated place settles them, from the GeoNames list of places with over 15,000 inhabitants, and `timezone_source` reads `nearest_city`.
The rule that keeps that honest is the border: the nearest place is searched inside the located country only, and within 250 km of the coordinates. A point in El Paso is nearer Ciudad Juárez than the centre of El Paso, and the answer must still be America/Denver, not Mexico's zone; an address on an island far from any listed place answers null rather than the nearest mainland zone.
## What to build on
- **Policy by country, defaults by city.** Consent banners, currency and language belong on `country`, `is_eu` and `is_eea`. City is good enough to pre-fill a form and not good enough to refuse anything.
- **Read `location_confidence` before `city`.** A `low` grade is the measurement telling you the city is a coin toss for that country this week.
- **Prefer the operator's word.** `location_source: operator` means a network published the place itself; treat it as the best answer available, and remember that for a VPN exit, a relay or a mobile gateway it locates the exit, not the person.
- **Never locate a person.** An address belongs to a network. The most a free database can honestly say is which country a network operator serves, and the measurement above is what that claim rests on.
The [IP intelligence guide](/docs/ip-intelligence) documents every location field, and the [status page](/status) carries the current week's measurement with its date.
---
# How to verify Googlebot and GPTBot
Anyone can send a Googlebot user agent. The two proofs that hold, the published ranges and forward-confirmed reverse DNS, and what is_crawler answers.
Date: 2026-09-12
The user agent string is free text. A scraper that wants to be let through says it is Googlebot, and a log that trusts the string will believe it. Two proofs do hold, both published by the crawler operators themselves, and one IP lookup applies both. This guide is about what they prove, what they do not, and how to read the three fields that carry the answer.
## Proof one: the operator's published ranges
The large crawler operators publish the IP ranges their crawlers fetch from, precisely so that sites can verify them. The files are public, machine-readable and refreshed by the operators as their fleets change:
- Google: `https://developers.google.com/static/crawling/ipranges/common-crawlers.json` for Googlebot, plus separate files for its special crawlers and its user-triggered fetchers.
- Microsoft: `https://www.bing.com/toolbox/bingbot.json` for Bingbot.
- OpenAI: `https://openai.com/gptbot.json` for GPTBot, `https://openai.com/chatgpt-user.json` for ChatGPT-User and `https://openai.com/searchbot.json` for OAI-SearchBot.
- Perplexity: `https://www.perplexity.ai/perplexitybot.json` and `https://www.perplexity.ai/perplexity-user.json`.
- DuckDuckGo: `https://duckduckgo.com/duckduckbot.json`.
- Ahrefs: `https://api.ahrefs.com/v3/public/crawler-ip-ranges`.
Spaw syncs every one of these daily and compiles them into one feed. An address inside a published range answers `is_crawler: true`, `crawler` names the operator's crawler (Googlebot, Bingbot, GPTBot, ChatGPT-User, OAI-SearchBot, PerplexityBot, Perplexity-User, DuckDuckBot, AhrefsBot, or Google's special crawlers and user-triggered fetchers), and `crawler_kind` says what it does: `search` for an index crawler, `ai_training` for a crawler collecting training data, `ai_assistant` for a fetcher acting on a user's request in an assistant, `seo` for a link-index crawler, `fetcher` for Google's on-demand fetchers. The feed is tri-state like every other: false is a checked negative against every published file, null means no file covered the address family. The current ranges, with the date each file was synced, are published on the [crawler pages](/tools/ip-lookup/crawlers).
## Proof two: forward-confirmed reverse DNS
Some operators document a second proof that works even for addresses their files do not list. Look up the PTR record that names the address, then resolve that name forward and check the address is among its answers. Anyone can publish a PTR name for a block they control; only the owner of the name's domain can make it resolve back. Google documents this for Googlebot (names under googlebot.com and google.com), Microsoft for Bingbot (search.msn.com), Apple for Applebot (applebot.apple.com — Apple publishes no range file, so this is the only proof for it), and Yandex and Baidu for theirs.
Spaw runs the check when a lookup asks for it with `hostname: true`, because it is a live DNS query with a two-second budget. `hostname` is the PTR name and `hostname_verified` whether it forward-confirmed; a verified name under one of those documented domains proves the crawler and sets `is_crawler` even where no published range does. An unverified name proves nothing and is never used as evidence. OpenAI documents ranges and not reverse DNS, so for GPTBot the range file is the proof.
## What the proofs do and do not say
A verified crawler is the operator's machine. That is all. It says nothing about intent: a real Googlebot address fetches whatever Google's systems decided to fetch, and whether you want GPTBot collecting training data from your pages is a policy question that `robots.txt` and the operators' documented directives answer, not the IP.
That is why a crawler carries no risk weight, and why the datacenter weight is skipped for it. Every crawler lives on infrastructure by definition; scoring it as a datacenter visitor would flag exactly the traffic a publisher most wants to recognise. A crawler on a published range scores 0 and reads `low`.
The reverse holds too. A request that claims to be Googlebot from an address outside Google's ranges, whose PTR name does not forward-confirm, is not Googlebot. It may be a benign tool, a research crawler, or a scraper hoping the name opens doors; the answer says only that the operator's own proofs do not vouch for it.
## Recipes
**Per request.** Look the address up and branch on `crawler` and `crawler_kind`. Serve search crawlers what you serve everyone; decide separately, per `crawler_kind`, what AI training crawlers and AI assistants may have; treat an unverified "Googlebot" as an ordinary visitor and let the rest of the answer — datacenter, attack-source, your own lists — speak.
**With your own lists.** If you have decided to allow a crawler through a stricter policy, an allow list keyed on its published ranges makes the decision explicit and auditable; the ranges are on the crawler pages, dated, and the list is yours to update when the operator changes its fleet.
**In logs.** For traffic already logged, a bulk run over the addresses answers `crawler` and `crawler_kind` for every row and the run's summary counts them, so a week of "Googlebot" hits resolves into the share that was Google and the share that was pretending.
**In `robots.txt`.** Verification tells you who is at the door; the directives tell them what they may take. The two are complementary, and the operators' documentation for each crawler names the user agent tokens the directives apply to.
**For the operators without a file.** Applebot, YandexBot and Baiduspider publish no range file, so the only proof for them is the reverse DNS check, which costs a live query. Ask for it only on the requests where it matters — a suspicious burst claiming one of those names — rather than on every hit, and remember that a two-second resolver stall answers nothing rather than delaying the request twice.
## Sources
- Google's verification guide: https://developers.google.com/search/docs/crawling-indexing/verifying-googlebot
- Microsoft's Bingbot verification guide: https://www.bing.com/webmasters/help/how-to-verify-bingbot-3905dc26
- OpenAI's crawler documentation and range files: https://platform.openai.com/docs/bots
- The range file URLs above, as synced daily on 12 September 2026; the current copies and their dates are on the [crawler pages](/tools/ip-lookup/crawlers).
The [IP intelligence guide](/docs/ip-intelligence) documents `is_crawler`, `crawler`, `crawler_kind`, `hostname` and `hostname_verified` in full.
---
# Why residential proxies cannot be detected from open data
A residential proxy is a home connection rented out. No public feed names one, so the API does not claim to. The signals that do exist, used honestly.
Date: 2026-09-12
Every IP intelligence vendor is asked the same question, and most of them answer it with a number. Spaw answers it with a sentence in its own documentation: residential proxies are not detectable with open data and are not claimed. This guide explains why that sentence is the honest one, what the answer says instead, and how to build a policy on evidence you actually hold.
## What a residential proxy is
A residential proxy routes someone else's traffic through a real consumer connection. The address a server sees belongs to a home broadband line or a phone on a carrier network, allocated by an ordinary ISP to an ordinary subscriber, and it looks exactly like a person because, at the network level, it is a person's line. The subscriber is in the pool because an app on their device bundled a proxy SDK for a few cents, because a browser extension sold their bandwidth, because they were paid a little to install a peer, or because their device is infected and was never asked.
That is the whole difficulty. A datacenter address is infrastructure and says so: the cloud publishes its ranges, the operator's name says hosting, the address has no home to be in. A residential proxy is the opposite. Everything the network knows about the address is true — the ISP, the country, the city — and none of it says the request was relayed.
## Why no open feed can name one
The signals the IP answer is built from are public, published lists: the clouds' range files, the VPN providers' server lists, Apple's relay egress list, the Tor Project's exit list, the registries' allocation statistics, the threat and attack-source lists. Every one of them is listed, with its terms and its size today, on the [data sources pages](/tools/ip-lookup/sources). Each is published by someone with a reason to publish it, and each names infrastructure, operators or hosts seen doing something.
Nobody publishes a list of subscribers renting out their connection. The ISP does not know, because the traffic is ordinary traffic from an ordinary customer. The proxy seller will not say, because the pool is the product. The subscriber often does not know either. What exists instead is proprietary: vendors that run honeypots and traps, buy access to proxy pools and watch which addresses arrive, or analyse traffic at a scale that reveals a home line behaving like a hundred people. That data is sold under licences that forbid reselling it, it cannot be verified from outside, and it goes stale in days as pools rotate. An address in a rented pool this week is a plain home connection next week.
A "residential proxy score" derived from open data would therefore be a guess dressed as a measurement — and a guess about a person's home connection, which is the one place a false positive does real harm. So the field does not exist. When such a signal ships, it will be a separately labelled field with its own source named in `sources`, never folded into the weights that make up `risk_score`.
## What the answer says instead
The address has to be a network the feeds know something about, and several fields carry evidence that a policy can weigh without pretending to know what it cannot.
**Velocity.** `seen_by_you` counts how many times your own account looked the address up in the last seven days, with the first sighting. A home address that hits your signup form forty times in an afternoon is a fact about your traffic, and it is yours: the count is kept under a keyed hash, never the address, and `privacy: true` skips it.
**Your own lists.** An address that already burned you goes on a deny list, and every later lookup answers `account_list` with `denylisted` at +100, whatever the feeds say. Office and partner ranges go on an allow list, which zeroes the score. Lists belong to the account and apply to every lookup it is billed for, including bulk runs and monitors.
**A clock that disagrees.** Send the browser's zone as `browser_timezone` and `timezone_matches` answers whether it keeps the same clock as the located zone. A device in one country routed through a home line in another produces a mismatch honestly, and the browser is the half that is hard to fake without noticing. It carries no weight, because travellers and unset laptops produce one too; it is a fact to weigh beside the others.
**Outcomes.** What you learn afterwards — a chargeback, a confirmed fraud, a scraper — can be reported. Your own reports add `reported_abuse` at +60 to your later lookups of the address, and once three or more accounts report the same address inside thirty days everyone's lookups carry `reported_abuse_widely` at +40. Reports are stored as salted hashes, and only the number of reporting accounts is ever read.
**Attack-source lists.** Some proxy peers are infected machines, and infected machines also scan and brute-force. The three attack-source lists synced hourly name hosts seen doing that in the last days, at +50, and the flag is a checked negative when the lists do not name the address.
**What the network really is.** `connection_type` reads `mobile` when the address belongs to a listed carrier, which means carrier-grade NAT and one address shared by many handsets; `hostname_kind` reads `dynamic` when a requested reverse DNS name says pool or DHCP. Both are positive-only, and neither says "proxy". They say what a home or mobile line looks like, which is the baseline everything else is measured against.
**Consistency.** The cross-signal endpoint compares the IP's country with the phone's numbering plan and the postal address, and says so when they disagree. It withholds the comparison when the address is a VPN exit, a relay or a datacenter egress, because that country is an operator's. A residential proxy passes that filter, which is the point: it is the one case where a plain country mismatch is meaningful, and the report names it as evidence rather than as a verdict.
## Building the policy
Three rules fall out of all this.
Build on evidence you can audit. Every field above names its dataset and its date in `sources`, or is your own account's record. A vendor's proxy score names neither, and when it is wrong about a home connection you will not be able to say why.
Step up rather than block. A velocity spike, a clock mismatch and a country disagreement together are a reason to ask for a confirmed email or a second factor. Each alone is a reason to do nothing.
Keep the feedback loop closed. The reports you send are the one signal in the answer that describes what happened on your site rather than what a feed knows about the Internet. A policy that reports outcomes gets better at your traffic every week; a policy that buys a score does not.
The [IP intelligence guide](/docs/ip-intelligence) documents every field named here, and the [datacenter guide](/guides/block-datacenter-and-bot-signups-with-an-ip-lookup) covers the half of the problem that open data does answer.
---
# A LEI check digit is a typing check
Twenty characters ending in an ISO 7064 checksum. What checksum_valid settles, what only a register can settle, and why the two are reported side by side.
Date: 2026-09-10
A Legal Entity Identifier is twenty characters, and the last two of them are a check digit computed with ISO 7064 MOD 97-10 — the same arithmetic that validates an IBAN. That makes it tempting to treat the identifier as self-verifying: run the sum, get a `true`, move on. The sum is worth running, and it is worth running in your own form before you spend anything. But it settles one question and not the one most integrations think it settles. This guide is about which is which, and how the answer keeps them apart.
## What the arithmetic actually does
Each character of the identifier becomes its base-36 value, the whole string is read as one very long number, and a correctly formed identifier leaves a remainder of 1 when divided by 97. The number runs far past what a 64-bit integer holds, so it is accumulated digit by digit; that is the only subtlety in it. There is no lookup, no network call and no dataset — the check is pure arithmetic over the twenty characters in front of you.
What that catches is a mistyped or transposed character. Someone reads an identifier off a PDF, swaps two digits, and the remainder stops being 1. Someone pastes nineteen characters because the twentieth fell off the end of a spreadsheet cell, and the string is not an identifier at all.
What it cannot catch is everything else. A check digit says the string is well formed. It does not say the identifier was ever issued, or issued to the company whose name sits beside it in your database, or that the registration behind it is still current. Those are register questions, and only a register answers them.
## The measurement, and the three exceptions
It is fair to ask how solidly the checksum holds on real data, because a rule that fails on live records is worse than no rule.
Measured against the published register on 8 September 2026: of 45,515 consecutive real identifiers, 45,512 satisfy the checksum. The three that do not are all `ANNULLED` registrations — identifiers issued in error and withdrawn afterwards.
So a record can exist in the register with a failing check digit. That is precisely why Spaw reports `checksum_valid` beside the register answer rather than letting one stand in for the other: `found: true` with `checksum_valid: false` is a real state, and it tells you something specific — this identifier is carried, and it was probably issued in error.
## Four fields, four questions
| Field | Question it answers | Where it comes from |
| --- | --- | --- |
| `checksum_valid` | Is this string a well-formed identifier? | Arithmetic over the characters. `null` for identifiers that carry no check digit — a UK company number has none. |
| `found` | Does a register on this server carry it? | A read of the synced register file. |
| `registration_status` | Has anyone renewed the registration? | The register's own status: `ISSUED`, `LAPSED`, `RETIRED`, `ANNULLED`, `MERGED`, `DUPLICATE`. |
| `status` | Is the entity behind it still live? | The register's status for the entity, folded to `active`, `inactive`, `dissolved` or `unknown`. |
Reading those four as one fact is the mistake this product is designed to make hard. An entity can be alive with a lapsed registration; an identifier can be well formed and never issued; a record can exist with a bad check digit. Each field is reported on its own so you can branch on the one you actually care about.
## The four ways an answer says no
`found: false` always carries a `reason`, and the four are not interchangeable.
| `reason` | What happened | What it does not mean | Cost |
| --- | --- | --- | --- |
| `malformed_identifier` | The string never reached a register: not twenty characters of the permitted alphabet, or a company number longer than the register's eight. | Nothing about any company. | Free |
| `unknown_identifier` | A register answered, and carries no such row. | That the entity does not exist. It means this register does not carry it. | Free |
| `unsupported_register` | No register is installed for that identifier type, or that country. Company numbers are unique only inside their own register, so `GB` is the one installed today. | That the number is wrong. | Free |
| `register_unavailable` | The register is installed but has not been synced, so there is nothing to read. | Anything at all about the identifier. | Free |
Every one of those is free, because none of them told you something you could not have worked out. Only an answer a register gave costs a credit.
And none of them is a verdict on the company. Write your integration so that `found: false` reads as *this server could not answer*, never as *this entity is not real*. The reasons above are the whole difference between an honest gap and an accusation.
## Validate before you spend
Because the checksum is public arithmetic, you can run it yourself at the point of entry. That is the cheapest possible improvement to a form that asks for an identifier: reject the typo where the person can still see what they typed, and send only well-formed identifiers to an API.
Two things to keep in mind if you do.
**A valid check digit is not a reason to skip the lookup.** It only means the string could be an identifier. The register answers whether it is one.
**A UK company number has no check digit at all.** Nothing can be validated offline there beyond its shape. What Spaw does with it is normalise it the way the register writes it: punctuation dropped, case folded, left-padded with zeros to eight characters, so `445790` and `00445790` are the same company. Anything longer than eight is a `malformed_identifier`. If you key your own records on a company number, pad them the same way, or you will hold one company twice.
## Where the answer comes from, and what it is not
Identifier reference data is published by the Global Legal Entity Identifier Foundation under a CC0 1.0 public-domain dedication, as a golden copy refreshed daily. Spaw syncs that file and answers from its own copy, so nothing about your lookup leaves this server. Spaw is not affiliated with, endorsed by or connected to that foundation in any way, and an answer here is Spaw's rather than theirs.
The response also carries nothing that would locate anybody. There is no registered address in it and there will not be: a registered address is very often somebody's home, and an identifier on its own does not say whether a limited company or a sole trader stands behind it. The lookup answers "is this a real, live entity" and stops there.
`sources` names every register behind the answer with the date of the copy it read. Check it whenever the age of a record could matter to the decision you are about to make.
## What to do next
- Run an identifier through the [free company lookup](/tools/company-lookup) and read `checksum_valid`, `found` and `registration_status` as three separate answers.
- Read the [business verification guide](/docs/business-verification) for every field and every reason.
- Check the request and response shape on the [POST /api/v1/entity reference](/docs/api/verify-entity).
- See what happens when a LEI record and a national register describe the same company differently in [when two registers disagree about a company](/guides/when-two-registers-disagree-about-a-company).
---
# How a UK dissolution shows up in open data
The free company file is a snapshot of the live register, so a struck-off company is removed rather than marked dissolved. What to watch instead.
Date: 2026-09-10
If you are watching a UK company for the moment it stops existing, the obvious thing to watch for is a status of `dissolved`. It is also the wrong thing to watch for, and a monitor built on it will stay quiet through exactly the event it was bought to catch. This guide explains why, what the event actually looks like in the data, and how to read the statuses that do appear.
## The file is a snapshot of the live register
UK company data here is the Companies House free company data product, used under the Open Government Licence v3.0. It is a snapshot of the **live** register: the companies that are on it today.
A company that is struck off is not marked dissolved in that file. It is **removed from it**. So the statuses the file carries are `Active`, `Active - Proposal to Strike off`, `Liquidation`, `In Administration` and similar — and effectively never `Dissolved`. There is no row left to carry a status.
That is not an omission by the publisher. A register of live companies is exactly what it says it is. But it means the shape of the event you are looking for is a **disappearance**, not a change of value, and every integration that watches this data has to be built around that fact.
## What a single lookup answers after a strike-off
Once the company is off the file and a copy without it has been synced, `POST /api/v1/entity` answers `found: false` with `reason: unknown_identifier`, and that answer is free.
Read that carefully, because it is the same answer a company number that never existed gets. A single lookup cannot tell the difference between "struck off last month" and "never a company at all" — both are absences from the same file. Spaw does not guess between them, and neither should your integration: `unknown_identifier` means *this register does not carry it*, and nothing more.
The difference is only visible over time — which is what a monitor is for.
## `left_register` is the dissolution status
A monitored entity list re-checks up to 500 identifiers on a weekly or monthly cadence. There is no daily option: the company file is republished monthly, so a daily run would bill for data that cannot have changed.
The first run sets the baseline and never alerts, so saving a list does not email you about what was already true when you saved it. From the run after that, an entry that was fine last time and is not any more raises an alert, and the statuses are these:
| `status` | What happened |
| --- | --- |
| `left_register` | A register carried the identifier at the last run and does not carry it now. For the UK, **this is what a dissolution looks like.** |
| `dissolved` | The register records the entity as dissolved. Rare on the UK file, for the reason above; it is the ordinary case on registers that keep dissolved entries. |
| `inactive` | The register no longer records the entity as active. |
| `registration_lapsed` | The identifier's registration is no longer current — about the identifier, not the company. |
| `registers_disagree` | The two registers have stopped agreeing about the name or the status. |
| `unverifiable` | No register on this server could answer for it. Never alerted on. |
| `ok` | Nothing to report. |
An entity matching several is reported as the first of those it matches, so the strongest fact is the one you are told.
The reason `left_register` exists as its own status is the point of this whole guide. Without it, the disappearance would land in `unverifiable` and pass in silence — a company striking off with your unpaid invoices on it, reported as "we could not check this one". That is the single worst thing this product could get wrong, so the two cases are kept apart: one is a change from carried to not carried, and the other is a register that never answered in the first place.
`unverifiable` is never alerted on for the same reason inverted. It is a limit of the data installed on this server, not a fact about the company, and an alert on it would be an alert about us.
## `Active - Proposal to Strike off` is the earlier warning
Strike-off is not instantaneous. A company that is on its way off the register carries the register's own wording — `Active - Proposal to Strike off` — and by a wide margin it is the most common non-plain status on the file.
Spaw folds that to `status: active`, because the company is still on the register and still a legal person, and puts the register's exact words in `status_detail`. If you want to act before a company goes rather than after, `status_detail` is the field to read. A monitor will not alert on it — it is still `ok`, and it would be wrong to say otherwise — so this is a check to run in your own code against the field.
The same applies to `Liquidation` and `In Administration`: they fold to a status that says the entity is still carried, with the register's wording beside it. The folded value tells you what kind of thing it is; the detail tells you what is happening to it.
## Up to five weeks behind
The free company data product is republished monthly, within five working days of the previous month end. An answer here can therefore be up to five weeks behind the live register, and a company struck off yesterday is still on the copy this server holds.
Nothing about that is hidden. Every response names the date of each copy in `sources`, and the [dataset status page](/status) lists both registers with the date each was last synced. Two consequences worth designing around:
- **A weekly monitor over a monthly file is mostly free.** A repeat of the same identifier on the same account inside seven days does not bill, so a weekly cadence costs about what a monthly one does after the first run — while catching a new copy within days of it landing rather than within weeks.
- **Do not build a same-day process on this file.** If you need the register as it stands this minute, this is not the dataset for it, and the copy date in `sources` is how you can tell.
## What a struck-off answer does not tell you
- **Why the company went.** A voluntary strike-off, a compulsory one and a liquidation are not the same event, and once the row is gone the file carries none of that. Before it goes, `status_detail` is your only distinction.
- **Whether it came back.** UK law allows a company to be restored to the register. If that happens it reappears in a later copy and the next run reports it as `ok`. The monitor alerts on going from fine to not fine, so a restoration is quiet.
- **Who was behind it.** This product carries no officers, no ownership and no addresses — a registered address is very often somebody's home, and an identifier alone does not say whether a company or a sole trader stands behind it.
- **Anything at all, when the file is missing.** A register that has not been synced answers `register_unavailable`, which is free and never cached, so the next lookup answers as soon as the file lands. Absence of data is never reported as absence of the company.
## What to do next
- Read the [business verification guide](/docs/business-verification) for the full monitor contract, the alert rules and the retention window.
- Set one up with [POST /api/v1/entity/monitors](/docs/api/create-entity-monitor) and let the first run take the baseline.
- Check when each register was last synced on the [dataset status page](/status).
- See what happens when the two registers describe one company differently in [when two registers disagree about a company](/guides/when-two-registers-disagree-about-a-company).
---
# How to read a consistency report
Four fields, ten comparisons, both sides of each one quoted, and deliberately no score. A field-by-field reading, and how to write a rule you can defend.
Date: 2026-09-10
Updated: 2026-10-02
One call takes any subset of an email address, a phone number, an IP address, a postal address and a business identifier, runs each through the product that owns it, and reports where the answers agree and where they do not. The output is deliberately not a score. It is a list of comparisons with both sides quoted, and reading it well is a matter of knowing which of three things each row is saying. This guide goes through the response in the order you will meet it.
## What comes back
`data.signals` carries one entry per product, and every key is present whether or not you asked for it. Each `data` is exactly what that product's own endpoint returns, field for field — nothing summarised away, nothing to learn twice. Beside it, `meta` says what that one signal cost and whether it was a seven-day repeat.
A `null` there means one of two things: you did not ask for that signal, or the call ran out of credits before reaching it. `meta.requested` against `meta.processed`, and `meta.stopped_reason`, tell you which.
`data.consistency` is the analysis on top, and it has four fields.
| Field | Type | What it holds |
| --- | --- | --- |
| `signals_compared` | `string[]` | Which signals answered and were available to compare. |
| `country_agreement` | `'agree' \| 'differ' \| null` | The roll-up over the three country comparisons between a person's own signals — and nothing else. |
| `flags` | `string[]` | Every flag raised, in comparison order. Each has its own reference page. |
| `comparisons` | `object[]` | Every comparison that could be made, including the ones withheld, each quoting both sides. |
## The ten comparisons
A comparison is emitted only when **both** of its signals answered. A signal nobody asked about produces silence, not a row saying nothing could be compared.
| `comparison` | Fields | What it says |
| --- | --- | --- |
| `phone_address_country` | `phone.country` ↔ `address.country` | The country the number was allocated under, against the country the address stands in. Both come from registers; neither is a guess. |
| `ip_address_country` | `ip.country` ↔ `address.country` | Where the request came from, against where the post would go. Withheld for a VPN exit, a relay or a datacenter egress. |
| `ip_phone_country` | `ip.country` ↔ `phone.country` | The same comparison against the numbering plan, withheld under the same conditions. |
| `ip_address_distance` | `ip` coordinates ↔ `address` coordinates | How far apart the two places are, in kilometres, as evidence only. It never raises a flag. |
| `email_ip_disposable_datacenter` | `email.disposable` ↔ `ip.is_datacenter` | A burner address arriving from hosting rather than a consumer network. |
| `email_ip_disposable_anonymous` | `email.disposable` ↔ `ip.is_anonymous` | A burner address arriving over a VPN, a proxy or Tor. |
| `email_address_free_provider_mail_drop` | `email.free_provider` ↔ `address.is_mail_drop` | A free consumer mailbox beside a commercial mail-receiving agency. |
| `email_phone_disposable` | `email.disposable` ↔ `phone.is_disposable` | Both contact details on disposable lists: neither can receive anything for long. |
| `entity_address_country` | `entity.country` ↔ `address.country` | The country a business is registered in, against the country of the address given for it. |
| `entity_address_organization` | `entity.name` ↔ `address.organization` | The registered legal name against the company name written on the address. |
## Three results, and a fourth column that explains them
Every row carries `left` and `right` — each naming the signal, the field and the value it held — so nothing is asserted that you cannot see the working for. `result` is one of three values.
**`agree`** — the two values matched. For a country pair that means the same country. For a pairing such as `email_ip_disposable_datacenter` it means both sides were true, which is the only positive that pairing has.
**`differ`** — the two values are both present and are not the same. This is a fact about two fields. It is not a fact about a person, and the response never dresses it up as one.
**`not_compared`** — no comparison was made. Either the pairing simply did not hold (two known-false fields are not a disagreement), or the comparison was withheld, in which case `withheld` says why: `missing_value` when one of the fields was null, `low_confidence` when the IP does not honestly speak for a place.
That distinction between a null and a "no" runs through every Spaw product. An invalid phone number carries no country; a reserved IP carries no location; an address outside every installed mail-drop list carries no mail-drop answer. Comparing against a null would turn a gap in the data into a finding about somebody.
`distance_km` is populated on exactly one row, `ip_address_distance`, and only when the two countries already agreed and the IP location is graded high confidence. It carries no flag at any value, because there is no honest distance at which a person stops being themselves.
## `country_agreement` is narrower than it looks
It is a roll-up over three comparisons only: `phone_address_country`, `ip_address_country` and `ip_phone_country`. It reads `differ` if any of them differed, `agree` if at least one agreed and none differed, and `null` when none of them could be made — so a partial answer never reads as agreement it did not establish.
The entity comparisons are deliberately not folded into it. That roll-up is about where a person is, and a company's country of registration is not that: a Delaware company with a London office is entirely ordinary, and counting it as a disagreement would report a top-level problem about nothing. The flag still names the difference for anyone who wants it.
## There is no score, and there will not be one
No score, no risk level, no verdict, no recommendation. The flags are the whole output.
That is a design decision, not a gap. People travel. People move house and keep the old number. People emigrate and keep a foreign mobile for a decade. People browse through a VPN because they would rather not be tracked. Rolling those facts into a single number hides which one drove it — and a number that decides about a person is a number you will one day have to explain to that person.
So the endpoint hands you the evidence and stops. Write your own rule from the named flags, and you will get a better rule than a generic one, because you know your own customers and we do not.
A workable shape for that rule:
- **Nothing on its own is a rejection.** One country mismatch is a person on holiday as often as it is anything else.
- **Count the flags rather than weighing them.** Two or more unrelated flags on one signup is a different situation from one, and it is a rule you can explain in a sentence.
- **Route, do not block.** Send a flagged signup to a manual review queue or to a second factor. The cost of a wrong hold is a message; the cost of a wrong block is a customer.
- **Log the flags, not a decision.** When somebody asks why an application was held, you want the row that says `left: {signal: ip, field: country, value: DE}` and `right: {signal: address, field: country, value: GB}` — not a number nobody can reconstruct.
- **Read the per-signal blocks too.** The flags cover what two signals say about each other. Each product's own risk signals are on its own block and are not repeated here.
## Billing, and what is stored
Each signal bills exactly as its own single-lookup endpoint: 1 credit for a definitive answer, free when the answer is no, and free for a seven-day repeat of the same input on the same account. The IP signal is one of five IP lookups to the credit, so it usually costs 0, and an email answer whose mailbox check was uncertain is free. `meta.credits_used` is the sum. The consistency analysis itself is free — it is arithmetic over fields the pipelines already computed, with no dataset behind it.
A call that runs out of credits part-way keeps the signals it paid for, answers 200 and reports `meta.stopped_reason`; only a call refused on its very first signal answers 402. Nothing you were billed for is discarded.
Suppression lists are not applied here: a consistency check is a fresh vetting decision, so every signal is looked up afresh. A fresh answer only ever clears a suppression entry a lookup put there, or a postal entry your own reports added once they no longer stand behind it (a delivery reported since, or reports aged out of the 90-day window) — never one you imported, and never one your reports still stand behind. The comparisons themselves are computed and discarded with the response.
## What to do next
- Read the [cross-signal consistency guide](/docs/cross-signal-consistency) for the request shape and every field.
- Check the request and response on the [POST /api/v1/consistency reference](/docs/api/check-consistency).
- Read [why a VPN withholds the country comparisons](/guides/why-a-vpn-withholds-the-country-comparisons) before you act on a `low_confidence` row.
- See how the four pairing comparisons work in [when two weak signals coincide](/guides/when-two-weak-signals-coincide).
---
# When two registers disagree about a company
A LEI record and a national register can carry different names and different statuses for one company. What Spaw compares, what it refuses to resolve, and why.
Date: 2026-09-10
Most company-data APIs answer from one source and present the result as the truth. Spaw holds two open registers, and when a Legal Entity Identifier record names its entry in a national register that is also installed here, both are read and compared field by field. Often they agree and there is nothing to say. Sometimes they do not, and this guide is about that case: what gets compared, how the comparison avoids reporting a spelling as a difference, and why Spaw quotes both sides instead of picking a winner.
## What is compared, and when
The comparison happens only when a record makes it possible. An identifier record carries `other_register` and `other_identifier` — the national register it names and the number it has there. If that register is installed on this server and carries the counterpart row, two comparisons run: the legal **name**, and the **status**.
If there is no counterpart — the record names none, or the register is not installed here — `agreements` comes back empty. That is not a disagreement, and it is not reported as one. An absent register says nothing at all.
Each comparison arrives as a row quoting both sides:
```json
{
"field": "name",
"lei": "ACME HOLDINGS LIMITED",
"national_register": "Acme Holdings Ltd.",
"result": "agree"
}
```
Note the result on that row. Those two strings are not the same string, and they are the same name.
## Folding a name before comparing it
One register uppercases, another punctuates; a form writes "Ltd" where a register writes "LIMITED". Comparing the characters as they arrive would report a difference on a large fraction of real companies, and every one of those reports would be about typography rather than about a company.
So both sides are folded first, and the rules are worth knowing because they explain what will and will not be called a difference:
- **Non-ASCII letters are transliterated**, so "Société Générale" and "Societe Generale" fold together. An earlier version of this rule deleted non-ASCII characters instead, which turned the first into a manufactured disagreement on almost every continental-European name in the register.
- **`&` becomes `and`**, so "Marks & Spencer" matches "Marks and Spencer".
- **Apostrophes and full stops are removed rather than spaced**, because they sit inside a word: "Sainsbury's" matches "Sainsburys", and "A.B.C." matches "ABC".
- **Everything else that is not a letter or digit becomes a space**, so a hyphen or a slash still separates words.
- **The company-form words are dropped** — limited, ltd, plc, llp, inc, incorporated, corp, corporation, company, co — because they say how an entity is constituted, not what it is called.
What survives is letters, digits and single spaces, lowercased. Two names that fold to the same string agree; two that do not, differ.
There is no similarity score in this, and there will not be one. "ACME GROUP" and "ACME HOLDINGS" are either the same company or they are not, and no threshold decides which. A percentage there would be a guess wearing the clothes of a measurement. So the answer reports `differ`, quotes both names, and leaves the judgement where it belongs.
Both names being compared come from registers, never from something a caller typed, so there is no case here where one side is missing: the comparison only runs at all when a LEI names a counterpart this server also holds. (The consistency endpoint, which compares a register name against an organisation line somebody did type, does have that case, and withholds the comparison rather than settling it.)
## The four flags
`flags` names what the registers disagree about, or what one of them warns on its own.
| Flag | What it says | What it usually means |
| --- | --- | --- |
| `name_differs_across_registers` | The two folded names are not the same name. | A rename that one register has and the other has not caught up with, or an identifier attached to the wrong entity. Both names are in `agreements`. |
| `status_differs_across_registers` | The two registers do not agree on whether the entity is live. | Usually the identifier record is stale: the national register is the authority on whether a company exists, and the identifier record reflects what was last reported to it. |
| `dissolved_in_national_register` | The national register records the company as dissolved. | The strongest signal this product carries. A dissolved company is no longer a legal person and cannot enter a contract. |
| `lei_registration_not_current` | The identifier's registration is not in a current state. | Nobody renewed it. It is about the identifier, not about the company — see below. |
Flags are additive and each one names one fact. There is no roll-up, no score and no recommendation on top of them.
## An entity can outlive its identifier
The single most misread pair in this response is `status` against `registration_status`.
`status` is about the **entity**: is this company still a company. `registration_status` is about the **identifier**: has anyone reconfirmed it. A Legal Entity Identifier is meant to be renewed each year against the entity's own national register, and a great many are not, so `LAPSED` is ordinary and common. It means the reference data has not been rechecked recently. It does not mean the company has gone.
The distinction matters in both directions. If a counterparty needs a current identifier for a regulatory reason, `lei_registration_not_current` is exactly the flag that says they do not have one — regardless of how healthy the company is. If what you need is a live trading company, that flag is not the one to act on; read `status` and, for a UK company, the register's own wording in `status_detail`.
## Why one register lags the other
Two publication rhythms sit behind these answers, and most disagreements are explained by them alone.
The identifier golden copy is republished daily, so it is rarely more than a day behind what has been reported to it — but what has been reported to it depends on the renewal cycle above.
The UK company data is a monthly product, republished within five working days of the previous month end. An answer can therefore be up to five weeks behind the live register. That is not a defect being hidden: every response names the date of each copy in `sources`, and `/status` lists both registers with the date each was last synced. Where the age of a record matters, read it there before you act.
## What to do with a disagreement
- **Treat it as evidence, not as a verdict.** Nothing here resolves which register is right, because nothing on this server could honestly do so.
- **Quote both sides back.** They are in `agreements` precisely so that a person deciding — a compliance reviewer, an account manager — can see what was compared rather than take a flag on trust.
- **Act on `dissolved_in_national_register` first.** Of the four, it is the one that changes what you are legally able to do.
- **Do not treat `lei_registration_not_current` as a company problem** unless a current identifier is itself the requirement.
- **Check the copy dates before escalating.** A month-old snapshot beside a daily one explains a great many differences, and a disagreement that resolves itself after the next sync was never worth an email to a customer.
## What is not compared, and never will be
The comparison covers the name and the status. It does not cover addresses, because this product holds none — a registered address is very often somebody's home, and an identifier alone does not say whether a company or a sole trader stands behind it. It does not cover officers, ownership or filings, because the free files these answers come from do not carry them in a form that could be compared honestly.
Identifier reference data is published by the Global Legal Entity Identifier Foundation under a CC0 1.0 public-domain dedication; Spaw answers from its own synced copy and is not affiliated with, endorsed by or connected to that foundation. UK company data is the Companies House free company data product, used under the Open Government Licence v3.0.
## What to do next
- Read the [business verification guide](/docs/business-verification) for every field, flag and reason.
- Read the [name_differs_across_registers reference page](/docs/reasons/name_differs_across_registers) for the exact wording and an example answer.
- See what the check digit does and does not settle in [a LEI check digit is a typing check](/guides/a-lei-check-digit-is-a-typing-check).
- Understand why a struck-off UK company disappears rather than changing status in [how a UK dissolution shows up in open data](/guides/how-a-uk-dissolution-shows-up-in-open-data).
---
# When two weak signals coincide
A burner mailbox is common. A datacenter address is common. Both on one signup is not. The four pairing comparisons, and the rule that keeps them honest.
Date: 2026-09-10
Most of the signals a single lookup produces are weak on their own, and the honest ones say so. A disposable email address is used by plenty of people who simply do not want another newsletter. A datacenter IP is where a corporate VPN, a server-side integration and a great deal of ordinary traffic comes from. A free consumer mailbox is what most of the world uses. Acting on any one of them alone means rejecting real customers at a rate you will not enjoy explaining.
Two of them together is a different fact. That is what the pairing comparisons in a consistency report are for, and this guide is about the four of them, the one rule they all follow, and how to act on them without building a machine that punishes ordinary people.
## The four pairings
| `comparison` | Fields | What the pairing catches |
| --- | --- | --- |
| `email_ip_disposable_datacenter` | `email.disposable` ↔ `ip.is_datacenter` | A burner mailbox arriving from hosting rather than a consumer network — the shape of a scripted signup. |
| `email_ip_disposable_anonymous` | `email.disposable` ↔ `ip.is_anonymous` | A burner mailbox arriving over a VPN, a proxy or Tor. |
| `email_address_free_provider_mail_drop` | `email.free_provider` ↔ `address.is_mail_drop` | A free consumer mailbox given alongside an address at a commercial mail-receiving agency. |
| `email_phone_disposable` | `email.disposable` ↔ `phone.is_disposable` | Both contact details on disposable lists: neither can receive anything for long. |
Every one of them pairs a field from one product with a field from another. None of them needs a new dataset, which is why the analysis is free: each half was computed by the product that owns it, and the pairing is arithmetic over the two.
## The rule: a pairing holds only when both sides are true
This is the whole logic, and it is deliberately narrow.
- Both sides `true` → `result: agree`, and the flag is raised.
- Either side `false` → `result: not_compared`, no flag. Two known-false fields are not a disagreement; they are simply not the thing being looked for.
- Either side `null` → `result: not_compared` with `withheld: missing_value`. A null means "not evaluated", never "no", so nothing is inferred from it.
That means the flags are one-directional. `email_ip_disposable_datacenter` fires on a burner from a datacenter and stays silent on everything else — a normal mailbox from a datacenter, a burner from a home connection, a normal mailbox from a home connection. There is no negative flag, no "these look fine" row, and no partial credit. If you want to act on either half by itself, read that half on its own signal block; the pairing is not the place for it.
## What each pairing is actually saying
**A burner from hosting.** A disposable mailbox is a person avoiding a mailing list. A datacenter address is a server. A disposable mailbox *typed at a server* is usually a script working through a signup form, and it is one of the cleanest tells there is. Read `hosting_provider` and `asn_type` on the IP block beside it: a signup from a cloud provider's own range and a signup from a corporate egress deserve different treatment.
**A burner over an anonymous network.** Same idea, weaker. Anonymity is a legitimate consumer choice — iCloud Private Relay is on by default in Safari for iCloud+ subscribers — so this one carries less on its own than the datacenter pairing. It earns its place when it lands beside something else.
**A free mailbox with a mail-drop address.** A mail drop is an address at a business that receives post on other people's behalf, detected from operators' published branch lists and the number of companies registered at the address. It is a heuristic about a *place*, never a judgement about a person: plenty of small businesses genuinely use one, and plenty of real people live at an address that also hosts a mailbox service. Paired with a free consumer mailbox it is worth a second look at a "company" that has given you neither a business domain nor a place anyone works. On its own it is nothing at all.
**Two disposable contact details.** A disposable email and a disposable number mean there is no way to reach this person tomorrow. For anything involving a one-time code, a free trial or a refund, that is the substantive fact: not that they are dishonest, but that the contact details will not be there when you need them. The phone half is covered at length in [stop OTP abuse with block data and disposable numbers](/guides/stop-otp-abuse-with-block-data-and-disposable-numbers).
## Reading them together with the rest
The pairings sit in the same `comparisons` array as the country comparisons, and each one that fires adds its flag to `flags` in comparison order. That is all that happens. There is no score, no risk level and no recommendation on top — the flags are the whole output.
The practical rule that falls out of it is a count, not a weighting:
- **One flag is a look, not a decision.** Almost every single flag has an innocent reading, and the innocent reading is usually the right one.
- **Two unrelated flags on one signup is a different situation.** A burner from a datacenter *and* a phone number nobody can be reached on is not a coincidence you have to argue about.
- **Route, do not block.** A manual queue or a second factor costs a message. A wrong block costs a customer, and they never tell you.
- **Log the rows, not a verdict.** Both sides of every comparison are quoted so that a person reviewing the case can see the actual values. Store those.
## Adding a business identifier
If the signup claims to be a company, a consistency call can carry the business identifier too, and two comparisons of its own come back: the registered country against the address's country, and the registered legal name against the organisation written on the address. Neither is a pairing, and neither feeds `country_agreement` — a Delaware company with a London office is ordinary. They are named differences, quoted on both sides, exactly like the rest.
The register answers behind them have their own guides: [when two registers disagree about a company](/guides/when-two-registers-disagree-about-a-company) and [a LEI check digit is a typing check](/guides/a-lei-check-digit-is-a-typing-check).
## What the pairings do not tell you
- **Intent.** Every flag here names a combination of facts about contact details. None of them says why, and none of them is evidence of fraud on its own.
- **That the absence of a flag is a clean signup.** These four pairings cover what two signals say about each other. Each product's own risk signals live on its own block and are not repeated in the consistency report.
- **Anything about a person from a place.** A mail drop is a fact about an address, and nothing in any Spaw product says who lives or works anywhere.
- **A threshold.** There is none, by design, and the endpoint will not grow one. Where the line falls is a decision about your own customers, and you are the only one in a position to make it.
## What to do next
- Read [how to read a consistency report](/guides/how-to-read-a-consistency-report) for the other six comparisons and the response shape.
- Read the [cross-signal consistency guide](/docs/cross-signal-consistency) for every field and every withholding rule.
- Read the [disposable_email_from_datacenter_ip reference page](/docs/reasons/disposable_email_from_datacenter_ip) for the exact wording and an example.
- Handle the phone half properly with [stop OTP abuse with block data and disposable numbers](/guides/stop-otp-abuse-with-block-data-and-disposable-numbers).
---
# Why a VPN withholds the country comparisons
A VPN exit, a relay or a datacenter egress locates the operator, not the person. What low_confidence withholds, what it does not, and what to read instead.
Date: 2026-09-10
A consistency check compares the country an IP address is in with the country of the postal address and the country the phone number was allocated under. When the IP is a VPN exit, an iCloud Private Relay egress or a datacenter address, those comparisons are not made. The row still appears, with `result: not_compared` and `withheld: low_confidence`, and no flag is raised. This guide explains why that rule exists, exactly what it covers, and what to read instead when the network itself is what you want to act on.
## The rule
Three fields on the IP answer decide it: `is_anonymous` (true when any of Tor, a known VPN or the Apple relay matched), `is_datacenter`, and `is_relay`. If any of them is `true`, the IP does not speak for a place, and every country comparison involving that IP is withheld:
- `ip_address_country` — withheld
- `ip_phone_country` — withheld
- `ip_address_distance` — withheld too, because it is measured only once the countries already agree and `location_confidence` is `high`
What is *not* withheld: `phone_address_country`, which does not involve the IP at all and is compared exactly as it would have been. A withheld IP takes its own comparisons out of the report and touches nothing else.
`country_agreement` follows from what survives. If the phone-and-address pair is the only comparison left and it agrees, the roll-up reads `agree`; if nothing at all could be compared, it reads `null` rather than pretending to agreement it never established.
## Why the answer is withheld rather than reported
The location of a VPN exit is a fact about the VPN provider. A person in Manchester connected to a server in Frankfurt genuinely is coming from Frankfurt as far as the network is concerned, and there is no arithmetic that recovers Manchester from it. Comparing that country against their postal address does not measure a discrepancy about the person; it measures the provider's server estate.
The scale is what makes it a design rule rather than a nicety. iCloud Private Relay is on by default in Safari for iCloud+ subscribers, most of whom did nothing but pay for storage. Commercial VPNs are ordinary consumer software, bought by ordinary consumers, often for exactly the reason you would want a customer to be careful. If the endpoint reported `ip_country_differs_from_address_country` for all of them, that flag would stop meaning anything, and any rule built on it would fall hardest on the people being most careful with their own data.
So the comparison is withheld and says so. That is a different thing from silence: the row is in the response, with both values quoted, so you can see the country that was not compared and why.
## `low_confidence` is not the same as `missing_value`
Two reasons appear in `withheld`, and they mean different things.
| `withheld` | Cause | How to read it |
| --- | --- | --- |
| `missing_value` | One of the two fields was `null`. An invalid phone number has no country; a reserved IP range has no location; an address the installed lists do not cover has no mail-drop answer. | The data was not there. Nothing was established either way. |
| `low_confidence` | Both values are present, but the IP's country belongs to an operator rather than to the person. | The comparison was possible and was deliberately not made. |
Across every Spaw product a `null` means "not evaluated" and never "no", and both of these reasons exist so that neither case can quietly become a finding. If you store comparison results, keep `withheld` beside `result`; collapsing `not_compared` into `false` throws away the whole distinction.
## What to read instead
Withholding the comparison does not hide the network. The IP block in the same response carries everything you need if the network is what you want to act on:
- **`is_relay`** — an iCloud Private Relay egress, from Apple's own published list. It is a default for iCloud+ subscribers in Safari, not a signal of intent.
- **`is_vpn`** and **`privacy_service`** — a known VPN exit, and which provider's own list named it. `privacy_service` is null for Tor and for ranges known only from an aggregate list.
- **`is_tor`** — a Tor exit, from the Tor Project's own data.
- **`is_datacenter`**, **`hosting_provider`** and **`asn_type`** — hosting rather than a consumer network, and whose. A signup arriving from a cloud provider is a different proposition from one arriving over a consumer VPN.
- **`location_source`** and **`location_confidence`** — whether the operator declared the location itself, and how far the city answer is to be trusted.
The IP product also publishes its own `risk_score` over those flags, with the weights documented. That score is about the network. The consistency report is about whether several signals describe the same person. Keeping them apart is the point: a VPN is a fact about the connection, and it should be weighed as one rather than smuggled in as a geography mismatch.
There is a whole guide on writing a policy that treats those flags proportionately: [a VPN and relay policy that does not punish real people](/guides/a-vpn-and-relay-policy-that-does-not-punish-real-people).
## Two practical consequences
**A flagged network is not a reason to ask for more signals.** If a customer's IP is a relay egress, running the check again with a different address will not un-withhold the row. What you have is a phone-and-address comparison plus the network flags, and that is the honest total.
**A withheld row is not a failed check.** Some integrations treat any `not_compared` as a soft fail and add friction. That turns a privacy default into a penalty. If you need a rule that fires on network anonymity, write it against `is_anonymous` explicitly, so that it says what it does and can be explained to the person it affects.
## What this does not tell you
- **Where the person actually is.** Nothing recovers that from a VPN exit, and nothing here tries.
- **Whether the VPN was used to hide something.** The flag says a network was used, not why.
- **Anything about a datacenter address in isolation.** Plenty of legitimate traffic comes from hosting: server-side integrations, corporate egress, tunnels. `asn_type` and `hosting_provider` are how you tell an obvious bot farm from a company VPN, and neither is decided here.
- **A verdict.** As everywhere in this endpoint, there is no score, no risk level and no recommendation. The flags and the withheld reasons are the whole output, and the decision stays yours.
## What to do next
- Read [how to read a consistency report](/guides/how-to-read-a-consistency-report) for the rest of the comparisons and the fields around them.
- Read the [cross-signal consistency guide](/docs/cross-signal-consistency) for the request shape and the withholding rules in full.
- Read the [ip_country_differs_from_address_country reference page](/docs/reasons/ip_country_differs_from_address_country) for the flag this rule prevents.
- Build the network half of your policy with [a VPN and relay policy that does not punish real people](/guides/a-vpn-and-relay-policy-that-does-not-punish-real-people).
---
# Can you look up UK addresses from a postcode for free?
Not the addresses: every full UK list derives from Royal Mail’s licensed PAF. Free: whether a postcode exists, its status, centroid, places, district streets.
Date: 2026-09-05
Updated: 2026-10-14
Not the addresses themselves. Every complete list of UK delivery addresses derives from Royal Mail's Postcode Address File, and using it needs a licence. What is free and open is most of what people actually need from a postcode: whether it exists, whether it is live or retired, roughly where it is, which places it is written with, and which streets run through its postcode district. This guide sets out precisely where that line is, what sits on the other side of it, and how an honest API should behave when you ask for something that is not open.
"Just look up the postcode" is the most reasonable-sounding request in address handling, and the one that runs into a wall fastest in the United Kingdom, because a UK postcode is not an address and does not contain a list of addresses you can have.
## What a postcode is
A UK postcode is a routing unit. It identifies a group of delivery points that are convenient to walk or drive together — typically one side of a street, or a single large building, or a single business that receives enough post to need its own code. It is not a place, not a boundary, and not an address.
The scale is published by Royal Mail, which maintains the file behind it: the Postcode Address File holds "over 32 million UK postal delivery addresses" and "1.8 million UK postcodes" (https://www.poweredbypaf.com/, accessed 5 September 2026). Divide one by the other and the average postcode covers roughly eighteen delivery points — but the average is nearly useless, because the distribution is extreme. A residential code might cover twelve houses; a single office block or a large mailroom can have a code entirely to itself.
That same page reports 10,798 changes made to the file in the preceding week. Addresses are not a static reference table; they are a stream.
## What the open data answers
Three open releases carry British postcodes and streets, and all three are published under the Open Government Licence v3.0.
- **The ONS Postcode Directory**, published on the ONS Open Geography Portal (https://geoportal.statistics.gov.uk/, accessed 5 September 2026), lists current and terminated postcodes with a centroid and the dates each was introduced and terminated. The August 2026 release installed here holds 2,665,797 postcodes: 1,760,216 live and 905,581 terminated.
- **OS Code-Point Open**, from Ordnance Survey (https://www.ordnancesurvey.co.uk/products/code-point-open, accessed 5 September 2026), carries live codes with coordinates and is about a third smaller, because it does not carry the retired ones.
- **OS Open Names**, also from Ordnance Survey, carries "place names, roads numbers and postcodes for Great Britain" and is "updated every three months" (https://www.ordnancesurvey.co.uk/products/os-open-names, accessed 22 September 2026). It files every named and numbered road under a postcode district — the SW1A of SW1A 2AA — and names the populated place, borough, county and region each postcode is written with. Spaw has installed it since 16 September 2026.
The first two figures are worth sitting with. More than a third of the postcodes in the historical directory are retired, and a retired postcode is not a typo — it is very often a real address whose code was reorganised years ago and is still sitting in someone's CRM. A checker that refuses it as invalid throws away a real customer. Spaw keeps it, answers `postcode_status: terminated`, and adds 30 points to the risk score so the row surfaces for correction instead of deletion.
Northern Ireland is a separate matter. The ONS directory carries BT postcodes, but reusing them requires a licence from Land & Property Services, so they are excluded from the copy installed here — 63,293 codes in the August 2026 release — and Open Names carries no Northern Irish roads. The honest answer for a BT postcode is "not evaluated": not knowing and knowing-not are different answers, and that distinction is the whole discipline of this product. An address with a BT postcode is therefore answered with `postcode_status` null, as though no directory were installed for it, rather than as though the postcode did not exist. It carries no centroid and no town or county comparison, and it is never refused as `postcode_not_found`.
Two more kinds of British code are answered the same way, because the directory never lists them: the British Forces Post Office's numbers (`BFPO 15`) with their `BF1` postcodes, and the non-geographic `BX` codes some government departments receive post on, such as HMRC's `BX9 1AS`. The August 2026 release carries no row under either area, so a missing code there says nothing about whether it exists.
## What the open data does not answer
It does not answer which addresses are inside a postcode.
That direction — postcode to list of delivery points — is the Postcode Address File itself, and the file is licensed. Royal Mail publishes licensing terms, pricing and a licensee search precisely because commercial use requires an agreement. The derived product most developers meet is Ordnance Survey's AddressBase family, which is sold through the OS Data Hub rather than released openly; Ordnance Survey has announced that AddressBase and AddressBase Plus reach end of life in Autumn 2027, replaced by OS GB Address and OS Islands Address (https://www.ordnancesurvey.co.uk/products/addressbase, accessed 5 September 2026).
There is no open substitute for the delivery-point list. Every complete UK address list traces back to that file, so a product that hands you one either holds a licence or is redistributing somebody else's.
What Spaw's postcode endpoint answers for Great Britain is therefore the open half. `GET /api/v1/address/postcode/{postcode}` lists the streets Open Names files under the postcode's district, in Open Names spelling, with no house numbers. It costs 1 credit; asking again for the same postcode within seven days is free. A district Open Names files no street under is not answered with an empty list. Open Names files each road under one district, though a long road runs through several, so it cannot vouch that it lists every street at a postcode and its silence is not evidence that nothing is there: the endpoint answers `501 ADDRESS_LIST_UNAVAILABLE` instead, free, with a message saying the listing does not cover the postcode.
One answer carries at most 100 streets, in alphabetical order, and most British districts hold more: in the listing installed here on 22 September 2026, 2,250 of the 2,752 districts carried more than 100 street names, and the median district carried 291. `GET /api/v1/address/streets` answers a district's whole list in one call, at the same credit and under the same seven-day repeat. The district is wider than the postcode — a street listed there may not reach the code you asked about, and a long road filed under a neighbouring district will not appear — so treat the answer as the streets of the area, not the streets of the postcode.
The full delivery-point list still needs a licence derived from the Postcode Address File; no licensed partner for it is enabled, and the request and response shapes are fixed now so nothing in your client changes if one ever is. `501 ADDRESS_LIST_UNAVAILABLE` is what the endpoint answers, free, for a country that has neither a licensed list nor an open street listing. It answers the same, free, for a British code Open Names does not cover — a Northern Irish BT postcode, a forces BFPO number or BF1 postcode, or a non-geographic BX code — and, as above, for a district Open Names lists no street under; the message says why. See the [GET /api/v1/address/postcode reference](/docs/api/find-addresses-by-postcode) for the exact contract.
## What a licence buys you
Three things, roughly, and it is worth knowing which one you actually need:
1. **The delivery-point list.** Every address behind a postcode, with the identifiers that let you join addresses across systems. This is what "address autocomplete" is made of, and it is the expensive part.
2. **Deliverability.** Whether post is actually delivered to a given address, plus the operator's own classifications of the delivery point. Spaw's `deliverability`, `is_deliverable`, `is_cmra`, `is_vacant` and `is_residential` fields are shaped for this and answer `deliverability_checked: false` until a partner is configured.
3. **Redistribution rights, or the lack of them.** Licences constrain what you may do with the output. Deriving your own address list from a licensee's answers and selling it is the thing the terms exist to prevent, so a cache of whole partner answers with a bounded lifetime is legitimate where an accumulating Spaw-owned copy of the file would not be.
If what you need is validation rather than autocomplete, you may need none of it.
## What you can answer without a licence
Quite a lot, as it turns out, and it is the part people skip past on the way to asking for the address list.
- **Does this postcode exist?** Yes or no, against the national directory. When the shape is right but the code is absent, Spaw answers `valid: false` with the reason [`postcode_not_found`](/docs/reasons/postcode_not_found), free of charge. A code the directory does not cover — BT, BFPO, BF1 or BX — answers `postcode_status: null` instead.
- **Is it live or retired?** `postcode_status`, from the same directory.
- **Where is it, roughly?** The directory's published centroid, reported as `latitude`, `longitude` and `location_source: postcode_centroid`. It is a centroid, not a rooftop, and calling it one would be a lie of precision.
- **Does the town belong to it?** British postcodes now carry the places Open Names writes them with, so `locality_matches_postcode` answers for the postcodes Open Names lists rather than staying null. Most terminated codes are not among them, and those still answer null. The check is lenient on purpose: the populated place, the borough, the county and the region all confirm, a town the directory lists elsewhere in the postcode area reads as a near match with no weight, and only a town found nowhere in the area carries the `locality_mismatch` weight of 30. [What a street match means in Great Britain](/guides/what-a-street-match-means-in-great-britain) has the detail.
- **Which streets are in its district, and is this one of them?** A written street Open Names files under the postcode's district answers `match_level: street`; `exists` stays null, because a register of streets can say nothing about a building. Every district also has a public page in the [British postcode district directory](/tools/address-checker/gb/districts), with its places, its live and terminated postcode counts and its streets.
- **Does the address stand up for its own country?** The format, the required lines, the postcode pattern, the house-number position — none of that needs the address list.
- **What kind of delivery point is it?** A box, a private mailbox, general delivery, a parcel locker or a street address, read from the words the operator uses itself.
- **What do the open company registers say about it?** How many companies give the address as their registered office, which is how the mail-drop signal is built.
Try any of it on the [free postal address checker](/tools/address-checker), which runs the same pipeline as the API.
## What to do with it
- **Validate on the postcode, correct on the address.** Reject `postcode_not_found`; review, do not delete, a terminated one.
- **Use the district's streets as a pick-list, not as proof.** They tell you which street names belong to the area; they do not tell you that a house number exists on one of them.
- **Do not build a postcode-to-address table from lookups.** That is the thing the licence terms are about, and rebuilding a licensed file from a licensee's answers is not a loophole.
- **Do not treat a centroid as a location.** It is fine for routing, region assignment and rough distance; it is not fine for "is this delivery inside the zone" at street granularity.
- **Handle Northern Ireland explicitly.** A BT postcode is not in the directory installed here, so it answers `postcode_status: null`. Null there means the directory was not consulted, not that the postcode is wrong.
## What to do next
- Run a postcode through the [free postal address checker](/tools/address-checker) and see the live, terminated and not-found answers side by side.
- Browse a district in the [British postcode district directory](/tools/address-checker/gb/districts) to see the streets and places the open data holds for it.
- Read [what a street match means in Great Britain](/guides/what-a-street-match-means-in-great-britain) for the street and town checks in full.
- Read the [address verification guide](/docs/address-verification) for every field and its dataset.
- Read [how to read an address-existence answer](/guides/how-to-read-an-address-existence-answer) for the register half of the question.
- Check the request, response and billing of the postcode listing on the [GET /api/v1/address/postcode reference](/docs/api/find-addresses-by-postcode).
---
# How to read an address-existence answer
Premise, street and postcode matches prove different things, and absence from a partial register proves nothing at all. A field-by-field reading of exists.
Date: 2026-09-05
Updated: 2026-10-14
"Does this address exist?" sounds like a yes-or-no question. It is not, and the products that answer it as one are quietly making things up. Whether a given building can be found depends on which register you asked, how much of the country that register covers, and how far into it the address could be followed. This guide is about reading that answer honestly: the three match levels, the three states of `exists`, and what each of them is worth in a decision.
## Two fields, read together
Spaw answers existence with a pair.
`match_level` says how far the address was followed into the register: `premise` (the building itself), `street` (the street, not that number on it), `postcode` (the postcode area only), `none` (nothing matched), or `null` (no register was consulted at all).
`exists` turns that into a verdict, and it is deliberately hard to make it say no:
- **`true`** only on a `premise` match. The building itself was found.
- **`false`** only when the register that answered has complete coverage — `register_coverage: "complete"` — and does not carry this one.
- **`null`** in every other case: no register covers the country, the one that answered is a register whose coverage is partial, or the address is a box with no street behind it.
Two more fields tell you who answered and how far to trust the answer: `register` names the dataset, and `register_coverage` reports `complete` or `partial`. That is how the publisher describes its own file, except where Spaw marks a register partial because its copy cannot vouch for an address it lacks; the [address data sources page](/tools/address-checker/sources) says which and why. Read `exists` against `register_coverage` every time.
## The four answers, measured
These are real answers from the Swiss building-address directory, a register whose publisher describes its coverage as complete, run through the lookup on 5 September 2026.
| Address | match_level | exists | risk |
| --- | --- | --- | --- |
| Bahnhofstrasse 1, 8001 Zürich | premise | true | 0 |
| Bahnhofstrasse 9999, 8001 Zürich | street | false | 20 |
| Nonexistentstrasse 999, 8001 Zürich | postcode | false | 20 |
| Bahnhofstrasse 1, 9999 Zürich | none | false | 20 |
Read the second row carefully, because it is the interesting one. The street is real and was found; the house number on it is not. The answer keeps both facts — `match_level: street` and `exists: false` — instead of collapsing them, so you can tell "wrong number on a real street" (usually a typo, worth asking about) from "street nobody has heard of" (usually a fabrication, or a very new development).
The last row is the reverse: a real street and number with a postcode that does not belong to it. A complete register that carries neither the premise nor the street in that area answers `false`, and `match_level: none` tells you the search never got anywhere at all.
## Why absence from a partial register proves nothing
Change the register and every `false` above becomes `null`.
Some national registers are contributed to voluntarily. The United States National Address Database is assembled from what states and local authorities choose to submit, and the French Base Adresse Nationale from what communes choose to publish; both describe their own coverage as partial. In a register like that, "not found" carries no information about the address at all. It might not exist. It might sit in a county that never sent its data.
So Spaw does not say `false` there. It says `null`, which means *not evaluated* — and the difference is enforced right through to the score: the `not_in_register` signal fires only when `exists` is `false`, so an address missing from a partial register contributes exactly zero points. There is no quiet penalty for living somewhere whose local authority did not take part.
This is the single most important line in this guide, so it is worth writing plainly: **absence from a partial register is not evidence that an address does not exist.** Any product that scores it as if it were is charging you for a bug.
## Null is not no
The same discipline runs through every flag in the response, and it is worth mapping it into your own schema properly. `null` is not `false`. It means the dataset that would have answered is not installed for that country, or was not applicable to that address.
- `exists: null` — no register answered. Say nothing about the building.
- `is_po_box: null` — the box naming for that country is not carried, so "not a box" was never established.
- `is_mail_drop: null` — neither the operator lists nor a company register covered it.
- `deliverability_checked: false` — the licensed delivery check did not run, so `is_deliverable`, `is_cmra`, `is_vacant` and `is_residential` are all `null`, and none of them is a "no".
If your database column is a boolean, you have already lost this distinction. Make the column nullable, or store the string.
## Existence and validity are different questions
They arrive in different fields, and conflating them is the most common integration mistake.
**Validity** is whether the address stands as written for its own country: are the required lines there, does the postcode match the country's pattern, and — where a national directory is installed — does that postcode exist? A failure here answers `valid: false` with a typed reason such as `missing_locality`, `invalid_postal_code` or `postcode_not_found`, and it is free of charge. Something is wrong with the text, and a person can usually fix it.
**Existence** is whether the building is in the register. An address can be perfectly valid and still answer `exists: false`, which is why that case is a 20-point risk signal on a *valid* address rather than an invalid verdict. Nothing about the writing is wrong; the place just is not in the book.
Keep them in separate branches of your code. "Fix your address" and "we could not confirm this address" are different messages to send someone, and only one of them is their fault.
## What to do with each answer
- **`exists: true`** — the strongest confirmation open data offers. It is still not deliverability: a real building can be vacant, and whether post arrives is a licensed answer that this lookup does not make.
- **`exists: false` with `match_level: street`** — treat as a probable typo. Show the person the street you found and ask for the number again.
- **`exists: false` with `match_level: postcode` or `none`** — worth a human look before shipping anything expensive. Combine it with the rest of the response rather than rejecting on it alone; new-build addresses appear in registers late, and register updates lag the ground.
- **`exists: null`** — carry on as if you had not asked. Use the parts of the answer that did run: the format, the delivery-point kind, the postcode status.
- **Never store a `null` as a `false`,** and never show a customer "this address does not exist" on the strength of a partial register.
## Where the register facts come from
Every answer names its own sources. `register` gives the dataset, `register_coverage` its declared coverage (the publisher's own description, unless Spaw has marked the register partial), and the `sources` block lists the version of each dataset installed when the answer was given — so an answer from last month can be checked against the file that produced it rather than against today's. The [free postal address checker](/tools/address-checker) prints the whole block, including the coverage table for the datasets this server currently holds.
## What to do next
- Try the four cases above on the [free postal address checker](/tools/address-checker) and watch `match_level` change while `exists` stays honest.
- Read the [address verification guide](/docs/address-verification) for every field, every reason and every weight.
- See what the postcode half of the answer can and cannot settle in the guide to [looking up UK addresses from a postcode for free](/guides/a-uk-postcode-is-not-an-address).
- Understand how the 20 points fit into the rest in [how Spaw computes the risk score](/guides/how-spaw-computes-the-risk-score).
---
# Stop OTP abuse with block data and disposable numbers
Which numbers soak up one-time codes and free trials, the four open signals that catch them, and a signup policy that does not punish real people.
Date: 2026-09-05
Updated: 2026-09-27
A one-time code is only as good as the number it goes to. If anyone can read the code, the number is not proof of a person, and every product that gates a free trial, a coupon or a second account behind SMS has met the same numbers doing it: rented virtual numbers, numbers published on websites that show incoming texts to the world, and the digit patterns people type when they do not want to be reached. This guide walks through the four open signals Spaw reads for every lookup, what each one proves, and how to gate a signup with them without blocking the people you want.
## Where the numbers come from
Most abuse does not come from a handset. It comes from a number that was never meant to be one person's phone:
- **Virtual numbers.** Wholesalers such as Bandwidth, Onvoy and Telnyx in the United States, Iristel and Fibernetics in Canada, and a handful of European carriers hold blocks whose numbers are resold by the thousand to apps, call centres and SMS-receiving services. A number in one of those blocks can be rented for minutes.
- **Numbers published on SMS-receiving websites.** These sites show every incoming message on a public page. Anyone can pick a number, use it for a signup, and read the code without owning anything.
- **Fictional ranges.** The North American 555-01XX block and Ofcom's drama ranges exist so that films and television never dial a real person (https://www.ofcom.org.uk/phones-and-broadband/phone-numbers/numbers-for-drama, accessed 5 September 2026). Nobody will ever answer them, which makes them a favourite for people filling in a required field.
- **Digit patterns.** 555-5555 and 123-4567 are what people type when the form insists on a number and they do not intend to give one.
None of these is fraud in itself. All of them mean the code will not reach a person's own phone.
## The four signals and what each proves
Spaw reads all four from data it holds locally, so every signal is in the same answer at the same single credit.
| Signal | Field | Where it comes from |
| --- | --- | --- |
| Virtual-number block | `is_virtual: true`, `line_type: "voip"` with `line_type_source: "block_allocation"` | The regulator's allocation table names the block holder (in a pooled US code, the holder of the number's own block of a thousand); a wholesaler's name settles it. |
| Published on an SMS-receiving website | `is_disposable: true`, `disposable_seen_at` | Two open lists of numbers scraped from those sites, synced daily, with the date each number was last seen; a number that leaves the lists keeps that date and ages. |
| Fictional range | `is_fictional: true` | The published fiction and drama ranges, bundled with the service. |
| Digit pattern | `digit_pattern` in `risk_signals` | The last seven digits repeat one digit or run straight up or down. |
The disposable list deserves one honest note. The open list Spaw syncs (https://github.com/iP1SMS/disposable-phone-numbers, accessed 5 September 2026) records when each number was last seen on a site, and most entries are old: of the 126,076 numbers on the copy synced on 5 September 2026, 109,509 were last seen more than a year earlier. A number a site showed two years ago may have been returned to the carrier and reassigned to a real subscriber since. That is why a listing the lists still carry scores 70 points while it was seen within the last year and 40 after that, and why the date is in the answer for you to weigh yourself. Since 24 September 2026 a second list is merged in (https://github.com/castle/disposable-phone-numbers, accessed 24 September 2026), a rolling list of about a thousand numbers the sites are showing now, dated by the day of the sync; a number that drops off every list is kept at its last-seen date for two years instead of vanishing, and ages faster than one still listed: it scores 70 for 90 days after it was last seen, then 40, because a number the sites no longer show may already have been handed back.
The sites rent numbers in runs and the lists lag the sites, so the neighbours of listed numbers carry a signal of their own. A number that is not listed itself, in a 1,000-number range holding ten or more listings that still score 70, scores `disposable_range`, 30 points: `medium` on its own, never `high`, and never added to a listing's own weight.
The virtual-block signal has its own caveat. A wholesaler's block backs plenty of legitimate use: a business phone system, a customer's second line, a number from an app that people use as their everyday number. `is_virtual` means the number was allocated to a wholesaler, not that the person behind it is up to something.
## Read the score, then decide
The risk score is a sum of published weights over these flags, capped at 100, and `risk_signals` lists every term so you can recompute it. The weights that matter here: fictional 80, disposable 70, disposable_stale 40, virtual 40, voip 30, digit_pattern 30. A number in a wholesaler block that an SMS-receiving site shows now scores 110 before the cap, so 100 and `high`. A number in a wholesaler block and nothing else scores 40 and `medium`. A plain mobile in a carrier's block scores 0.
Spaw does not call any of this fraud. The score measures how unlikely a number is to reach one person's own handset, which is exactly the question a one-time code asks.
## A policy that does not punish real people
Block only on the signals that prove the code cannot land, and step up instead of blocking on the signals that merely lower the odds:
1. **Reject invalid numbers outright.** `valid: false` with any reason, including `unassigned_block`, costs nothing and never reaches anyone. Show the reason in plain words: a missing country, a number that is too short, a block nobody holds.
2. **Reject fictional numbers and fresh disposable listings.** `is_fictional` and a fresh listing, the `disposable` signal, are the two signals that mean "anyone or no one can read this code". Ask for another number.
3. **Step up on medium.** A wholesaler block, a stale listing or a digit pattern is a reason to send the code and also require something else, such as an email confirmation or a delay before the trial unlocks, not a reason to refuse the person. Wholesaler blocks are full of real customers.
4. **Never block on `fixed_line_or_mobile`.** In North America that is the plan's honest answer for most real mobiles. Blocking it blocks your customers.
5. **Keep the answer.** Store `e164`, the flags and `risk_score` next to the account so a later review can see why a signup was allowed.
Run the policy from the browser and the server both. The form helper marks the field as the person types, with `data-spaw-phone`, and the server checks the same number again on submit, because a page can be edited. Both calls answer from the same tables, and a repeat of the same number within seven days is free.
On the server call, send `client_ip`, the address the signup came from, before each code goes out. It is never looked up or billed, only counted: `client_velocity` answers how many distinct numbers your account has looked up from that address in the last hour, and five or more add `numbers_per_client` (30). `range_seen_by_you` does the same for one 1,000-number range, and five or more add `range_burst` (40): one script cycling numbers through a form, or walking a range where someone is paid for every message sent there, looks clean one number at a time and obvious as a count. Both signals can be named in the SMS policy's blocked signals so the answer refuses the code outright; the [own-traffic section of the phone docs](/docs/phone-intelligence#own-traffic) explains the windows and what is kept.
Report what happened next, too. Send your delivery receipts to `POST /api/v1/phone/feedback`, and report `verified` when the code is entered: the summary's `verified_share` is then your conversion, and a block that is delivered but never verified is where pumped traffic shows first.
## Test it before you ship it
The eight test numbers answer fixed results and never cost a credit: +1 202 555 0102 is a virtual number, 0103 is a fresh disposable listing, 0113 sits in a 1,000-number range crowded with SMS-receiving numbers (disposable_range 30), 0105 is fictional, 0104 sits in an unassigned block and 0100 is a clean mobile. Wire them into your integration tests so the policy above is exercised on every deploy, without depending on what the open lists say that week.
## What to do next
- Read the [phone intelligence guide](/docs/phone-intelligence) for every field and the full weight table.
- Check the request and response on the [POST /api/v1/phone reference](/docs/api/validate-phone).
- Validate from a page with the [publishable-key endpoint](/docs/api/validate-phone-public) and the form helper.
- Tighten the SMS policy for SMS pumping, as [SMS pumping: what the default SMS policy lets through](/guides/sms-pumping-what-the-default-sms-policy-lets-through) shows.
- Understand the plan behind the `fixed_line_or_mobile` answer in [why US numbers cannot be split into mobile and landline](/guides/why-us-numbers-cannot-be-split-into-mobile-and-landline).
- See which regulator table stands behind the block data in each country in [phone facts by country](/tools/phone-number-lookup/countries).
---
# What a PMB (private mailbox) is, and why it cannot prove where you live
A PMB is a box at a mail-receiving agency, written the way USPS requires. What that spelling proves, why banks need a street address, and why no list exists.
Date: 2026-09-05
Updated: 2026-09-22
A private mailbox is a box rented from a business rather than from the post office. The business — the United States Postal Service calls it a commercial mail receiving agency, or CMRA — receives mail on its customers' behalf at its own street address, and each customer has a numbered box behind that address. Print shops, parcel stores, virtual-office providers and mail-forwarding services all run them. They are entirely legitimate, they are useful, and they are the single most commonly misread kind of address in any customer database, because on the page a private mailbox looks exactly like an office.
This guide covers what a private mailbox is, the addressing rule that makes it recognisable, why an address of that kind cannot do the job a bank needs an address to do, and why no product can honestly hand you a list of the agencies.
## The address is the agency's, the box is the customer's
The mechanics matter, because they explain everything else. A CMRA registers with the Post Office responsible for delivering to it: "Each CMRA must register with the Post Office responsible for delivery. Any person who establishes, owns, or manages a CMRA must provide PS Form 1583-A, Application to Act as a Commercial Mail Receiving Agency, to the postmaster" (Domestic Mail Manual 508.1.8.2, https://pe.usps.com/text/dmm300/508.htm, accessed 5 September 2026). Each of its customers then completes a PS Form 1583 naming themselves as an addressee at that agency.
So the delivery address is the agency's own street address. What separates one customer's mail from another's is a box number appended to it. That is the whole arrangement: one street address, hundreds or thousands of recipients, no relationship at all between the address and where any of them lives or works.
## The spelling is not a convention, it is a rule
Because the address belongs to the agency, the Postal Service requires the box number to be written in a way that cannot be mistaken for anything else. Publication 28 puts it plainly: "Mail sent to an addressee at a CMRA must include either the PMB identifier or the #, followed by the appropriate private mailbox number" (Publication 28 §285, https://pe.usps.com/text/pub28/28c2_040.htm, accessed 5 September 2026). The same section rules out the obvious substitute: "The words *POST OFFICE BOX* or *PO BOX* and the private mailbox number cannot be used on the Delivery Address Line."
The Domestic Mail Manual says it from the agency's side — "A CMRA must represent its delivery address designation for the intended addressees by the use of 'PMB' (private mailbox) or the alternative '#' sign" — and forbids folding the agency's own suite number and the customer's box number into a single element, giving `10 MAIN ST STE 11-234` as the thing not to write (DMM 508.1.8.3).
That rule is the reason a private mailbox is detectable at all. `2261 Market St PMB 4321, San Francisco, CA 94114` is not an ordinary street address that happens to have a number on the end; it is an address written the way the Postal Service requires a mail-receiving agency's customer to be addressed. Spaw reads the `PMB` spelling, and answers `address_type: private_mailbox` with `address_type_source: address_rules`, lifting `PMB 4321` out of the street line into `unit`. The alternative `#` is read as a unit and nothing more: `2261 Market St # 4321` answers `address_type: street`, because the same sign is how an apartment or a suite is written too. You can watch it happen on the [free postal address checker](/tools/address-checker).
## Why a bank needs a street address
United States banks must run a customer identification programme, and the rule spells out what an address means for that purpose. For an individual, the bank must obtain "a residential or business street address"; only where the person has none may it fall back to "an Army Post Office (APO) or Fleet Post Office (FPO) box number" or "the residential or business street address of next of kin or of another contact individual". For a customer that is not an individual, it must obtain "a principal place of business, local office, or other physical location" (31 CFR 1020.220(a)(2)(i)(A)(3), https://www.ecfr.gov/current/title-31/subtitle-B/chapter-X/part-1020/subpart-B/section-1020.220, accessed 5 September 2026).
Read the text closely and the logic is clear. The requirement is not for an address that receives post; it is for a physical location associated with the customer. A post-office box is not a street address at all, and the only box the rule accepts is a military one, for people who genuinely have no other. A private mailbox is a street address — but it is the agency's location, not the customer's, and a box at a mail-receiving agency is neither the customer's residence nor their place of business.
None of this makes a box address a bad address. It makes it the wrong evidence for one particular question. A box is an excellent way to receive post, a perfectly ordinary thing for a small business to use, and a terrible answer to "where is this person?".
## Why nobody can sell you the list, honestly
The obvious way to catch an agency address would be a list of agencies. There isn't a public one. Registration goes to the postmaster responsible for delivery, and the resulting records live in what the Manual calls the USPS CMRA Customer Registration Database — an internal system, not a published register. Nothing in the addressing standards points at a downloadable directory of agencies, because none is published.
That has a consequence worth stating out loud when you are comparing address products. A flag that claims to identify every mail-receiving agency is either licensed from the Postal Service under an agreement that keeps it query-only, or it is a guess dressed up as data. Spaw does not carry such a list. What it carries instead are two things it can stand behind: the addressing rule above, and the branch addresses that mailbox operators publish themselves. On 22 September 2026 that meant six operators' lists — The UPS Store, PostNet, PostScan Mail, Opus Virtual Offices and Davinci Virtual Offices in North America, and Mail Boxes Etc in the United Kingdom and Ireland. When an address matches one of those published lists, `mail_drop_provider` names the operator; when it does not, the flag says so rather than inventing a match.
## What the answer means, and what it does not
- `address_type: private_mailbox` means the address carries the `PMB` identifier the Postal Service requires of a mail-receiving agency's customer. It is a reading of the address, not a claim about a company or a person.
- `is_private_mailbox` answers `true` when the spelling says so and `null` otherwise. Reading the spelling can confirm a private mailbox but never rule one out, so it never answers `false`: only the postal service's own agency flag could, and that check is licensed and not enabled. Null is never "no".
- An agency address written *without* the box number, or with the `#` alternative, reads as an ordinary street address, and the answer says so. That is a real limit of reading the spelling, and it is a better failure than a fabricated match.
- The `private_mailbox` risk signal adds 60 points, the joint largest weight any open signal carries — `placeholder`, an address field filled with a test word or a refusal, carries the same — because an address of this kind is the least likely to be somebody's own front door. It is not an accusation, and the arithmetic is published so you can disagree with it.
## What to do with it
- **In identity and onboarding flows**, treat a box — post-office or private — as a mailing address, not a location. Ask for a street address separately when the rule you are subject to needs one, and say why; people who use a mailbox are used to the question.
- **In shipping and fulfilment**, do the opposite: a private mailbox is often the *best* address a customer has, and refusing it loses good orders. What matters is that couriers who do not deliver to agencies know in advance.
- **In fraud review**, use it as a signal about a place and combine it with everything else. On its own, "this is a mailbox" describes a large number of legitimate freelancers, expatriates and small companies.
- **Never store it as a fact about a person.** The response deliberately carries no field saying who lives or works anywhere, and nothing here should be written into a customer record as if it did.
## What to do next
- Run an address through the [free postal address checker](/tools/address-checker) and watch the box number move out of the street line.
- Read the [address verification guide](/docs/address-verification) for every field, the nine address types and the published risk weights.
- Check the request and response shapes on the [POST /api/v1/address reference](/docs/api/validate-address).
- See what the register half of the answer can and cannot prove in [how to read an address-existence answer](/guides/how-to-read-an-address-existence-answer).
---
# What an unassigned block means
A number can pass its numbering plan and still belong to nobody. How the regulators' tables settle it, what the statuses mean, and why the answer is free.
Date: 2026-09-05
Updated: 2026-09-26
Phone validation usually stops at the numbering plan: does the number have the right shape for its country? That question has a precise answer, and libphonenumber gives it for every region on earth. But the plan describes what a number could be, not whether anyone was ever given it. A regulator's allocation table answers the second question, and Spaw reads it for the countries whose regulators publish one, from the United States and the United Kingdom to France, Mexico, Poland and Italy. This guide explains the two layers, what each status in the answer means, and why a number in a block nobody holds is free.
## Two layers of validity
The first layer is the numbering plan. For the United States it says a number is an area code, a central office code that does not start with 0 or 1 and is not a service code such as 911, and four more digits. Every number of that shape is "valid" in the plan's sense: it could be assigned. Spaw reports this layer through `valid` and the reasons `too_short`, `too_long`, `invalid_length` and `invalid_number`.
The second layer is allocation. Regulators hand numbers to carriers in blocks and publish which blocks went where. A number can pass the plan and still sit in a block that was never handed to anyone, in which case no carrier can have connected it, no handset rings when it is dialled, and no text message reaches a person. Spaw reports this layer through `block_status` and, for a geographic number in a block nobody holds, through `valid: false` with the reason `unassigned_block`.
## Where the tables come from
- **United States and its territories.** NANPA publishes a report of every utilized central office code with its holder (https://www.nanpa.com/reports/co-code-reports, accessed 5 September 2026). The report lists assigned codes only, so a geographic code absent from it has not been assigned. The service codes such as 911 and 555 appear marked as unavailable. Inside a pooled code, NANPA's thousands-block report (https://www.nanpa.com/reports/thousands-block-reports/region, accessed 25 September 2026) lists every block of a thousand with its status, so a block it lists as available is unassigned too; one still partly in use by its former holder, or available only from a future date, is not read as free.
- **Canada.** The Canadian Numbering Administrator publishes a status table for every code in every Canadian area code (https://www.cnac.ca/co_codes/co_code_status.htm, accessed 5 September 2026): in service, assigned, available, for special use, plant test and a few more.
- **United Kingdom and the Crown dependencies.** Ofcom publishes its National Numbering Scheme as a set of tables, one per leading digit, with a status for every block: allocated, free, protected, quarantined, reserved and designated (https://www.ofcom.org.uk/phones-and-broadband/phone-numbers/numbering-data, accessed 5 September 2026).
- **France and its overseas departments.** ARCEP publishes the ranges it has attributed to each operator and, separately, the numbers it has frozen (https://www.data.gouv.fr/fr/datasets/ressources-en-numerotation-telephonique/), under the Licence Ouverte.
- **Australia.** ACMA's numbering register lists every range with its status: allocated or spare (https://www.thenumberingsystem.com.au).
- **Netherlands.** ACM's number register lists assigned ranges, ranges cooling off after being returned, and blocked ones (https://www.acm.nl/nl/telefoonnummers-zoeken).
- **New Zealand.** The NAD's number register lists assigned, spare, protected and reserved codes (https://www.nad.org.nz).
- **Mexico.** The public numbering plan of the CRT, which replaced the IFT in October 2025, lists every assigned range with its operator (https://sns.ift.org.mx, still under the IFT's name).
- **Poland.** UKE's numbering tables list fixed and mobile assignments and the ranges it holds in reserve (https://numeracja.uke.gov.pl).
- **Italy.** MIMIT's assigned-numbering workbook lists the mobile and nomadic ranges it has assigned to each operator (https://www.mimit.gov.it/it/comunicazioni/telefonia/risorse-di-numerazione).
Spaw syncs the ten tables weekly, and the United States report daily, and names the table it used in `sources`, with the date the file declared or the date it was synced. Five of them list every block they have assigned (the United States, Canada, France, the Netherlands and Mexico), so there a geographic number in a block the table leaves out has no holder either. The others answer only for the blocks they list, and say nothing about a number outside them. What each table holds today, and whether it is complete, is on its country's page: [the United States](/tools/phone-number-lookup/countries/us), [Canada](/tools/phone-number-lookup/countries/ca), [the United Kingdom](/tools/phone-number-lookup/countries/gb), [France](/tools/phone-number-lookup/countries/fr), [Australia](/tools/phone-number-lookup/countries/au), [the Netherlands](/tools/phone-number-lookup/countries/nl), [New Zealand](/tools/phone-number-lookup/countries/nz), [Mexico](/tools/phone-number-lookup/countries/mx), [Poland](/tools/phone-number-lookup/countries/pl) and [Italy](/tools/phone-number-lookup/countries/it). Who publishes each table, the files it comes in, the terms it is published under and what the installed copy holds is on [the phone data sources page](/tools/phone-number-lookup/sources).
## What the three statuses mean
Spaw folds each regulator's vocabulary into three values so an integration never has to learn them all.
| `block_status` | Meaning | Effect |
| --- | --- | --- |
| `allocated` | A carrier or communications provider holds the block. | The number is valid; the holder is reported as `carrier`. |
| `unassigned` | Nobody holds the block: absent from the NANPA code report or "available" in its thousands-block report and not contaminated, "available" in the Canadian table, "free", "protected" or "quarantined" in Ofcom's, frozen in ARCEP's, spare in ACMA's or the NAD's, cooling off in ACM's, held in UKE's regulatory reserve. | A geographic number answers `valid: false`, reason `unassigned_block`, free — except one in a range reserved for fiction, such as an Ofcom drama range, which stays valid with `is_fictional` true. |
| `reserved` | Held back for a special purpose: the 555 and N11 service codes, plant test codes, Ofcom's designated blocks. | The number stays valid and gains the `reserved_block` risk signal. |
The mapping is deliberately conservative. A block Ofcom lists as "protected" is withheld from allocation, and a "quarantined" one has been returned by a provider and is resting before reuse; in both cases no number in it can be in service today, so both read as unassigned. A "designated" block is set aside for a purpose the table does not spell out, so it stays valid with a warning rather than being called empty.
## Why the answer is free
Spaw charges for answers that are usable, not for attempts. A number in an unassigned block is an answer of the same kind as a number that is too short: it tells you the number cannot reach anyone, and there is nothing further to learn about it. So `unassigned_block` costs nothing, like every other invalid answer, and the response still carries the parsed number, its formats and the block status, so you can see what was checked and drop the record with confidence.
## The freshness gate
Calling a block unassigned is a strong claim, and it rests on the table being current. Codes are assigned every week, and a code assigned after the last sync would look free to a stale copy. Spaw therefore infers "nobody holds it" only from a table that lists every assigned block of its plan and was synced within the last 45 days. When the copy is older, the inference switches off: the number answers `valid: true` with `block_status: null`, and `sources` shows the stale date so you can see why. Ofcom's tables list unassigned blocks explicitly, so for the UK the status is read, never inferred.
Two more guards protect the answer. A download that would shrink a table by more than a few percent is refused, because a truncated file or a changed format would otherwise turn real numbers into unassigned blocks at scale. And the tables are only ever consulted for the regions they cover: a German number answers `block_status: null` and is judged by the plan alone, because no German table is installed.
## What to do with it
- **At signup**, reject `unassigned_block` the way you reject `too_short`: show the person the number that was checked and ask for another.
- **In a CRM clean-up**, drop those rows or move them to a review list. They were never real numbers, whatever the export says.
- **Do not treat `reserved` as invalid.** A number in a reserved block can connect to a service; it just is not a subscriber. Use the risk signal and decide per use case.
- **Read `sources`.** The table's date tells you how fresh the claim is, and `block_status: null` tells you no claim was made.
Where the numbering plan and the allocation table disagree, the table wins, because it is the regulator's own record of what exists. Where the table is silent, the plan's answer stands and nothing is inferred.
## What to do next
- Read the [unassigned_block reason page](/docs/reasons/unassigned_block) for the exact wording and an example answer.
- Read the [phone intelligence guide](/docs/phone-intelligence) for every field and where it comes from.
- Check the request and response on the [POST /api/v1/phone reference](/docs/api/validate-phone).
- See what the same tables say about the holder of an allocated block in [why US numbers cannot be split into mobile and landline](/guides/why-us-numbers-cannot-be-split-into-mobile-and-landline).
---
# Why US numbers cannot be split into mobile and landline
The North American plan hands mobiles and landlines the same blocks. What a number can still tell you, what the block holder proves, and what porting breaks.
Date: 2026-09-05
Updated: 2026-09-27
Ask most phone APIs whether +1 415 555 0142 is a mobile and you get a confident answer. Ask where the answer came from and the confidence fades. In the North American Numbering Plan, the plan that covers the United States, Canada and most of the Caribbean, the number itself does not say. This guide explains why, what the open data can still prove, and how Spaw reports the difference so you never build a policy on a guess.
## The plan does not separate them
Every North American number is an area code, a three-digit central office code and four subscriber digits. The plan hands out numbers to carriers in 10,000-number blocks, one per central office code, and since the late 1990s in 1,000-number thousands-blocks inside pooled codes. NANPA, the plan's administrator, describes the mechanism on its central office codes and thousands-blocks page (https://www.nanpa.com/numbering/co-codesthousands-blocks, accessed 5 September 2026).
What the plan never did is reserve ranges for a service type. A block goes to whichever carrier applied for numbers in that rate centre, and a wireless carrier's block sits next to a cable company's block with nothing in the digits to tell them apart. Contrast the United Kingdom, where Ofcom's National Telephone Numbering Plan sets aside 071 to 075 and 077 to 079 for mobile services (version with effect from 22 April 2025, https://www.ofcom.org.uk/siteassets/resources/documents/phones-telecoms-and-internet/information-for-industry/numbering/other/national-numbering-plan.pdf, accessed 27 September 2026) and Ofcom publishes block-by-block allocation tables (https://www.ofcom.org.uk/phones-and-broadband/phone-numbers/numbering-data, accessed 5 September 2026). A UK number starting 071 to 075 or 077 to 079 is a mobile number by construction; the rest of 07 is personal numbering (070) and radiopaging (076, apart from the Isle of Man's 07624), as [UK number ranges in a phone lookup](/guides/uk-number-ranges-in-a-phone-lookup) sets out range by range. A US number wearing 415 is a San Francisco number and nothing more.
That is why libphonenumber, the numbering metadata behind Android and behind Spaw, answers `fixed_line_or_mobile` for every geographic North American number. It is not a gap in the data. It is the honest reading of the plan.
## What the block holder can prove
The plan does not encode the service type, but the regulator knows who holds each block. NANPA publishes a daily report of every assigned central office code with the holder's operating company number and name (https://www.nanpa.com/reports/co-code-reports, accessed 5 September 2026), and the Canadian Numbering Administrator publishes the same table for Canada (https://www.cnac.ca/co_codes/co_code_status.htm, accessed 5 September 2026). Spaw syncs NANPA's daily and the Canadian table weekly, and what each table holds today is summarised on the phone facts pages for [the United States](/tools/phone-number-lookup/countries/us) and [Canada](/tools/phone-number-lookup/countries/ca). NANPA's code report of 26 September 2026 lists 198,523 allocated codes across the fifty states, the District of Columbia and the territories, 197,177 of them in the states and the District of Columbia; the 6,504 codes it marks unavailable, such as the N11 and 555 service codes, are not counted.
A holder named "Cellco Partnership dba Verizon Wireless" or "T-Mobile USA, Inc." allocates its blocks to handsets. A holder named "Bandwidth.com CLEC" or "Onvoy" is a wholesaler whose numbers back apps, softphones and SMS-receiving services. When the holder's name settles the question, Spaw settles `line_type` with it and says so:
```json
"line_type": "mobile",
"line_type_source": "block_allocation",
"carrier": "CELLCO PARTNERSHIP DBA VERIZON WIRELESS - NJ",
"carrier_source": "block_allocation",
"block_status": "allocated"
```
When the holder's name settles nothing, because the holder is a regional telephone company that sells both landlines and mobile service, or a name nobody can classify, the answer stays `fixed_line_or_mobile` with `line_type_source: "numbering_plan"`. Spaw never promotes a guess to a fact. In NANPA's code report of 26 September 2026, 60,319 of the 198,523 allocated codes (30%) belonged to holders on Spaw's wireless list and 28,638 (14%) to holders on its VoIP list, lower bounds because a holder not on the list settles nothing; the rest keep the plan's answer at code level, unless the number's own thousands-block settles it (below).
Pooling adds one more caveat: in a pooled US code the blocks of a thousand inside it can belong to other carriers than the code's holder, so `carrier`, `carrier_code` and a `line_type` settled by the holder describe the holder of the number's own block where NANPA's thousands-block report lists it, as [number pooling: why a US code's holder is often not your block's holder](/guides/number-pooling-why-a-us-codes-holder-is-often-not-your-blocks-holder) explains with the figures and the exceptions.
## Porting breaks the link between block and carrier
Number portability lets a subscriber keep a number while changing carrier, and it lets a landline number move to a mobile carrier and back. The block does not move with it. A number ported from a cable company to a wireless carrier still sits in the cable company's block, and every allocation table on earth will still list the cable company as its holder.
This is the reason Spaw reports the block holder as `carrier` with the note that it is the original allocation, and why `line_type_source: "block_allocation"` means "the block holder is a wireless carrier", not "this handset is on a wireless network today". For a number that was ported after it was allocated, both fields can be stale. Nothing in open data corrects that. Only a query into the carrier networks, an HLR or number-portability lookup, can say who serves the number now and whether it was ported. The API reserves that query behind `hlr: true` and reports it in its own fields, `ported` and `current_carrier`, so the two sources are never blended. The live carrier-network check (`hlr: true`) is not enabled on the service: `hlr_checked` answers false and nothing extra is charged.
## What Spaw claims, and what it does not
- `line_type` is the numbering plan's type, except when a North American block holder settles it. `line_type_source` always says which.
- `carrier` is the block holder as the regulator lists it, or the prefix metadata where no table covers the region. Both are the original allocation.
- Neither field is port-aware. A confident mobile-or-landline answer for a ported number is a guess, and Spaw does not make it.
- `valid: true` means assignable under the plan and, where a table is installed, inside an allocated block. It does not mean a handset is switched on.
## How to use the answer
Branch on the pair, not on `line_type` alone. Treat `mobile` with `block_allocation` as a strong signal that an SMS will arrive, treat `voip` with `block_allocation` as a reason to look at `is_virtual` and the risk score before sending a one-time code, and treat `fixed_line_or_mobile` as unknown rather than as a landline. A signup form that rejects every `fixed_line_or_mobile` number would reject a large share of real US mobiles.
For a CRM you clean once, the block holder is a good enough sort key: group by `carrier` and `line_type_source`, and let the sales team see which contacts were in wireless blocks. For a workflow that must know whether a text message will land, no open table can say, and the live check is not enabled on the service. Send, then report the delivery receipt your SMS provider returns through [POST /api/v1/phone/feedback](/docs/api/report-phone-feedback): a number that failed scores higher on your own next lookup, and one that failed for good lands on your suppression list, which batch and bulk runs answer free.
## What to do next
- Read the [phone intelligence guide](/docs/phone-intelligence) for every field and where it comes from.
- Check the request and response on the [POST /api/v1/phone reference](/docs/api/validate-phone).
- See what the same tables say when nobody holds the block in [what an unassigned block means](/guides/what-an-unassigned-block-means).
- Turn the flags into a signup policy with [stop OTP abuse with block data and disposable numbers](/guides/stop-otp-abuse-with-block-data-and-disposable-numbers).
- See the calling code, an example number of each line type and the regulator's table for any country in [phone facts by country](/tools/phone-number-lookup/countries).
---
# A VPN and relay policy that does not punish real people
Relay users are ordinary Apple customers and VPN users are often paying ones. How privacy_service, is_relay and risk_signals let you challenge instead of block.
Date: 2026-09-04
Blocking VPN traffic used to be a cheap way to cut fraud. It is now a cheap way to lose customers. Security-conscious people, remote workers on a corporate tunnel, travellers on hotel networks, and every iPhone owner who turned on iCloud Private Relay arrive through an anonymising network, and most of them are exactly who you want. The question is no longer "is this a VPN" but "which one, and what should that change". This guide lays out a policy that answers it with the fields in one lookup.
## Three anonymisers, three different people
The response separates the networks that hide an address into three flags, because they carry three different meanings.
`is_tor` is a listed Tor exit, from the Tor Project's own bulk exit list (https://check.torproject.org/torbulkexitlist, checked 2026-09-04). Tor is a general-purpose anonymity network with a real population of journalists, researchers and people in censored countries, and a real population of abuse. It carries the highest weight, 70, because the operator of an exit has no relationship with the person behind it and no way to hold them accountable.
`is_vpn` is a known commercial VPN exit. Three providers publish their server lists and are matched by name, Mullvad, NordVPN and Private Internet Access, with `privacy_service` naming the one that matched; the open X4BNet list covers the rest without a name. A VPN customer has an account, often a paid one, with a provider that can be served legal process. That is a very different person from a Tor exit, which is why the weight is 30.
`is_relay` is an iCloud Private Relay egress address, from the list Apple publishes at https://mask-api.icloud.com/egress-ip-ranges.csv, complete with the city Apple assigns to each range. Private Relay is on by default for iCloud+ subscribers in Safari and hides the user's address from websites while still placing them in roughly the right region (Apple's description: https://support.apple.com/en-us/102602, checked 2026-09-04). A relay user is an ordinary Apple customer who did nothing but pay for storage. The weight is 10, and it comes with two rules that keep it honest.
## The two relay rules
Relay egress runs on CDN infrastructure, so the same addresses sit inside datacenter ranges. Without a rule, every relay user would score 40 for datacenter plus 10 for relay and land in the medium band as a suspected bot. So the datacenter weight is skipped when `is_relay` is true. The `is_datacenter` flag itself still reports true, because the address really is on hosting infrastructure; only the arithmetic changes, and `risk_signals` shows it:
```json
"is_datacenter": true,
"hosting_provider": null,
"is_vpn": false,
"is_relay": true,
"privacy_service": "iCloud Private Relay",
"is_anonymous": true,
"risk_score": 10,
"risk_level": "low",
"risk_signals": [
{ "signal": "datacenter", "weight": 0, "dataset": "datacenter-ranges" },
{ "signal": "relay", "weight": 10, "dataset": "relay-ranges" }
]
```
The second rule concerns the aggregate VPN list, which classifies Apple's relay ranges as VPN space. Apple's own list is the first-party source, so on relay egress `is_vpn` answers false unless a VPN provider's own list names the address. A relay user is therefore never counted as a VPN user by accident.
## A policy in four tiers
Write the policy against the flags, and use `is_anonymous`, which is true when any of the three is, only as the coarse switch for "apply the anonymiser tier at all".
- **Nothing anonymised, `is_anonymous: false`.** No friction. This is the large majority of traffic.
- **Relay, `is_relay: true`.** No friction either. Treat the location as regional rather than exact, since Apple assigns the city, and never rate-limit by address: many relay users share each egress address, so one hot address is a crowd, not an attacker.
- **Named VPN, `is_vpn: true` with a `privacy_service`.** Allow the action, and step up only where the action is sensitive: a payment from a new device, a password change, a withdrawal. Use a challenge you already have, such as an email confirmation or a one-time code, rather than a refusal. A paying customer on Mullvad should never see "VPN detected, access denied".
- **Tor, `is_tor: true`.** Decide per product. A publisher may want Tor readers; a marketplace may require a verified email before listing anything; a bank may refuse. Whatever the answer, make it a deliberate rule for Tor alone, not a side effect of a VPN block.
Unnamed VPN ranges, `is_vpn: true` with `privacy_service: null`, come from the aggregate list, which classifies whole networks. Treat them like a named VPN for stepping up, but expect more false positives, and let a confirmed email or a completed payment clear the address for that account.
## Challenge, do not block
Every step-up above shares a property: a person can pass it and a script usually cannot. That is the whole design. A block tells a legitimate visitor nothing they can act on and tells an attacker exactly which network to switch to. A challenge lets the person through at the cost of a minute and leaves the attacker holding a burner mailbox.
Pick the challenge by what the action puts at risk, not by the flag:
| Action | Anonymised visitor | What to ask for |
| --- | --- | --- |
| Reading, browsing, pricing | any | nothing |
| Free signup | relay | nothing |
| Free signup | VPN or Tor | a confirmed email before the trial starts |
| First payment | VPN | the card check you already run |
| First payment | Tor | your policy for Tor, applied consistently |
| Withdrawal or payout | any anonymiser | a second factor |
Record the flags with the decision, so a support agent looking at a complaint can see that the account was challenged because `is_tor` was true on a payout, not because a score crossed a line nobody can explain. `risk_signals` is built for exactly that log line.
## What the flags cannot see
Residential proxies rent out addresses on home connections. They look like a person on a home line in every field of the response: `asn_type: "isp"`, `is_datacenter: false`, no anonymiser flag. No open dataset can tell them apart, and this API does not claim to. If your abuse is coming through residential proxies, the network flags will be clean and the evidence will be behavioural: velocity, address reuse across accounts, a located country that never matches the billing country. Read the [datacenter guide](/guides/block-datacenter-and-bot-signups-with-an-ip-lookup) for the complementary check, and keep the anonymiser tier for what it can actually see.
## Keep the location honest for VPN users
A VPN exit's location is the exit's, not the person's. When Mullvad or NordVPN place the exit in Tirana, the response says `country: "AL"` with `location_source: "operator"`, because the provider's own server list is the authority on where its exit is. That is the right answer to "where does this visitor appear to be" and the wrong answer to "where does this visitor live". For currency, consent and time zone defaults, prefer the account's stored preferences over the lookup whenever `is_anonymous` is true; the [consent and defaults guide](/guides/gate-consent-and-defaults-by-country-from-an-ip) covers the rest of that decision.
## What to do next
- Read the [IP intelligence guide](/docs/ip-intelligence) for the field list, the risk arithmetic and worked examples.
- Try a Mullvad exit and an iCloud Private Relay address on the [product page](/products/ip-intelligence) to see the two rules in the response.
- Pair this policy with [block datacenter and bot signups with one IP lookup](/guides/block-datacenter-and-bot-signups-with-an-ip-lookup) for the traffic that is not a person at all.
- Check the [endpoint reference](/docs/api/lookup-ip) for the exact shape of `risk_signals` and `sources`.
---
# Block datacenter and bot signups with one IP lookup
Scripted signups come from cloud boxes. How is_datacenter, hosting_provider and asn_type separate them from people, and the proxy mistake that hides it.
Date: 2026-09-04
Most scripted signups do not come from someone's living room. They come from a virtual machine at a cloud provider, because that is where scripts run: cheap, disposable, and reachable from a shell. A person filling in your form from home arrives on a broadband or mobile network with an operator whose name says telecom, cable or wireless. That single difference, infrastructure versus eyeball network, is the highest-yield check you can run on a registration, and one IP lookup answers it.
This guide shows what to read in the response, which decision to attach to each field, and the one server misconfiguration that quietly turns the check into noise.
## What the response says about infrastructure
Three fields carry the answer, from broad to specific.
`is_datacenter` is true when the address sits in a known hosting or cloud range. It is built from two kinds of source: the providers' own published range lists (Amazon Web Services publishes `ip-ranges.json`, Google Cloud `cloud.json`, Microsoft its weekly service-tags file, Oracle its public ranges, DigitalOcean, Linode and Vultr RFC 8805 geofeeds, Cloudflare and Fastly their edge ranges) and the open X4BNet list, which classifies whole autonomous systems as datacenter space. The flag is tri-state: `false` is a checked negative, `null` means no feed covered the address family, which today never happens for this signal, since the feeds carry IPv6.
`hosting_provider` names the cloud when the address is inside that provider's own feed: Amazon Web Services, Google Cloud, Microsoft Azure, Oracle Cloud, DigitalOcean, Linode, Vultr, Cloudflare or Fastly. It stays null when the flag came only from the aggregate list, so a null does not mean "not hosting"; it means "hosting, provider not named".
`asn_type` classifies the network operator: `hosting`, `isp`, `education`, `government`, or null when the operator's name settles nothing. It is `hosting` whenever `is_datacenter` is true, and it is derived from the operator name otherwise, conservatively. An unrecognised name answers null rather than a guess.
Together they let you write the policy in plain terms. A signup from `is_datacenter: true` with `hosting_provider: "Amazon Web Services"` and a free-mail address is a script until proven otherwise. A signup from `asn_type: "isp"` with `is_datacenter: false` is a person on a home line, whatever else you know about them.
## Attach a decision, not a score
The `risk_score` adds 40 for a datacenter address, which lands in the medium band on its own. That is deliberate: hosting space is not proof of abuse. Plenty of legitimate traffic comes from servers, including your own customers' backends calling your API, corporate VPN concentrators hosted in a cloud, and developers testing from a workstation in a datacenter office. A score is the wrong tool for a decision that depends on what the visitor is doing.
Branch on the flag and the context instead:
- **Free trial signups.** Require email confirmation before the trial starts when `is_datacenter` is true. A script that cannot read a mailbox stops here; a person is delayed by a minute.
- **Account creation with a payment method.** Let it through. The card check is stronger evidence than the network.
- **Contact and lead forms.** Tag the lead with `hosting_provider` and route datacenter submissions to a review queue rather than to sales. Reading the provider name in the queue is often enough to close the case: a burst of leads from one cloud region is not a campaign result.
- **API calls to your own service.** Never block on this flag. Your customers' servers live in datacenters by definition.
The [IP intelligence guide](/docs/ip-intelligence) documents every field the policy can read, and the [endpoint reference](/docs/api/lookup-ip) carries the full example response.
## Run it where the signup happens
A server-side check is one request. Send the address the visitor connected from, not the one in a form field, and read the flags:
```bash
curl https://spaw.co/api/v1/ip/203.0.113.9 \
-H "Authorization: Bearer sk_live_…"
```
```json
{
"success": true,
"data": {
"ip": "203.0.113.9",
"asn": 16509,
"org": "Amazon.com, Inc.",
"asn_type": "hosting",
"is_datacenter": true,
"hosting_provider": "Amazon Web Services",
"is_tor": false,
"is_vpn": false,
"is_relay": false,
"is_anonymous": false,
"risk_score": 40,
"risk_level": "medium",
"risk_signals": [
{ "signal": "datacenter", "weight": 40, "dataset": "datacenter-ranges" }
]
},
"meta": { "credits_used": 1, "credits_remaining": 99, "cache_hit": false, "ip_lookups_prepaid": 4 }
}
```
A repeat of the same address inside seven days is free, so a visitor who reloads the form does not cost a second credit. If your policy forbids keeping even that marker, add `?privacy=1` and every call is a fresh, billable lookup with no trace kept.
For a form that should react before the submit, the [browser endpoint](/docs/api/lookup-ip-public) runs the same lookup from the page with a publishable key locked to your domains. Leave the `ip` field out and the visitor's own address is looked up. Give the key a daily credit cap, because it sits in page source; once the cap is spent the endpoint answers `429 KEY_SPEND_CAP_REACHED` until the next day, and free answers never count toward it.
## Batch the backlog
Existing accounts deserve the same look. Export the registration addresses of the last quarter and send them to `POST /api/v1/ip/batch`, up to 1,000 per call, or queue up to 100,000 as a bulk run and download the CSV. The `hosting_provider` column groups cleanly in a spreadsheet: a spike of accounts from one cloud, created minutes apart, with sequential usernames, is a pattern you can act on without any scoring at all. Bulk runs are billed per fresh answer, and repeats inside the run are looked up once.
## The misconfiguration that makes everyone Cloudflare
If your application sits behind Cloudflare, or any reverse proxy, and you pass the connecting address to the lookup, every visitor comes back as `hosting_provider: "Cloudflare"` with `is_datacenter: true`. Nothing is wrong with the data. The address you sent really is Cloudflare's, because the proxy connected to you on the visitor's behalf.
Cloudflare puts the visitor's address in the `CF-Connecting-IP` header and appends it to `X-Forwarded-For` (Cloudflare's header reference, checked 2026-09-04: https://developers.cloudflare.com/fundamentals/reference/http-headers/). Your framework needs to be told which proxies to trust before it reads those headers, otherwise a visitor could forge them. In Laravel that is the trusted-proxies middleware; in Django, `SECURE_PROXY_SSL_HEADER` and a middleware that reads the forwarded address; in Express, `app.set('trust proxy', …)` with the proxy's ranges. Trust exactly the proxy's published ranges, never every address, and read only the headers the proxy sets itself.
The tell in your data is unmistakable: a steady stream of Cloudflare or Fastly addresses at signup, one per visitor, spread across the day. Real traffic from a CDN edge does not look like that. Fix the proxy configuration first, then re-run the batch; the datacenter flags that remain are the ones worth reading.
## What the flag cannot tell you
Open range lists know where clouds and hosting providers are. They do not know whether a specific address at a residential ISP is being rented out as a proxy. A residential proxy looks, by construction, like a person on a home line: `asn_type: "isp"`, `is_datacenter: false`, a plausible city. No open dataset can tell the two apart, and this API does not pretend to. If residential proxies are your problem, treat the datacenter flag as the first filter, not the last, and look at behaviour on your side: velocity per address, reuse of the same address across accounts, mismatches between the located country and the billing country.
Sources for the range data are named in every response. AWS documents its range file at https://docs.aws.amazon.com/vpc/latest/userguide/aws-ip-ranges.html, Google Cloud at https://cloud.google.com/compute/docs/faq#find_ip_range, and RFC 8805 (https://www.rfc-editor.org/rfc/rfc8805) defines the geofeed format DigitalOcean, Linode and Vultr publish. All were checked on 2026-09-04.
## What to do next
- Read the [IP intelligence guide](/docs/ip-intelligence) for every field, the risk arithmetic, and the privacy rules.
- Wire the [browser endpoint](/docs/api/lookup-ip-public) into the signup page so the policy runs before the submit.
- Decide how to treat VPN and relay users with [a VPN and relay policy that does not punish real people](/guides/a-vpn-and-relay-policy-that-does-not-punish-real-people).
- Try the flags on any address, without an account, on the [IP intelligence product page](/products/ip-intelligence).
---
# Gate consent and defaults by country: is_eu, timezone and currency from an IP
Show the consent banner where the law requires it, pick a currency and clock the visitor recognises, and know where IP geolocation stops being reliable.
Date: 2026-09-04
The first paint of a page has to make three decisions before the visitor has told you anything: whether to ask for consent before setting non-essential cookies, which currency to quote, and what clock to show. An IP lookup answers all three well enough for a default, and badly enough that you must let the visitor override every one of them. This guide covers what to read, what to build on it, and where the answer genuinely stops.
## Consent: is_eu is a floor, not a border
The GDPR applies to the processing of personal data of people who are in the Union, whether or not they are citizens or residents, and to controllers established there regardless of where the processing happens (Article 3, Regulation (EU) 2016/679, https://eur-lex.europa.eu/eli/reg/2016/679/oj, checked 2026-09-04). The consent requirement for storing information on a visitor's device comes from Article 5(3) of Directive 2002/58/EC, the ePrivacy Directive (https://eur-lex.europa.eu/eli/dir/2002/58/oj), as transposed by each member state. Neither text says "show a banner to EU IP addresses". Both say the rules follow the person and the establishment.
What an IP lookup gives you is a reasonable proxy for "in the Union right now". The response carries `country` as an ISO code and `is_eu` as a boolean for the 27 member states, so the gate is one line:
```js
if (data.is_eu) showConsentBanner();
```
Three consequences follow, and each is a design decision rather than a lookup detail.
First, `is_eu` is false for the United Kingdom, Switzerland, Norway and Iceland, and for the EEA members among those the GDPR applies through the EEA agreement while the UK applies its own GDPR. Your policy should probably treat the EEA and the UK like the Union, which means checking `country` against your own list rather than relying on `is_eu` alone. The boolean is the floor.
Second, an EU resident on holiday in the United States is still protected, and your lookup will say `country: "US"`. If you serve EU customers at all, the cheapest safe policy is to ask for consent everywhere and use the lookup only to pick the default state of the toggles. Several jurisdictions outside the Union also require consent for tracking; a global banner sidesteps the question of keeping a country list current.
Third, `is_eu` is null when the country is unknown. Reserved addresses, the rare unlocated block, and lookups that could not resolve a country all answer null. Treat null as "ask".
## Currency and locale: a default the visitor can change
`currency` is the ISO 4217 code for the located country, taken from ICU's region data, the same source your platform's number formatter uses. `calling_code` comes from the same libphonenumber data that validates phone numbers. Both are country-level facts and are exactly as reliable as the country, which on the database used here is dependable at country level and approximate below it.
Use them for the first quote and the phone field's default prefix, and let the visitor switch. A German visitor sees euros; a German visitor connecting through a company VPN that exits in Ireland also sees euros, because Ireland is in the euro area, but one whose VPN exits in the United States sees dollars until they change it. That is why the location answer carries `location_source`: when a VPN provider's own server list places the address, the value is `operator` and the country is the exit's country, not the person's. If `is_vpn` or `is_relay` is true, prefer the visitor's stored preference or their browser language over the lookup.
Language is not in the response on purpose. Belgium, Switzerland, Canada and India each have several official languages, and the browser already tells you which one the visitor reads in `Accept-Language`. Use that header for language and the lookup for currency.
## Time zones: only where the answer is settled
`timezone` is an IANA identifier such as Europe/Berlin, from the tz database maintained by IANA (https://www.iana.org/time-zones). It is filled in only when the country has a single zone, or when the located region is known to lie entirely in one zone. Germany, Japan and France answer a zone; a visitor located in California answers America/Los_Angeles; a visitor located in Texas answers null, because the state straddles Central and Mountain time and the city-level location is not precise enough to pick one. A null here is not a failure. It is the API declining to guess between two clocks that differ by an hour.
Two lines cover both cases:
```js
const zone = data.timezone ?? Intl.DateTimeFormat().resolvedOptions().timeZone;
```
The browser knows its own zone precisely; the lookup is for the server side, where you have no browser: scheduling a digest email, rendering a receipt, choosing a support hours message. On the server, treat null as "use UTC and say so".
## Where the location comes from
The base answer is the DB-IP Lite database, refreshed monthly. Country-level accuracy from this data is dependable; city-level is approximate and varies by region. Where the network operator publishes the location itself, that declaration replaces the database answer: DigitalOcean, Linode and Vultr publish RFC 8805 geofeeds (https://www.rfc-editor.org/rfc/rfc8805), Apple publishes the egress ranges of iCloud Private Relay with their cities, and Mullvad, NordVPN and Private Internet Access publish their server lists. The response marks these with `location_source: "operator"`, and `network` tells you how wide the answer is: an answer for a /12 is a region, an answer for a /24 is a neighbourhood.
When the operator's country or city disagrees with the database, the region and the coordinates answer null rather than a contradicting point. Coordinates in the response are the centre of the located area, never a visitor's position; do not draw a pin with them.
## Reading the lookup on first paint
From a server you already have the address; from a page you do not, so the [browser endpoint](/docs/api/lookup-ip-public) looks up the visitor's own address when `ip` is omitted, authenticated by a publishable key locked to your domains:
```js
const response = await fetch('https://spaw.co/api/v1/ip/public', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ key: 'pk_live_…' }),
});
const { data } = await response.json();
document.documentElement.dataset.consent = data.is_eu === false ? 'optional' : 'required';
if (data.currency) setDefaultCurrency(data.currency);
```
Cache the answer in session storage. The address does not change between pages, and a repeat of the same address inside seven days is free anyway. From a server, `GET /api/v1/ip/me` ([reference](/docs/api/get-my-ip)) answers for the address the call came from, which is the quickest way to confirm what your own egress looks like to the outside world.
## What is kept
An IP address is personal data. Nothing about an IP lookup is stored on the service: no history row, no log line with the address. The only trace is a keyed seven-day marker that makes a repeat free, and `privacy: true` skips even that. Every signal is a local database or a compiled feed, so the address never reaches a third-party processor unless you opt into `abuse_contact`, which queries the Internet registry's public RDAP service. The [security page](/security) states the same facts with the rest of the data handling, and the [IP intelligence guide](/docs/ip-intelligence) documents every field used above.
## What to do next
- Read the [IP intelligence guide](/docs/ip-intelligence) for the full field list and the operator-location rules.
- Put the [browser endpoint](/docs/api/lookup-ip-public) behind a publishable key with a daily cap before it goes on a public page.
- Use the same lookup to keep scripts out of your forms with [block datacenter and bot signups with one IP lookup](/guides/block-datacenter-and-bot-signups-with-an-ip-lookup).
---
# Disposable email domains and how detection works
Temporary inboxes live minutes. How the open disposable-domain list is built, why matching on MX hosts catches new burners, and what Spaw returns for one.
Date: 2026-09-03
Updated: 2026-09-13
A disposable email domain belongs to a service that hands out throwaway inboxes: you pick or receive an address, read the one message you were waiting for, and the mailbox expires minutes or hours later. Detection is a list problem with a DNS twist. An open list catches the domains people already know about, and matching a domain's mail servers against the same list catches the new ones, because a fresh burner domain still routes mail through an operator that is already listed.
## What is a disposable email domain?
Services such as Mailinator, Guerrilla Mail, 10 Minute Mail and hundreds of smaller clones run thousands of domains whose only purpose is to receive mail nobody intends to keep. Some inboxes are public: anyone who guesses the address can read it. Most expire on a timer. None of them belong to a customer who wants to hear from you.
The same infrastructure also powers legitimate uses: testing a signup flow, avoiding a newsletter you were forced to subscribe to, or protecting privacy from a site you do not trust. The address is real and accepts mail. It is simply not a relationship.
## Why do people use them?
Three motives dominate. Fraud and abuse: creating many accounts to farm free trials, coupons or votes. Privacy: not wanting a permanent address in yet another database. Friction: a site demanded an email address for something that did not need one. The third group is the largest, which is why the right response depends on what your form is for.
## How does detection work?
Spaw combines three layers, in this order.
**The open list.** The domain is matched against the `disposable/disposable-email-domains` project, an open dataset of tens of thousands of burner domains, synced weekly and unioned with a bundled seed so detection works before the first sync. Matching includes parent domains, so a subdomain a burner service hands out is caught by its parent.
**Local overrides.** Two small files in the repository correct the upstream: an allowlist for false positives, which always wins, and a denylist for burners the upstream has not caught yet. Either beats waiting for the next sync.
**MX host matching.** New burner domains appear every day, faster than any list can follow. What does not change as fast is the mail infrastructure behind them. Spaw resolves the domain's MX records and matches those hosts against the same list, so a domain registered this morning that routes mail through a listed operator's servers is caught on its first lookup.
| Layer | Catches | Misses |
| --- | --- | --- |
| Open list, with parent-domain matching | Known burner domains and their subdomains | Domains created since the last weekly sync |
| Allow and deny overrides | Corrections either way, effective immediately | Anything nobody has reported yet |
| MX host matching | New domains on known burner infrastructure | Burners on their own, unlisted mail servers |
Two more layers sit beside the list. A domain whose mail servers resolve into a network on the threat blocklists the IP product installs — the Spamhaus DROP lists and the abuse.ch botnet list — answers `mx_blocklisted: true` with its own reason, `mx_blocklisted`, whether or not any list has the domain yet: mail served from a hijacked netblock is a burner signal in itself. And the relay services are taken out of the burner list on purpose. Firefox Relay, SimpleLogin, Addy.io, DuckDuckGo Email Protection and their kind appear on the open list because they hand out addresses on demand, but an address there is a real person's real mailbox reached through a name they control and often kept for years. Spaw answers `is_relay: true` with the service in `relay_service`, forces `disposable` to false, sorts the domain as `domain_category: "relay"`, and adds 5 to the score for the withheld identity rather than the disposable 80.
## Why is the list never complete?
Burner operators register domains in bulk precisely to stay ahead of lists, and a domain that is on every list gets rotated out. That is why the third layer exists, and why a disposable check that only looks the domain up in a table will always run a week or more behind. It is also why an address that passed a disposable check at signup can be on the list a month later; a scheduled monitor re-verifies a list on a cadence and reports when its makeup changes.
## What does Spaw return for a disposable address?
The verdict is `risky` with `reason: "disposable"` and `disposable: true`. The mailbox is never probed, since spending a handshake on a burner would only confirm what the list already says, so `smtp_checked` is `false`. The disposable flag adds 80 to the risk score, the high band on its own.
```bash
curl https://spaw.co/api/v1/email \
-H "Authorization: Bearer sk_live_…" \
-H "Content-Type: application/json" \
-d '{"email": "disposable@spaw.test"}'
```
```json
{
"success": true,
"data": {
"email": "disposable@spaw.test",
"deliverable": "risky",
"reason": "disposable",
"risk_score": 90,
"risk_level": "high",
"disposable": true,
"domain_category": "disposable",
"smtp_checked": false,
"mailbox_exists": null,
"// 35 more fields": "see the endpoint reference"
},
"meta": { "credits_used": 0, "credits_remaining": 10, "cache_hit": false }
}
```
That test address is free. A real disposable answer is a risky answer and costs 1 credit, because it is a usable answer: you learned the address should not go on a list. A typo-squat domain that also sits on the disposable list is reported as `likely_typo` instead, since the correction is the more useful fact.
The domain endpoint answers the same question for a whole domain, with `disposable: true` or `false` and `is_relay`, alongside its category, provider and provider type, whether its mail hosts sit on a threat blocklist, its SPF, DMARC, DKIM, MTA-STS, TLS-RPT and BIMI records, and its registration age and expiry.
## Should you block disposable addresses?
It depends on what the address is for.
| Form | Recommendation |
| --- | --- |
| Free trial, credits, coupons, anything with value per account | Block, and say why. Ask for a different address. |
| Newsletter or marketing signup | Block, or accept and never send. Nobody reads a burner twice. |
| Purchase receipt or transactional flow | Accept. The customer chose where the receipt goes, and the message must be sent. |
| Contact or support form | Accept. Replying to a burner costs you nothing, and the sender chose the risk. |
| Existing list before a campaign | Remove. Burner domains attract spam-trap conversions and never engage. |
When you block, tell the user what happened and let them correct the address. The form helper that ships with Spaw runs the check on blur and shows the verdict inline without ever blocking the submit; blocking is a decision for your server.
## Is my domain wrongly listed?
It happens: an open list occasionally includes a small privacy-focused provider or a company that once hosted mail for a burner service. The domain endpoint shows the current answer and the dataset version that produced it. Spaw's allowlist fixes a confirmed false positive immediately, ahead of any upstream change; send the domain to support@spaw.co with a line about who runs it.
## What to do next
- Check a domain in the [free disposable domain checker](/tools/disposable-email-checker); each result has a permalink you can share.
- Read the [`disposable` reason page](/docs/reasons/disposable) for the exact contract, including why the mailbox is not probed.
- Ask the [domain intelligence endpoint](/docs/api/domain-intelligence) for the domain's provider, records and age in one call.
- Schedule a list monitor from the dashboard so burners that appear on the list later are caught before the next send.
---
# Email bounce rate benchmarks and what "acceptable" means
Published averages put hard bounces near 0.2% and soft bounces under 1% for opted-in lists. What mailbox providers enforce, and how to set your own target.
Date: 2026-09-03
Updated: 2026-10-02
For opted-in marketing lists, the published averages sit around 0.2% hard bounces and 0.7% soft bounces per send, with the worst industries still under 1% hard. Anything above 2% total bounces on a regular send is a list problem, and above 5% a reputation problem in progress. The mailbox providers do not publish a bounce threshold, but they publish a spam-complaint threshold of 0.3%, and the two move together: dead addresses, recycled traps and unengaged recipients are the same list.
## What do the published benchmarks say?
The most widely cited figures come from Mailchimp's platform data as analysed by Mailerio in 2025: across billions of emails the average hard bounce rate was [0.21% and the average soft bounce rate 0.70%](https://mailerio.com/blog/email-bounce-rate-benchmark/). By industry, the same analysis reports the lowest bounce rates for daily deals and e-coupon senders at 0.17% and the highest for software and web apps at 0.93%, as of the June 2025 publication.
| Figure | Value | Source |
| --- | --- | --- |
| Average hard bounce rate | 0.21% | [Mailerio's analysis of Mailchimp data, 2025](https://mailerio.com/blog/email-bounce-rate-benchmark/) |
| Average soft bounce rate | 0.70% | same |
| Lowest industry (daily deals and e-coupons) | 0.17% | same |
| Highest industry (software and web apps) | 0.93% | same |
Those numbers describe senders on a platform that already suppresses hard bounces automatically and enforces opt-in. A list that has never been verified, was collected years ago, or came from a partner will bounce at many times these rates on its first send, which is exactly the situation a benchmark is least useful for.
Other benchmark reports circulate with higher overall bounce figures, often in the 2% to 3% range across all industries. They are quoted widely but the source pages were not reachable for verification while this guide was written, so they are described here only qualitatively: they generally count all bounces together and include less strictly opted-in traffic, which explains most of the gap.
## What do the mailbox providers enforce?
Gmail and Yahoo publish requirements rather than benchmarks, and the requirements are what get mail blocked.
Google's [sender guidelines](https://support.google.com/a/answer/81126), in force since February 1, 2024, apply to anyone sending more than 5,000 messages a day to Gmail accounts: authenticate with SPF and DKIM, publish a DMARC policy, support one-click unsubscribe, and keep the spam rate reported in Postmaster Tools below 0.10%, never reaching 0.30% or higher. Yahoo's [sender requirements](https://senders.yahooinc.com/best-practices/) match: SPF and DKIM, a DMARC policy of at least `p=none` that passes, one-click unsubscribe, a spam rate below 0.3%, and, in their words, monitoring hard and soft bounces and removing invalid recipients promptly.
Neither publishes a numeric bounce threshold, but both treat bounce behaviour as a reputation input. A sender that keeps hitting `550 5.1.1` replies at a provider is doing what an address harvester does, and providers throttle or defer such senders long before any published number is crossed.
## Which tiers are used in practice?
Deliverability teams tend to work with tiers rather than a single number. One commonly quoted framing, published by [verified.email](https://verified.email/blog/email-deliverability/email-bounce-rate-benchmark) as its own synthesis of several reports, puts total bounces under 1% as excellent, 1% to 2% as acceptable, 2% to 5% as concerning, and above 5% as dangerous. Treat the tiers as a working convention rather than a measured fact; the measured facts are the platform averages above and your own history.
| Total bounce rate on a regular send | Reading |
| --- | --- |
| Under 1% | Healthy, opted-in, verified list |
| 1% to 2% | Normal for a list that has not been verified in a while |
| 2% to 5% | Clean the list before the next send |
| Above 5% | Stop, verify everything, and expect reputation repair to take weeks |
## Why do bounce rates rise?
Addresses die at a steady rate: people change jobs, companies rename domains, consumers abandon inboxes. A list that was perfect at collection decays every month it is not checked, and the decay is invisible until the next send turns it into bounces. Three sources dominate.
Typos at signup, which a form-time check catches before they enter the list. Job changes on B2B lists, which turn a confirmed corporate mailbox into a `550 5.1.1` within a year. And old consumer addresses that mailbox providers first disable and then, in some cases, recycle into spam traps, so that the address stops bouncing and starts hurting you instead.
## How do you measure your own rate correctly?
Count hard and soft bounces separately, per send, over the delivered attempts of that send. Exclude `5.7.x` policy rejections from the address side of the ledger, since those are about your authentication and reputation rather than the recipient; they belong in a different chart. Track the rate per segment and per acquisition source, because a 0.3% overall figure can hide a partner list bouncing at 8%.
Spaw's feedback loop makes the measurement part of the verification data. Post delivered and bounced outcomes from your sending platform, or connect its bounce webhook, and the feedback summary endpoint reports your outcomes crossed with the verdicts on file over the last 90 days: how many addresses verified as deliverable actually delivered, how many bounced anyway, and where the risky segment landed.
```bash
curl https://spaw.co/api/v1/email/feedback/summary \
-H "Authorization: Bearer sk_live_…"
```
The same reports change future answers. A bounce joins your suppression list, a delivery turns later lookups of that address into a confirmed mailbox, and the per-domain bounce rate feeds the confidence estimate for catch-all addresses at that domain.
## How do you get under the benchmark?
Verify before the first send and on a schedule after it. A bulk verification run removes the addresses that would hard bounce, and undeliverable answers cost nothing within a fair-use allowance, so the price of the run is the share of the list that is still good. Segment the risky verdicts rather than sending to them blindly, warm the catch-all segment in small batches, and keep a monitor on the list so decay shows up in a report instead of in a send.
Set your own target from your own history rather than from a published average. A B2B list that turns over 20% of contacts a year will never match a consumer newsletter's 0.2%, and does not need to; it needs a verification cadence that keeps each send under the tiers above.
## What to do next
- Run the list through the [cleaning checklist](/guides/email-list-cleaning-checklist) before the next campaign.
- Verify it with one [bulk job](/docs/api/create-bulk-job); undeliverable answers are free within a fair-use allowance.
- Connect your bounce webhook to the [feedback endpoint](/docs/api/report-delivery-feedback) and read your measured accuracy from the [summary endpoint](/docs/api/feedback-summary).
- Learn which replies count as hard bounces in [SMTP bounce codes explained](/guides/smtp-bounce-codes-explained).
---
# Email list cleaning checklist before a campaign
Ten steps in order: normalize and dedupe, import known bounces, verify in bulk, segment by verdict, authenticate, warm up, then keep it clean with monitors.
Date: 2026-09-03
Updated: 2026-10-02
Cleaning a list means removing addresses that will bounce, segmenting the ones that carry risk, and setting up the feedback that keeps the list clean after the send. Done in the right order it takes one bulk verification run and a few filters, and the order matters because every earlier step makes the later ones cheaper: duplicates and known bounces cost nothing to verify, so remove them first.
## 1. Normalize and dedupe
Lowercase every address, strip plus-tags, fold the dots out of Gmail usernames, and unwrap the forms people paste: display names, `mailto:` links, quotes, trailing punctuation. `Mia.K+promo@gmail.com` and `miak@gmail.com` are the same inbox. Spaw does this on every lookup and returns the result as `normalized_email`; bulk uploads count repeated normalized addresses as `duplicate_count` and verify them once.
## 2. Import what you already know is bad
If your sending platform has a bounce or unsubscribe export, load the hard bounces into your suppression list before verifying anything. Imports are free, take up to 10,000 addresses per request, and a suppressed address is answered without any work in every later batch, bulk and monitor run.
```bash
curl https://spaw.co/api/v1/email/suppressions \
-H "Authorization: Bearer sk_live_…" \
-H "Content-Type: application/json" \
-d '{"emails": ["old@acme.com", "gone@example.org"]}'
```
## 3. Verify the whole list in one bulk job
Upload the file as it is. A bulk job keeps your columns, appends the verdict columns, and answers up to 100,000 addresses per job with a signed webhook when it finishes. Duplicates, suppressed addresses and answers whose mailbox check was uncertain are free, undeliverable answers are free within a fair-use allowance, and you pay one credit per deliverable or risky answer.
```bash
curl https://spaw.co/api/v1/email/bulk \
-H "Authorization: Bearer sk_live_…" \
-H "Content-Type: application/json" \
-d '{"emails": ["mia@acme.com", "info@example.org"], "webhook_url": "https://www.example.com/hooks/spaw"}'
```
```json
{
"success": true,
"data": {
"job": {
"id": 512,
"status": "queued",
"total": 2,
"duplicate_count": 0,
"webhook_secret": "8fJ2…40 characters…Qk1",
"// 10 more fields": "see the endpoint reference"
}
}
}
```
Download the results as one CSV, or with `?variant=deliverable`, `risky` or `undeliverable` to get each segment as its own file.
## 4. Segment by verdict and reason
Three verdicts, and inside `risky` the reason decides the action.
| Verdict and reason | Action |
| --- | --- |
| `undeliverable`, any reason | Remove. Already on your suppression list. |
| `risky` · `disposable` | Remove from marketing sends. |
| `risky` · `likely_typo` | Fix with `did_you_mean` only if you can confirm with the contact; otherwise remove. |
| `risky` · `role` | Keep for B2B and transactional mail; exclude from promotional sequences. |
| `risky` · `catch_all` | Send in moderation; branch on `mailbox_confidence`. |
| `risky` · `mailbox_full` | Hold, retry in a few days. |
| `risky` · `implicit_mx` | Treat as probably dead unless the handshake confirmed the mailbox. |
| `risky` · `unverified` | The handshake could not confirm the mailbox. Send with care and branch on `mailbox_confidence`; a `callback_url` re-check settles many of these, billing 1 credit only when it confirms the mailbox. |
| `deliverable`, `mailbox_exists: true` | Send. |
The `risk_score` gives the same information as one number for tools that need a single threshold: 60 and above is high, 30 to 59 medium. Because the score is a published sum of weights, you can recompute it from the booleans in the CSV if your policy differs.
## 5. Decide the disposable and role policy per list
Marketing lists lose both. B2B lead lists keep role addresses and lose burners. Transactional recipients keep everything that is not undeliverable, because the message must be sent to the address the customer chose. Write the policy down; it is the difference between a clean list and a list that gets cleaned differently every quarter.
## 6. Check engagement, not just deliverability
A mailbox that exists but has not opened anything in a year is a complaint waiting to happen and, at some providers, a recycled spam trap waiting to be created. Set an engagement window that fits your cadence, move addresses past it into a re-permission segment, and stop sending to the ones that do not respond.
## 7. Authenticate before you send
SPF, DKIM and DMARC are required for bulk mail at Gmail and Yahoo, and a `550 5.7.x` reply from a receiver is about your authentication, not the recipient. Check your records before the campaign, and check the DMARC policy of your own domain is at least `p=none` with a reporting address.
## 8. Warm up the segments you are unsure about
Send the confirmed deliverable segment first, then the risky segments in small daily batches while watching bounces and complaints. A catch-all segment that bounces at 5% needs a lower confidence threshold, not a bigger batch.
## 9. Feed outcomes back
Point your sending provider's bounce webhook at Spaw, or post delivered and bounced events yourself. Hard bounces join the suppression list automatically, deliveries turn later lookups into confirmed mailboxes, and the per-domain bounce rate feeds the confidence score for every catch-all address at that domain. The dashboard shows measured accuracy: your outcomes crossed with the verdicts on file.
## 10. Keep it clean with a monitor
Lists decay. Schedule a monitor on the list from the dashboard and it re-verifies on a cadence, applies the suppression list, and alerts you when the deliverable share drops. Repeats of an address inside seven days are free, and undeliverable answers are free within a fair-use allowance, so the ongoing cost is the share of the list that is still good.
## What does it cost?
| Item | Credits |
| --- | --- |
| Duplicate of an address already in the run | 0 |
| Address on your suppression list | 0 |
| Risky verdict with reason `unverified` (the mailbox check could not say) | 0 |
| Undeliverable verdict, within the fair-use allowance | 0 |
| Undeliverable verdict past the fair-use allowance | 1 |
| Deliverable or risky verdict | 1 |
| Repeat of a charged lookup within 7 days | 0 |
| Monthly free grant, every account | +100 |
The fair-use allowance is 500 undeliverable answers that needed a mailbox check in any 30 days, or as many as the deliverable and risky answers billed in those 30 days when that is more. A 50,000-address list that is 20% dead and 5% duplicates costs at most 37,500 credits to verify once, plus 1 credit for any undeliverable answer past the allowance, and the monitor that keeps it clean costs only for addresses whose answer changes.
## What to do next
- Create the [bulk job](/docs/api/create-bulk-job) with your file as it is; the columns come back with the verdicts appended.
- Import your platform's bounce export into the [suppression list](/docs/api/add-suppressions) first.
- Decide the segment policies with the [role](/guides/role-based-email-addresses) and [disposable](/guides/disposable-email-domains) guides.
- Set targets using the [bounce rate benchmarks](/guides/email-bounce-rate-benchmarks) and the mailbox providers' own thresholds.
---
# How Spaw computes the risk score
The risk score is a sum of published weights over documented flags, capped at 100, with undeliverable fixed at 100. Every weight, thresholds, examples.
Date: 2026-09-03
Updated: 2026-10-08
Spaw's `risk_score` is not a model. It is the sum of fixed weights over the boolean flags already in the response, capped at 100, and an undeliverable verdict is always exactly 100. You can recompute it from the fields next to it, branch on the flags instead, or change the weights in your own code if your policy differs. This page lists what the code does, weight by weight.
## Which flags carry weight?
| Signal | Field it comes from | Points |
| --- | --- | --- |
| Disposable domain (`disposable`) | `disposable: true` | +80 |
| Parked or for-sale domain that still accepts mail (`parked_domain`) | `parked: true` | +60 |
| Known typo-squat domain (`likely_typo`) | `reason: "likely_typo"` | +60 |
| Mail served from a blocklisted network (`mx_blocklisted`) | `mx_blocklisted: true` | +60 |
| Domain has no MX record, only the address-record fallback (`implicit_mx`) | `mx_implicit: true` | +30 |
| Role address (`role`) | `role: true` | +30 |
| Catch-all server (`catch_all`) | `catch_all: true`, not behind a security gateway | +30 |
| Domain registered less than 30 days ago (`young_domain`) | `domain_age_days` under 30 | +30 |
| Mailbox that was dead and exists again (`recycled_mailbox`) | `previously_invalid: true` | +30 |
| Mailbox full (`mailbox_full`) | `smtp_reason: "mailbox_full"` | +20 |
| Gibberish username (`gibberish`) | `is_gibberish: true` | +20 |
| Machine-made or placeholder username (`generated`) | `looks_generated: true` | +20 |
| No-reply address, on top of the role row (`no_reply`) | `role_type: "no_reply"` | +20 |
| Looked up by three or more other accounts in a day (`widely_seen`) | `widely_seen: true` | +20 |
| Catch-all behind a security gateway, instead of the catch-all row (`catch_all_gateway`) | `catch_all: true` and `mx_provider_type: "security_gateway"` | +15 |
| Domain registered less than 180 days ago (`new_domain`) | `domain_age_days` under 180 | +10 |
| No SPF record on a business domain (`no_spf`) | `has_spf: false` and `free_provider: false` | +10 |
| Mailbox not SMTP-verified (`unverified`) | `smtp_checked: false` | +10 |
| Registration ending within 30 days (`expiring_domain`) | `domain_expires_at` within 30 days | +10 |
| Plus-tag alias (`alias`) | `is_alias: true` | +5 |
| Relay or forwarding service (`relay`) | `is_relay: true` | +5 |
The age rows are exclusive: a domain under 30 days gets 30, one between 30 and 179 days gets 10, older domains and unknown ages get nothing. The two catch-all rows are exclusive too: behind Proofpoint, Mimecast or Barracuda a catch-all is the gateway's design rather than a squat, so it weighs 15 instead of 30. The no-reply row is added on top of the role row, because a no-reply address is a role address: 50 in all. A relay address (Firefox Relay, SimpleLogin, Addy.io, DuckDuckGo Email Protection) is never disposable — it is a real person's real mailbox reached through a name they control — so it carries 5 for the withheld identity and never the 80. The SPF row only applies to business domains, because a Gmail user's risk has nothing to do with Google's SPF record. The sum is capped at 100, and `risk_signals` lists exactly the rows that fired, each with its weight and the dataset it was read from; an undeliverable verdict lists nothing and answers 100.
## What are the levels?
| `risk_score` | `risk_level` |
| --- | --- |
| 60 and above | `high` |
| 30 to 59 | `medium` |
| 0 to 29 | `low` |
An undeliverable verdict skips the sum entirely and reports 100 and `high`.
## Worked examples
| Address and situation | Flags | Score |
| --- | --- | --- |
| `mia@acme.com`, mailbox confirmed, old domain with SPF | none | 0, low |
| `support@acme.com`, mailbox confirmed | role 30 | 30, medium |
| `mia+news@acme.com`, catch-all server | catch-all 30, alias 5 | 35, medium |
| `mia@newstartup.example`, registered 20 days ago, no SPF, greylisted | young domain 30, no SPF 10, unverified 10 | 50, medium |
| `mia@gmail.con` | typo-squat 60 | 60, high |
| `x9@mailinator.com` | disposable 80, unverified 10 | 90, high |
| `qzxvbn@acme.com`, catch-all server, no SPF | gibberish 20, catch-all 30, no SPF 10 | 60, high |
| `noreply@acme.com`, mailbox confirmed | role 30, no-reply 20 | 50, medium |
| `mia@acme.com`, catch-all behind Mimecast, old domain with SPF | catch-all behind a gateway 15 | 15, low |
| `mia@old-brand.example`, parked domain that still accepts mail | parked domain 60 | 60, high |
| `x@relay.firefox.com`, catch-all server | catch-all 30, relay 5 | 35, medium |
| `nobody@acme.com`, server says no such user | undeliverable | 100, high |
Disposable domains are never probed, so the unverified weight always joins the disposable one; that is why the disposable test address answers 90 rather than 80.
## What counts as gibberish?
The check is deliberately conservative, because a false positive here insults a real person. A username is only considered when it has six or more letters. Anything the name dictionary recognises, a first name, a surname, `first.last`, an initial plus a surname, or the two run together, is never gibberish. Beyond that, a username is flagged if it contains no vowel at all, with `y` counted as a vowel to protect names like Krzysztof, or if it contains a run of six consonants. Digit-only usernames are never flagged; numeric IDs at large Chinese providers are ordinary.
## How does the verdict interact with the score?
The verdict comes first and is decided by precedence, not by the score. Undeliverable reasons win over everything: null MX, unresolvable MX, no mail server, an impossible username, a disabled mailbox, a missing mailbox, in that order. Among risky reasons the order is typo-squat, parked domain, disposable, blocklisted mail network, implicit MX, mailbox full, no-reply, role, recycled mailbox, catch-all, unverified. The score then adds up every flag that applies, so an address reported as `role` on a catch-all server carries both weights even though `reason` names only one.
```bash
curl https://spaw.co/api/v1/email \
-H "Authorization: Bearer sk_live_…" \
-H "Content-Type: application/json" \
-d '{"email": "mia@acme.com"}'
```
```json
{
"success": true,
"data": {
"email": "mia@acme.com",
"deliverable": "deliverable",
"reason": null,
"risk_score": 0,
"risk_level": "low",
"is_alias": false,
"is_gibberish": false,
"disposable": false,
"role": false,
"catch_all": false,
"has_spf": true,
"free_provider": false,
"domain_age_days": 10402,
"smtp_checked": true,
"risk_signals": [],
"// 31 more fields": "see the endpoint reference"
},
"meta": { "credits_used": 1, "credits_remaining": 9, "cache_hit": false }
}
```
Every flag the score reads is in that response, which is what makes it recomputable.
## How is mailbox_confidence different?
The risk score answers "how risky is sending here". `mailbox_confidence` answers a narrower question, "does this specific mailbox exist", and only when the handshake could not say: catch-all domains and unanswered probes. It is null for confirmed mailboxes and for undeliverable verdicts.
It starts at 50 and moves by fixed amounts.
| Signal | Adjustment |
| --- | --- |
| Delivery outcomes every account reported for the domain, five or more: bounce rate 50% or higher | −20 |
| Delivery outcomes every account reported for the domain, five or more: bounce rate 10% or lower | +15 |
| DMARC policy reject or quarantine | +15 |
| DMARC policy none | +5 |
| SPF record present | +10 |
| SPF record absent | −10 |
| Mail hosted at Google, Microsoft, Proofpoint, Mimecast, Barracuda, Zoho or Fastmail | +10 |
| Username recognised as a name | +15 |
| Username flagged as gibberish | −25 |
| Role address | +5 |
| Domain registered under 90 days ago | −15 |
| Domain registered two years ago or more | +10 |
| Plus-tag alias | −10 |
| Username follows the naming convention seen in the domain's confirmed deliveries (first.last, flast, …) | +15 |
| Username is name-shaped but breaks that convention | −10 |
The result is clamped to 0 to 100. The bounce-rate rows and the naming-convention rows read delivery outcomes reported through the API and the provider webhooks, pooled across accounts as counts per domain — never an address, and never a report typed into a dashboard.
A relay address (`is_relay: true`) is the exception. It stays at 50 and moves only on the evidence against it: the bounce rate of 50% or more (−20) and the plus-tag (−10). The relay's records describe the operator rather than the alias, and an alias's name, whether the relay generated it or its owner chose it, says nothing about whether it exists, so the other rows do not apply.
## The score is not the decision
`ok_to_send` on every answer is. It comes from your account's sending policy, three optional rules applied after the verdict: the signals that block whenever they fire, a ceiling on the score, and a floor for a catch-all or unverified mailbox's confidence. An undeliverable verdict never passes, and `blocked_by` names the rule that stopped the rest: `undeliverable`, the blocking signal's own name, `risk_score` or `mailbox_confidence`. Until you set a policy the defaults apply — the disposable, typo-squat, parked-domain, blocklisted-network and no-reply signals block, so does any score over 60 — and `GET`, `PUT` and `DELETE /api/v1/email/policy`, or the Sending policy tab on the verification page, change them. Branch on `ok_to_send` when one decision is what you need, and keep reading the flags when it is not.
## Every signal has a page
Each weight in the table is a signal with its own reference page — what it reads, the dataset behind it, the verdict reason it comes with and what to do about it — at `/docs/signals/{signal}`, and the index at [/docs/signals](/docs/signals) lists all 21 in the published order: [disposable](/docs/signals/disposable), [parked_domain](/docs/signals/parked_domain), [likely_typo](/docs/signals/likely_typo), [mx_blocklisted](/docs/signals/mx_blocklisted), [implicit_mx](/docs/signals/implicit_mx), [role](/docs/signals/role), [catch_all](/docs/signals/catch_all), [young_domain](/docs/signals/young_domain), [recycled_mailbox](/docs/signals/recycled_mailbox), [mailbox_full](/docs/signals/mailbox_full), [gibberish](/docs/signals/gibberish), [generated](/docs/signals/generated), [no_reply](/docs/signals/no_reply), [widely_seen](/docs/signals/widely_seen), [catch_all_gateway](/docs/signals/catch_all_gateway), [new_domain](/docs/signals/new_domain), [no_spf](/docs/signals/no_spf), [unverified](/docs/signals/unverified), [expiring_domain](/docs/signals/expiring_domain), [alias](/docs/signals/alias) and [relay](/docs/signals/relay).
This page is about email. A phone answer adds up its own `risk_score` from a separate set of weights, and a name can weigh differently there: `disposable` is +80 on an email answer and +70 on a phone one. The phone weights are listed under [/docs/signals#phone](/docs/signals#phone), each with its own page at `/docs/signals/phone/{signal}`, such as [reported_undelivered](/docs/signals/phone/reported_undelivered), which fires when your own latest delivery report for the number was a failure.
## How should you use the score?
Branch on the flags when you have a policy, and on the score when you need one threshold. A typical signup form rejects undeliverable, asks for another address on disposable, and accepts the rest. A typical campaign sends to scores under 30, holds 30 to 59 for a warm-up batch, and drops 60 and above. If your bounce data says the weights are wrong for your audience, recompute the score with your own weights from the same flags; nothing about the API assumes you use its number.
## What to do next
- Read the [verdict reasons](/docs/reasons) to see which flag each reason sets and what it costs.
- Wire your bounce webhook into the [feedback endpoint](/docs/api/report-delivery-feedback) so the confidence score learns from your own sends.
- See how the catch-all case plays out in [what a catch-all address is](/guides/catch-all-email-addresses).
- Check the authentication rows against your own domain with [SPF, DKIM and DMARC for developers](/guides/spf-dkim-dmarc-for-developers).
---
# How to verify an email address without sending an email
Parse the address, resolve the domain's mail servers, then ask the server about the mailbox in an SMTP handshake that ends before any message is sent.
Date: 2026-09-03
Updated: 2026-10-02
You can verify an email address without sending anything by doing exactly what a sending mail server does up to the moment it would transmit the message: parse the address, look up the domain's mail servers in DNS, open an SMTP session, ask whether the recipient exists, and disconnect. The server's reply to that question is the verification. No message is transmitted, nothing lands in an inbox or a spam folder, and the recipient never knows.
## What happens in an SMTP handshake?
SMTP is a conversation. A sending server connects to one of the recipient domain's mail exchangers on port 25 and the two exchange a fixed sequence of commands before any content moves. The recipient check happens at the `RCPT TO` step, which is why a verifier can stop right after it.
| Step | Command | What the reply tells a verifier |
| --- | --- | --- |
| 1 | connect | Whether the host answers on port 25 at all |
| 2 | `EHLO` | Which extensions the server supports; nothing about the mailbox yet |
| 3 | `MAIL FROM` | Whether the server accepts the sender at all |
| 4 | `RCPT TO` | Whether the server accepts mail for this exact recipient: `250` yes, `550` no, `4xx` come back later |
| 5 | `QUIT` | The verifier leaves without ever sending `DATA` |
A real message only starts with `DATA`, the step a verifier never issues. Everything before it is a question, and questions do not generate bounces or deliveries.
## Why not just send a test email?
Sending a test message tells you less than a handshake and costs more. A test to a dead address produces a bounce, and bounces are the metric mailbox providers use to judge your reputation. A test to a spam trap gets your domain listed. A test to a real person is an unsolicited message. And the answer arrives asynchronously, minutes or hours later, in the form of a bounce notification you then have to parse.
The handshake answers in a second or two, synchronously, and leaves no trace on the receiving side beyond a log line.
## Which checks come before the handshake?
Most addresses are settled before any SMTP connection is opened, which is cheaper and often more decisive.
| Check | What it catches | Spaw result |
| --- | --- | --- |
| RFC 5322 syntax | Missing `@`, spaces, malformed domains | `invalid_syntax` |
| Provider username rules | Usernames Gmail, Outlook, Yahoo, iCloud, AOL or Proton would never issue | `invalid_local_part` |
| DNS: MX and address records | Domains with no mail server, a null MX, or MX targets that do not resolve | `no_mx_records`, `null_mx`, `mx_unresolvable` |
| Implicit MX | Domains with only an A record, which almost never accept mail | `implicit_mx` |
| Disposable list | Temporary inbox providers, including new burner domains routed through a listed operator's mail hosts | `disposable` |
| Role list | Shared inboxes such as `info@` or `billing@` | `role` |
| Typo-squat map | `gmail.con`, `hotmial.com` and similar, with a suggested correction | `likely_typo` |
Every one of those is a live DNS query or an open list lookup. The DNS part matters more than it looks: a domain whose MX records point at loopback or private addresses will never receive mail, and a domain publishing a null MX record has declared that it does not want any. Both are answered without a handshake, and Spaw answers them for free.
## What can the handshake not tell you?
Four things, and an honest verifier reports each of them instead of guessing.
A **catch-all server** answers `250` to every recipient, so the reply proves nothing about the specific mailbox. Corporate domains behind security gateways and many Microsoft 365 tenants work this way. Spaw reports `catch_all: true` with a risky verdict and adds `mailbox_confidence`, a recomputable estimate built from the domain's other signals.
**Greylisting** answers the first connection from an unknown sender with a temporary failure and expects a retry minutes later. A verifier cannot wait, so the mailbox stays unverified.
**Timeouts and refused connections** happen when a server slows sessions down on purpose, or when a firewall sits in front of it.
**Providers that hide the answer.** Some large providers accept every `RCPT TO` and reject later, or throttle verification traffic aggressively. Their addresses come back unverified more often than others.
In the last three cases Spaw returns `smtp_checked: false`, `mailbox_exists: null`, an `smtp_reason` naming the cause, and a `risky` verdict with reason `unverified`, so an unconfirmed mailbox is never read as deliverable; the checks above still decide the risk score and the confidence estimate. An unverified answer is free, whatever the cause, including Spaw's own handshake partner being unavailable. You can ask for a second attempt: pass `callback_url` and the address is re-checked after 5 and 20 minutes, with the settled verdict pushed to you; a re-check bills 1 credit only when it confirms the mailbox.
## How to do it with the Spaw API
One request, one credit for a deliverable or risky answer, nothing for an unverified one, and nothing for an undeliverable one within a fair-use allowance.
```bash
curl https://spaw.co/api/v1/email \
-H "Authorization: Bearer sk_live_…" \
-H "Content-Type: application/json" \
-d '{"email": "mia@acme.com"}'
```
```json
{
"success": true,
"data": {
"email": "mia@acme.com",
"deliverable": "deliverable",
"reason": null,
"risk_score": 0,
"mx_found": true,
"mx_provider": "google",
"smtp_checked": true,
"mailbox_exists": true,
"catch_all": false,
"smtp_reason": null,
"// 16 more fields": "see the endpoint reference"
},
"meta": { "credits_used": 1, "credits_remaining": 9, "cache_hit": false }
}
```
The same lookup runs behind the free email checker, the dashboard, the batch endpoint for up to 50 addresses, and bulk jobs for up to 100,000. Six fixed addresses at `spaw.test`, such as `undeliverable@spaw.test`, answer canonical verdicts at no cost for integration tests.
## How to do it yourself
The handshake is simple enough to type by hand, which is the best way to understand it.
```text
$ nc aspmx.l.google.com 25
220 mx.google.com ESMTP
EHLO verifier.example.com
250-mx.google.com at your service
MAIL FROM:
250 2.1.0 OK
RCPT TO:
550-5.1.1 The email account that you tried to reach does not exist.
QUIT
221 2.0.0 closing connection
```
Doing this at scale is where the difficulty starts. Most cloud providers block outbound port 25 by default. Receiving servers check the connecting IP's reverse DNS and reputation, and an IP that opens thousands of sessions without ever sending mail looks like an address harvester and gets throttled or blocked. Greylisting needs retry queues, catch-all detection needs per-domain memory, and every provider has its own quirks. That is the work a verification service does for you, and it is why Spaw runs the handshake through partner infrastructure with the guards described above rather than from the same machines that serve the API.
## What to do next
- Try an address in the [free email checker](/tools/email-checker) and read the `reason` and `smtp_reason` fields against the [verdict reference](/docs/reasons).
- Verify a list before a campaign with the [batch](/docs/api/verify-email-batch) or [bulk](/docs/api/create-bulk-job) endpoint; undeliverable answers are free within a fair-use allowance.
- Report delivery outcomes back through the [feedback endpoint](/docs/api/report-delivery-feedback) so catch-all and unverified addresses at your domains get settled by real deliveries.
- Read [what a catch-all address is](/guides/catch-all-email-addresses) before deciding how to treat risky verdicts.
---
# Role-based email addresses and how to treat them
info@, support@, billing@ and about a thousand more belong to a function, not a person. How Spaw detects them, why senders flag them, and when to keep them.
Date: 2026-09-03
Updated: 2026-10-02
A role-based email address is one whose username names a job or a group rather than a person: `info@`, `support@`, `sales@`, `billing@`, `admin@`, `noreply@`. The mailbox is usually real and usually monitored, but by a rotating set of people, through filters and forwarding rules you cannot see, which is why marketing senders treat it differently from `mia@`.
## What is a role-based address?
The distinction is purely about the part before the `@`. A role address is addressed to a function. Mail to `support@acme.com` goes to whoever is on the support rota this week; mail to `mia.k@acme.com` goes to Mia. The domain, the mail server and the handshake are identical for both.
Because the local part is the only signal, detection is a list lookup. Spaw uses the open role-address list maintained by Mixmax, synced weekly and unioned with a bundled seed, and matches the lowercased username exactly. The list has grown past a thousand entries, from the obvious `info` and `sales` to `abuse`, `postmaster`, `accessibility`, `hr`, `jobs`, `press`, `security` and dozens of localized variants.
| Group | Typical usernames |
| --- | --- |
| Front door | info, hello, contact, enquiries, office, mail |
| Sales and marketing | sales, marketing, partnerships, press, media |
| Support and operations | support, help, helpdesk, service, ops, it |
| Money | billing, invoices, accounts, finance, payments |
| Technical and abuse | admin, administrator, webmaster, postmaster, abuse, security, noc |
| People | hr, jobs, careers, recruiting, team |
| Machines | noreply, no-reply, donotreply, mailer-daemon, bounce, notifications |
Every role address also carries `role_type`, which names the family: `no_reply`, `postmaster` (the RFC 2142 set — postmaster, abuse, hostmaster, webmaster, security), `support`, `sales`, `info`, `admin`, `billing`, `marketing`, `careers`, `legal`, or `other` for a listed username outside those. The machines row is the one that changes the verdict: a no-reply shape is recognised by pattern even when the list lacks the spelling, forces `role: true`, and answers its own reason, `no_reply`, because mail to it is discarded by design.
## Why do senders treat role addresses differently?
Four practical reasons, all of them about outcomes rather than the address itself.
**Nobody opted in.** A person who types `sales@` into your form is often not the person who will read your mail, and the people who do read it did not ask for it. That is the setup for spam complaints, and complaints are the metric mailbox providers weigh most heavily.
**They churn silently.** Role inboxes are forwarded, split, auto-replied and abandoned as teams change. The address keeps accepting mail long after anyone reads it, so engagement drops without a bounce to warn you.
**They are shared.** One person unsubscribing does not stop the others from seeing the next message, and one person reporting spam counts against you regardless.
**Sending platforms police them.** Several email service providers refuse role addresses on list import, hold them for review, or exclude them from campaigns by default. A list heavy with `info@` addresses can fail an import before you send anything.
None of this means role addresses bounce more. Many are the most reliable mailboxes at a company. The problem is consent and engagement, not deliverability.
## What does Spaw return for a role address?
The verdict is `risky` with `reason: "role"` and `role: true`, and the mailbox is still probed, so `mailbox_exists` can be `true`. The role flag adds 30 to the risk score, which is the medium band on its own. A no-reply address answers `reason: "no_reply"` with `role_type: "no_reply"` instead, and carries 20 on top of the role's 30, because nobody reads it.
```bash
curl https://spaw.co/api/v1/email \
-H "Authorization: Bearer sk_live_…" \
-H "Content-Type: application/json" \
-d '{"email": "role@spaw.test"}'
```
```json
{
"success": true,
"data": {
"email": "role@spaw.test",
"deliverable": "risky",
"reason": "role",
"risk_score": 30,
"risk_level": "medium",
"role": true,
"role_type": "other",
"smtp_checked": true,
"mailbox_exists": true,
"// 35 more fields": "see the endpoint reference"
},
"meta": { "credits_used": 0, "credits_remaining": 10, "cache_hit": false }
}
```
`role@spaw.test` is a fixed test address and free. A real role address is billed like any risky answer: 1 credit, repeats free for seven days.
Precedence matters when several signals apply. A role address at a disposable domain is reported as `disposable`, a no-reply address is reported as `no_reply` ahead of `role`, and a role address on a catch-all server is reported as `role` because the role flag ranks above catch-all in Spaw's verdict order; both flags still appear as booleans, both weights are added to the score, and `risk_signals` lists each one.
## When should you keep role addresses?
Keep them wherever the function is the point.
| Context | Treatment |
| --- | --- |
| B2B contact and quote forms | Accept. `procurement@` is exactly who you want. |
| Support ticket systems | Accept. Role inboxes are the normal case. |
| Transactional mail: receipts, invoices, alerts | Accept. `billing@` is the correct recipient for an invoice. |
| Newsletter and marketing signups | Ask for a personal address, or accept and segment out of promotional sends. |
| Purchased or scraped lists | Drop. Role addresses on a cold list are the fastest route to complaints. |
| Cold outreach sequences | Drop or route to a manual step. Personalized sequences to `info@` read as spam. |
A useful rule for signup forms: accept the address, since blocking `info@` at a B2B company loses real leads, but mark the contact as role-based and keep it out of drip campaigns until a person replies.
## How do you find role addresses in an existing list?
Verify the list. Every answer carries `role: true` or `false`, so a bulk run gives you the segment as a column, alongside the verdict and the risk score. From the dashboard, upload the file and filter the results; from the API, create a bulk job and download the CSV with the verdict columns added to your own.
```bash
curl https://spaw.co/api/v1/email/bulk \
-H "Authorization: Bearer sk_live_…" \
-H "Content-Type: application/json" \
-d '{"emails": ["info@acme.com", "mia@acme.com"], "webhook_url": "https://www.example.com/hooks/spaw"}'
```
The job answers `202` with an id and a signing secret shown once; the completion webhook is signed with it. Undeliverable answers in the run are free within a fair-use allowance, and addresses already on your suppression list are answered without any work.
## What to do next
- Read the [`role` reason page](/docs/reasons/role) for the exact fields and precedence.
- Run your signup list through the [batch endpoint](/docs/api/verify-email-batch) and branch on `role` before the next campaign.
- Follow the [email list cleaning checklist](/guides/email-list-cleaning-checklist) to decide what else to segment out.
- Check a single address in the [free email checker](/tools/email-checker).
---
# SMTP bounce codes explained, from 550 5.1.1 to 421 4.7.0
What each SMTP reply and enhanced status code means, which are hard bounces, and how a verification handshake maps them to smtp_reason values.
Date: 2026-09-03
Updated: 2026-10-02
An SMTP bounce code is the three-digit reply a receiving mail server gives when it refuses a message, usually followed by a dotted enhanced status code that says why. `550 5.1.1` means the mailbox does not exist and is a hard bounce; `452 4.2.2` means the mailbox is full and is a soft bounce; `421 4.7.0` means the server is throttling you and says nothing about the mailbox at all. Reading the second number is what turns a bounce log into a decision.
## How are SMTP reply codes structured?
[RFC 5321](https://www.rfc-editor.org/rfc/rfc5321) defines the three-digit reply. The first digit is the verdict, the second the category, the third a detail.
| First digit | Meaning | What a sender does |
| --- | --- | --- |
| 2xx | Success | Continue |
| 3xx | More input needed (only `354` after `DATA`) | Send the content |
| 4xx | Transient failure | Queue and retry later |
| 5xx | Permanent failure | Stop and bounce |
The second digit narrows it: `x0x` syntax, `x1x` informational, `x2x` connection, `x5x` mail system. In practice the second digit of the plain code is coarse, which is why almost every modern server appends an enhanced status code.
## What are enhanced status codes?
[RFC 3463](https://www.rfc-editor.org/rfc/rfc3463) adds a `class.subject.detail` triple after the reply code, and it is the part worth parsing.
| Class | Meaning |
| --- | --- |
| 2.x.x | Success |
| 4.x.x | Persistent transient failure: retrying may succeed |
| 5.x.x | Permanent failure: retrying will not help |
| Subject | Area |
| --- | --- |
| x.0.x | Other or undefined |
| x.1.x | Addressing: the recipient or sender address |
| x.2.x | Mailbox: full, disabled, over quota |
| x.3.x | Mail system: the receiving system itself |
| x.4.x | Network and routing: cannot reach the next hop |
| x.5.x | Mail delivery protocol: bad commands or sequence |
| x.6.x | Message content or media |
| x.7.x | Security or policy: authentication, spam, rate limits |
A `5.1.1` is therefore a permanent addressing failure, and `4.7.1` a transient policy failure. The subject tells you whether the problem is the address, the mailbox, the network or your own reputation, and that is the axis a list-hygiene decision turns on.
## Which codes will you actually see?
| Reply | Meaning | Bounce type | What it says about the address |
| --- | --- | --- | --- |
| `250 2.1.5` | Recipient accepted | none | The server will take mail for it; on a catch-all server that is all it proves |
| `550 5.1.1` | Bad destination mailbox address, "user unknown" | hard | The mailbox does not exist. Remove the address |
| `550 5.1.2` | Bad destination system address | hard | The domain part is wrong or unroutable |
| `550 5.1.3` | Bad destination mailbox syntax | hard | The address itself is malformed |
| `550 5.2.1` | Mailbox disabled, not accepting messages | hard | The account exists but is closed or suspended |
| `552 5.2.2` | Mailbox full, permanent | hard | Over quota and not expected to clear; treat as risky at best |
| `452 4.2.2` | Mailbox full, transient | soft | Over quota right now; retry in days |
| `450 4.2.1` | Mailbox temporarily unavailable | soft | Often a hold or migration; retry |
| `450 4.7.1` or `451 4.7.1` | Try again later, policy | soft | Greylisting: the server wants a retry from the same sender |
| `421 4.7.0` | Too many connections or messages | soft | Throttling of the sender; nothing about the address |
| `421 4.3.2` | System not accepting network messages | soft | The server is shutting the session; retry later |
| `451 4.4.1` or `4.4.2` | Connection or transmission failure | soft | Network trouble on the receiving side |
| `550 5.7.1` | Delivery not authorized, message refused | hard for this message | A policy rejection: blocklist, reputation, missing authentication. The mailbox may be fine |
| `550 5.7.26` | Unauthenticated email is not accepted | hard for this message | The sender failed SPF or DKIM alignment; fix authentication, not the list |
Two rules of thumb follow. A `5.1.x` or `5.2.1` reply is about the address and justifies removing it. A `5.7.x` reply is about you, the sender, and removing the address would hide a problem you need to fix.
## What is the difference between a hard and a soft bounce?
A hard bounce is a permanent `5xx` failure about the recipient: the mailbox or domain does not exist. Sending platforms suppress hard-bounced addresses automatically and mailbox providers count repeated hard bounces against your reputation. A soft bounce is a transient `4xx` failure: the mailbox is full, the server is busy, the message is too large. Senders retry soft bounces for a day or two and then give up.
The line blurs at the edges. A `552 5.2.2` is permanent by class but describes a full mailbox, and a `550 5.7.1` is permanent for this message but says nothing about the recipient. Classifying purely by the first digit gets both wrong, which is why the enhanced code matters.
## How does a verification handshake use these codes?
A verifier never sends `DATA`, so it only sees replies up to `RCPT TO`. That is enough to classify the mailbox, and Spaw reports the classification in `smtp_reason` next to the verdict.
| Server reply during the handshake | `smtp_reason` | Verdict |
| --- | --- | --- |
| `5.1.1` user unknown at `RCPT TO` | `no_mailbox` | `undeliverable`, reason `mailbox_not_found` |
| `5.2.1` mailbox disabled | `disabled` | `undeliverable`, reason `mailbox_disabled` |
| `5.2.2` or `4.2.2` mailbox full | `mailbox_full` | `risky`, reason `mailbox_full` |
| `4.7.x` come back later | `greylisted` | `risky`, reason `unverified`, `smtp_checked: false` |
| No reply within the time limit | `timeout` | `risky`, reason `unverified` |
| Connection refused on port 25 | `no_connect` | `risky`, reason `unverified` |
| Host unreachable | `mx_unreachable` | `risky`, reason `unverified` |
| `5.7.x` policy rejection or any ambiguous refusal | unverified, never guessed | `risky`, reason `unverified` |
Only an explicit "no such user" becomes `no_mailbox`. A policy rejection during verification is not evidence about the mailbox, so Spaw leaves `mailbox_exists` at `null` rather than turning a reputation problem on the verifier's side into a bounce prediction for you.
```bash
curl https://spaw.co/api/v1/email \
-H "Authorization: Bearer sk_live_…" \
-H "Content-Type: application/json" \
-d '{"email": "undeliverable@spaw.test"}'
```
```json
{
"success": true,
"data": {
"email": "undeliverable@spaw.test",
"deliverable": "undeliverable",
"reason": "mailbox_not_found",
"risk_score": 100,
"smtp_checked": true,
"mailbox_exists": false,
"smtp_reason": "no_mailbox",
"// 20 more fields": "see the endpoint reference"
},
"meta": { "credits_used": 0, "credits_remaining": 10, "cache_hit": false }
}
```
Undeliverable answers are free within a fair-use allowance, and the address is added to your suppression list so batch and bulk runs answer it without another handshake.
## What should you do with bounce codes from your own sends?
Feed them back. The codes your sending platform records after a real delivery attempt are stronger evidence than any handshake, in both directions: a delivery proves the mailbox, a `5.1.1` disproves it. Post the outcome to the feedback endpoint with the diagnostic string, or point your provider's bounce webhook at Spaw and let the adapters for Postmark, Amazon SES, Mailgun and SendGrid map their event types. Hard bounces join your suppression list, deliveries turn later lookups into confirmed mailboxes, and the per-domain bounce rate feeds the confidence score for catch-all addresses.
## What to do next
- Map your bounce log against the `smtp_reason` values on the [mailbox-check details page](/docs/reasons#smtp).
- Verify addresses before sending with the [handshake described here](/guides/verify-email-without-sending); the test address `undeliverable@spaw.test` shows the exact shape of a hard-bounce answer.
- Send your provider's bounce events to the [feedback endpoint](/docs/api/report-delivery-feedback) so `5.1.1` replies from real sends suppress addresses automatically.
- Treat every `5.7.x` reply as a sender problem and check your [SPF, DKIM and DMARC](/guides/spf-dkim-dmarc-for-developers) before touching the list.
---
# SPF, DKIM and DMARC for developers
What each record proves, what it looks like in DNS, how receivers combine them, and what Gmail and Yahoo require from bulk senders since 2024.
Date: 2026-09-03
Updated: 2026-10-08
SPF says which servers may send mail for a domain, DKIM signs each message so a receiver can prove it was not altered and came from the signing domain, and DMARC tells receivers what to do when neither check lines up with the address the recipient sees. All three are DNS TXT records, all three are checked on every delivery by Gmail, Yahoo and Microsoft, and since February 2024 the large mailbox providers refuse bulk mail that lacks them.
## What does SPF prove?
SPF, defined in [RFC 7208](https://www.rfc-editor.org/rfc/rfc7208), lists the hosts allowed to send mail using a domain in the SMTP envelope sender, the `MAIL FROM` address. The record is a TXT record at the domain itself.
```text
acme.com. IN TXT "v=spf1 ip4:203.0.113.0/24 include:_spf.google.com -all"
```
| Mechanism | Matches |
| --- | --- |
| `ip4:` / `ip6:` | A literal address or range |
| `a` / `mx` | The domain's own A records or MX hosts |
| `include:` | The SPF record of another domain, typically a sending service |
| `all` | Everything else; its qualifier decides the default |
| Qualifier | Result for a non-matching sender |
| --- | --- |
| `+` (default) | pass |
| `~` | softfail: accept but mark |
| `-` | fail: reject or quarantine |
| `?` | neutral: no opinion |
Two limits catch people out. A domain may publish only one SPF record; two records mean SPF fails for everyone. And evaluation may perform at most ten DNS lookups, counting every `include:`, `a`, `mx` and `redirect`. Large stacks of third-party senders hit that limit and silently break.
SPF checks the envelope sender, not the `From:` header the recipient sees. Forwarding breaks it, because the forwarding server is not in the original domain's record. Both weaknesses are why DKIM and DMARC exist.
## What does DKIM prove?
DKIM, defined in [RFC 6376](https://www.rfc-editor.org/rfc/rfc6376), adds a cryptographic signature to each message. The sending server signs selected headers and a hash of the body with a private key, and publishes the public key in DNS under a selector.
```text
DKIM-Signature: v=1; a=rsa-sha256; c=relaxed/relaxed; d=acme.com; s=mail2026;
h=from:to:subject:date:message-id; bh=…; b=…
```
```text
mail2026._domainkey.acme.com. IN TXT "v=DKIM1; k=rsa; p=MIIBIjANBgkq…"
```
The receiver fetches `selector._domainkey.domain`, verifies the signature, and learns two things: the signed parts were not modified in transit, and whoever holds the private key for `d=acme.com` vouched for the message. DKIM survives forwarding, which SPF does not, as long as the forwarder leaves the signed headers alone. Rotate keys by publishing a new selector and switching signing to it; the old selector can be removed once mail in transit has cleared.
## What does DMARC add?
DMARC, defined in [RFC 7489](https://www.rfc-editor.org/rfc/rfc7489), ties SPF and DKIM to the visible `From:` domain and gives receivers a policy. The record lives at `_dmarc.domain`.
```text
_dmarc.acme.com. IN TXT "v=DMARC1; p=reject; rua=mailto:dmarc@acme.com; adkim=r; aspf=r; pct=100"
```
A message passes DMARC when at least one of SPF or DKIM passes **and** its domain aligns with the `From:` header domain. Relaxed alignment (`r`) accepts subdomains; strict (`s`) requires an exact match.
| Policy | Receiver action on failure |
| --- | --- |
| `p=none` | Deliver normally, send reports |
| `p=quarantine` | Deliver to spam or hold |
| `p=reject` | Refuse the message |
The `rua` address receives daily aggregate XML reports listing every source that sent mail as your domain and whether it passed. Start at `p=none`, read the reports until every legitimate sender aligns, then move to `quarantine` and `reject`. A domain that stays at `p=none` for years has told receivers it does not mind being spoofed.
## What do Gmail and Yahoo require?
Since February 1, 2024, Google's [sender guidelines](https://support.google.com/a/answer/81126) require anyone sending more than 5,000 messages a day to Gmail accounts to authenticate with SPF and DKIM, publish a DMARC policy, support one-click unsubscribe with the `List-Unsubscribe` headers, and keep the spam rate reported in Postmaster Tools below 0.10% and never at 0.30% or above. Yahoo's [sender requirements](https://senders.yahooinc.com/best-practices/) ask the same of bulk senders: both SPF and DKIM, a DMARC policy of at least `p=none` that passes, one-click unsubscribe, and a spam rate below 0.3%. Yahoo also asks senders to monitor bounces and remove invalid recipients promptly.
For a developer the practical checklist is short: every sending service you use appears in SPF, every one of them signs with DKIM under your domain, `_dmarc` exists with a policy and a reporting address, and marketing mail carries the unsubscribe headers.
## How does Spaw use these records?
Every email verification answer reports `has_spf` and `dmarc_policy` for the address's domain, and the domain endpoint returns the raw records alongside MX hosts, provider and registration age.
```bash
curl https://spaw.co/api/v1/email/domain/acme.com \
-H "Authorization: Bearer sk_live_…"
```
```json
{
"success": true,
"data": {
"domain": "acme.com",
"mx_found": true,
"mx_provider": "google",
"has_spf": true,
"dmarc_policy": "reject",
"domain_registered_at": "1998-03-12",
"// 6 more fields": "see the endpoint reference"
},
"meta": { "credits_used": 1, "credits_remaining": 8, "cache_hit": false }
}
```
The records also feed the scores. A business domain without SPF adds 10 to the risk score, since a domain nobody bothered to authenticate is less likely to be a well-run mailbox provider for real people. In the mailbox confidence estimate for catch-all domains, a DMARC policy of `reject` or `quarantine` adds 15, `none` adds 5, an SPF record adds 10 and a missing one subtracts 10, except for a relay alias, whose estimate the relay operator's records do not move. Free consumer providers are exempt from the SPF penalty, because Gmail's records say nothing about the person using it.
## Which mistakes come up most?
| Mistake | Effect | Fix |
| --- | --- | --- |
| Two SPF records | Permanent SPF failure | Merge into one record |
| More than ten DNS lookups | SPF permerror | Flatten includes or drop unused senders |
| `+all` at the end | Everyone passes; the record is useless | Use `~all` or `-all` |
| DKIM key never rotated | A leaked key signs forever | Publish a new selector each year |
| Third-party sender not aligned | DMARC fails for newsletters | Sign with your own domain at the provider |
| `p=none` forever | No protection, receivers notice | Move to `quarantine`, then `reject` |
| No `rua` address | Failures are invisible | Add a mailbox or a reporting service |
## What to do next
- Check any domain's live records in the [SPF and DMARC checker](/tools/spf-dmarc-checker); each result has a permalink you can send to whoever runs the DNS.
- Pull the records programmatically from the [domain intelligence endpoint](/docs/api/domain-intelligence).
- See exactly how SPF and DMARC move the numbers in [how Spaw computes the risk score](/guides/how-spaw-computes-the-risk-score).
- If a receiver answers `550 5.7.26`, read [SMTP bounce codes explained](/guides/smtp-bounce-codes-explained): that reply is about authentication, not the recipient.
---
# What is a catch-all email address, and how should you handle it?
A catch-all domain accepts mail for every address, so no handshake can confirm one mailbox. What Spaw reports, how the confidence score works, when to send.
Date: 2026-09-03
Updated: 2026-10-08
A catch-all email address is any address at a domain whose mail server accepts mail for every recipient, whether or not the mailbox exists. Because the server says yes to everything, an SMTP handshake cannot tell a real mailbox from a made-up one, and a verifier has to report the address as unconfirmed rather than deliverable. Roughly, catch-all means "the domain is real and receiving, the mailbox is unknown."
## What is a catch-all address?
Every domain's mail server decides, at the `RCPT TO` step of the SMTP conversation, whether it will accept mail for a recipient. A normal server checks its directory and answers `550 5.1.1` for an unknown user. A catch-all server, also called accept-all, answers `250 OK` for every recipient and sorts the mail out afterwards: it delivers what matches a mailbox, forwards the rest to a default inbox, or silently drops it.
The effect on verification is total. The handshake that settles most addresses proves nothing here, because the same `250` comes back for `mia@acme.com` and for `xq7pw@acme.com`.
## Why do catch-all servers exist?
Four common reasons, and none of them is a mistake.
| Setup | Why it accepts everything |
| --- | --- |
| Security gateways (Proofpoint, Mimecast, Barracuda) | The gateway sits in front of the real mail system and often accepts first, then filters or bounces internally |
| Microsoft 365 tenants | Directory-based recipient checking is optional; many tenants accept all recipients at the edge |
| Small businesses and agencies | One default inbox collects mail to any address at the domain so nothing is lost |
| Anti-harvesting policy | Rejecting unknown users lets an attacker enumerate valid addresses; accepting everything hides the directory |
The last reason matters: some of the most carefully run domains are catch-all on purpose. The first one changes the arithmetic: when the domain's mail provider is a security gateway (`mx_provider_type: "security_gateway"` — Proofpoint, Mimecast or Barracuda), the catch-all is the gateway's design rather than a squat, so the score carries `catch_all_gateway` at 15 instead of `catch_all` at 30. The reason stays `catch_all` either way.
## How does verification detect a catch-all?
The tell is a server that accepts a recipient no real person could have. Verification providers probe with an address that cannot exist and, if it is accepted, mark the domain catch-all. Spaw's mailbox provider reports that classification directly, and Spaw remembers it: a catch-all answer is stored per domain for seven days, so later addresses at that domain are answered from memory and no further handshakes are spent on a question the server will not answer.
The domain endpoint exposes the same memory. `GET /api/v1/email/domain/acme.com` returns `catch_all: true` once any address at the domain has been seen to be catch-all in the last week, and `null` when that is not known.
## What does Spaw return for a catch-all address?
The verdict is `risky` with `reason: "catch_all"`, `mailbox_exists` stays `null`, and the handshake detail explains itself.
```bash
curl https://spaw.co/api/v1/email \
-H "Authorization: Bearer sk_live_…" \
-H "Content-Type: application/json" \
-d '{"email": "catch-all@spaw.test"}'
```
```json
{
"success": true,
"data": {
"email": "catch-all@spaw.test",
"deliverable": "risky",
"reason": "catch_all",
"risk_score": 30,
"risk_level": "medium",
"smtp_checked": true,
"mailbox_exists": null,
"catch_all": true,
"mailbox_confidence": 65,
"risk_signals": [{ "signal": "catch_all", "weight": 30, "dataset": "mailbox-smtp" }],
"// 36 more fields": "see the endpoint reference"
},
"meta": { "credits_used": 0, "credits_remaining": 10, "cache_hit": false }
}
```
That request is free because `catch-all@spaw.test` is one of the six fixed test addresses. A real catch-all answer is billed like any other risky answer: 1 credit, with repeats of the same address free for seven days. The catch-all flag adds 30 to the risk score, which alone puts the address in the medium band.
## What is mailbox_confidence?
Since the handshake cannot say whether the mailbox exists, Spaw adds an estimate built only from signals already in the response, so you can recompute or ignore it. It starts at 50 and moves as follows.
| Signal | Adjustment |
| --- | --- |
| Delivery outcomes every account reported for the domain, five or more: bounce rate of 50% or more | −20 |
| Delivery outcomes every account reported for the domain, five or more: bounce rate of 10% or less | +15 |
| DMARC policy reject or quarantine | +15 |
| DMARC policy none | +5 |
| SPF record published | +10 |
| No SPF record | −10 |
| Mail hosted at an established provider (Google, Microsoft, Proofpoint, Mimecast, Barracuda, Zoho, Fastmail) | +10 |
| Username looks like a name (dictionary of first names and surnames) | +15 |
| Username looks like keyboard mash | −25 |
| Role address such as info@ | +5 |
| Domain registered less than 90 days ago | −15 |
| Domain registered two years ago or more | +10 |
| Plus-tag alias | −10 |
| Username follows the naming convention seen in the domain's confirmed deliveries (first.last, flast, …) | +15 |
| Username is name-shaped but breaks that convention | −10 |
The result is clamped to 0 to 100. Delivery outcomes only count once five or more have been reported for the domain through the API or a provider webhook, and they are read as counts per domain — the share that bounced, and the shape the delivered usernames take — never as addresses. A domain administered with DMARC and SPF, hosted at a serious provider, with a name-shaped username in the house style, reaches the top of the range; a plus-tagged mash of consonants at a domain registered last month lands near zero.
A relay alias (`is_relay: true`) is the exception. At a forwarding service such as Firefox Relay or Brave Email Aliases, the domain's DMARC, SPF, provider and age describe the relay operator, and an alias's name, whether the relay generated it or its owner chose it, proves nothing, so none of the rows that argue for the mailbox applies. A relay address stays at 50, and only the evidence against it moves it: a bounce rate of 50% or more takes 20 off, and a plus-tag takes 10.
## How should you send to catch-all addresses?
Treat them as a segment, not as a verdict to act on blindly.
Send in moderation first. A catch-all address that bounces is a hard bounce like any other, so warm the segment with small volumes and watch the results before sending the whole list.
Report what happens. Post delivered and bounced outcomes to the feedback endpoint, or point your sending provider's bounce webhook at Spaw. A delivered report turns later lookups of that address into confirmed mailboxes (`smtp_reason: "delivered_recently"`, `mailbox_exists: true`), and bounces move both the suppression list and the domain's bounce rate, which feeds the confidence score for every other address at the domain.
Branch on the confidence score for the rest. A common policy is to send to catch-all addresses with a confidence of 60 or more, hold the rest for a second attempt, and drop anything under 30. Treat relay aliases (`is_relay: true`) as a segment of their own: they stay at 50, and a second attempt cannot raise them. Because the score is recomputable, you can tune those thresholds against your own bounce data instead of trusting a label.
## What to do next
- Check a domain's catch-all status and provider with the [domain intelligence endpoint](/docs/api/domain-intelligence); it costs 1 credit only when the domain has mail servers.
- Read the [`catch_all` reason page](/docs/reasons/catch_all) for the exact response contract.
- Wire your bounce webhook into the [feedback endpoint](/docs/api/report-delivery-feedback) so the confidence score learns from your own sends.
- See how the risk score adds up in [how Spaw computes the risk score](/guides/how-spaw-computes-the-risk-score).
---
# Why an email validation regex is not enough
A regex only rejects the impossible. It cannot say whether the domain accepts mail, the mailbox exists, or the address is a burner. What to run after it.
Date: 2026-09-03
A regular expression can reject strings that cannot be email addresses; it cannot accept strings that are. `mia@gmail.con`, `info@acme.com` and `x9@mailinator.com` all pass every regex ever written, and each one is a problem for a different reason that only DNS, a list, or the mail server itself can reveal. Use a short pattern to catch typing errors instantly, then verify the address for real.
## What does the RFC actually allow?
[RFC 5322](https://www.rfc-editor.org/rfc/rfc5322) defines the address grammar, and it is far wider than most forms expect. The local part may be a quoted string containing spaces and `@` signs. Comments in parentheses are legal in several places. The domain may be an IP address in square brackets. The regex that implements the full grammar is thousands of characters long, and matching it still tells you nothing useful, because "syntactically valid" and "a mailbox someone reads" are different questions.
The HTML specification makes the same point from the other side. The WHATWG definition of a [valid e-mail address](https://html.spec.whatwg.org/multipage/input.html#valid-e-mail-address) for `` is deliberately narrower than RFC 5322 and describes itself as a willful violation of the RFC, because the real-world addresses browsers need to accept are simpler than the grammar allows.
## What is the minimum sane pattern?
Something that rejects obvious typing errors and nothing else.
```javascript
const looksLikeEmail = (value) => /^[^\s@]+@[^\s@]+\.[^\s@]+$/.test(value.trim());
```
It requires exactly one `@`, no whitespace, and at least one dot after the `@`. That catches the mistakes people make while typing: a missing `@`, a space, `mia@gmail`, `mia@@gmail.com`. It rejects a handful of technically legal addresses, such as a bare local domain without a dot, which is the right trade for a public signup form.
Do not tighten it further. Patterns that enumerate top-level domains break every time ICANN adds one; patterns that forbid `+` reject a common Gmail convention; patterns that limit the local part to letters reject real addresses at real companies.
## What can a regex not decide?
Every question that matters.
| Address | Regex | Reality | Spaw reason |
| --- | --- | --- | --- |
| `mia@gmail.con` | valid | A typo-squat domain that may accept mail from strangers | `likely_typo`, with `did_you_mean` |
| `mia@acme-corp.cmo` | valid | No such domain, no mail server | `no_mx_records` |
| `hello@brand-holding.example` | valid | The domain publishes a null MX: it refuses all mail | `null_mx` |
| `m!a@gmail.com` | valid | Gmail never issues usernames with `!` | `invalid_local_part` |
| `x9@mailinator.com` | valid | A disposable inbox that expires in minutes | `disposable` |
| `info@acme.com` | valid | A role inbox, not a person | `role` |
| `nobody@acme.com` | valid | The domain is real; the server says the mailbox is not | `mailbox_not_found` |
| `mia@acme.com` at a catch-all domain | valid | The server accepts everything; the mailbox is unproven | `catch_all` |
The pattern is correct on every row and useless on every row. The information lives in DNS, in open lists, in provider rules and in the SMTP handshake.
## What runs after the regex?
Spaw's pipeline, in the order it settles addresses.
**Extraction.** People paste what they have: `Mia K `, `mailto:mia@acme.com`, a value wrapped in spreadsheet quotes, an address with a trailing comma or an invisible zero-width character. Each is unwrapped before validation, and the cleaned value is echoed back so a form can show the user what was checked.
**RFC 5322 parsing.** What remains is parsed with a real grammar, not a pattern. Anything that fails is `invalid_syntax`, free, and every other field is null.
**Provider username rules.** Gmail, Outlook, Yahoo, iCloud, AOL and Proton each publish character sets and maximum lengths for usernames. An address that violates the rule of the provider behind its domain cannot exist and is rejected as `invalid_local_part` before any network work. Minimum lengths are deliberately not enforced, because legacy accounts predate them.
**DNS.** MX records, the address-record fallback, null MX and unresolvable targets settle whether the domain can receive mail at all.
**Lists and maps.** Disposable domains, role usernames and typo-squat domains.
**The handshake.** For everything still standing, an SMTP session asks the server about the mailbox and disconnects before any message.
```bash
curl https://spaw.co/api/v1/email \
-H "Authorization: Bearer sk_live_…" \
-H "Content-Type: application/json" \
-d '{"email": "Mia K ,"}'
```
```json
{
"success": true,
"data": {
"email": "mia@gmail.con",
"syntax_valid": true,
"deliverable": "risky",
"reason": "likely_typo",
"did_you_mean": "mia@gmail.com",
"risk_score": 60,
"// 21 more fields": "see the endpoint reference"
},
"meta": { "credits_used": 1, "credits_remaining": 9, "cache_hit": false }
}
```
The display name and trailing comma were stripped, the typo-squat was recognised, and the correction is ready to show in the form.
## How should a signup form use this?
Validate in three places, each doing what it is good at.
In the browser, run the short regex on every keystroke or on blur for instant feedback, and use `` so mobile keyboards show the `@` key. Do not block submission on anything more than that.
On blur, ask the verifier. The Spaw form helper attaches to any input marked `data-spaw-email`, runs a lookup when the field loses focus, shows the verdict inline, offers the `did_you_mean` correction as a one-click fix, and never blocks the submit. It authenticates with a publishable key locked to your domain, so nothing secret ships to the page.
On the server, verify again with your secret key before creating the account, and decide policy there: reject `undeliverable`, ask for another address on `disposable` if the account has value, accept `role` on B2B forms, and store `normalized_email` so `Mia.K+news@gmail.com` and `miak@gmail.com` cannot register twice.
## What to do next
- Replace any long regex in your codebase with the short pattern above and move the real checks server-side.
- Read the [`invalid_syntax`](/docs/reasons/invalid_syntax) and [`invalid_local_part`](/docs/reasons/invalid_local_part) pages for the exact rules that run after parsing.
- Add the form helper from the [email verification guide](/docs/email-verification#widget) to your signup page.
- See what the full handshake adds in [how to verify an address without sending](/guides/verify-email-without-sending).
---
# Area-code overlay
An area-code overlay is a new area code added over the same area as an existing one, instead of splitting it, so neighbours can hold different codes and local calls need ten digits.
Date: 2026-09-27
An area-code overlay is a new area code introduced to serve the same geographic area as one or more existing codes, instead of cutting the area in two. The US rules describe "an all services area code overlay, which occurs when a new area code is introduced to serve the same geographic area as one or more existing area code(s)", set it beside the geographic split, and allow one only where ten-digit dialling is mandatory "for every telephone call within and between all area codes in the geographic area covered by the overlay area code" (47 CFR 52.19(c)(1), (c)(3) and (c)(3)(ii), https://www.law.cornell.edu/cfr/text/47/52.19, accessed 27 September 2026).
## How overlays show up in the data
NANPA's NPA report lists each North American area code with the overlay complex it belongs to and the code it came from, and Spaw syncs it daily. In the report of 26 September 2026, 628 sits in the 415/628 complex with 415 as its parent and has been in service since 21 March 2015, and 465, in service since 18 June 2026, is the newest code of the complex 718 shares with 347, 917 and 929. Codes in one complex share rate centres: NANPA's code file of the same date lists 483 codes of 415 and 76 of 628 in San Francisco's central rate centre. Whether a code was created by an overlay or a split is derived from the report, from whether its parent sits inside its complex.
## What Spaw answers
The lookup has no overlay field, because an overlay is a fact about a whole area code rather than about one number. What an overlay changes is how far an area code can be read as a place: neighbours can hold numbers in different codes, and the area code alone no longer separates them. A number's `region` comes from its [rate centre](/glossary/rate-centre) where the metadata names only the state, and the codes of an overlay share those, so a 628 number and a 415 number from the same rate centre both answer "San Francisco, CA" with `region_level` `locality`. [The United States phone facts](/tools/phone-number-lookup/countries/us) link a page for every state with more than one area code, listing each code's overlays and in-service date, and a page of new and planned area codes; [Canada's](/tools/phone-number-lookup/countries/ca) do the same for its provinces.
## What it does not mean
An overlay says nothing about who holds a number or when it was issued. A number in the newest code of a complex is not new for that reason alone, and one in the oldest code may have been assigned last week: `block_assigned_at` dates the number's own code or block where the table dates it. The ten-digit dialling an overlay brings is about how callers dial, not about the number: a lookup reads the ten digits and the eleven with the leading 1, sent with `country: "US"`, and the E.164 form to the same `e164`.
---
# Calling window
A calling window is the span of local hours a sender contacts people in. Spaw answers in_call_window from the number's time zones: clock arithmetic, not compliance advice.
Date: 2026-09-27
A calling window is the span of local hours in which a sender is prepared to call or text someone, such as 09:00 to 20:00 at the recipient's end. Spaw checks a window against the time zones of the number, which are the zones where it was issued and not always where the person is.
## What Spaw answers
Every answer carries `utc_offsets`, the distinct current UTC offsets across the number's `timezones`, sorted west to east, and `local_time`, the local time now, when every one of those zones shares a single offset. Send `call_window` as two 24-hour local times, `"09:00-20:00"`, with a single lookup or a batch and the answer carries `in_call_window`: true when the local time in every zone is inside the window, false when any is outside. The window runs from its start up to, not including, its end, may run past midnight (`"21:00-08:00"`), and is empty when start and end are equal, which always answers false. `in_call_window` is null without a window, when the number has no zone (an [international network number](/glossary/international-network-number) has none), and on an answer served from the suppression list; bulk runs take no window. Nothing is stored: the clock is worked out on every request.
Most numbers have one zone, but not all. When the example mobile number of each region in libphonenumber 9.0.37 was looked up on 27 September 2026, 15 of the 244 answered more than one zone: the Russian one thirteen, the Australian one six. There `local_time` is null whenever the zones disagree, and a window has to hold in all of them at once. `in_call_window` is not one of the [SMS policy](/glossary/sms-policy)'s rules, so it never changes `ok_to_send`.
## What it does not mean
The zone is where the number was issued, not where the person is: a mobile travels with its owner, a VoIP number can be answered anywhere, and a ported number keeps the zones of its code. `in_call_window` is clock arithmetic, not compliance advice. The US rule, for example, counts the hours at the called party's location: it bars telephone solicitation of a residential subscriber "before the hour of 8 a.m. or after 9 p.m. (local time at the called party's location)" (47 CFR 64.1200(c)(1), https://www.law.cornell.edu/cfr/text/47/64.1200, accessed 27 September 2026). No holiday calendar or do-not-call register is checked. You choose the window. Whether a country keeps one clock or several is on its page under [phone facts by country](/tools/phone-number-lookup/countries).
---
# Fictional number
A fictional number sits in a range kept out of service for films, TV and other fiction: 555-0100 to 555-0199 in North America, and ranges Ofcom, ARCEP, ACMA and ComReg reserve elsewhere.
Date: 2026-09-27
A fictional number is a phone number in a range that a numbering administrator keeps out of service so that it can be printed or broadcast in films, television, radio, advertising and other works of fiction without ringing anyone. In North America that is the 555-0100 to 555-0199 lines of every area code; in the United Kingdom it is the drama ranges Ofcom lists for each kind of number.
## The ranges Spaw carries
Spaw bundles the ranges in one file, `fictional-ranges` (version 2026-09-27), and every valid answer outside the test numbers names the copy it used in `sources`. The North American block is one pattern, a hundred lines under each area code. Ofcom's ranges are twenty blocks of a thousand ten-digit numbers each:
| Ofcom drama range | `line_type` Spaw answers |
| --- | --- |
| 020 7946 0xxx (London) and fourteen other city ranges, from 0113 496 0xxx (Leeds) to 029 2018 0xxx (Cardiff) | `fixed_line` |
| 01632 960xxx, which belongs to no area | `fixed_line` |
| 07700 900xxx | `mobile` |
| 08081 570xxx | `toll_free` |
| 0909 8790xxx | `premium_rate` |
| 03069 990xxx | `uan` |
A number a digit short or long that merely starts with one of these prefixes is not in the range: it answers `valid: false`, with `invalid_number` when it is short and `too_long` when it is long, like any other number the plan does not allow.
The same file holds ARCEP's six audiovisual blocks (five under +33 as `fixed_line`, and 06 39 98, Mayotte's, under +262 as `mobile`), ACMA's eight geographic blocks and 41 single numbers (30 mobiles, six 1800 and five 1300), and ComReg's 089 011 0xxx (`mobile`) and 020 (no type, so its numbers stay invalid).
## What Spaw answers
A number in one of these ranges answers `valid: true`, with the type the regulator gives the range even where libphonenumber's metadata lacks it (Ofcom's 0115 496 0xxx, 01632 960xxx and 07700 900xxx and ARCEP's 06 39 98 under +262 are missing from it), and `is_fictional: true` with the [`fictional`](/docs/signals/phone/fictional) signal, +80. Two kinds of entry stay invalid: a 555-01XX number under an area code that does not exist, and Ireland's 020 range, to which the plan gives no type. `fictional` is a default block signal of the [SMS policy](/glossary/sms-policy), so the answer is `ok_to_send: false` with `blocked_by: "fictional"`. Other signals add to it, and the score is capped at 100:
- In the United States, its territories and Canada the 555 code itself is held back in NANPA's and CNAC's tables, so a 555-01XX number there usually carries [`reserved_block`](/docs/signals/phone/reserved_block) (+40) as well: +1 212 555 0199 answers `block_status: "reserved"` and a score of 100. Under an area code neither table covers, such as Jamaica's 876, the score stays 80.
- 020 7946 0018 answers `fixed_line`, 07700 900123 answers `mobile`, and both score 80. A number in the premium-rate drama range also carries `premium_rate` (+60) and scores 100.
- Ofcom's table lists some drama ranges as protected blocks, so their `block_status` is `unassigned`. The reason is never [`unassigned_block`](/glossary/unassigned-block): the range exists to be printed, not dialled.
A fictional number costs one credit like any valid number, and it is never sent to the [live carrier check](/glossary/live-carrier-check). The live carrier-network check (`hlr: true`) is not enabled on the service: `hlr_checked` answers false and nothing extra is charged. The test number +1 202 555 0105 answers the fictional scenario, free.
## What it does not mean
It cannot say who typed the number or why. Test data, a placeholder in a form and a number copied from a film look the same.
It covers the ranges in the bundled file: North America's, Ofcom's, ARCEP's six audiovisual blocks, ACMA's creative-works numbers and ComReg's drama ranges, set out in [fictional and test phone numbers](/guides/fictional-and-test-phone-numbers). Other regulators may set numbers aside for drama too, and `is_fictional: false` means only that the number is outside the ranges Spaw carries, not that anyone can be reached on it. Each country's plan, and which tables Spaw reads for it, is under [phone facts by country](/tools/phone-number-lookup/countries).
---
# International (exit) prefix
An international prefix is what is dialled to leave a country before the calling code: 00 in most of the world, 011 in North America. Spaw reads it and suggests a fix in did_you_mean.
Date: 2026-09-27
An international prefix, or exit code, is what a caller dials to leave their own country's network before the calling code of the country being called: 00 in most of the world, 011 in the North American plan, 0011 in Australia, 010 in Japan. It belongs to the way a number is dialled, not to the number, so the [E.164](/glossary/e164) form replaces it with a plus sign. It is not the trunk prefix, the 0 (or the 1 in North America) dialled in front of a number inside its own country, which E.164 drops as well.
In libphonenumber 9.0.37's metadata, 185 of the 245 regions dial 00 to leave the country and 26 dial 011: the 25 regions of the North American plan and the Marshall Islands. Sixteen store a pattern of several prefixes with none preferred, and the rest have one of their own, such as Russia's 8, a pause for the tone, then 10. The [phone number formatter](/tools/phone-number-formatter) lists every region's international and trunk prefix from the same metadata.
## What Spaw answers
A prefix the lookup can read leaves no trace in the answer: `e164` is the same with or without it. A number written with the international prefix of the country sent is read as international: 011 44 … with `country: "US"`, 00 44 … with `"GB"`. A leading 00 is also tried as a plus when no country is sent or the national reading fails, and kept when that gives a valid number.
When the input still does not parse as a number of the country sent, the answer is invalid and free, and `did_you_mean` carries the E.164 form of the one valid reading of two common mistakes: a missing plus, or a foreign international prefix left in (such as 011 sent with a country that does not dial it). `01112025550142` sent with `country: "GB"` answers `too_long` with `did_you_mean: "+12025550142"`, and `447700900123` sent with `"US"` answers `too_long` with `did_you_mean: "+447700900123"`. The suggestion is never applied. It is null when no reading or more than one is valid, when the reading lands in a block the regulator's table says nobody holds (unless the block sits in a range reserved for fiction), when the digits could be a national number with one digit too many or too few, for input that already starts with a plus, and for a `short_number` or `emergency_number`.
## What it does not mean
`did_you_mean` is a reading of the digits, not a claim that the suggested number is in service or belongs to the person who typed it; looking the suggestion up is a separate lookup. A number whose prefix Spaw can already read answers `valid` with nothing to suggest, which is why the field is set only on invalid answers. Which prefix a country uses, and how its numbers are written for display, is on its page under [phone facts by country](/tools/phone-number-lookup/countries).
---
# International network number
An international network number sits under a calling code the ITU gives a global service rather than a country, such as +870, +881, +882 or +883, so its country is null.
Date: 2026-09-27
An international network number is a number under a calling code the ITU assigns to a global service instead of a country, so it belongs to no national numbering plan and no national regulator's table. The ITU's list of assigned E.164 country codes (the annex to Operational Bulletin 1114, position on 15 December 2016, https://www.itu.int/dms_pub/itu-t/opb/sp/T-SP-E.164D-2016-PDF-E.pdf, accessed 27 September 2026) lists ten codes assigned to global services, and libphonenumber 9.0.37 reads nine of them, all but +991, a shared code for trials of a proposed new service: +800 International Freephone Service, +808 International Shared Cost Service, +870 Inmarsat, +878 Universal Personal Telecommunication, +881 Global Mobile Satellite System, +882 and +883 International Networks, +888 Telecommunications for Disaster Relief and +979 International Premium Rate Service.
## What Spaw answers
`country`, `country_name` and `region` are null and `timezones` is empty: no country's plan or table covers these numbers. `carrier_code` and `carrier_brand` are null too, and `carrier` is null unless the numbering metadata's prefix data names a network for the range, as it does for every +881 range (a +881 6 number answers "iridium satellite") and for a few +882 ranges; no regulator's table stands behind those names. `calling_code` carries the code and `line_type` the numbering metadata's type for the range. The [`international_network`](/docs/signals/phone/international_network) signal, +30, fires for four codes only, +870, +881, +882 and +883, the satellite and international networks, and it adds to any line-type weight. That metadata types every +883 range as VoIP, so before any other signal a +883 number scores `voip` +30 and `international_network` +30, 60 in all; +870 and +882 hold both mobile and VoIP ranges, so theirs score 30 or 60, and +881 numbers, typed mobile, 30. A score of 60 is `high` but still passes the default SMS policy, whose ceiling blocks only a score above 60.
The other five codes are scored by their type alone. +979 numbers are `premium_rate` (+60), which the default SMS policy blocks as a signal; +878 numbers are `voip` (+30) and +808 numbers `shared_cost` (+20); +800 numbers are `toll_free` and +888 numbers `uan`, and neither type carries a weight.
## What it does not mean
The signal says which kind of calling code the number sits under, not who uses it or why. A ship, an aircraft, a remote site and a machine network all number from these codes. What they share is that no country answers for them, which is why an SMS policy with `allowed_countries` set refuses numbers under all nine codes with `blocked_by: country`: a number with no country is outside every allow-list, a business choice about where you send rather than a fraud verdict. Without a zone, `local_time` is null and a [calling window](/glossary/calling-window) cannot be checked.
---
# Live carrier check (HLR lookup)
A live carrier check asks the mobile networks, at the moment of the lookup, whether a number is in service, which network serves it and whether it is roaming. On Spaw it is not enabled.
Date: 2026-09-27
A live carrier check, often called an HLR lookup after the home location register in which a mobile network keeps its subscribers, is a query sent into the carrier networks at the moment of the lookup to learn whether a number is in service now, which network serves it after any port, and whether the handset is roaming. Numbering plans and regulators' allocation tables cannot answer those three questions: they record how a number was allocated, not what is happening to it today.
## What Spaw answers
The live carrier-network check (`hlr: true`) is not enabled on the service: `hlr_checked` answers false and nothing extra is charged.
The API accepts `hlr: true` on single, batch and bulk lookups, and monitored lists take `live_check`. The four fields the check would fill are `reachable`, `ported`, `current_carrier` and `roaming`; each is null until a check runs, so every answer today carries them null. A lookup made with `hlr: true` answers exactly as one made without it.
Everything else in the answer is read from local data. `carrier` names the operator the number was allocated to, from the regulator's block holder where a table covers it and from the numbering plan's prefix metadata otherwise, and a number ported to another network keeps the block it was born in (see [NPA/NXX](/glossary/npa-nxx)). Only a query into the carrier networks, an HLR or number-portability lookup, can say who serves it today, which is why the check's answer has fields of its own and is never merged into `carrier`.
One rule of the [SMS policy](/glossary/sms-policy) depends on the check. `require_reachable` refuses a number unless a live check reached the handset: `blocked_by: "unreachable"` when the network could not reach it, `"unconfirmed"` when no check answered. While the check is not enabled, every lookup answers `unconfirmed` under that rule, so leave it off.
When a check runs, it is the one call that leaves Spaw's servers: it sends that number, and only that number, to a carrier-network partner, and the network's answer is held for a day under a hash of the number. A number in a block nobody holds, or in a range reserved for [fiction](/glossary/fictional-number), is never checked.
## What it does not mean
A reachable number is not a verified owner. The check reports what the network knows about the subscription, not who is typing the number into a form, and it is not SIM-swap detection.
It names nobody. There is no caller name, no reverse lookup and no subscriber detail in any answer, with or without the check.
Without it, `valid: true` means a number is assignable, not that it is in service. Which regulator tables Spaw reads for each country, and what they can settle, is under [phone facts by country](/tools/phone-number-lookup/countries).
---
# Number pooling (thousands-blocks)
Number pooling splits a North American central office code into ten blocks of a thousand numbers, each assigned to a carrier separately, so one code can hold several carriers' numbers.
Date: 2026-09-27
Number pooling is the North American practice of splitting a central office code's 10,000 numbers into ten blocks of 1,000, the thousands-blocks written NPA-NXX-X, and assigning each block to a carrier separately. The US rule defines it as the process by which "the 10,000 numbers in a central office code (NXX) are separated into ten sequential blocks of 1,000 numbers each (thousands-blocks), and allocated separately within a rate center" (47 CFR 52.20(a), https://www.law.cornell.edu/cfr/text/47/52.20, accessed 27 September 2026). A pooled code can hold the numbers of several carriers, so the holder of the code is not necessarily the holder of a given number's block.
## How much of the plan is pooled
In NANPA's tables of 26 September 2026, 141,810 of the 197,177 central office codes assigned in the United States (71.9%) are pooled. The thousands-block report lists 1,303,876 blocks held inside those codes and 88,562 more that are free in the pool with none of their numbers still in use, and 663,918 of the held blocks (50.9%) are held under a different [operating company number](/glossary/operating-company-number) than the code they sit in. NANPA marks each block assigned (AS), retained (RT), available (AV), available pending (AP) or available with a future effective date (AF) (https://www.nanpa.com/reports/thousands-block-reports/region, accessed 27 September 2026). The Canadian administrator's table of 23 September 2026 marks 2 of its 28,710 assigned codes pooled. The [phone statistics page](/statistics/phone) carries the US counts and is measured again every week.
## What Spaw answers
`block_pooled` is true when the number's code is pooled, false when it is not, and null when no allocated block or code answers, as for an unassigned block, and outside the NANPA and CNAC tables. In a pooled US code, `carrier`, `carrier_code`, `carrier_brand`, a `line_type` the holder settles and `is_virtual` describe the holder of the number's own thousands-block, and `block_assigned_at` is the day NANPA assigned that block. A block the report lists as available (AV or AP) answers [`unassigned_block`](/docs/reasons/unassigned_block) while the copy is fresh. A block still partly in use by its former holder, or available only from a future date, keeps the code holder's answer, and so does every Canadian code, which is read at code level. A retained block carries no date, so `block_assigned_at` is null there. So is the date of 112,611 of the 197,177 US codes (57.1%) in NANPA's tables of 26 September 2026, including every code assigned before 2001, because NANPA dates none earlier: a null does not mean new.
## What it does not mean
A thousands-block is an allocation, like the code around it: it names who NANPA assigned the numbers to, not who serves one of them after a [port](/glossary/number-portability). Pooling is not a risk signal either. `block_pooled` carries no weight; it says only that the number's code is pooled, not whether the block or the code answered. The state pages linked from [the United States phone facts](/tools/phone-number-lookup/countries/us) count the pooled codes of every area code.
---
# Number portability
Number portability lets a subscriber keep a phone number when changing carrier. The number's block stays listed under the carrier it was issued to, so block data names the original holder.
Date: 2026-09-27
Number portability is a subscriber's ability to keep a telephone number when moving to another carrier. The US rules define it as the ability "to retain, at the same location, existing telecommunications numbers without impairment of quality, reliability, or convenience when switching from one telecommunications carrier to another" (47 CFR 52.21(m), https://www.law.cornell.edu/cfr/text/47/52.21, accessed 27 September 2026), and 47 U.S.C. 251(b)(2) (https://www.law.cornell.edu/uscode/text/47/251, accessed 27 September 2026) makes providing it, to the extent technically feasible, a duty of every local exchange carrier. The number moves. The block it was issued from does not, and the regulator's allocation table goes on listing the carrier the block was given to.
## What Spaw answers
`carrier`, `carrier_code` and `carrier_brand` name the holder of the number's block as the regulator's table records it, or, where no table answers, `carrier` names the network the numbering metadata's prefix data gives for the range: the original allocation either way, never who serves the number today. `line_type` and `is_virtual` read the same holder where it settles them, so a number ported away from a VoIP provider's block still answers `voip` and scores the [`virtual`](/docs/signals/phone/virtual) signal (+40), and a number from a wireless carrier's block that was later ported to a VoIP service still answers `mobile`. `block_assigned_at` dates the block, not the day the current subscriber got the number.
Only a query into the carrier networks can say whether a number was ported and who serves it now. The answer keeps that in fields of its own, `ported`, `current_carrier`, `reachable` and `roaming`, which stay null until a live check has answered, so the table's answer and the network's are never blended. The live carrier-network check (`hlr: true`) is not enabled on the service: `hlr_checked` answers false and nothing extra is charged.
## What it does not mean
A carrier read from a block is not wrong after a port; it answers a different question. It says who the numbers were issued to, which is what a regulator's table can prove, and the [phone intelligence guide](/docs/phone-intelligence#sources) says so where it lists the sources. A port also leaves the number's code where it was: the number keeps its area code and the [rate centre](/glossary/rate-centre) its code was assigned in, so `region` describes where the number was issued before and after the move, never where the subscriber is. Nothing in the answer names the subscriber either, because Spaw does no reverse lookup. The US block tables are summarised on [the United States phone facts](/tools/phone-number-lookup/countries/us).
---
# Operating company number (OCN)
An operating company number (OCN) is the four-character code North American numbering tables use to identify the company that holds a central office code or thousands-block.
Date: 2026-09-27
An operating company number (OCN) is the four-character code North American numbering records use to identify a telephone company. NANPA's central office code and thousands-block reports, and the Canadian administrator's code table, list every assigned code or block with its holder's OCN beside the holder's name. In NANPA's tables of 26 September 2026, 3,020 OCNs name 2,817 distinct holders: no OCN names more than one holder, while 104 holder names appear under more than one OCN, one of them under 31. OCNs mix digits and capital letters; 913 of those 3,020 contain a letter.
## What Spaw answers
`carrier_code` is the regulator's code for the holder of the number's block: the OCN in the NANPA and CNAC tables, ARCEP's operator code in France and UKE's operator ID in Poland. In a pooled US code it is the OCN of the number's own [thousands-block](/glossary/number-pooling) where NANPA lists that block. The British, Dutch, Italian, Australian, New Zealand and Mexican tables name holders without a code, so there, and wherever `carrier` comes from the numbering plan's prefix metadata rather than a table, `carrier_code` is null.
`carrier_brand` is matched on the OCN first and the name second, from a bundled, versioned brand map (`carrier-brands` in `sources`), because the tables list legal entities rather than the brands people know: NANPA lists OCN 6010 as NEW CINGULAR WIRELESS PCS, LLC, which answers `carrier_brand` "AT&T". A name the map does not know answers null; nothing is guessed.
## What it does not mean
An OCN identifies the company a code or block was assigned to. After a [port](/glossary/number-portability) another company serves the number, and no open table lists that company's code against it, so `carrier_code` keeps naming the original holder. An OCN is also not a carrier identification code (CIC), the four-digit code NANPA assigns that is "used to route and bill calls in the public switched telephone network" (https://www.nanpa.com/numbering/carrier-identification-codes-cic, accessed 27 September 2026). How the holders of North American codes group under network brands is on the phone facts for [the United States](/tools/phone-number-lookup/countries/us) and [Canada](/tools/phone-number-lookup/countries/ca).
---
# Rate centre
A rate centre is the local area a North American central office code is assigned to. Spaw answers it as the number's region, with region_level locality, where the metadata names only a state.
Date: 2026-09-27
A rate centre (rate center in US usage) is the local area a North American central office code is assigned to: every code, and every thousands-block pooled inside it, belongs to one rate centre, and calls to its numbers have traditionally been rated as local or long distance from it. NANPA's rate-centre report of 24 September 2026 holds 18,452 pairs of a state and a rate-centre code across the 50 states, the District of Columbia and Puerto Rico, under 12,317 distinct names, because the same name recurs from state to state. In Canada the same part is played by the exchange area in the Canadian administrator's table.
Rate centres change slowly and by order: NANPA publishes a list of rate centres split among several area codes by a state commission's order, and a summary of rate-centre consolidations since 2005 (https://www.nanpa.com/reports/co-code-reports, accessed 27 September 2026).
## What Spaw answers
`region` names the rate centre when the numbering metadata's own description of the number names only a state or a country. NANPA writes the names in capitals with the parts it splits a metro area into for billing, and those parts are dropped: `SAN FRANCISCO:CENTRAL DA` and `LOS ANGELES:DA 06` answer "San Francisco, CA" and "Los Angeles, CA". `region_level` then says `locality`, as it does for a description that already names a city with its state; it is read from where the description came from and its shape, not measured. A Canadian exchange area is answered the same way ("Toronto, ON"). For a whole prefix, the [block report](/docs/api/phone-block-report) lists up to three rate centres or exchange areas in `places` when it can only name the state or province, and the page of every state with more than one area code, linked from [the United States phone facts](/tools/phone-number-lookup/countries/us), counts its rate centres and lists its area codes with their overlays.
## What it does not mean
A rate centre is where the number was issued, not where the person is. A mobile goes wherever its owner does, a VoIP number can be answered anywhere, and a [ported](/glossary/number-portability) number keeps the rate centre of its code. The codes of an [overlay](/glossary/area-code-overlay) share their rate centres, so the rate centre, not the area code, is the finer place name the tables give.
---
# SMS pumping (artificially inflated traffic)
SMS pumping is fraud in which bots make a business send texts, usually one-time codes, to numbers where someone is paid a share of each message's delivery fee.
Date: 2026-09-27
SMS pumping, also called artificially inflated traffic (AIT), is fraud in which automated requests make a business send text messages, usually one-time codes from a signup or login form, to numbers where someone is paid a share of the fee for delivering each message. The business pays for every message, nobody enters a code, and each destination number looks ordinary on its own.
## How it differs from IRSF and wangiri
The three schemes all live on a share of what a network charges to reach a number, and they are easily confused:
| Scheme | What is abused | Who pays | What a Spaw lookup can see |
| --- | --- | --- | --- |
| SMS pumping (AIT) | A business's own SMS sending, triggered through its forms or API | The business, per message | The destination's country, line type and range, and the account's own lookup pattern |
| International revenue share fraud (IRSF) | Calls, and sometimes messages, pushed to premium-rate or international numbers whose termination fee is shared, often through a hijacked phone system | The operator or business whose line placed them | `premium_rate`, `international_network` and the country |
| Wangiri | One-ring missed calls from a premium-rate or international number, hoping the person rings back | The person who calls back | Nothing about the call: Spaw sees no calls. A lookup of the number that rang shows only its line type and country |
## What Spaw's signals flag
Every lookup applies the account's [SMS policy](/glossary/sms-policy) and answers `ok_to_send`, with `blocked_by` naming the rule that decided. The parts that bear on pumping:
- **Countries.** An allow-list of the countries you send to is the first control. A number's country is its own: libphonenumber 9.0.37 files 25 regions under +1, 23 of them outside the US and Canada, so under `allowed_countries: ["US", "CA"]` a +1 876 (Jamaica) or +1 787 (Puerto Rico) number answers `blocked_by: "country"`. A number with no country, such as one under +800, +870, +881, +882 or +883, is outside every allow-list.
- **Line type and network.** [`premium_rate`](/docs/signals/phone/premium_rate) (+60) is blocked by default. [`international_network`](/docs/signals/phone/international_network) (+30) is not: +882 34 2 1234, which the plan types as mobile, scores 30; +882 16 123456 and +883 510012345, typed as VoIP, score 60 (voip + international_network); both pass the default ceiling, which blocks only a score above 60. Name the signal in `block_signals`, or set an allow-list, to refuse them.
- **Your own traffic.** [`range_burst`](/docs/signals/phone/range_burst) (+40) fires when your account looked up five or more distinct numbers in one 1,000-number range within 60 minutes; [`numbers_per_client`](/docs/signals/phone/numbers_per_client) (+30) when one client address you sent as `client_ip` tried five or more numbers in the same window. Under the defaults `range_burst` alone passes, and the two together (70) answer `blocked_by: "risk_score"`. `destination_new_for_you` is true when your account has not looked up a valid number in that country in the last 30 days; it carries no weight.
- **Alerts.** The account owner is emailed on a range burst, on a destination spike (an hour with at least 20 lookups to one country and ten times its hourly average over the previous 14 days) and on enumeration (an hour with at least 50 lookups to one country, half or more of them invalid or in blocks nobody holds).
- **Outcomes.** Report `delivered` and `verified` to the phone feedback endpoint and the summary's `verified_share` falls when codes are delivered and never entered.
## What they cannot see
Spaw sees lookups, not messages. It cannot tell whether you sent a text, what it cost, or who was paid for it.
A pumped number in a country you serve can be an ordinary mobile number, and a single lookup of it scores nothing. Your allow-list does not stop it; what stands in the way is the pattern of your own lookups and your own outcome reports, and each has limits. The own-traffic fields count single and browser lookups only: batch, bulk and monitor runs, the test numbers and the guest demo are never counted, and no other account's lookups are read. `numbers_per_client` does not fire for an address on a mobile carrier or a corporate proxy, where many people share one address, and it does not catch an attack rotated through residential proxies. `destination_new_for_you` and the spike alert stay silent until a week of the account's own lookups has been counted.
An allow-list is a business choice about where you send, not a verdict that a country's numbers are fraudulent, and Spaw publishes no list of risky countries or pumping figures of its own. Each country's calling code, and the line types its plan uses, is under [phone facts by country](/tools/phone-number-lookup/countries). The guide [SMS pumping: what the default SMS policy lets through](/guides/sms-pumping-what-the-default-sms-policy-lets-through) walks through the destinations the defaults pass and the one policy request that adds a country list and the `range_burst`, `numbers_per_client` and `international_network` signals.
---
# SMS-receiving number (disposable number)
An SMS-receiving number is one a public website publishes so anyone can read the texts sent to it, verification codes included. Spaw matches two open lists daily.
Date: 2026-09-27
An SMS-receiving number, often called a disposable or temporary phone number, is a number that a public website publishes so that anyone can read the text messages sent to it. A verification code sent there can be read by whoever opens the page, so the number proves nothing about the person who typed it.
## What Spaw answers
Spaw merges two open lists of such numbers, from iP.1 Networks and Castle (both MIT), every day. `is_disposable` is true when the merged table holds the number, whether a list carries it today or every list has dropped it and it was last seen within the last 730 days, and `disposable_seen_at` gives the date it was last seen. Three signals follow from the listing:
| Signal | When | Weight |
| --- | --- | ---: |
| [`disposable`](/docs/signals/phone/disposable) | Listed and seen within the last 365 days (an undated listing counts as fresh), or dropped by every list and seen within the last 90 days | +70 |
| [`disposable_stale`](/docs/signals/phone/disposable_stale) | Listed or dropped, but not seen inside those windows; a dropped number is kept at its last-seen date for 730 days | +40 |
| [`disposable_range`](/docs/signals/phone/disposable_range) | Not listed itself, but in a 1,000-number range holding 10 or more listings that still score `disposable`; `is_disposable` stays false | +30 |
`disposable` is one of the default block signals of the [SMS policy](/glossary/sms-policy), so a fresh listing answers `ok_to_send: false` with `blocked_by: "disposable"`. The other two pass the defaults on their own, because +40 and +30 are within the ceiling of 60. The test numbers show both ends: +1 202 555 0103 answers `is_disposable: true`, a score of 70 and `blocked_by: "disposable"`; +1 202 555 0113 answers `disposable_range`, a score of 30 and `ok_to_send: true`.
## What the lists hold
The two lists synced on 24 September 2026 held 128,030 numbers under 103 calling codes. North American numbers were the largest group, 35,988 (28.1%), followed by +44 with 15,877 (12.4%) and +86 with 13,130 (10.3%). Most listings are old: on 27 September 2026, 18,934 of them (14.8%) were fresh enough to score `disposable`, and the rest scored `disposable_stale`. Of the 25,659 listed numbers with a US area code, at least 64.7% sit in blocks whose holders are on Spaw's classification list of [virtual-number](/glossary/virtual-number) wholesalers and VoIP providers (the holder the block was allocated to, not who serves the number today), against at least 19.7% of all allocated US numbers in NANPA's tables of 26 September 2026. The full table by calling code is on the [phone statistics](/statistics/phone) page.
## What it does not mean
A listing says a website published the number on a date. It says nothing about who holds the number today: the sites give numbers up, and a carrier can issue one to a subscriber who never saw the site, which is why the weight drops once a listing is stale.
`is_disposable: false` means not listed, not safe. Many SMS-receiving numbers are never scraped into a list; `disposable_range` covers only the unlisted neighbours of listed numbers, and a real subscriber can hold a number beside a run of rented ones.
The lists name no owner and neither does Spaw: there is no reverse lookup and no caller name in any answer.
---
# Short number
A short number is a service code dialled or texted inside one country, such as 911, 311 or an SMS short code. It has no international form, and Spaw answers it invalid and free.
Date: 2026-09-27
A short number is a service code dialled or texted inside one country, shorter than a subscriber number and with no international form: an emergency number such as 911 or 112, a service code such as 311, a directory or helpline number, or the codes businesses send text messages from. It reaches a service rather than a subscriber, and it means something only inside the country whose plan defines it; the same digits can be another service, or nothing, elsewhere.
In North America the three-digit N11 codes are the best-known set. NANPA lists 211 for community information and referral, 311 for non-emergency police and other government services, 411 for local directory assistance, 511 for traffic and transportation information (weather and traveller information in Canada), 611 for repair service, 711 for the telecommunications relay service, 811 for the one-call service before digging (a non-urgent health line in Canada) and 911 for emergencies. In the US the FCC administers them and recognises 211, 311, 511, 711, 811 and 911 as nationally assigned (https://www.nanpa.com/numbering/abbreviated-codes, accessed 27 September 2026).
## What Spaw answers
A short number is not a subscriber number, so the answer is `valid: false` with the reason [`short_number`](/docs/reasons/short_number), or [`emergency_number`](/docs/reasons/emergency_number) for a code the country's metadata lists as an emergency number, with the number's fields null and `blocked_by: invalid`. Invalid answers are free, and neither reason ever carries a `did_you_mean` suggestion. Both are read from libphonenumber's short-number metadata for the country sent, so the lookup needs one: with `country: "US"`, 911 and 112 answer `emergency_number` and 211, 311, 411 and 611 answer `short_number`; with `"GB"`, 999 and 112 answer `emergency_number` and 101 and 111 answer `short_number`. The US metadata (libphonenumber 9.0.37) accepts any four- to six-digit code starting with 2 to 9 as a short number, so an SMS short code sent with `country: "US"` answers `short_number` too.
## What it does not mean
`short_number` says the digits are a short code in that country's plan, not that a service answers on them today or who runs it. Without a country, digits this short answer `missing_country`, because a short number has no calling code to be read from, and written with one (+1 911) they are simply `too_short`. A short number never has an [E.164](/glossary/e164) form, so there is nothing to store for it as a contact number. Which numbers a country's plan treats as subscriber numbers, with an example of each type, is on its page under [phone facts by country](/tools/phone-number-lookup/countries).
---
# Unwanted-call complaints
Unwanted-call complaints are reports consumers file with the FTC naming the number shown as the caller ID. Spaw counts them per North American number over 90 days.
Date: 2026-09-27
Unwanted-call complaints are the reports consumers file with the US Federal Trade Commission about calls they did not want, each naming the number that appeared as the caller ID. The FTC publishes them every weekday as its Do Not Call reported-calls data, and Spaw counts them for every North American number over the last 90 days.
## What Spaw answers
`complaints_90d` is the number of complaints naming the number in the last 90 days and `complaints_last_at` the date of the latest. Both come from the dataset `us-complaint-reports`, installed once a day, with the consumer's own location dropped when the file is read. `complaints_90d` answers 0 when nobody complained, and null outside the North American plan or before the first sync.
Three or more complaints, filed on at least two different days within the 90 days, raise the [`reported_unwanted_calls`](/docs/signals/phone/reported_unwanted_calls) signal, +20 on the risk score. The two-day rule keeps one person filing three times in an afternoon from counting as a pattern. It is not a default block signal of the [SMS policy](/glossary/sms-policy), and +20 is within the default ceiling of 60, so on its own it never refuses a number.
## What the counts are not
Three statements go with every complaint figure Spaw shows:
- They are not the Do Not Call Registry, which lists consumers' own numbers. Spaw does not check the registry, and `ok_to_send` is not a do-not-call scrub.
- They say nothing about whether you may call or text a number.
- They are unverified, and caller ID is often spoofed, so a number can collect complaints without its holder having placed any call. That is why the weight is small.
The counts name nobody. Spaw does no reverse lookup, has no caller name for any number, and publishes no complaint detail about an individual number: only the count and the latest date reach an answer.
## The figures
In the 90 days of FTC files Spaw held on 24 September 2026, the newest dated that day, 691,715 complaints filed between 26 June and 23 September 2026 named 588,360 North American numbers as the caller ID:
| Numbers named | Count | Share |
| --- | ---: | ---: |
| Named once | 540,075 | 91.8% |
| Named three or more times on two or more days (scores `reported_unwanted_calls`) | 9,301 | 1.6% |
| Toll-free numbers (800, 833, 844, 855, 866, 877, 888), all | 72,715 | 12.4% |
| Toll-free numbers among the 9,301 | 4,584 | 49.3% |
Of the 646,612 complaints that answered the question, 73.6% were marked as a recorded message or robocall. These are shares of what consumers reported, not of calls placed. The [phone statistics](/statistics/phone) page re-measures the complaints weekly and places the reported US numbers by the holder NANPA assigned their block to, never the network that carried a call or serves the number today; the phone facts page for [the United States](/tools/phone-number-lookup/countries/us) describes the numbering plan they sit in.
---
# Virtual number
A virtual number is rented from a provider that holds number blocks for internet calling. Spaw flags one when a regulator's table shows its block held by such a provider.
Date: 2026-09-27
A virtual number is a phone number rented out by a provider that holds number blocks for internet calling and messaging, rather than one tied to a SIM card or a physical line. In Spaw it has a narrower, checkable meaning: the regulator's allocation table shows the number's block held by a virtual-number wholesaler or VoIP provider.
## What Spaw answers
`is_virtual` is true when the holder of the number's block is on Spaw's classification list of virtual-number wholesalers and VoIP providers, false for any other block the table lists, and null where no regulator table lists the number's block. A true answer raises the [`virtual`](/docs/signals/phone/virtual) signal, +40 on the risk score, and `risk_signals` names the table that answered. The default [SMS policy](/glossary/sms-policy) does not refuse it on its own, because +40 is within the default ceiling of 60.
In North America the holder also settles the line type, since the plan cannot tell mobiles from landlines (see [NPA/NXX](/glossary/npa-nxx)). A number in such a block answers `line_type: "voip"` with `line_type_source: "block_allocation"`, and the plan-based [`voip`](/docs/signals/phone/voip) signal (+30) does not fire beside it. In a pooled US code the holder is that of the number's own thousands-block where NANPA lists one; in Canada it is the code holder. Elsewhere the plan's type stands: a British geographic number in a block Ofcom's table gives to a VoIP provider answers `fixed_line` with `is_virtual: true`. The test number +1 202 555 0102 answers `is_virtual: true`, `line_type: "voip"` and a score of 40.
## How common it is
NANPA's tables of 26 September 2026 put 370,513,000 of the 1,883,208,000 allocated US numbers, 19.7%, in blocks held by VoIP providers on the classification list. The numbers published by SMS-receiving websites lean much further that way: of the 25,659 numbers with a US area code on the two open lists Spaw synced on 24 September 2026, 16,589 (64.7%) sit in such blocks. The first is a share of the regulator's table and the second a share of the listed numbers, neither of traffic, and both are lower bounds, since they count only holders on Spaw's classification list: 45.3% of allocated US numbers, and 15.9% of the listed ones, sit with holders the list does not classify. The [phone statistics](/statistics/phone) page re-measures them weekly, and each country's table is described under [phone facts by country](/tools/phone-number-lookup/countries).
## What it does not mean
Spaw does not separate fixed from non-fixed VoIP. Some lookup services split the two: fixed VoIP, a virtual number associated with a physical device, and non-fixed VoIP, one obtained online without a physical device. The regulator tables record only who holds the block, so a business line on a hosted phone system and a number rented through an app answer the same.
The table records the original allocation, never who serves the number today. A number ported out of a provider's block keeps `is_virtual: true`, and one ported into it does not.
Plenty of people and businesses use virtual numbers. The weight says a number is cheap to obtain and to let go, not that the person typing it is lying. For a one-time code, read it beside the account's own [range_burst](/docs/signals/phone/range_burst) count and the [SMS-receiving](/glossary/sms-receiving-number) signals.
---
# SMS policy
An SMS policy is the set of rules an account attaches to its phone lookups — countries, line types, blocked signals, a score ceiling, a live check, opt-outs — deciding ok_to_send.
Date: 2026-09-24
Updated: 2026-09-26
An SMS policy is the set of seven optional rules an account attaches to every phone lookup it runs — the countries it sends to or never sends to, the line types it refuses, the risk signals that block whenever they fire, a ceiling on the risk score, whether a live check must have reached the handset, and whether a number its suppression list holds as opted out is refused — from which Spaw answers `ok_to_send` on each response, with `blocked_by` naming the rule that decided. It turns a 49-field answer into the one boolean a verification step branches on before it pays to send a code.
## What Spaw answers
A number that is not valid is blocked first and always, with `blocked_by: invalid`. Otherwise the rules are applied in a fixed order: an opt-out (`opted_out`, when the account's suppression list holds the number as opted out and the rule is on), the country (`country`, which also blocks a number with no country when an allow-list is set), the line type (`line_type`; `fixed_line_or_mobile` can never be blocked, because the North American plan does not say which of the two a number is), the first blocked signal in the [published weights'](/docs/signals#phone) order, the score ceiling (`risk_score`), and last the live-check requirement (`unreachable` when the network could not reach the handset, `unconfirmed` when no live check answered). Until an account sets a policy the defaults apply: `fictional`, `disposable`, `reported_abuse`, `reported_abuse_widely` and `premium_rate` block, so does any score over 60 and so does an opt-out, every country and line type is allowed, and no live check is required. A sender of one-time codes may switch the opt-out rule off, because an opt-out from marketing does not always forbid a verification code and the person typing the number may be its owner asking for one again. The policy endpoints or the SMS policy tab on the phone page change them for every member of the account, and single lookups, batches, bulk runs, monitors and the browser endpoint all apply the stored policy. A number served from the account's suppression list on a batch, bulk or monitored run answers `blocked_by: suppressed`.
## What it does not mean
`ok_to_send` is a fraud and deliverability decision computed from the answer and the account's rules. It is not consent to message anyone and not a do-not-call scrub: neither is part of the answer, and both stay the sender's obligation. A country allow-list is the first control against SMS pumping to destinations a business never serves, and it is a business choice about where you send, not a verdict that a country's numbers are fraudulent. The policy is not the score either: the weights stay published and recomputable, and the policy only says which of them the account refuses to send past.
---
# CEDEX (business-volume mail)
CEDEX is La Poste's delivery for organisations that receive a lot of post, written after the town: 75384 PARIS CEDEX 08. Spaw answers address_type bulk_mail with a weight of 20.
Date: 2026-09-22
Updated: 2026-09-23
CEDEX — courrier d'entreprise à distribution exceptionnelle — is La Poste's service for organisations that receive a large volume of post, which is sorted to them under a code of their own instead of being carried to their street address; the envelope writes that code where the postcode goes and CEDEX after the town: 75384 PARIS CEDEX 08.
## What Spaw answers
`address_type` reads `bulk_mail`. In France it is read from CEDEX and AUTORISATION anywhere in a line and from CS and TSA at the start of one followed by a number; the same type covers the Antwoordnummer reply numbers of the Dutch and Belgian postal services and Sweden's Svarspost and Frisvar. A CS or TSA line — `CS 70001`, `TSA 70011` — lands in `po_box` while `is_po_box` stays false; CEDEX itself stays on the town line as La Poste writes it (`75384 PARIS CEDEX 08`), so `po_box` stays null and the street stays in the answer and in `formatted`. Either way the `bulk_mail` risk weight is +20: an organisation's mailroom is a real [delivery point](/glossary/delivery-point), but it is nobody's front door.
The postcode is checked like any other. A CEDEX code is almost always five digits of its own: in the GeoNames file of 16 September 2026, 14,332 French codes carry CEDEX and four of them are also a town's ordinary code. France's [postcode directory](/glossary/postcode-directory) lists these codes under their five digits, with the town as the envelope writes it — Paris CEDEX 08 — among the code's places, so `postcode_status` reads `live` and the town written with CEDEX matches. France's directory is complete, so a CEDEX code it does not list answers [`postcode_not_found`](/docs/reasons/postcode_not_found).
## What it is not
A CEDEX or a reply number is not a box rented by a person and not a mail drop: it is an organisation's own arrangement with its postal service, and the post reaches the organisation it names. The answer says so about the address and nothing about anybody who works there.
---
# General delivery (poste restante)
General delivery is post held at a post office counter until the addressee collects it: General Delivery, Poste Restante, Postlagernd. Spaw answers address_type general_delivery, weight 40.
Date: 2026-09-22
Updated: 2026-09-23
General delivery is post addressed to a post office rather than to a door, held at the counter until the addressee collects it in person — General Delivery in the United States and Canada, Poste Restante as the international term, Postlagernd in Germany, Austria and Switzerland, Fermo Posta in Italy, Lista de Correos in Spain.
## What Spaw answers
`address_type` reads `general_delivery`, and the words that said so land in `po_box` — `GENERAL DELIVERY` (Canada's `GD` is folded onto it, and a station written after it stays: `GD STN A` reads `GENERAL DELIVERY STN A`), `POSTE RESTANTE`, `POSTLAGERND` — while `is_po_box` stays false, because a counter is not a rented box. The line stands in for the street, so an address written as general delivery, a town and a postcode is valid without a street; with no street written there is no building to follow into a register, so `exists` stays null.
The `general_delivery` risk weight is +40, and unlike the [PO box](/glossary/po-box) weight it is not waived anywhere. The score measures how far an address is from somebody's own front door, not fraud: a counter is a real [delivery point](/glossary/delivery-point) a postal service serves, but nobody can be visited there and a courier cannot leave a parcel at it.
## Which spellings are read
The address rules name the general-delivery spellings of 19 countries' postal operators, and in those countries only those are read — Canada's bare `GD` among them, which counts only at the start of a line, Japan's 郵便局留 and 局留, and Australia's `CARE PO`. Everywhere else the two international spellings, General Delivery and Poste Restante, are read. A country's page in the [address checker](/tools/address-checker) lists the words its operator uses.
## What it is not
It is not a statement about the person collecting the post. General delivery is a service for anyone who cannot, or would rather not, receive post at a fixed address, and the answer describes the delivery point and nothing else.
---
# Military mail (APO, FPO, DPO, BFPO)
Military mail is post routed through a forces or diplomatic postal service — the US APO, FPO and DPO, Britain's BFPO, Canada's CFPO — so its address is a routing code rather than a place.
Date: 2026-09-22
Updated: 2026-09-23
Military mail is post addressed through a forces or diplomatic postal service rather than to a place: in the United States an APO (Army or Air Force Post Office), FPO (Fleet Post Office) or DPO (Diplomatic Post Office), written with a unit, PSC or CMR number and the codes AA, AE or AP where a state would go; for the British forces a BFPO number; for the Canadian forces a CFPO number.
## What Spaw answers
`address_type` reads `military` in the United States, the United Kingdom and Canada, the three countries whose address rules carry these words. APO, FPO, DPO, BFPO and CFPO are read anywhere in a line; PSC and CMR only at the start of one and followed by a number. The Armed Forces codes AA, AE and AP are read as the state, like any other US state code.
It carries no risk weight, and it is not a box. The routing line stands in for the street, so [`missing_street`](/docs/reasons/missing_street) does not fire, and the unit or PSC line stays in `street`. The British postcode pattern accepts a BFPO number where the postcode goes. Great Britain's postcode directory lists neither BFPO numbers nor the BF1 codes, so it is not asked about them: a BFPO number is accepted and answers `postcode_status` null — not evaluated — and never [`postcode_not_found`](/docs/reasons/postcode_not_found). `exists` stays null: none of the three countries has a complete register of buildings, and none of their registers lists a forces post office.
## What it is not
It is not a place. The address tells the forces' own postal service where to route the post, and nothing in it says where anybody is — or who they are.
---
# Parcel locker
A parcel locker is a machine or collection point where post waits for the addressee: Deutsche Post's Packstation, Australia Post's Parcel Locker. Spaw answers address_type parcel_locker, unscored.
Date: 2026-09-22
A parcel locker is an automated machine, or a staffed collection point, where post waits until the addressee collects it instead of being carried to a door. The address rules carry two operators' services: Deutsche Post's Packstation in Germany, and Australia Post's Parcel Locker and Parcel Collect, with their abbreviations PCL LKR and PCL COL.
## What Spaw answers
`address_type` reads `parcel_locker`, and the locker line — `PACKSTATION 123` — lands in `po_box` while `is_po_box` stays false, because a locker is not a rented box. Otherwise it is handled as a box: it stands in for the street line, and when no street is written beside it no register is asked, so `exists` stays null. There is no building to confirm.
It carries no risk weight. A checkout that needs a door can branch on `address_type` itself; the [delivery point](/glossary/delivery-point) entry lists the other kinds it can take.
## What is not recognised
Only the services named above are read. A locker network whose name the rules do not carry reads as whatever else the address is written as — usually `street` — so an absent `parcel_locker` is not proof that an address is a door. A country's page in the [address checker](/tools/address-checker) lists the words that are read there.
---
# Rural route
A rural route is a delivery that follows a road, addressed by route and box number instead of a house on a named street: RR and HC in the US, RR, SS and MR in Canada, RMB and RSD in Australia.
Date: 2026-09-22
Updated: 2026-10-14
A rural route is a postal delivery that follows a road rather than a grid of named streets, so an address on it is the route's number and a box on it — RR 2 Box 152 — instead of a house number on a street: RR (rural route), HC (highway contract route), RFD and Star Route in the United States, RR, SS (suburban service) and MR (mobile route) in Canada, and seven abbreviations Australia Post uses for rural and community delivery, RMB, RSD, RMS, MS, CMB, CMA and CPA.
## What Spaw answers
`address_type` reads `rural_route` in those three countries. Every one of these words counts only at the start of a line and followed by a number, because RR, HC and MS turn up in ordinary text and a bare "Rural Route" could be a road's name.
The type itself carries no risk weight: a rural route is how post reaches homes where there is no street delivery, and it is not a box. It stands in for the street line, so an address written as a route, a town and a postcode is valid and [`missing_street`](/docs/reasons/missing_street) does not fire; the route line itself stays in `street`, and `po_box` stays empty.
## What it cannot confirm
No register of buildings lists a route and box number — the registers list civic addresses — so the answer can confirm the town, the postcode and the region, but never the [delivery point](/glossary/delivery-point) itself, and `exists` stays null. Australia is the exception today: G-NAF is a register whose coverage is complete, so where it is installed a route line it does not list answers `exists: false` and scores `not_in_register`, +20. Read that as "G-NAF lists no civic address written this way", not as proof that the route is not served. Where a property also has a civic address, that is the one a register can find.
---
# Address velocity
Address velocity is how many times one account has looked up one address inside a window: the seen_by_you field, kept under the premise digest and never anyone else's lookups.
Date: 2026-09-17
Address velocity is how often one account has looked up the same address inside a window — the signal that a signup flow which keeps seeing one address, or a checkout that receives the same delivery point under fifty names in an afternoon, is looking at one person or one script rather than fifty customers.
## What Spaw answers
`seen_by_you` is an object on every billed address lookup: `count`, how many times your own account looked the address up inside the window before this call, `first_seen_at`, when the first of those was, and `window_days`, seven by default. The first lookup answers zero; a repeat the seven-day marker serves free still counts, because velocity is about your traffic, not about what was charged. The counter is kept under the premise digest — the same key `premise_key` carries — or a hash of the folded input when the address names no building, never the address itself, and the window closes from its first sighting so a steady trickle cannot keep one alive for ever.
## What it does not mean
It is your account's own lookups and nothing else. An address seen a hundred times across other customers answers zero for you: a postal address is where somebody lives, and there is no cross-account address reputation at Spaw and never will be. The guest demo and the five test addresses answer null. It carries no weight; what a count of three in a day should mean is a rule for your shipping policy or your own code, not for the score.
---
# Derived postcode places
Derived postcode places are the towns and centroids a national address register adds to a country's postcode directory as it compiles — beside the directory's own rows, never in their place.
Date: 2026-09-17
Derived postcode places are what a national address register contributes to a country's postcode directory as it is compiled: for every postcode the register's buildings carry, the towns those buildings are written with and the mean of their positions, added to the directory beside what the directory already lists.
## What Spaw answers
Ten registers declare it — Switzerland, Austria, Czechia, Norway, the three Belgian regions, Luxembourg, Australia and Latvia — and after each compiles, codes the directory lacks are inserted under the register's own `-postcodes` dataset, and codes it holds gain the register's places after their own and a centroid only where they had none. `locality_matches_postcode` then knows every town a register writes under a code, not only the operator's default one, and `postcode_status: live` answers for codes the postal-code dump never carried. The weekly places sync re-applies it after the dump's own replace.
## What it does not mean
It does not make a directory complete, and it does not replace one. A register of buildings never lists box-only and large-recipient codes — Swiss and Austrian Postfach codes, Australian GPO boxes, Czech special codes — so replacing the directory would have turned real box addresses into `postcode_not_found`; coverage moves to complete only when the operator's published total is configured and the union reaches it, and no country's total is configured yet. The United States gets its extra places from a different source, the Census Bureau's ZCTA-to-place file, and stays unscored on a town mismatch because that file lists places whose land a ZIP touches, not the names the postal service accepts.
---
# Local-script rendering
Local-script rendering is the formatted_local field: an address written in Japanese, Chinese or Korean handed back in that script and in the country's own largest-first line order.
Date: 2026-09-17
Local-script rendering is the address written the way its own postal operator prints it in the country's own script and order — prefecture, city, ward, block for Japan; province, city, district, road for China; the largest part first and the building last — where the format table carries such a layout and the address was written in that script.
## What Spaw answers
`formatted_local` carries that rendering for Japan, China, Taiwan, Hong Kong, Macao and the two Koreas when every place part of the address — the street line, a building, the district, the town, the area — is written in Han, kana or Hangul with no Latin word among them; the organisation line may stay Latin. The postal mark 〒 is kept, no uppercase rule applies, a named building stands above the street, and the whole thing is null for everything else: a Japanese address written in Latin letters, a mixed-script one, or any country whose local layout repeats the Latin order, which is the case for the seven other entries the table carries.
## What it does not mean
It is not a transliteration. Rendering a Latin-written Tokyo address in kanji would mean inventing characters the customer never typed, so the field answers null rather than guess; `formatted` still carries the Latin layout for every address. A one-line Japanese address is best sent as separated fields, because the parser reads a single line of kanji as one street.
---
# Placeholder address
A placeholder address is what somebody types to get past a form rather than to give an address: a test word, a keyboard run, a refusal. Spaw answers looks_placeholder and scores it 60.
Date: 2026-09-17
A placeholder address is what a person types into an address field when they do not intend to give one: "asdf", "test", "n/a", "no address", "same as above", "123 Fake Street", "Anytown" — a word chosen to satisfy the form rather than to name a place, and one that no register, directory or operator list will ever confirm.
## What Spaw answers
`looks_placeholder` is true when the street or the town of a valid address is a whole placeholder name or a shape nobody would give a place — four or more letters with no vowel, one character repeated, digits alone, a run along a keyboard row — false when neither is, and null when there is no street and no town to judge. A true answer carries the `placeholder` signal, weight 60, citing the `address-placeholders` dataset in `sources`, which is enough for `risk_level: high` on its own, and the default shipping policy blocks it.
## What it does not mean
Only whole names count. "Test Valley Road" is a real road in Hampshire and "Sample Road" exists in several American towns; each folds to a different key from the words "test" and "sample" alone, so neither is flagged, and a short real name like Rye or Ely is never judged. A placeholder is also not a typo: a misspelt real street answers `street_suggestion` from the register, not this flag.
---
# Postcode district
A postcode district is the outward code of a British postcode — SW1A, RG5, B91, the half before the inward code — the unit Open Names files its streets under and Spaw publishes one page for.
Date: 2026-09-17
Updated: 2026-09-23
A postcode district is the outward code of a British postcode, everything before the space — SW1A, RG5, B91 — the area Royal Mail sorts to before the inward code narrows the post to a street or a building, and the unit Ordnance Survey's Open Names files each road under. The August 2026 ONS Postcode Directory installed here holds 3,016 of them in Great Britain's 120 postcode areas.
## What Spaw answers
Two things read the district. The register of streets for Great Britain is keyed by it, so a British street match means the road is filed under the district of the postcode you wrote; a road that runs through several districts is filed under one, which is why a miss there never scores. And every district has a page of its own, listed on [the British postcode districts index](/tools/address-checker/gb/districts): the places Open Names lists for its postcodes, the live and terminated postcode counts from the national directory, sector by sector, every road filed under it (named roads first, and past sixty the rest folded away), a centroid, the twelve nearest districts that are indexable, how many companies Companies House lists with a registered office there, and a hand-off into the address checker with the district prefilled. Only a district with at least one live postcode and one listed road is indexable: with that directory and the Open Names listing of 22 September 2026 installed here, 2,752 of the 3,016 are. The Channel Islands and the Isle of Man, which the directory also carries, have pages too, but they are not part of Great Britain and not counted among them.
## What it does not mean
A district is not an address, and neither is a full postcode: the guide [Can you look up UK addresses from a postcode for free?](/guides/a-uk-postcode-is-not-an-address) says why a code that identifies a street or a few buildings still says nothing about a particular one. Northern Irish districts are absent because BT postcodes are not in the directory — their reuse needs a Land & Property Services licence — and a district page says whether codes are in use, never who is there.
---
# Shipping policy
A shipping policy is the set of rules an account attaches to its address lookups — which signals block, a score ceiling, whether a register must confirm the building — deciding ok_to_ship.
Date: 2026-09-17
A shipping policy is the set of three optional rules an account attaches to every address lookup it runs — the signals that block whenever they fire, a ceiling on the risk score, and whether a national register has to have confirmed the building — from which Spaw answers `ok_to_ship` on each response, with `blocked_by` naming the rule that decided. It turns a 61-field answer into the one boolean a checkout branches on.
## What Spaw answers
An address that cannot stand as written is blocked first and always, with `blocked_by: invalid`. Otherwise the rules are applied in a fixed order: the first blocked signal in the published weights' order, then the score ceiling (`blocked_by: risk_score`), then the register requirement (`blocked_by: unconfirmed`, when `exists` is anything but true). Until an account sets a policy the defaults apply — `undeliverable`, `placeholder` and `vacant` block, so does any score over 60, and no register is required — and the policy endpoints or the Shipping policy tab change them for every member of the account, on the single and batch endpoints, bulk runs, monitors and the dashboard alike. An address on the account's suppression list answers `blocked_by: suppressed`.
## What it does not mean
The policy is not the score. The seventeen weights stay published and recomputable, and reading the flags is still the right thing to do when a checkout needs more than one decision; the policy exists so that the common case needs one field. It is also the account's decision, not Spaw's: a mail drop scores 50 and passes the default ceiling, because whether a parcel may go to a mailbox shop is a merchant's call.
---
# Unit check
The unit check reads the flat or suite written on an address against the units a national register lists at the building: ok, missing, unexpected or null. Two of those are risk signals.
Date: 2026-09-17
The unit check is what the flat, apartment or suite written on an address says against the units a national register lists at the building it was followed to — nothing written at a building with several units, a suite written at a single residence, or the two agreeing — and it is answered only where a register found the building and declares its units.
## What Spaw answers
`unit_check` reads `ok`, `missing`, `unexpected` or null. `missing` — the register lists several units and none was written — carries the `unit_missing` signal at 20, because post for a block of flats with no flat number is the delivery-point problem the postal services sell an answer to. `unexpected` — a flat or suite written at a building the register lists as a single residence — carries `unit_unexpected` at 30, the classic invented-suite tell. Null means the register did not find the building, does not say how many units it has, or lists one unit at a building whose use is not residential, where a suite is ordinary; a private-mailbox number ("PMB 5") is a box, not a flat, and never counts as a unit here.
## Which registers can say
Only a register that publishes its units: the Austrian register counts dwellings per building, the Belgian one carries box numbers, the Australian G-NAF lists flats as rows of their own, and the United States National Address Database carries a unit column on a minority of its rows, which is why it never calls a building single. A register that lists no units answers null, never false.
---
# Address confirmation
The confirmation map says, part by part, which dataset stood behind the postcode, the street, the house number, the town and the region of an address — and which parts nothing could check.
Date: 2026-09-16
The confirmation map is the part of an address answer that says, for each of five parts, which dataset stood behind it: the postcode, the street, the house number, the town and the region each get a source name, `unconfirmed`, or `null`.
## What Spaw answers
`confirmation` is an object with the keys `postal_code`, `street`, `house_number`, `locality` and `administrative_area`. A value names the dataset that confirmed the part: `postcode_directory` (the national postcode directory lists it), `postcode_area` (the town is one the directory lists elsewhere in the postcode's area rather than under the code itself, or, where the postcode's region is a single city such as Mexico City, that region's own name), `register` (a national address register followed the address that far), `range` (the house number sits inside a range the register publishes for the street), `company_register` (an open company register lists companies at exactly that building), `mail_drop_list` (a mailbox operator or a registered agent publishes the address), and `pattern` (the postcode only matched the country's shape). `unconfirmed` means a dataset that could have confirmed the part did not; `null` means nothing could check it at all.
## What it is not
`unconfirmed` is not `wrong`. A partial register that does not carry a street has not said the street is missing, and the map reports exactly that: not confirmed, by a dataset that might have. The verdicts live elsewhere — `exists` for the building, `locality_matches_postcode` and `administrative_area_matches_postcode` for the town and the region — and the map is the evidence beside them.
---
# Address range
An address range is the span of house numbers a street-range register publishes for one side of a street segment, which places a number on a street without listing the building.
Date: 2026-09-16
An address range is the span of house numbers a street-range register publishes for one stretch of a street — 100 to 198 on one side, 101 to 199 on the other — which places a number on a street without listing the building itself.
## What Spaw answers
`match_level: range` means a US address whose building no premise register carries fell inside a range the Census Bureau's TIGER/Line tables publish for that street in that ZIP; `confirmation.house_number` reads `range` and `exists` stays `null`, because a range says the number is plausible, not that a building stands there. A street the table knows whose number falls outside every published range answers `match_level: street` with the `number_out_of_range` weight of +20. A street the table does not know answers `none` with no weight: the table is declared partial, since not every edge carries a range and a street on a ZIP boundary can be filed under its neighbour's ZIP alone.
## What it is not
It is not a delivery-point check. The postal service's own record of whether post reaches a number is licensed data that stays behind `is_deliverable`; a range is the public map's idea of which numbers a street segment could hold.
---
# Building use
Building use is what a national address register declares a building is for — residential, commercial, mixed, industrial, institutional, agricultural or other — where the register publishes it.
Date: 2026-09-16
Building use is what a national address register declares a building is for: `residential`, `commercial`, `mixed`, `industrial`, `institutional`, `agricultural` or `other`, read from the register's own category codes and never inferred from the address text.
## What Spaw answers
`building_use` is filled only on a `premise` match against a register that publishes a use — the US National Address Database where the contributing authority filled its address type in, the Swiss building register's category and the Austrian register's building attributes as of 16 September 2026 — and is `null` everywhere else, including on a premise match in a register that carries positions but no use. `is_multi_unit` sits beside it: `true` when the register lists two or more flats or suites at the building, `false` only when the register declares its units and lists none, `null` when it lists one or carries no units at all.
## What it is not
It is not the licensed residential delivery indicator. `is_residential` is what a postal service's own delivery-point data says and stays `null` until a partner is enabled; `building_use` is what the cadastre or the address authority recorded about the building. The two can differ, and the answer keeps them apart so nobody reads one as the other.
---
# Forward sortation area
A forward sortation area is the first three characters of a Canadian postal code, the part the open postcode data publishes and the part a Canadian address is checked against.
Date: 2026-09-16
A forward sortation area is the first three characters of a Canadian postal code — the M5V of M5V 3L9 — which names the delivery area the code belongs to; the last three characters, the local delivery unit, narrow it to a block or a building.
## What Spaw answers
Canada Post licenses the full code list, and the open data Spaw installs carries the forward sortation areas alone, so the Canadian postcode directory is a prefix directory: a written code is matched on its first three characters, `postcode_status` reads `live` when the area exists, and the town, province and centroid are the area's. It is recorded as `partial` whatever its size, which means a code the directory lacks is never called `postcode_not_found`. The Dutch four-digit district and the Irish routing key are handled the same way, at four and three characters.
## What it is not
It is not the full postcode, and the answer never pretends it is: `confirmation.postal_code` reads `postcode_directory` for the area, and nothing about the last three characters is checked beyond the country's pattern.
---
# Postcode area
The postcode area is the neighbourhood a written town is checked against when it is not one of the postcode's own places: the letters of a British outward code, the first three digits of a ZIP.
Date: 2026-09-16
Updated: 2026-09-24
The postcode area is the neighbourhood a written town is checked against when it is not one of the places the directory lists under the postcode itself: the letters of a British outward code, the first three digits of a US ZIP, and the first two characters of a postcode everywhere else.
## What Spaw answers
`locality_matches_postcode` is strict: `true` only when the town is one the directory publishes for that postcode. When it is `false`, the town is looked for among every place the directory lists across the postcode area. A hit sets `confirmation.locality` to `postcode_area` and adds no weight, because a post town written for a settlement — Reading for a Woodley postcode, Toronto for a district the Canadian directory names by its neighbourhood — is how envelopes are addressed, not an error. Where the postcode's region is a single city — Mexico City, Delhi, Jakarta, Bangkok, Moscow, Seoul and Tokyo's wards among them — the region's own name (Distrito Federal for 06000, Tokyo for 100-0005) reads `postcode_area` too, reported and never scored. Only a town found nowhere in the area carries the `locality_mismatch` weight of +30, and only where the directory lists every place a postcode is written with; a directory that carries one default place per code, as the United States one does, reports the mismatch and never scores it.
## What it is not
It is not a claim that the town is right. `postcode_area` means the directory knows the name in the vicinity; the strict field beside it still says the postcode's own places did not include it.
---
# Postcode directory
A postcode directory is a country's list of every postcode with its status, places, region and centroid, installed whole so a missing code is an answer and not a gap.
Date: 2026-09-16
Updated: 2026-09-23
A postcode directory is a country's list of postcodes with, for each one, whether it is in use, the places it is written with, the region it lies in and a centroid — installed as a whole, so that a code the directory lacks is an answer rather than a gap in the data.
## What Spaw answers
Great Britain's directory is the ONS Postcode Directory, which carries introduction and termination dates, so `postcode_status` there can read `terminated`. Every other country's comes from the GeoNames postal-code dump under CC BY 4.0, which carries places, a region and a centroid but no dates, so a code found there is `live`. Each directory is recorded as `complete` or `partial`: only a complete one — the British directory, and the countries whose GeoNames copy reaches the operator's published total, fifteen of them on 16 September 2026 — can answer `postcode_not_found`, which makes the address invalid and free, and only about a code inside its scope: Great Britain's leaves out Northern Ireland's BT codes on licence grounds, and does not carry BFPO numbers or the BF1 and BX codes, so those are not looked up and answer `postcode_status` null. A partial directory confirms what it holds and denies nothing. The directory also feeds `locality_matches_postcode`, `administrative_area_matches_postcode` and, where no register located the building, `latitude` and `longitude` as a `postcode_centroid`. The region is compared under the names and codes people write for it, so `CDMX` matches a Mexico City postcode the directory files under the old name Distrito Federal, and `Co. Dublin` matches a Dublin routing key it files under the province of Leinster.
## What it is not
It is not an address list. A postcode identifies an area, and the streets and buildings inside it come from the national address registers, not from the directory.
---
# Premise key
A premise key is the digest of one building — country, postcode or town, folded street name and house number — that every spelling of the same address shares.
Date: 2026-09-16
A premise key is the digest of one building: the country, the postcode or the town where there is no postcode, the street name folded to its comparison form and the house number, hashed so that "12 High St", "12 High Street" and "12, HIGH STREET" all share one key.
## What Spaw answers
`premise_key` carries that digest in hex on every valid answer that names a street and a house number, and `null` where there is no building to key — a box, a street without a number, an address with neither postcode nor town. It is the same key the national registers are compiled under, the key an account's own delivery reports are filed under, and the key the suppression list holds, so a customer can join lookups, feedback and suppressions on one value, or deduplicate a list without re-implementing the folding.
## What it is a fact about
A place, and only a place: the key is derived from the address parts and nothing else, carries no name, and is the same for everyone who writes that address. It changes only when the folding rules change, which the `address-rules` version in `sources` records.
---
# Registered agent address
A registered agent address is the published office of a company-formation or registered-agent service, where thousands of companies are registered because the agent stands there, not the company.
Date: 2026-09-16
Updated: 2026-09-23
A registered agent address is the published office of a company-formation or registered-agent service: an address that thousands of companies name as their registered office because the agent receives their legal post there, not because any of them occupies the building.
## What Spaw answers
The bundled list of agents' addresses is consulted for every street address. Each entry is an address the agent publishes as its own, or one that a state listing or filing, or a Companies House filing, naming the service carries, and the list records which. A match answers `is_mail_drop: true`, `mail_drop_source: registered_agent` and `mail_drop_provider` set to the agent's name, and the risk score carries the `mail_drop` weight of +50, exactly as an operator's mailbox branch does. Where an operator's branch list and the agent list both know an address, `operator_list` is reported; the company-register cluster (`company_cluster`) is reported only when neither list does, so the answer names the most specific fact it holds.
## What it is a fact about
A place. A company registered at an agent's address has done something ordinary, and the answer never says otherwise; what the field says is that the address is the agent's, which is what a proof-of-address check needs to know before it treats the address as somebody's home. The list ships with the app and names its sources, and the agent's name is published because it is a fact about the agent's own business.
---
# Street register
A street register lists a country's streets and the district each runs through, so it can confirm that a street exists where an address says it does — and never that a building does.
Date: 2026-09-16
A street register lists a country's streets and the postcode district each one is filed under, so it can confirm that a street exists where an address says it does, and can never confirm that a building does.
## What Spaw answers
Great Britain's is OS Open Names, published by Ordnance Survey under the Open Government Licence and keyed by postcode district. A street it lists answers `match_level: street` with `confirmation.street: register`, and `exists` stays `null` because a register of streets has nothing to say about a building; a street it does not list answers `match_level: none` with `confirmation.street: unconfirmed` and no weight, because a road that runs through several districts is filed under one of them. The same file names the places each postcode is written with, which is what `locality_matches_postcode` reads for British addresses.
## What it is not
It is not the Postcode Address File. Every full British address list derives from Royal Mail's licensed file, which is why `exists` for a British building comes only from a licensed partner and is otherwise `null`.
---
# Street suggestion
A street suggestion is the street a national register most likely meant when the one written on an address is not in the postcode but a close spelling is.
Date: 2026-09-16
A street suggestion is the street a national register most likely meant when the one written on an address is not listed in its postcode but a close spelling is: Banhofstrasse against Bahnhofstrasse, Limatquai against Limmatquai.
## What Spaw answers
`street_suggestion` carries the register's own spelling of the closest street, and `null` when the written street was found, when nothing in the postcode is close enough, or when no street listing is installed for the register. It is answered only when the address could be followed no further than its postcode — a register that found the street has nothing to suggest — and the comparison runs over the streets of that one postcode, never across a country. The same listing answers `GET /api/v1/address/streets`, which lists every street a register holds under a postcode.
## What it is not
It is not a correction the answer applied. The address is measured as written, `street` echoes what was sent, and the suggestion is a separate field for the caller to accept or ignore; the confirmation map still reads `unconfirmed` for the street until it is.
---
# Test address
A test address is a documented input that answers a fixed result every time, free and without a repeat marker, so an integration test never spends credits or depends on the day's data.
Date: 2026-09-16
A test address is a documented input that answers a fixed result every time, free and without a repeat marker, so an integration test never spends a credit or depends on what data the server has installed that day.
## What Spaw answers
The postal product has five: the numbered houses 1 to 5 on Spaw Test Street, Springfield, IL 99999 — a ZIP the postal service has never assigned, on a street that does not exist. Number 1 answers a valid low-risk address, 2 a mail drop on an operator's list, 3 an address a complete register does not carry, 4 a terminated postcode, and 5, written as "5 Spaw Test Street PMB 5", a private mailbox. Each answers every field of the contract with `register` reading `test-postal-addresses`, `sources` naming the same bundled dataset, `credits_used: 0`, and no marker, no feedback read and no suppression touch; batch, bulk, monitors, the browser endpoint and the guest demo all recognise them, in any spelling of the street and postcode. The email and phone products have theirs too: the addresses at spaw.test and the numbers in the Washington 555-01XX block, and the published sandbox key answers all three families.
## What it is not
It is not a sample of real data. Nothing about a test address is looked up; the answer is written down in advance, which is the point.
---
# BIMI
BIMI (Brand Indicators for Message Identification) is a DNS record at default._bimi. pointing at a brand logo that some mailbox providers show beside mail from a domain with enforcing DMARC.
Date: 2026-09-13
BIMI, Brand Indicators for Message Identification, is a specification under which a domain publishes a TXT record at `default._bimi.` pointing at an SVG logo, and a participating mailbox provider shows that logo beside messages from the domain — but only when the message passes DMARC with an enforcing policy of quarantine or reject, and, at most providers, only when the logo comes with a verified mark certificate.
## What Spaw answers
The domain endpoint reports `bimi`: whether the record exists. It is read beside `dmarc_policy`, because a BIMI record on a domain whose policy is none is decoration nobody will display.
## What it does not mean
The record does not mean the logo appears anywhere: display depends on the provider, the DMARC policy, the certificate and the sender's reputation. Spaw reads the DNS record and nothing further.
---
# DKIM
DKIM (DomainKeys Identified Mail, RFC 6376) is a signature a sending server adds to a message, verified by the receiver against a public key the domain publishes in DNS under a selector.
Date: 2026-09-13
DKIM, DomainKeys Identified Mail, is the mechanism of RFC 6376 by which a sending server signs a message with a private key and the receiver verifies the signature against a public key the domain publishes in DNS. The signature covers chosen headers and the body, so a message that is altered in transit, or that merely claims a domain it was never signed by, fails verification.
## What Spaw answers
The domain endpoint reports `dkim_selectors`: which of the common selector names publish a key under `._domainkey.`. An empty list means none of the common names answered, which is not the same as no DKIM at all — a domain may sign under a selector nobody else uses — and null means the lookups failed. The free DKIM checker reads the same list.
## What it does not mean
A published key proves the domain can sign, not that a given message was signed or that the signature aligned with the visible sender. DMARC is the policy that ties DKIM and SPF to the address a person sees.
---
# DKIM selector
A DKIM selector is the label that names one of a domain's signing keys; the receiver looks up ._domainkey. in DNS to find the public key that verifies a signature.
Date: 2026-09-13
A DKIM selector is the label a signing server writes into the signature header to say which of the domain's keys it used; the receiver joins it to the domain as `._domainkey.` and reads the public key from that DNS name. Selectors let a domain run several keys at once — one per sending service, or an old and a new one during a rotation — which is why Google signs under `google`, Microsoft 365 under `selector1` and `selector2`, and marketing platforms under names of their own.
## What Spaw answers
Because a selector is only revealed inside a signed message, a DNS-only check can test known names but never enumerate them. Spaw asks for a fixed list of the common selectors and reports the ones that publish a key in `dkim_selectors`. A domain that signs under an unusual name shows an empty list, and the pages say so rather than calling it unsigned.
## What it does not mean
Finding a key under a selector says a service is set up to sign for the domain; it does not say the domain still uses that service, or that its messages align under DMARC.
---
# DMARC
DMARC (RFC 7489) is a DNS policy at _dmarc. telling receivers what to do with mail that fails SPF and DKIM alignment — none, quarantine or reject — and where to send reports.
Date: 2026-09-13
Updated: 2026-10-08
DMARC, Domain-based Message Authentication, Reporting and Conformance, is the policy of RFC 7489 that a domain publishes as a TXT record at `_dmarc.` to tell receivers what to do with a message that fails both SPF and DKIM alignment with the visible sender — `p=none` to only report, `p=quarantine` to filter, `p=reject` to refuse — and where to send the aggregate reports that show who is sending as the domain.
## What Spaw answers
Every email answer carries `dmarc_policy`; the domain endpoint adds `dmarc_pct`, the share of failing mail the policy applies to (100 when the tag is absent), and `dmarc_rua`, whether a reporting address is published. A reject or quarantine policy raises `mailbox_confidence` on a catch-all domain, because an administered domain is likelier to hold the mailbox than a squat; a relay alias is the exception, since the policy describes the relay operator, not the alias.
## What it does not mean
A policy of none is a maintained domain that has not finished the rollout, not an unauthenticated one, and a reject policy says nothing about whether a particular mailbox exists. DMARC is about who may use the name, never about who reads the mail.
---
# Generated username
A generated username is a local part that looks produced by a program or typed as a placeholder rather than chosen by a person: a hex string, a UUID, two letters and four digits, or test or asdf.
Date: 2026-09-13
A generated username is the part of an address before the @ that looks produced by a program, or typed as a placeholder, rather than chosen by a person: a hexadecimal string, a UUID, a long run of mixed-case letters and digits, one or two letters followed by four or more digits, a word followed by six or more digits, or a placeholder word such as test, fake, asdf or noname. Signup bots, scrapers and people filling a required field with anything produce them; customers rarely do.
## What Spaw answers
`looks_generated` reports the shape, and the `generated` weight of 20 applies. The rules are fixed and published on the field, and they are deliberately conservative: `mia1987` is a person and `krzysztof` is a name. The check is independent of `is_gibberish`, which reads a different shape — consonant mash — and both can fire on one address.
## What it does not mean
A generated-looking username can be a real mailbox: developers test with `test@` and some providers hand out numeric IDs. The signal is about how the username was made, and it earns its weight beside `widely_seen` and a young domain rather than on its own.
---
# MTA-STS
MTA-STS (RFC 8461) is a policy a domain publishes in DNS at _mta-sts. and over HTTPS that tells sending servers to deliver only over authenticated TLS to its named mail hosts.
Date: 2026-09-13
MTA-STS, SMTP MTA Strict Transport Security, is the mechanism of RFC 8461 by which a receiving domain tells sending servers that mail for it must be delivered over TLS with a valid certificate to the mail hosts it names: a TXT record at `_mta-sts.` announces the policy's version, and the policy itself is fetched over HTTPS from `mta-sts.`. Without it, SMTP falls back to plain text whenever a connection cannot negotiate TLS, which is exactly what an attacker on the path arranges.
## What Spaw answers
The domain endpoint reports `mta_sts`: whether the DNS record that announces a policy exists. True means the domain has published one; false is a checked negative; null means the lookup failed. The free checker reads it beside TLS-RPT and BIMI.
## What it does not mean
The record's presence does not prove the policy file is reachable or valid, or that the named hosts match the MX records; a full validation fetches and parses the policy, which Spaw does not do. And a domain without MTA-STS is not insecure in itself — most mail still travels over opportunistic TLS — it is a domain that has not closed the downgrade.
---
# Mailbox confidence
Mailbox confidence is Spaw's published 0 to 100 estimate that a specific mailbox exists when the handshake could not say, on a catch-all domain or after an unverified answer.
Date: 2026-09-13
Updated: 2026-10-08
Mailbox confidence is the `mailbox_confidence` field: a 0 to 100 estimate, published as a heuristic, that a specific mailbox exists when the SMTP handshake could not say so — because the domain accepts every address, or because the server would not answer. It is computed only in those two cases and is null whenever the handshake settled the mailbox.
## What Spaw answers
The estimate starts at 50 and moves on signals already in the answer: an enforcing DMARC policy, an SPF record, an established mail provider and an old registration argue for the mailbox; a gibberish username, a fresh domain and a plus-tag argue against; a username that follows the naming convention seen in the domain's confirmed deliveries is marked up, a name-shaped one that breaks it is marked down; and once a domain has enough reported outcomes its bounce share outweighs the rest. A relay alias is the exception: it stays at 50, and only a high bounce share or a plus-tag lowers it, because the domain's records describe the relay operator, and an alias's name, whether the relay generated it or its owner chose it, says nothing about whether it exists. Every step and its points are published in the risk-score guide, so the figure can be recomputed from the response.
## What it does not mean
It is not a probability measured against the world, and the measured tables on the status page exist to show whether it predicts anything. A sending policy can set a floor for it, which is the honest way to use an estimate: decide how sure you need to be, rather than treating 51 as a yes.
---
# No-reply address
A no-reply address is one a system sends from and nobody reads at — noreply@, donotreply@, mailer-daemon@ — so mail sent to it is discarded by design; Spaw reports it as role_type no_reply.
Date: 2026-09-13
A no-reply address is an address a system sends from and nobody reads at — `noreply@`, `no-reply@`, `donotreply@`, `mailer-daemon@` and their kin — created so that receipts, alerts and notifications carry a sender without inviting an answer. Mail sent to one is discarded, bounced or filed unread by design, which makes it the one kind of address that can exist and still be pointless to mail.
## What Spaw answers
The shape is recognised by pattern, as the whole username or as a token inside one (`noreply-billing@`), even when the open role list lacks the spelling. It forces `role` true, answers `role_type` `no_reply`, gives the verdict its own reason `no_reply` ahead of the generic role one, and adds the `no_reply` weight of 20 on top of the role 30 — 50 in all. The default sending policy blocks on it.
## What it does not mean
A no-reply address on your own list is usually a scraping artefact or a form that captured the sender of a notification rather than a person. It is not a bounce, and the mailbox often does exist; it is simply a place where a message goes to be ignored.
---
# Plus addressing
Plus addressing is the convention by which mia+promo@ delivers to the same mailbox as mia@; the part after the plus is a tag the owner chooses, reported as is_alias and stripped in normalized_email.
Date: 2026-09-13
Plus addressing, also called sub-addressing, is the convention most mailbox providers follow of delivering `mia+anything@acme.com` to the mailbox of `mia@acme.com`: the part after the plus sign is a tag the owner chooses per signup, useful for filtering and for seeing who leaked an address, and invisible to the mailbox itself. Gmail adds a second rule of its own, ignoring dots in the username, so `m.i.a@gmail.com` and `mia@gmail.com` are one inbox too.
## What Spaw answers
`is_alias` reports a plus-tag, and `normalized_email` carries the canonical inbox with the tag removed and Gmail's dots folded away. Dedupe a list, key a suppression entry and count a repeat on `normalized_email`, never on the raw input, or one person becomes many. The `alias` weight is 5: a tag is a hint that a signup bonus is being farmed, not a fault.
## What it does not mean
A tagged address is a real address, and the mailbox is verified like any other. The tag is the owner's, and a form that rejects it loses a careful customer rather than a fraudster.
---
# Recycled spam trap
A recycled spam trap is a real mailbox that was abandoned, bounced as unknown for a period, and was then reactivated by its provider to catch senders who never removed it.
Date: 2026-09-13
A recycled spam trap is a real mailbox that its owner abandoned, that the provider then rejected as unknown for months, and that the provider switched back on purely to see who still sends to it. Nobody reads it; a message arriving there proves the sender ignored the bounces or never sent at all during the dead period, which is why recycled traps carry the most weight with the operators who keep them.
## What Spaw answers
The shape is exactly what `previously_invalid` reports: a mailbox that answered "no such mailbox" earlier — to the handshake, or in a bounce you reported whose reason read as user unknown — and answers yes now. The date of the last not-found rides in `previously_invalid_at`, the verdict is `risky` with the reason `recycled_mailbox`, and the `recycled_mailbox` weight of 30 applies. The memory is kept for a year under a keyed hash of the address.
## What it does not mean
A mailbox that revives is not always a trap: a company rehires, a person reopens an account. The signal says the address was dead and came back, and that adding it to a campaign on the strength of one fresh confirmation is exactly the mistake a recycled trap exists to catch.
---
# SPF
SPF (Sender Policy Framework, RFC 7208) is a DNS TXT record in which a domain lists the servers allowed to send mail using its name, ending in a qualifier that says what to do with the rest.
Date: 2026-09-13
SPF, the Sender Policy Framework of RFC 7208, is a DNS TXT record beginning `v=spf1` in which a domain lists the addresses, hosts and included policies allowed to send mail using its name, and ends with an `all` term whose qualifier — `-all` fail, `~all` soft fail, `?all` neutral — says what a receiver should make of everything else. A receiver evaluates it against the envelope sender of each message.
## What Spaw answers
An email answer carries `has_spf`; a business domain without a record adds the `no_spf` weight of 10, while free providers are exempt because theirs is known. The domain endpoint adds `spf_all`, the effective qualifier, and `spf_lookup_count`, the number of DNS-querying terms the policy needs once `include:` and `redirect=` are followed — RFC 7208 allows ten, and a policy over that limit fails permanently at every receiver, which is the most common SPF fault there is.
## What it does not mean
SPF says who may send as the domain's envelope sender. It says nothing about the address a person sees in their mail client, which is what DKIM alignment and DMARC cover, and it breaks on forwarding, which is why a `-all` policy is not by itself a reason to expect delivery.
---
# Security gateway
A security gateway is a filtering service — Proofpoint, Mimecast, Barracuda — that a company points its MX records at so that inbound mail is scanned before it reaches the real mailbox server.
Date: 2026-09-13
A security gateway is a filtering service that sits in front of a company's real mailbox servers: the domain's MX records point at the gateway, which scans every inbound message for malware, phishing and spam and only then hands it to Microsoft 365, Google Workspace or an on-premises server behind it. Proofpoint, Mimecast and Barracuda are the ones most domains use.
## What Spaw answers
`mx_provider_type` answers `security_gateway` when the MX hosts belong to one. Many gateways accept every recipient during the handshake and sort them out behind, so a catch-all answer at such a domain is the gateway's design rather than a sign of a squat: the `catch_all_gateway` weight of 15 takes the place of the catch-all 30, the verdict reason stays `catch_all`, and `mailbox_confidence` still estimates the mailbox from the domain's posture and the username's shape.
## What it does not mean
A gateway does not make an address deliverable. A message to a mailbox that does not exist behind the gateway still bounces — later, and from the server behind, which is why reporting your bounces sharpens the confidence score for the whole domain.
---
# Sending policy
A sending policy is the set of rules an account attaches to its email lookups — which signals block, a score ceiling, a mailbox-confidence floor — that decides ok_to_send on every answer.
Date: 2026-09-13
A sending policy is the set of three optional rules an account attaches to every email lookup it runs — the signals that block whenever they fire, a ceiling on the risk score, and a floor for the mailbox confidence of an unconfirmed mailbox — from which Spaw answers `ok_to_send` on each response, with `blocked_by` naming the rule that decided. It turns a 46-field answer into the one boolean most integrations actually branch on.
## What Spaw answers
An undeliverable verdict is always blocked, whatever the policy says. Otherwise the rules are applied in a fixed order: the first blocked signal in the published weights' order, then the score ceiling, then the confidence floor. Until an account sets a policy the defaults apply — the disposable, typo-squat, parked-domain, blocklisted-network and no-reply signals block, and so does any score over 60 — and the policy endpoints or the Sending policy tab change them for every member of the account.
## What it does not mean
The policy is not the score. The weights stay published and recomputable, and reading the flags is still the right thing to do when a form needs more than one decision; the policy exists so that the common case needs one field.
---
# Spam trap
A spam trap is an email address nobody uses, kept by a mailbox provider or blocklist operator so that any message sent to it proves the sender did not verify consent or clean the list.
Date: 2026-09-13
A spam trap is an email address that no person uses, kept by a mailbox provider, an anti-abuse group or a blocklist operator so that any message sent to it proves the sender did not verify consent or clean the list. Traps are never published as traps: a pristine trap is planted on web pages for harvesters to scrape, a recycled trap is an abandoned mailbox switched back on, and a typo trap sits on a misspelled domain.
## What Spaw answers
No verifier can name a trap, because a trap that is known stops working. What Spaw can see are the shapes traps take: a domain on the typo-squat map answers `likely_typo`, a mailbox that once said it did not exist and exists again answers `recycled_mailbox`, and a username that looks machine-made carries the `generated` weight. Reporting your bounces through the feedback endpoint feeds the not-found memory that the recycled shape is read from.
## What it does not mean
A clean verification is not proof that an address is not a trap, and a trap hit is not a fine: the cost is reputation at the receivers that read the operator's list. The defence is the list itself — confirmed opt-in, prompt bounce handling, and never mailing an address nobody has heard from in years.
---
# TLS-RPT
TLS-RPT (RFC 8460) is a DNS record at _smtp._tls. naming an address where sending servers report failures to deliver over TLS, so a broken MTA-STS policy is seen rather than silent.
Date: 2026-09-13
TLS-RPT, SMTP TLS Reporting, is the mechanism of RFC 8460 by which a receiving domain publishes a TXT record at `_smtp._tls.` naming an address, or an HTTPS endpoint, where sending servers send daily reports of connections they could not secure — an expired certificate, a hostname that does not match, an MTA-STS policy they could not fetch. It is the feedback loop that makes a strict transport policy safe to switch on.
## What Spaw answers
The domain endpoint reports `tls_rpt`: whether the record exists. True means someone at the domain will hear when TLS delivery fails; false is a checked negative; null means the lookup failed.
## What it does not mean
A reporting address says nothing about whether TLS is enforced — that is MTA-STS — and the reports go to the domain's own operators, not to the sender. It is a sign of a maintained domain, not a delivery guarantee.
---
# Typo-squat
A typo-squat is a domain registered on a common misspelling of a popular one — gmial.com for gmail.com — that resolves and accepts every address so that misdirected mail can be harvested.
Date: 2026-09-13
A typo-squat is a domain registered on a common misspelling of a popular one — gmial.com, hotmial.com, yaho.com — by someone who wants the mail people send there by mistake. It resolves, it runs mail servers, and it accepts every address, so a verifier that only checks DNS and the mailbox handshake reports a typo-squatted address as deliverable while the password reset goes to a stranger.
## What Spaw answers
A curated map of typo-squats is consulted before the handshake: an address at one of them answers `risky` with the reason `likely_typo`, the `likely_typo` weight of 60, and the intended address in `did_you_mean`, without any probe being spent. The map lists only domains that can never be a legitimate mailbox provider; a misspelling that does not resolve at all is answered by the dead-domain suggester instead.
## What it does not mean
Not every look-alike is a squat — some belong to the provider itself, some to real companies — which is why the map is curated by hand rather than generated, and why an unmapped domain that accepts mail is never second-guessed.
---
# Webmail provider
A webmail provider is a service such as Gmail, Outlook.com, Yahoo or iCloud that hands out free mailboxes to anyone, which Spaw reports as free_provider true and domain_category webmail.
Date: 2026-09-13
A webmail provider is a service that hands out free mailboxes to anyone who asks — Gmail, Outlook.com, Yahoo Mail, iCloud Mail, GMX, Proton and thousands of smaller ones — as opposed to a company running mail for its own domain. Most consumer addresses live at one, which is why a signup form that refuses them refuses most people.
## What Spaw answers
`free_provider` reports whether the domain is on the open free-provider list, refreshed weekly, and `domain_category` answers `webmail`. Two checks are skipped on purpose for these domains: the registration age, because the provider's domain is decades old and says nothing about the person, and the `no_spf` weight, because the provider's authentication posture is known. The mailbox is still probed, and the provider's own username rules are applied before the handshake so an impossible username never spends one.
## What it does not mean
A webmail address is not a lower-quality address, and it is not disposable: those are separate lists with separate weights. It is an address whose domain says nothing about the owner, so every remaining signal comes from the username and the handshake.
---
# Anycast
Anycast announces one IP address from many places at once, so the machine that answers is whichever is nearest on the network, not one fixed location.
Date: 2026-09-12
Updated: 2026-10-02
Anycast is a way of announcing one IP address from many places at once: the same prefix is routed from dozens or hundreds of sites, and a request lands at whichever site is nearest on the network. The public DNS resolvers (1.1.1.1, 8.8.8.8, 9.9.9.9) and the edge networks of the large CDNs work this way, which is why the address you see is the same in Sydney and in Stockholm while the machine answering is different.
## What Spaw answers
`is_anycast` is true when the address sits in a published anycast range: the bundled resolver ranges shipped with the service, plus the edge ranges Cloudflare and Fastly publish, which carry the anycast signal alongside `is_datacenter`. It carries no risk weight. Its job is to qualify the location fields: a geolocation database has to put an anycast prefix somewhere, and wherever it chose is at best the nearest instance, not a place a person is. So `location_confidence` answers `low` for an anycast address, even where the operator's own geofeed names a city.
## What it does not mean
An anycast address is infrastructure, not a visitor. Traffic that arrives *from* one is unusual — a resolver does not browse — so the flag is rarely the reason a request is interesting. Read it as a warning label on `country`, `city` and the coordinates rather than as a signal about whoever sent the request.
---
# Attack-source list
An attack-source list names single hosts recently seen attacking honeypots and sensors; it churns daily and weighs less than a blocklist of hijacked netblocks.
Date: 2026-09-12
An attack-source list names single IP addresses that were seen attacking in the last few days — scanning ports, brute-forcing logins, tripping honeypots and intrusion sensors run by the list's publisher. It is a different kind of evidence from a threat blocklist: a blocklist such as Spamhaus DROP names whole netblocks hijacked or leased by cybercrime operations, changes slowly and is a judgement about an operator; an attack-source list names individual hosts, many of them compromised machines on ordinary networks, and turns over daily as infections are cleaned and new ones appear.
## What Spaw answers
`is_attack_source` is true when one of the three lists synced hourly names the address — the Emerging Threats compromised-hosts list, the CINS Army list, or blocklist.de — and `attack_source` names the first list that does. The signal weighs +50, between a report by your own account (+60) and a datacenter range (+40), and it is kept apart from `is_blocklisted`, which weighs +80: a host seen attacking yesterday is evidence, a netblock run by criminals is a verdict. A monitored list flags an address the day it appears on one, with the list named in the alert.
## What it does not mean
A listed host is often somebody's infected computer or a rented box that has since been wiped; the weight is sized for that. The lists are also incomplete by nature — they see what their sensors see — so a false answer means "not on these lists today", never "clean".
---
# Bogon
A bogon is an IP address that should not appear on the public Internet: private, reserved or documentation space, or a block no registry has allocated to anyone.
Date: 2026-09-12
A bogon is an IP address that should not appear as the source of traffic on the public Internet: private space (10/8, 172.16/12, 192.168/16), loopback, link-local, the carrier-grade NAT block, the documentation ranges, multicast and class E, and, more interestingly, unallocated space — blocks that exist on the map but that no regional Internet registry has handed to any operator. A packet claiming to come from one is forged or misrouted, and a request from one deserves no benefit of the doubt.
## What Spaw answers
Two fields cover the two halves. Private, reserved and documentation space answers `reason: reserved_range`: every other field is null and the lookup is free, because there is nothing to say about it. Unallocated space answers `is_unallocated: true`, evaluated against the five registries' daily delegated statistics, and it is the heaviest feed signal in the score at +100 — such an address has no legitimate owner to be behaving on behalf of. The flag is null rather than false until every registry file covering that address family is installed, so a partial set of registries never fabricates a negative.
## What it does not mean
Unallocated is not the same as unused. Space can be allocated and dark, or reserved by a registry for a future purpose; the flag says only that the registries recorded no allocation or assignment on the day the files were read.
---
# Carrier-grade NAT (CGNAT)
Carrier-grade NAT puts many subscribers behind one public IP address, so one address is many people and a per-address decision reaches all of them.
Date: 2026-09-12
Updated: 2026-10-02
Carrier-grade NAT is address sharing at the operator's scale: because there are not enough IPv4 addresses for every subscriber, a mobile carrier or a large ISP gives each customer a private address from the 100.64.0.0/10 block reserved for the purpose (RFC 6598) and translates it to one public address shared by hundreds or thousands of them. The public address a server sees is the carrier's gateway, and it changes as the handset moves.
## What Spaw answers
Two things follow. The shared-address block itself, 100.64.0.0/10, answers `reason: reserved_range` and is free, because it can never be a public visitor. And a carrier's public gateway addresses are where `is_mobile` matters: the flag comes from a curated, positive-only list of mobile-carrier autonomous systems, and `connection_type` reads `mobile` for them. Their located city is the gateway's, so `location_confidence` answers `low` for them even where the carrier's own geofeed names it. Nothing in the answer can separate one subscriber behind that gateway from the next.
## What it does not mean
A block or a rate limit keyed on a CGNAT address hits everyone behind it, which is a lot of innocent handsets. The velocity count in `seen_by_you` and any outcome you report are keyed by address too, so on a mobile network they describe the gateway, not the person; use them with that in mind, and prefer the signals that travel with the session rather than with the address.
---
# Corporate proxy
A corporate proxy is the egress of a company's secure web gateway: every employee reaches the Internet from the gateway's address, so the location is the gateway's.
Date: 2026-09-12
A corporate proxy, in the sense the IP answer uses, is the egress of a company's secure web gateway — the cloud service a business routes every employee's browsing through for filtering and inspection. Each employee reaches the Internet from the gateway's address in whichever of the provider's cities their office was steered to, so a whole company in Munich can appear to be a handful of addresses in Frankfurt, and the traffic is a workplace, not a home connection and not a bot.
## What Spaw answers
`is_corporate_proxy` is true on an address inside the cloud enforcement node ranges Zscaler publishes for each of its clouds so that customers can allow-list them; `corporate_proxy` names the operator. The flag carries no risk weight, and like relay egress and a verified crawler it switches the datacenter weight off, because the gateway sits on infrastructure by design and its operator is known. `connection_type` reads `corporate_proxy`, and the cross-signal consistency check withholds its country comparisons for such an address, since the country is the gateway's.
## What it does not mean
It says nothing about the company or the person behind the gateway, and a gateway address is shared by everyone routed through it, so a per-address decision reaches a whole office. Read it as "a workplace, located at the gateway", and stop there.
---
# Forward-confirmed reverse DNS
Forward-confirmed reverse DNS looks up an address's PTR name, then resolves that name back; a name that comes back to the same address proves who owns it.
Date: 2026-09-12
Forward-confirmed reverse DNS is a two-step check on an IP address: look up the PTR record that names the address (`crawl-66-249-66-1.googlebot.com`, say), then resolve that name forward to its A or AAAA records and see whether the original address is among them. Anyone who controls a block can publish any PTR name they like, so the reverse lookup alone proves nothing; the forward confirmation is what turns a name into evidence, because only the owner of the name's domain can make it resolve back.
## What Spaw answers
The check runs only when a lookup asks for it with `hostname: true`, because it is a live DNS query with a two-second budget. `hostname` is the PTR name, `hostname_verified` whether it forward-confirmed, and a verified name under an operator's documented domain — googlebot.com, google.com, search.msn.com, applebot.apple.com, yandex.*, crawl.baidu.* — proves the crawler and sets `is_crawler` even where no published range does. `hostname_kind` reads the naming convention (`dynamic` for pool and DHCP names, `static` for fixed and hosting names) and `hostname_domain` is the registrable domain, so the operator behind a name is one field.
## What it does not mean
An unverified name is just text the block's owner chose; it is never used to prove anything. And a verified name proves the operator, not the intent: a real Googlebot address is Google's, whatever it happens to be fetching.
---
# Geofeed (RFC 8805)
A geofeed is a CSV file a network operator publishes to say where its own IP prefixes are, the closest thing to a first-party answer geolocation has.
Date: 2026-09-12
Updated: 2026-10-02
A geofeed is a plain CSV file a network operator publishes to declare where its own IP prefixes are, one line per prefix with a country, a region and a city, in the format RFC 8805 fixed in 2020. The registries let an operator point at its file from the registration record of its address space, so anyone reading the registry can find the operator's own answer instead of inferring one from measurements.
## What Spaw answers
Where a geofeed or an operator's own list covers an address, its declaration wins over the database and `location_source` reads `operator`; `location_confidence` is then `high`, because the network operator said so, unless the address is anycast or sits on a listed mobile carrier or satellite operator: there the declared place is a gateway, an anycast instance or a ground station, and the grade is `low`. The service discovers every geofeed the five registries point at once a week and compiles them into one feed, beside the provider lists it reads directly (Apple's relay egress file, the server lists of the VPN providers, the cloud providers' RFC 8805 files). When an operator's declaration disagrees with the database on the country or the city, the region and the coordinates answer null rather than a point in the wrong place.
## What it does not mean
A geofeed is a statement, not a measurement: it is as accurate as the operator keeps it, and it describes where a prefix is deployed, not where a particular subscriber sits inside a city. Most consumer ISPs publish none, which is why the database, and the weekly measurement against the geofeeds that do exist, still carry most addresses.
---
# Parked domain
A parked domain is one whose nameservers belong to a parking or for-sale service, so nobody runs a website or mailboxes there; if it still accepts mail, whoever collects it is not your customer.
Date: 2026-09-12
A parked domain is one whose nameservers belong to a parking or for-sale service — Sedo, ParkingCrew, Bodis, Afternic, Dan and their kind — so nobody runs a website or mailboxes there.
The registration is held for sale, for a lapsed project, or for the advertising a parking page earns. Most parked domains accept no mail at all; the ones that do are collecting it rather than delivering it to anyone who signed up.
## What Spaw answers
The field is `parked`, on both the address and the domain answer. It is read from the domain's NS records against a curated list of parking nameservers, and only once the domain has passed the mail-server check, so it is null on a domain with no MX record — that domain already answers [`no_mx_records`](/docs/reasons/no_mx_records). A parked domain that still accepts mail answers the verdict `risky` with the reason [`parked_domain`](/docs/reasons/parked_domain) and carries +60 on the score.
## What it does not mean
A registrar's default DNS is never on the list, because real sites run on it too; a domain a small company keeps on its registrar's own nameservers is not parked. And parked is a fact about the domain today, not about the address: a domain that was a real company and is parked now has lost its people, which is why the answer is to ask for another address rather than to keep sending.
---
# RIR allocation
An RIR allocation is a block of IP addresses one of the five regional Internet registries handed to an operator, recorded with a country and a date in its daily statistics.
Date: 2026-09-12
An RIR allocation is a block of IP addresses that one of the five regional Internet registries — ARIN for North America, RIPE NCC for Europe and the Middle East, APNIC for Asia-Pacific, LACNIC for Latin America and AFRINIC for Africa — handed to a network operator, together with the country the registry recorded for it and the date it did so. Each registry publishes the whole ledger every day as its delegated-extended statistics file, which is the most authoritative public record of who was given which space and when.
## What Spaw answers
Three fields come straight out of those files: `registry` names the registry (`arin`, `ripencc`, `apnic`, `lacnic`, `afrinic`), `registry_country` the country it recorded, and `allocated_at` the date. They are facts about the block, not weights. `is_unallocated` is their negative: an address in none of the five files was never handed out and scores +100. A `registry_country` that disagrees with the geolocation is worth a second look, and a block allocated or transferred a few weeks ago is a classic tell in fraud reviews — the response states the date and leaves the judgement to you.
## What it does not mean
The registry's country is where the operator registered the block, which is not always where the addresses are used; a multinational's space can be registered in one country and deployed in twenty. That is exactly why it sits beside the geolocation rather than replacing it.
---
# Relay address
A relay address is an alias handed out by a forwarding service such as Firefox Relay or SimpleLogin that delivers to a real person's real mailbox without revealing it.
Date: 2026-09-12
Updated: 2026-10-07
A relay address is an alias handed out by a permanent forwarding service — Firefox Relay, SimpleLogin, Addy.io, DuckDuckGo Email Protection and their kind — that delivers to a real person's real mailbox without revealing it. The person controls the alias, can turn it off, and often keeps it for years; it is routinely the only address a privacy-minded customer will give a form.
## What Spaw answers
The fields are `is_relay` and `relay_service`, and the domain sorts as `domain_category: "relay"`. A relay domain sits on the open burner list because the service hands out addresses on demand, so a verifier that only reads the list reports it as disposable. Spaw does not: `disposable` is forced false for a relay, the mailbox is probed like any other, and the score carries the `relay` weight of 5 for the withheld identity rather than the disposable 80.
The flag is read from the domain, so only a domain where every address is an alias carries it: Proton Pass's `passmail.net`, Sign in with Apple's `privaterelay.appleid.com` and `private.icloud.com`, and Brave's `bravealias.com` do. iCloud Mail and Proton Mail addresses are ordinary mailboxes and answer `is_relay: false`, and an iCloud+ Hide My Email alias sits on icloud.com beside them, where nothing tells it apart.
## What it does not mean
A relay address is not a throwaway. The question a burner list answers — will this inbox exist tomorrow — has a different answer here from the one a signup form is asking — will this person still read mail sent to it in a year. The alias can be switched off by its owner, which is a reason to keep the relationship worth keeping rather than a reason to refuse the address.
---
# Time zone mismatch
A time zone mismatch is when the zone a visitor's browser reports keeps a different clock from the zone their IP address is located in, a cheap and strong signup signal.
Date: 2026-09-12
A time zone mismatch is the disagreement between the zone a visitor's browser reports — `Intl.DateTimeFormat().resolvedOptions().timeZone`, which the operating system sets — and the zone the visitor's IP address is located in. The two are set by different things: the address by the network the request arrived from, the browser zone by the device the person is holding. When someone in one country routes through an address in another, the clocks disagree, and the browser is the half that is hard to fake without noticing.
## What Spaw answers
Send the browser's zone as `browser_timezone` with a lookup (the browser endpoint takes it too) and `timezone_matches` answers: true when the browser's zone is the located zone or keeps the same clock right now (Paris and Berlin both do), false when the clocks differ, null when there is nothing to compare or the located zone is unknown. The located zone is `timezone`, settled by the curated country and region map or, for split countries such as the United States, by the nearest listed place inside the located country, which `timezone_source` reports.
## What it does not mean
A mismatch is one signal, not a verdict: a traveller, a laptop that never had its clock changed, and a corporate gateway in another city all produce one honestly. It carries no weight in the score for that reason; it is a fact for a signup flow to weigh beside the others.
---
# iCloud Private Relay
iCloud Private Relay is Apple's two-hop relay for Safari and iCloud+ subscribers; a site sees an Apple egress address with a coarse location, never the subscriber's own.
Date: 2026-09-12
iCloud Private Relay is the privacy service Apple switches on for iCloud+ subscribers in Safari: traffic passes through two hops, one run by Apple that knows the subscriber and one run by a partner that knows the destination, so neither sees both. A website sees the second hop's address — an egress address Apple publishes in a list with the coarse city or region it stands for — and never the subscriber's own.
## What Spaw answers
`is_relay` is true on an address from Apple's published egress list, `privacy_service` reads `iCloud Private Relay`, and the operator-declared location from that list wins over the database with `location_source: operator`. The weight is +10, the smallest of the anonymity signals, because Private Relay is on by default for a paying subscriber and says almost nothing about intent; the datacenter weight is skipped for relay egress, since it sits on infrastructure by design. Apple's egress ranges also appear in the aggregate VPN list, so `is_vpn` answers false on them unless a VPN provider's own list names the address — the first-party declaration outranks the aggregate classification.
## What it does not mean
A relay address is not a VPN in the sense fraud teams mean: there is no server to pick, no country to choose, and the location Apple publishes is honest to the region. Treating +10 as a block would refuse a large share of iPhone users for no signal at all.
---
# ASN
An Autonomous System Number identifies one network that announces its own routes to the internet under a single routing policy.
Date: 2026-09-10
An Autonomous System Number identifies one network that announces its own routes to the internet under a single routing policy. Every address on the public internet belongs to exactly one at a time, and the AS is the closest thing to an answer to "whose network is this?".
## What Spaw answers
`asn` is the number, `org` the organisation that holds it, and `asn_type` the kind of network it is: hosting, ISP, business, education, government or similar. Three registry facts sit beside them — `registry`, `registry_country` and `allocated_at`, the date the block was allocated.
`asn_type` is what most policies actually want. A signup from a hosting AS is a different thing from a signup from a consumer ISP, and the type says which without anyone having to keep a list of cloud providers.
## What it does not say
An AS belongs to an operator, not to a person. On a hosting, VPN or relay network the country, the city and the organisation are the operator's, and the risk score's country comparisons are withheld for that reason. Nothing about an ASN identifies who is using the address.
---
# Address register
An address register is a country's official file of every address its authorities recognise, usually keyed on buildings rather than letterboxes.
Date: 2026-09-10
Updated: 2026-10-14
An address register is a country's official file of the addresses its authorities recognise, usually keyed on buildings rather than letterboxes. Spaw reads the openly published registers synced to its servers — the [address verification guide](/docs/address-verification) lists each with its licence — and each publisher states how complete its own file is, except where Spaw marks a register partial because its copy cannot vouch for an address it lacks: the [address data sources page](/tools/address-checker/sources) says which and why.
## What Spaw answers
`exists` is the answer, and the rule behind it is the strictest thing in the address product. It is true only on a `premise` match — the building itself was found. It is false only when the register that answered has complete coverage and does not carry this one. In every other case it is null.
`match_level` says how far the address could be followed: `premise`, `range` (the house number falls inside a published range of numbers on the street), `street`, `postcode` or `none`. `register` names the file that answered and `register_coverage` reports `complete` or `partial`. Always read `exists` against that last field.
## Why null is the common answer
Several registers are partial. Two are partial by their publishers' own account: the American national database, to which states and local authorities contribute voluntarily, and the French one, which is still filling in. Spaw marks the others partial on its own grounds. Brazil's is the address list of the 2022 census, a snapshot rather than a maintained register. Each German register covers one Land, and Germany has no national address register. Great Britain's, Ordnance Survey's Open Names, files each road under one postcode district, though a long road runs through several; it lists streets and never buildings, so it can never answer for a building at all. Denmark's copy is no longer refreshed: Klimadatastyrelsen lists every Danish address, but the copy here was taken on 26 September 2026, before the interface it was read through closed on 1 October 2026, so an address created since may be missing from it. An address missing from a partial register may be in a part nobody has shared yet, outside the area the register covers, or newer than the copy, so absence there is not evidence and the field says null instead of false.
The `not_in_register` risk signal (+20) fires only where `exists` is false — that is, only where a complete register was asked and answered.
---
# CIDR
CIDR notation writes a range of IP addresses as one address and a prefix length, so 203.0.113.0/24 means the 256 addresses sharing those first 24 bits.
Date: 2026-09-10
CIDR notation writes a range of IP addresses as one address and a prefix length: `203.0.113.0/24` means the 256 addresses that share those first 24 bits. Classless Inter-Domain Routing replaced the old fixed classes in 1993, and every allocation, route and blocklist has been written this way since.
## Where it appears in Spaw
`network` on an IP answer is the CIDR block the address was found in — the unit the registries allocate and the feeds list, so it is the right thing to remember if you are caching or aggregating.
CIDR is also the input format in two places: a secret API key can carry an allowlist of source addresses or ranges, and every threat and datacenter feed read here is compiled from published CIDR lists.
## Why a range and not an address
Almost nothing about an IP address is a fact about the address alone. Allocation, hosting, blocklisting and geolocation are all properties of a block, and an answer that pretends otherwise is claiming a precision the data does not have. `location_confidence` says which of country, region or city the underlying data actually supports for a given block.
---
# Catch-all domain
A catch-all domain is one whose mail server accepts mail addressed to every recipient, whether or not that mailbox exists.
Date: 2026-09-10
Updated: 2026-10-08
A catch-all domain is one whose mail server accepts mail addressed to every recipient, whether or not that mailbox exists. It is also called accept-all. The server answers `250 OK` at the `RCPT TO` step for any name, then sorts the mail out afterwards — delivering what matches a real mailbox, forwarding the rest, or dropping it.
## What Spaw answers
The field is `catch_all`. When it is true the handshake proved that the domain receives mail and proved nothing about the mailbox, so the verdict is `risky` with the reason [`catch_all`](/docs/reasons/catch_all) and the risk score carries +30. `mailbox_exists` stays null, because no honest answer can be given.
Spaw does not report a catch-all address as deliverable, ever. The verdict names what was established rather than what is likely.
## What it does not mean
Catch-all is a property of the domain, not of the address. Most catch-all domains are ordinary businesses whose server was configured that way on purpose, and mail to a real person there arrives normally. It is a reason to treat the address as unconfirmed, not a reason to discard it.
Where a probe ran and left `mailbox_exists` null, `mailbox_confidence` carries a recomputable 0–100 estimate that this particular mailbox exists, built from the domain's mail authentication, its registration age, the shape of the local part and, once a domain has five or more reported delivery outcomes, the share of them that bounced. A relay alias is the exception: it stays at 50, and only a high bounce share or a plus-tag lowers it. It is a published heuristic, not a probe, and the page that lists its inputs says so.
---
# Check digit
A check digit is a digit computed from the rest of an identifier so that a mistyped one usually fails arithmetic before anybody looks it up.
Date: 2026-09-10
A check digit is a digit computed from the rest of an identifier so that a mistyped one usually fails arithmetic before anybody looks it up. It catches typos and transpositions; it proves nothing about whether the identifier was ever issued.
## Where Spaw computes one
Two places, and both say the same thing about what it means.
`checksum_valid` on a business answer is the ISO 7064 MOD 97-10 check over a [LEI](/glossary/lei). It is reported separately from the register lookup precisely so the two are never confused: a well-formed LEI that no register carries is a well-formed LEI that no register carries.
The [card number checker](/tools/card-number-checker) computes the Luhn digit over a card number entirely in the visitor's browser — no request, no endpoint, no log line, and the number never appears in a URL.
## Why the distinction is worth a page
"Valid" is the most overloaded word in identifier checking. A check digit answers one narrow question: were these characters plausibly copied correctly. Whether the thing exists, whether it is current, and whether it belongs to who you were told, are three further questions with three further answers — and this product keeps them in separate fields rather than collapsing them into one boolean.
---
# Company number
A company number is the identifier a national registrar assigns to an incorporated company, unique within that register and never within the world.
Date: 2026-09-10
A company number is the identifier a national registrar assigns to an incorporated company. It is unique within that register and never within the world, which is why every lookup of one needs to know which country's register to read.
## What Spaw answers
One register is installed: the United Kingdom's. A UK number is eight characters, and a shorter one is left-padded with zeros before it is looked up — `445790` and `00445790` are the same company.
The answer carries the legal name, the status in the register's own wording, the jurisdiction, the category, the legal form and the incorporation date, plus `register` naming the file that answered.
## The staleness that has to be stated
The free UK company file is republished monthly and only refreshes within a few working days of month end, so an answer here can be up to five weeks behind the live register. Every response names the date of the copy it read in `sources`, and the monitor cadences stop at weekly for the same reason: re-checking a monthly file daily would be theatre.
An identifier from a register that is not installed answers `unsupported_register`, and one no installed register carries answers `unknown_identifier`. Both mean "not carried here" and neither means the company does not exist.
---
# Credit
A credit is the billing unit of the whole API: one credit buys one fresh lookup that produced a usable answer on any of the six signals, or five on IP.
Date: 2026-09-10
Updated: 2026-10-02
A credit is the billing unit of the whole API: one credit buys one fresh lookup that produced a usable answer, on any of the six signals, except IP, where one credit buys five. There is one balance and it does not care which endpoint spends it.
On IP the credit is spent in a block: the answered lookup that opens a block shows `credits_used` 1 and leaves four lookups prepaid on the account, the next four show `credits_used` 0, and `meta.ip_lookups_prepaid` says how many are left.
## What does not cost one
Three things, and they are the reason the rate card exists rather than a single sentence.
A **non-answer** is free. What counts as one differs by signal and is published per signal on the [pricing page](/pricing): an invalid phone number, an unparseable postal address, an identifier no installed register carries, an email address with no working mail server. A **rejected request** is free — a validation error, a refused key, a rate limit. And a **repeat** of the same value inside seven days is free, held as a keyed marker that stores a hash rather than the value.
An email answer whose mailbox check could not settle — reason `unverified`, whether the receiving server would not say or the partner infrastructure behind the handshake was down — is also free, because nothing was established. An undeliverable email verdict that needed a mailbox check is free within a fair-use allowance and costs a credit past it, with `meta.fair_use_billed` saying so.
## When there are none left
The API answers [`402 INSUFFICIENT_CREDITS`](/docs/errors/INSUFFICIENT_CREDITS) *before* running the lookup, so an empty balance never bills for work that could not complete. Free credits renew at the start of each month and pack credits never expire. Plan credits roll over up to three months' allowance and stay usable for 60 days after you cancel.
`meta.credits_used` on every response says what that call actually cost, including zero.
---
# Datacenter IP
A datacenter IP is an address inside a range operated by a hosting, cloud or server provider rather than by a consumer access network.
Date: 2026-09-10
A datacenter IP is an address inside a range operated by a hosting, cloud or server provider rather than by a consumer access network. Somebody rented the machine behind it.
## What Spaw answers
`is_datacenter` is the flag and `hosting_provider` names the operator when a feed identifies one. The risk score carries +40, above a commercial VPN (+30) and well below an unallocated address (+100), a blocklisted one (+80) or a [Tor exit](/glossary/tor-exit-node) (+70).
## What it means and what it does not
A person signing up from a datacenter address is not browsing from their sofa: they are on a VPS, a scraper, a CI runner, or a proxy someone rented. For a consumer signup that is unusual enough to be worth a second factor. For an API caller, a webhook receiver or a business customer it is completely ordinary, which is why the flag is a signal with a weight rather than a verdict.
The important limit is the other direction. An address that is *not* flagged is not thereby a person's home connection — see [residential proxy](/glossary/residential-proxy) for what open data cannot see.
---
# Delivery point
A delivery point is the smallest unit a postal service delivers to: one letterbox, one door, one box at a counter.
Date: 2026-09-10
Updated: 2026-09-22
A delivery point is the smallest unit a postal service delivers to: one letterbox, one door, one box at a counter. An address names a delivery point; a building may contain hundreds of them.
## What Spaw answers
`address_type` names the kind of delivery point an address describes, in one of nine values: `street`, `po_box`, `private_mailbox`, `general_delivery`, `rural_route`, `military`, `parcel_locker`, `bulk_mail` or `unknown`. `address_type_source` says what settled it — `address_rules` when the operator's own naming conventions did.
Each kind that is not a street has an entry of its own: [PO box](/glossary/po-box), [private mailbox](/glossary/private-mailbox), [general delivery](/glossary/general-delivery), [rural route](/glossary/rural-route), [military mail](/glossary/military-mail), [parcel locker](/glossary/parcel-locker) and [CEDEX](/glossary/cedex), which covers the other business-volume arrangements too.
Four of the nine carry weight on the risk score: a private mailbox +60, general delivery +40, a PO box +30 (zero in the nine countries where a box is the ordinary way to receive mail) and bulk mail +20. None of them is a fault. They are all real delivery points that a postal service serves; the weights exist because a parcel courier, a KYC check and a marketing list want different things from an address.
## What the field is not
It is not a deliverability answer. Whether post actually arrives at a delivery point is a licensed check that this service does not run: `deliverability_checked` answers false and `is_deliverable` stays null rather than guessing.
And nothing in an address answer says who is behind the letterbox. The product is about places.
---
# Disposable domain
A disposable domain is a mailbox provider handing out throwaway addresses that stop being read minutes or hours after they are created.
Date: 2026-09-10
A disposable domain is a mailbox provider handing out throwaway addresses that stop being read minutes or hours after they are created. They exist to receive one confirmation link, and they are the single strongest signal in the email product.
## What Spaw answers
`disposable` is true and the reason is [`disposable`](/docs/reasons/disposable). The verdict is `risky` and the risk score carries +80, the largest weight on the email rate card.
A domain is matched two ways: by name against an open burner-domain list synced onto this server, and by the mail infrastructure behind its MX hosts, so a fresh vanity domain pointed at a known burner operator is caught even though its own name is on no list. Two override files in the repository settle the arguments — an allow list and a deny list, with allow winning.
## The limits worth stating
The list is a list. A burner opened this morning under a new domain and its own MX is not on it, and a legitimate domain is sometimes listed by mistake; the check is a match against public data, not a judgement about the person typing. Spaw answers `risky` rather than `undeliverable` for exactly that reason — the mailbox usually does exist, it just will not exist for long.
---
# E.164
E.164 is the ITU-T standard that gives every phone number one unambiguous international form: a plus sign, a country calling code, and at most fifteen digits.
Date: 2026-09-10
E.164 is the ITU-T standard that gives every phone number one unambiguous international form: a plus sign, a country calling code, and at most fifteen digits in total, with no spaces, brackets or dashes. `+14155550142` is E.164; `(415) 555-0142` is the same number written for a person.
## What Spaw answers
`e164` is the canonical form and the field to store. Three others carry the same number written differently: `national` for display inside its own country, `international` for display across borders, and `rfc3966` as a `tel:` URI for a dial link.
## Why the country parameter exists
Every region's calling code, with an example number in each form, is on its page under [phone facts by country](/tools/phone-number-lookup/countries).
E.164 is unambiguous only when the calling code is present. A number typed without one — `4155550142`, or a national form beginning with a trunk `0` — could belong to several countries, so a lookup needs `country` to know which numbering plan to read it against. Without one the answer is [`missing_country`](/docs/reasons/missing_country), which is free.
An API key can carry a default country, applied only when the request body names none at all; an explicit `country: null` still means "read it from the calling code".
---
# Greylisting
Greylisting is a mail server's habit of refusing an unfamiliar sender's first delivery attempt with a temporary error and accepting the retry.
Date: 2026-09-10
Updated: 2026-10-02
Greylisting is a mail server's habit of refusing an unfamiliar sender's first delivery attempt with a temporary error — a 4xx — and accepting the retry a few minutes later. It is a cheap spam defence: real mail servers retry, and much bulk-sending software does not.
## What Spaw answers
A greylisted handshake ends without an answer about the mailbox. `smtp_checked` is true, `mailbox_exists` stays null, and `smtp_reason` is [`greylisted`](/docs/reasons/greylisted). The verdict then rests on the free signals alone, which usually means `risky` with the reason [`unverified`](/docs/reasons/unverified) and +10 on the risk score.
## Why it is free
An answer that leaves the mailbox unknown is not billed. When the verdict is `risky` with the reason `unverified` — the server greylisted the handshake, it timed out, or the partner infrastructure behind it was down — the lookup costs nothing and is not cached, so the next lookup of the address asks the server again and bills 1 credit only once the mailbox is confirmed. A verdict another signal decides, such as a role address whose handshake was greylisted, still costs a credit as that verdict.
Retrying immediately rarely helps. The same address checked again after the greylisting window has passed will often settle; pass `callback_url` with the lookup and Spaw re-checks it after 5 and 20 minutes, billing 1 credit only if a re-check confirms the mailbox.
---
# Idempotency key
An idempotency key is a caller-chosen string sent with a create request so that a retry of the same request returns the original result instead of creating a second one.
Date: 2026-09-10
An idempotency key is a caller-chosen string sent with a create request so that a retry of the same request returns the original result instead of creating a second one. It exists because a network timeout tells you nothing about whether the server acted.
## What Spaw answers
Send `Idempotency-Key` (1 to 128 printable ASCII characters) when creating a bulk job. A retry carrying the same key and the same request answers `200` with the same body — the same job, the same webhook secret — and the header `Idempotent-Replayed: true`. The same key with a *different* request answers [`409 IDEMPOTENCY_KEY_REUSED`](/docs/errors/IDEMPOTENCY_KEY_REUSED) rather than quietly returning the wrong job.
The request is fingerprinted as the lookup will read it, with a list-level country already folded into each item, so a retry that spells the same request differently still replays rather than being refused.
## Where it does not apply
Single lookups have no idempotency key and do not need one: a repeat of the same value inside seven days is free anyway, which gives a retried lookup the same billing outcome without any bookkeeping.
---
# Implicit MX
Implicit MX is RFC 5321's fallback: a domain with no MX record but with an A or AAAA record is treated as its own mail exchanger.
Date: 2026-09-10
Implicit MX is RFC 5321's fallback: a domain with no MX record but with an A or AAAA record is treated as its own mail exchanger, and mail is delivered to that host. The rule dates from a time when a domain's web server and mail server were the same machine.
## What Spaw answers
`mx_implicit` is true and the reason is [`implicit_mx`](/docs/reasons/implicit_mx). The verdict is `risky`, not undeliverable, and the risk score carries +30 under "web-only domain, no MX record".
The judgement behind that weight is plain: by the letter of the standard the address is deliverable, and in practice the host at a modern domain's A record is a web server that will refuse the connection. Spaw will not call it deliverable and will not call it dead, because both would be claims the DNS does not support.
## What to do with it
Treat it the way you treat a [catch-all](/glossary/catch-all-domain): a signup can proceed, a bulk send probably should not. If the address matters, ask the person to confirm it another way — an implicit-MX domain that really does receive mail is rare enough to be worth checking rather than assuming.
---
# LEI
A Legal Entity Identifier is a 20-character code identifying a legal entity that takes part in financial transactions, defined by ISO 17442.
Date: 2026-09-10
A Legal Entity Identifier is a 20-character code identifying a legal entity that takes part in financial transactions, defined by ISO 17442. Eighteen characters identify the entity and the last two are a check digit.
## What Spaw answers
`identifier_type` is `lei`, and the answer carries the legal name, the status in the register's own wording, the country, the jurisdiction and the incorporation date. Three fields belong to the LEI alone: `checksum_valid`, `registration_status` and `next_renewal_at`.
That last pair matters because a LEI is a subscription. It lapses if nobody renews it while the company behind it carries on trading, so `registration_status` is a fact about the identifier and never about whether the business is alive. The flag `lei_registration_not_current` names it when it happens.
## What it is not
It is not official reference data and Spaw is not affiliated with the organisation that runs the LEI system. The answer here is a derived verdict read from a daily copy of the public file, held on this server so that no third party sits in the request path, and every response names the date of the copy it read.
There is no registered address in the answer, deliberately: an identifier does not say whether a company or a sole trader stands behind it, and a registered address is very often somebody's home.
---
# Line type
Line type is the kind of service a phone number was allocated for: mobile, fixed line, VoIP, toll-free, premium rate, shared cost and the rest.
Date: 2026-09-10
Updated: 2026-09-27
Line type is the kind of service a phone number was allocated for. Spaw answers one of `mobile`, `fixed_line`, `fixed_line_or_mobile`, `voip`, `toll_free`, `premium_rate`, `shared_cost`, `personal_number`, `pager`, `uan`, `voicemail` or `unknown`.
## What Spaw answers
`line_type` carries the value and `line_type_source` says which dataset decided it: `numbering_plan` when the numbering plan decided it (the metadata for the prefix, or the regulator's plan where it types a range differently, as for Ofcom's drama ranges and the UK 084 and 087 ranges below), `block_allocation` when a North American `fixed_line_or_mobile` was settled by looking up who holds the block.
In the United Kingdom, 084 and 087 numbers answer `shared_cost`, not `premium_rate`. Ofcom's numbering plan (the version of 22 April 2025) designates 0843 to 0845 and 0870 to 0873 as non-geographic numbers whose service charge is capped, at 5.833p and 10.83p a minute or a call before VAT, and keeps premium rate for the 09 range, capped at 300p a minute. The numbering metadata types those two ranges as premium rate, so Spaw reads them the way the plan does. Ofcom's premium-rate-services rules still cover 0871 to 0873, so a sender that treats those as premium rate should block `shared_cost` as well.
Which line types a country's plan uses, with an example number of each, is on its page under [phone facts by country](/tools/phone-number-lookup/countries); [the United Kingdom](/tools/phone-number-lookup/countries/gb) is one.
Four line types carry weight on the phone risk score: [premium rate](/docs/signals/phone/premium_rate) +60, [VoIP](/docs/signals/phone/voip) +30, [pager](/docs/signals/phone/pager) +30 and [shared cost](/docs/signals/phone/shared_cost) +20. The reasoning is about who pays and who can be reached, not about the technology being worse.
## Toll-free, premium rate, shared cost and personal numbers
Four types describe who pays for a call, or where it is sent, rather than the network behind the number, and the numbering plan always decides them (`line_type_source: numbering_plan`). A `toll_free` number is free to the caller: 800, 833, 844, 855, 866, 877 and 888 in the North American plan, and 080 in the United Kingdom, a range Ofcom's plan marks "Free to caller". A `premium_rate` number charges the caller for a service on top of the call: 900 in North America, 09 in the United Kingdom. A `shared_cost` number splits the charge, as the UK's 084 and 087 above do. A `personal_number` follows its holder: Ofcom's plan designates 070 for Personal Numbers, a number-translation service that lets someone be called at one number and take the call "at almost any Telephone Number, including a Mobile Number", and the metadata types several North American 5XX codes, 500 among them, the same way. In libphonenumber 9.0.37's metadata, 184 of the 245 regions have a toll-free range, 150 a premium-rate range, 59 a personal-number range and 53 a shared-cost range.
Premium rate is the only one of the four the default [SMS policy](/glossary/sms-policy) blocks, through its `premium_rate` signal. `toll_free` and `personal_number` carry no risk weight, and an account that never sends to them can list them, like any type but `fixed_line_or_mobile`, in `blocked_line_types`.
## What it is not
Line type is not a claim that the handset is reachable today, and it is not port-aware — it describes the allocation. `fixed_line_or_mobile` is a real answer, not a failure: in the North American plan the two share blocks, and when the block holder settles nothing the honest answer is that both remain possible.
---
# MX record
An MX record is the DNS record naming a host that accepts mail for a domain, with a preference number that orders the candidates.
Date: 2026-09-10
An MX record is the DNS record naming a host that accepts mail for a domain, with a preference number that orders the candidates — lower is tried first. A domain may publish several, and the set of them is the first thing any verification reads.
## What Spaw answers
`mx_found` says whether a host that can accept mail was found. `mx_provider` names the operator behind the hosts, recognised from their patterns — Google, Microsoft, Proton, Fastmail, Mimecast and the rest each have a page under [email providers](/email-providers).
Three reasons come straight out of this step: [`no_mx_records`](/docs/reasons/no_mx_records) when DNS returns neither an MX nor an A record, [`mx_unresolvable`](/docs/reasons/mx_unresolvable) when every published target fails to resolve or points somewhere that cannot receive mail from the internet, and [`null_mx`](/docs/reasons/null_mx) when the domain publishes the standard "no mail here" record.
## What it cannot tell you
An MX record is a claim about the domain, not about a mailbox. It is checked live on every lookup rather than read from a cache, because DNS is the one part of the answer that costs nothing to get right and changes without warning.
---
# Mail drop
A mail drop is an address that exists to receive post for people and companies who are not there, such as a mail-forwarding branch or a formation agent.
Date: 2026-09-10
Updated: 2026-09-22
A mail drop is an address that exists to receive post for people and companies who are not there: a mail-forwarding branch, a virtual-office suite, a company formation agent's registered-office service.
## What Spaw answers
`is_mail_drop` is the flag, `mail_drop_provider` names the operator or agent where one is known, and `mail_drop_source` says which of three things decided it. `operator_list` means the address appears on a mail-forwarding operator's own published branch list. `registered_agent` means it is an address a registered-agent or formation service publishes, or that a public listing or filing naming the service carries. `company_cluster` means neither list knows it, but at least 25 companies name the address in an open company register — the published threshold, and the signal that catches a formation agent's office the list does not hold.
The risk score carries +50, and `companies_registered` carries the count itself so you can see the evidence rather than take the flag on trust.
## What it is a fact about
An address. Not a person and not a company. Plenty of legitimate businesses use a formation agent's address, and one registered there has done nothing wrong; a cluster is a property of a building. `company_cluster` in particular is a heuristic with a threshold, and the number that produced it is published beside the flag for exactly that reason.
A mail drop is also not a "drop address" in the parcel-fraud sense — an ordinary address a fraudster has goods sent to for collection. Spaw holds no list of those, and the flag does not detect them. For the United Kingdom, where the registered office is a legal duty and Companies House keeps a default address for companies that fail it, see [how to tell a UK virtual-office or formation address from a trading address](/guides/how-to-tell-a-uk-virtual-office-or-formation-address-from-a-trading-address).
---
# Mailbox handshake
A mailbox handshake is an SMTP conversation opened with a domain's mail server as far as the RCPT TO step and abandoned before any message is sent.
Date: 2026-09-10
A mailbox handshake is an SMTP conversation opened with a domain's mail server as far as the `RCPT TO` step and abandoned before any message is sent. The server's response to that one command is what says whether a mailbox exists. No mail is transmitted, so nobody receives anything.
## What Spaw answers
`smtp_checked` says whether the handshake ran, `smtp_checked_at` when, `mailbox_exists` what it established, and `smtp_reason` why it could not establish anything when it could not — [`greylisted`](/docs/reasons/greylisted), `timeout`, `no_connect`, `mx_unreachable`, `provider_unavailable` and the rest.
## When it runs, and when it does not
The handshake is the only part of an email lookup that leaves this server, so it is gated deliberately. It runs only when the domain's MX accepts mail and no free signal has already settled the address: a disposable domain, a typo-squat or a local part the provider's own rules reject is answered from DNS and open lists alone and never leaves Spaw.
A mailbox the handshake could not confirm is reported `risky` with the reason [`unverified`](/docs/reasons/unverified). It is never reported deliverable — that word is reserved for a mailbox a server confirmed.
---
# NPA/NXX
NPA/NXX is the North American Numbering Plan's name for the first six digits of a US or Canadian number: the area code and the central office code.
Date: 2026-09-10
Updated: 2026-09-27
NPA/NXX is the North American Numbering Plan's name for the first six digits of a US or Canadian number: the three-digit area code (Numbering Plan Area) and the three-digit central office code (NXX). Numbers are allocated to carriers in blocks keyed on those six digits, and the regulators publish the tables. Most US codes are pooled: split again into blocks of a thousand (NPA-NXX-X) that other carriers can hold, which NANPA publishes too.
## The central office code
The NXX is the central office code, which NANPA defines as "the second three digits (also called digits D-E-F) of a ten-digit telephone number in the form of NPA-NXX-XXXX" (https://www.nanpa.com/numbering/co-codesthousands-blocks, accessed 27 September 2026). NANPA notes that it is also known as the exchange or the prefix. A code is ten thousand numbers, assigned to one carrier in one [rate centre](/glossary/rate-centre) or [pooled](/glossary/number-pooling) into thousands-blocks that carriers there hold separately. Not every code can be assigned. N11 digits such as 211, 311 and 911 are dialled on their own as [short numbers](/glossary/short-number), and in NANPA's code file of 26 September 2026 no assigned code starts with 0 or 1 or ends in 11; the file marks the N11 codes and 555 unavailable, so a number in one answers `block_status: reserved`. Not every NPA is a place either: the toll-free codes 800, 833, 844, 855, 866, 877 and 888, the premium-rate 900 and several 5XX personal-number codes carry a service rather than an area, answer `is_geographic: false` and are typed by [line type](/glossary/line-type).
## What Spaw answers
The block behind a number sets three fields. `block_status` says what the table records — allocated, unassigned or reserved; a reserved block, such as the 555 code, raises the [`reserved_block`](/docs/signals/phone/reserved_block) signal. `carrier` names the block holder as the regulator lists it; in a pooled US code, the holder of the number's own block of a thousand where NANPA lists one. `line_type_source` says `block_allocation` when the block, rather than the numbering plan, settled the line type.
That last one is the whole reason the tables are carried. The plan itself cannot separate American mobiles from landlines, because they share blocks; it can only answer `fixed_line_or_mobile`. The block holder often settles it — a wireless carrier's block is mobile, a virtual-number wholesaler's is VoIP and raises the [`virtual`](/docs/signals/phone/virtual) signal.
## What it does not know
A block describes the original allocation, never a later port. A number moved to another carrier keeps the block it was born in, so `carrier` is who the numbers were given to and not who serves the handset today.
Every area code with published blocks has a page of its own under [the phone lookup](/tools/phone-number-lookup), and the plan as a whole is summarised on the phone facts pages for [the United States](/tools/phone-number-lookup/countries/us) and [Canada](/tools/phone-number-lookup/countries/ca).
---
# Null MX
A null MX record is a single MX record with preference 0 pointing at the root, which is how a domain states in DNS that it accepts no mail at all.
Date: 2026-09-10
A null MX record is a single MX record with preference 0 pointing at the root — `0 .` — which is how a domain states in DNS that it accepts no mail at all. It was standardised in RFC 7505 so that a sending server could stop trying immediately rather than queueing and retrying for days.
## What Spaw answers
`mx_found` is false and the reason is [`null_mx`](/docs/reasons/null_mx). The verdict is `undeliverable`, which is the strongest thing the email product says: the domain's own operator has published that nothing can be delivered there.
Undeliverable answers are free, so a list full of null-MX domains costs nothing to clean.
## Why it matters more than an absent record
An absent MX record is ambiguous — it might mean the operator forgot, and RFC 5321 says to fall back to the A record. A null MX is a deliberate statement. Domains used only for a website, or parked, increasingly publish one, which is why it is worth distinguishing from [implicit MX](/glossary/implicit-mx) rather than folding both into "no mail server".
---
# PO box
A PO box is a numbered box rented at a postal facility, where mail is held for collection instead of being carried to a street address.
Date: 2026-09-10
A PO box is a numbered box rented at a postal facility, where mail is held for collection instead of being carried to a street address. Every postal service has its own name and its own written form for one — Post Office Box, Boîte Postale, Postfach, Casella Postale — and 52 countries' naming rules are held here.
## What Spaw answers
`is_po_box` is the flag and `address_type` reads `po_box`. The risk score carries +30 — but only where a box is unusual. In the nine countries where a box is the ordinary way to receive mail (the Gulf states, Jordan, Lebanon and Hong Kong) the weight is deliberately zero, because scoring somebody for the way their country delivers post is not a risk signal. `exists` stays null either way: a box with no street behind it is not a building, and no address register lists it.
## What it means
A PO box is a real, deliverable address that names no premises. That is exactly right for a small business collecting post and exactly wrong for a delivery that needs a door or a check that needs a residence — which is the difference the weight is trying to express, and the reason the flag is reported rather than the address refused.
A box rented from a private company rather than the postal service is a different thing with a different weight: see [private mailbox](/glossary/private-mailbox).
---
# Postcode
A postcode is the code a postal service uses to sort mail to a delivery area, called a ZIP code in the United States, an Eircode in Ireland, a PIN in India and a CEP in Brazil.
Date: 2026-09-10
Updated: 2026-09-23
A postcode is the code a postal service uses to sort mail to a delivery area. It is called a ZIP code in the United States, an Eircode in Ireland, a PIN in India and a CEP in Brazil; `postal_code_type` answers `zip`, `eircode` or `pin` where the country's format names one of those, and `postal_code` everywhere else that has postcodes.
## What Spaw answers
Four fields, and they answer different questions. `postal_code_valid` says whether the code matches the country's own pattern. `postcode_status` says what the national postcode directory records — `live`, `terminated`, or null where no directory is installed or the code lies outside the one that is. `locality_matches_postcode` says whether the town given agrees with the directory. Two reasons come out of this: [`invalid_postal_code`](/docs/reasons/invalid_postal_code) for a code that cannot be one, and [`postcode_not_found`](/docs/reasons/postcode_not_found) for a well-formed code that a complete directory does not list.
A terminated postcode is a real address whose code was retired, not a typo. It scores +30 and is reported rather than refused.
## Coverage, stated plainly
Great Britain's directory is the ONS Postcode Directory, and the places a British postcode is written with come from Ordnance Survey's Open Names; Northern Ireland's BT postcodes are left out of it, because their reuse needs a Land & Property Services licence. So the directory is not consulted for them: an address with a BT postcode is answered with `postcode_status` null, as though no directory were installed for it — no town or county comparison, no centroid — and never with `postcode_not_found`. Null there means the code was not evaluated, not that it is wrong. BFPO numbers and the BF1 and BX codes, which the release does not carry, are answered the same way. About a hundred other countries have a directory from the GeoNames postal-code file. Only a complete directory — Great Britain's, and the GeoNames copies that reach the operator's published total, fifteen countries on 16 September 2026 — can call a code `postcode_not_found`; a partial one confirms the codes it holds and denies none. The [postcode directory](/glossary/postcode-directory) entry explains the difference, and the [address verification guide](/docs/address-verification#postcodes) names the complete countries.
A postcode is also not an address. A UK code is a routing unit covering a group of delivery points — one side of a street, one office block — and turning a code into the addresses inside it needs a licence this service does not hold; the postcode endpoint answers the streets of the code's district instead, never the addresses. Only a country with neither a licensed list nor an open street listing answers `501 ADDRESS_LIST_UNAVAILABLE`.
---
# Private mailbox (PMB)
A private mailbox (PMB) is a box rented at a commercial mail receiving agency (CMRA), written as a PMB or # number after that agency's own street address.
Date: 2026-09-10
Updated: 2026-09-22
A private mailbox — a PMB — is a box rented at a commercial mail receiving agency (CMRA) rather than from the postal service, written as a `PMB` or `#` number after that agency's own street address. On paper it looks like a suite in an office building, because that is the form the postal service requires.
## What Spaw answers
`is_private_mailbox` is the flag and `address_type` reads `private_mailbox`. At +60 it is the heaviest of the four delivery-point weights, though not the heaviest weight in the address product: an address the deliverability check reports undeliverable carries +80.
The detection is the interesting part: it comes from the way the postal service requires such an address to be written, not from a licensed database of agencies. A `PMB 214` after a street address is the tell, and it is a rule about spelling rather than a lookup. The postal service also accepts `# 214`, but flats and suites are written with a # too, so a bare # number is read as a unit and never as a private mailbox. South Africa's PostNet Suite numbers are the same arrangement and are read the same way.
## The limit that follows from that
A private mailbox written without its PMB number, or with a # in its place, is indistinguishable from a suite. Where the licensed deliverability check would answer the question outright, this service does not run it — `is_cmra` stays null and the postal service's own CMRA record is not checked — so the flag catches the addresses that are written correctly and misses the ones that are not.
A PMB is not a lie. It is a mailing address, useful and legal, that says nothing about where anybody lives.
---
# Request id
A request id is the identifier stamped on one API call, returned in the X-Request-Id header and repeated inside the response body.
Date: 2026-09-10
A request id is the identifier stamped on one API call. It comes back in the `X-Request-Id` response header and is repeated inside the body — in `meta.request_id` on a success and in `error.request_id` on a failure — so the same string exists on both sides of the call.
## What it is for
Quoting one to support turns "a call failed yesterday" into a row somebody can read. Every request an account makes is recorded with its id, the endpoint, the status, the typed error code, the credits it cost and how long it took; the account's own list is at `/requests`, and the [support form](/support) offers the newest ids so nobody has to go and find one.
The header is exposed to browser JavaScript on purpose, because the documentation tells callers to read it and a browser cannot read a header that is not listed in the CORS policy.
## What the record does not hold
Not the value the call was about. The route is stored as its template, never as its path, so a request log row says that a phone lookup happened and never which number. Rows are deleted after 30 days.
---
# Residential proxy
A residential proxy routes traffic through a real consumer connection so it looks like ordinary home traffic. Spaw does not detect them and says so.
Date: 2026-09-10
A residential proxy routes traffic through a real consumer connection — often a phone or a home router enrolled by an app its owner installed — so that the request arrives from an address that genuinely belongs to a household. It is the standard way of defeating exactly the checks the IP product performs.
## What Spaw answers
Nothing. `is_datacenter`, `is_vpn` and `is_relay` all answer false, because the address really is a consumer address, and `is_anonymous` follows them. There is no field for this and there will not be one from open data.
## Why the entry exists anyway
Because the honest reading of a clean IP answer depends on knowing it. An address with no flags is not evidence of a real person at home; it is the absence of evidence of anything else. Residential proxy networks are detected commercially by traffic behaviour, and no free, openly-licensed dataset lists their addresses — so Spaw names the dataset behind every field it publishes and publishes nothing here.
Where this matters, the answer is not a better IP feed. It is another signal: a [disposable](/glossary/disposable-domain) email address, a [virtual phone number](/glossary/line-type), or the cross-signal check that names two weak signals coinciding.
---
# Risk score
The risk score is a 0-100 number that is the sum of named signals with published weights, capped at 100 — not a model output.
Date: 2026-09-10
The risk score is a 0–100 number that is the sum of named signals with published weights, capped at 100. It is arithmetic, not a model output, and you can recompute it yourself from the same response.
## What Spaw answers
`risk_score` is the number, `risk_level` bands it, and `risk_signals` lists every signal that fired with its weight and the dataset that produced it. Four products carry one — email, phone, IP and postal address — and each publishes its own weights on its own documentation page.
The design rule is that nothing may raise a score without appearing in `risk_signals`. A score you cannot take apart is a score you cannot argue with, and a verification answer you cannot argue with is not much use when a customer is on the phone.
## The two products with no score
Business verification and the cross-signal consistency check carry no score at all, by design. A register answer is a fact with a date on it, and a consistency report names which fields agree and which do not; turning either into a single number would add a judgement neither dataset supports. The consistency endpoint has no verdict and no recommendation either, and there will not be one.
---
# Role address
A role address is a mailbox named for a function rather than a person: info@, support@, billing@, postmaster@.
Date: 2026-09-10
A role address is a mailbox named for a function rather than a person: `info@`, `support@`, `billing@`, `postmaster@`, `abuse@`. It is usually a shared inbox or an alias fanning out to several people.
## What Spaw answers
`role` is true and the reason is [`role`](/docs/reasons/role). The verdict is `risky` and the risk score carries +30. The local part is matched against an open list of role names, so the check is about the name before the `@` and never about the domain.
A role address is still probed. It nearly always exists and nearly always accepts mail — the caution is about who reads it, not whether it works.
## Why it is risky rather than fine
Shared inboxes are monitored unevenly, and the person who filled in your form is often not the person who reads it. For a transactional message that is fine; for a marketing list, role addresses are where complaint rates come from, because whoever opens the mail did not sign up for it.
Nothing here recommends rejecting them. The verdict names the fact and leaves the policy to you.
---
# Tor exit node
A Tor exit node is the last relay in a Tor circuit, the machine that makes the request to the destination on the user's behalf.
Date: 2026-09-10
A Tor exit node is the last relay in a Tor circuit — the machine that makes the request to the destination on the user's behalf, and therefore the address a server sees. The Tor Project publishes the list of them, so this is one of the few network facts that is exact rather than inferred.
## What Spaw answers
`is_tor` is the flag, and it feeds `is_anonymous` along with `is_vpn` and `is_relay`. The risk score carries +70, the heaviest of the anonymity signals: a VPN is +30 and Apple's iCloud Private Relay is +10.
The gap between those three numbers is the whole judgement. Tor conceals origin deliberately and is rare in ordinary consumer traffic; a commercial VPN is a privacy product millions of people leave switched on; Private Relay is on by default for iCloud+ subscribers and says almost nothing about intent.
## What it does not mean
Tor is used by people who need it. Treating +70 as a block is a policy decision, not a reading of the data, and the guide beside this entry argues for a step-up rather than a refusal. Nothing in the answer identifies the person behind the circuit — that is the point of the circuit.
---
# Unassigned block
An unassigned block is a range of numbers that a regulator's published allocation table shows as belonging to no operator.
Date: 2026-09-10
Updated: 2026-09-26
An unassigned block is a range of numbers that a regulator's published allocation table shows as belonging to no operator — free, protected or quarantined. No carrier holds it, so no subscriber can be reached on a number inside it.
## What Spaw answers
`block_status` is `unassigned` and the reason is [`unassigned_block`](/docs/reasons/unassigned_block). The number is well-formed; it simply cannot be in service. A block the table marks reserved is a different answer: the number stays valid and the [`reserved_block`](/docs/signals/phone/reserved_block) signal carries the risk, while an allocated block held by a virtual-number wholesaler raises [`virtual`](/docs/signals/phone/virtual).
One exception: a number in a range reserved for fiction, such as Ofcom's drama ranges, answers `valid` true with `is_fictional` true even where the regulator's table lists its block as protected, because the range exists to be printed rather than dialled. Its `block_status` still says `unassigned`, and the `fictional` signal carries the risk.
## The gate on inferring it
A number's absence from a table is only evidence when the table claims to be complete. Five of the ten regulators' tables installed here do: the two North American allocation files list every assigned code, as do the French, Dutch and Mexican ones, so a geographic number missing from those can be reported unassigned. The UK's tables list free, protected and quarantined ranges explicitly and are never inferred from — an absent UK range answers null rather than a guess, and the Australian and New Zealand tables are read the same cautious way. The Polish tables are not complete either (they leave every zone's AB0 range and two whole zones out), but they list the space the regulator holds in reserve row by row, so a Polish number in a reserve answers unassigned from the row itself, never from absence; the Italian workbook lists assignments only and is never inferred from. Inside a pooled US code, NANPA's thousands-block report lists each block of a thousand, so a block it shows as available answers unassigned from its own row; one still partly in use by its former holder, or available only from a future date, does not.
There is a second gate. A table that has not been synced inside its freshness window is not allowed to infer anything either, because a stale copy is exactly how a newly-allocated block gets reported as dead.
Where no table covers the region, `block_status` is null. Null here means "not evaluated", never "no". Which table covers a country, and whether it claims to be complete, is on that country's page under [phone facts by country](/tools/phone-number-lookup/countries).
---
# Four datasets stopped refreshing and nobody was told
Four datasets behind the answers had stopped refreshing, the oldest copy dating from 6 September, and the daily check that exists to catch exactly that reached nobody. A fifth was flagged by a freshness window set too tight.
Date: 2026-10-14
By 14 October four datasets behind the answers had stopped refreshing, and the [status page](/status) said so, but nobody here had been told. The last copies installed were these: the geofeeds network operators publish about their own address ranges, from 20 September; the UK number ranges, from 27 September; the Danish address register, from 26 September; and the United States address register, from 6 September. A fifth row, the Norwegian address register, read stale as well. It was being refreshed every week as scheduled, and was flagged because its freshness window was set too tight.
Lookups kept answering throughout, from the copies already installed, and nothing was refused. Most answers were accurate about the copy they came from, which was older than the status page allows. One kind was wrong. The Danish register was treated as complete, because its publisher lists every address in the country, so an address created there after 26 September could be answered as one that does not exist. Every answer names the datasets and versions behind it in its `sources` field, so a caller can tell which of their answers came from an older copy.
## What happened to each dataset
- **United States address register.** On 30 September, hours before the monthly sync, the publisher replaced its file with a new release. The release is packed with a compression method, Deflate64, that the sync's archive reader did not support, so the run of 1 October could not install it whether or not the 8.5 GB download completed, and the copy of 6 September stayed. The reader now inflates Deflate64, and this register's download has a longer timeout to match the larger file.
- **Danish address register.** The Danish agency that publishes the register closed the interface the sync read on 1 October 2026, and every request to it now answers that the resource is gone for good. The register is still published, as separate files through the national data distributor, but the sync does not read those yet. Until it does, Spaw marks the register partial, so a Danish address the copy of 26 September does not hold now reads as unknown, not absent.
- **UK number ranges.** In September the regulator's website was recorded answering the server with a bot check instead of the files, and no weekly sync has replaced the copy of 27 September since. The files are fetched elsewhere and installed by hand, and a copy is now dated by the day the regulator published it, read from the files' own timestamps, rather than by the day it was installed.
- **Operator geofeeds.** These come from a long crawl every Sunday, and no crawl since 20 September has installed a result. Why is not yet established. A crawl that fails now reports the last step it reached, or the error it logged, so the next failure will explain itself.
- **Norwegian address register.** Not a failure. Its publisher releases each weekly extract on Saturday afternoon, hours after the Saturday sync, so every sync installs the extract from the week before. The window assumed a daily release, so it called a copy that was on schedule stale every Wednesday, Thursday and Friday, and it now allows for the weekly release. Two registers published twice a year were days from the same mistake. Their windows now follow their publishers' calendars.
## Why nobody heard
The freshness sweep runs every morning. It reads the same list of datasets the status page publishes, and writes a warning to the application log for each one past its window. It sends an email only when the server has an alert address configured. That email is queued rather than sent on the spot, so it also needs something on the server sending queued mail, and a mail transport that delivers it. If any one of those is missing, the warning ends as a line in a log file that nobody reads, and nothing reports that it went no further.
The [report of 8 September](/incidents/2026-09-08-two-datasets-had-stopped-syncing) said the sweep warns. That was true of the code. On the server, the warning depended on settings that nothing checked.
Which of those links broke in production is not yet established: whether an alert address was set, whether queued mail was being sent, or whether the transport delivered it. This report will be updated when it is.
## What changed
When no alert address is configured, every operational alert now goes to the people who run the admin panel instead of only to the log: stale datasets, a queue that has stopped moving, failing phone feeds and low balances alike. A server with neither set still only logs, and the page an operator checks now says so.
That page, the admin panel's operations page, now lists every dataset past its window, from the same assessment the sweep and the status page use. It also states where alert mail goes and how it is sent, so someone who never opens the mail still sees what it would have said.
A command sends a test alert along the same path, through the queue by default or directly on request, so a server's alerting can be checked rather than assumed.
## What it does not tell you
How long warnings had been going nowhere is not known, because the sweep's only record of a run is the log on the server. This report does not count how many answers drew on the older copies, or how many Danish addresses were answered as absent. And it does not yet say why the geofeed crawl stopped landing.
The fault is the one the September report described. A sync breaking was not the problem. The problem was that a broken sync could go unreported, this time past a check that exists to report it.
---
# Two datasets had stopped syncing and nothing said so
Two weekly data syncs were failing, and because a scheduled background task throws its output away, the failures left no trace anywhere. The datasets were missing from the status page before anyone noticed.
Date: 2026-09-08
Two of the weekly data syncs were failing: the discovery run that collects the geofeeds network operators publish about their own address ranges, and the Norwegian national address register. Both showed on the status page as datasets that had never been installed, which was true, and neither had reported a failure anywhere, which is the actual incident.
Nobody was told a wrong answer. What they got was a thinner one: an IP inside a range whose operator publishes its own location was located from the geolocation database like any other, and a Norwegian address was checked against its country's postal format rather than against the register of real addresses.
## What caused it
A scheduled command sends its output nowhere unless it is told otherwise, and the two syncs are also run in the background because they download and compile very large files. That hides the output a second time. So a task could fail every week, for as long as it liked, and produce not one line anywhere that anybody would ever read.
## What changed
Every scheduled task that runs in the background now appends its output to a log file, and a test walks the schedule and fails, naming the task, if one of them does not. That is the direct fix, and it only helps somebody who goes looking.
The check that does not need anybody to go looking is the daily freshness sweep. Once a day it reads the same list of datasets the [status page](/status) publishes and warns about anything that is not installed, or whose version has aged past the window that dataset is expected to be refreshed within. It reads the published list rather than a list of its own, so a dataset added later is covered the day it appears on the page. And because it judges the data rather than the job, it still fires when the sync command is not running at all — which was precisely the case here.
## What it does not tell you
How long the two syncs had been failing is not recorded, because nothing was recording it. That is the reason the incident is worth publishing: the fault was not that a download broke, it was that a download could break in silence.
---
# IP answers lost their location and network flags
Files a scheduled job had synced correctly were written into directories the web process could not enter, so IP lookups answered with no location and no network operator, and the datacenter, VPN and Tor flags read false.
Date: 2026-09-05
An IP lookup answered with `country`, `city` and `asn` all null, and with `is_datacenter`, `is_vpn` and `is_tor` all false — including for addresses that are plainly none of those things, such as a well-known public resolver that sits in a datacenter. A false flag is worse than a missing one: a caller scoring signups on it was being told an address was clean when the data to judge it was simply not being read.
## What caused it
The sync ran on a schedule, reported success, and had genuinely downloaded and installed everything. It created its directories with the default permissions of the account running the scheduler, which let nobody else in. The web process runs as a different account, so it could not enter those directories and read the files sitting in them. From inside the application it looked exactly like data that had never been synced.
Three separate sets of files were affected the same way: the geolocation databases, several network feeds under an older directory, and the report files two product pages read their measured figures from.
## What changed
Every command that writes a file the web process later reads now sets the permissions of the directory and of each installed file explicitly, rather than trusting the default of whichever account the scheduler happens to run as. The storage disk is configured the same way, and a test asserts the actual permission bits so a change that quietly reverts it fails.
The [status page](/status) is also how this was found, and it is the check to make after any first sync on a new server: it lists every dataset with its version and whether it is installed here, and a dataset that was synced but cannot be read reads as not installed — which is the truth, from the application's point of view.
---
# Documentation and content pages answered 500 in production
A class the app loads at runtime was only installed for development, so every documentation page, content page and the sitemap failed on the production server while the lookup endpoints kept answering.
Date: 2026-09-03
Every documentation page, every Markdown content page and the sitemap answered 500 on the production server. Anyone reading the docs got an error page instead. The API itself was unaffected: a lookup reads none of the files involved, so calls kept being answered and billed normally throughout.
## What caused it
The pages that failed all read Markdown front matter or the OpenAPI document, and both are parsed with a YAML library. That library was never declared as something the application needs. It was present on a development machine and in the test suite only because two development tools happened to pull it in, and a production deploy installs dependencies without those tools. So the class existed everywhere it was tested and nowhere it mattered.
Nothing caught it before the deploy for exactly that reason: locally, in the tests and in CI, the code worked.
## What changed
The parser is now a first-class requirement of the application rather than a coincidence of somebody's tooling.
More usefully, this class of fault can no longer reach production unnoticed. A test reads the list of packages that are installed for development only, walks every class the application imports, and fails the build if any of them resolves to one of those packages. The failure now happens on a machine where somebody is watching, which is the whole difference between a mistake and an outage.
## What it does not tell you
This report was written from the engineering record. The day the fault was found and fixed is known; how many hours the pages were failing before that is not, because nothing was watching them at the time. That gap is why the [status page](/status) now publishes a measurement rather than a promise — and why the availability figure it shows is the share of requests that reached us, not a claim about the times nothing did.
---
# Amazon SES and Amazon WorkMail: MX records and email verification
How domains that receive mail through Amazon SES or WorkMail look in DNS, how they answer an SMTP verification handshake, and which verdicts to expect.
Updated: 2026-10-08
Amazon Web Services receives mail in two ways. Amazon SES email receiving delivers incoming messages to S3, Lambda or SNS through rules the customer writes, and Amazon WorkMail is a hosted mailbox service for organisations that runs on the same inbound endpoints. AWS stopped taking new WorkMail customers on 30 April 2026 and ends the service on 31 March 2027. Both publish MX records under `amazonaws.com`, and Spaw reports them as `mx_provider: "amazon-ses"`.
## How Amazon shows up in DNS
A domain receiving through SES or WorkMail publishes a single regional MX record such as `inbound-smtp.us-east-1.amazonaws.com` or `inbound-smtp.eu-west-1.amazonaws.com` at priority 10. Custom MAIL FROM subdomains used for sending publish an MX of the form `feedback-smtp.{region}.amazonses.com`, which is a different zone and is not matched by this pattern. Spaw matches the inbound endpoints, `inbound-smtp.{region}.amazonaws.com`. Other mail servers with names under `amazonaws.com` (an EC2 instance, a load balancer, an SES Mail Manager ingress endpoint) are reported as `other`: they are the domain's own server or a gateway, not SES receiving. Amazon's own consumer-facing domains (amazon.com, audible.com) run on Amazon's internal mail platform and are classified as `other`.
Whether the domain is a mailbox host or an inbound-routing service depends on what sits behind the endpoint: WorkMail stores mail in mailboxes, SES receiving hands it to software. That software may be an application's own pipeline or a forwarding service: Firefox Relay (`mozmail.com`, `relay.firefox.com`) and Brave Email Aliases (`bravealias.com`) both receive on SES and pass each message on to a person's own inbox.
## How Amazon answers a verification handshake
SES email receiving accepts every recipient that matches a receipt rule, and receipt rules commonly match a whole domain, so from the outside the domain is a catch-all and no probe can prove that a specific address is handled. WorkMail knows its mailboxes and typically rejects unknown recipients at `RCPT TO`, so WorkMail domains answer cleanly unless the organisation configured a catch-all. The inbound endpoints are shared infrastructure with conventional rate limiting; deferred probes are uncommon but possible.
## What that means for your verdicts
Domains on SES receiving answer `risky` with `reason: catch_all` and `mailbox_exists: null`; Spaw remembers the catch-all for seven days and returns `mailbox_confidence`, which without the established-provider bonus leans on the domain's DMARC and SPF, its age, the shape of the username and the delivery outcomes every account has reported for the domain, once there are at least five. A relay on SES answers the same way and also carries `is_relay: true`, its `relay_service` and the relay weight of 5, and its `mailbox_confidence` stays at 50: the domain's records describe the relay operator, not the alias. WorkMail domains mostly answer `deliverable` with `mailbox_exists: true` or `undeliverable` with `reason: mailbox_not_found`. There are no consumer username rules for these domains.
Addresses on SES-receiving domains other than relays are frequently machine endpoints: support tickets, reply tracking, notification ingestion. `is_gibberish: true` and role-shaped usernames are common there, and a `risky` catch-all verdict on such an address is a reason to leave it out of human-facing campaigns. A relay address is the opposite case, a person reading mail through an alias they chose, so check `is_relay` before treating an SES address as a machine endpoint.
## SPF and DMARC at Amazon
Sending through SES uses a subdomain of `amazonses.com` as the default MAIL FROM domain, so SPF passes on the envelope without any record from the customer; customers who want alignment set up a custom MAIL FROM subdomain and publish the SPF TXT record and the single MX record that the SES console shows for their region, and SES provides DKIM through Easy DKIM CNAME records. DMARC alignment can then be met through DKIM, SPF or both, as the SES documentation describes. Spaw reports the live state of any domain in `has_spf` and `dmarc_policy`.
## Practical notes for sending to Amazon-hosted domains
WorkMail organisations behave like any business mailbox host and can be sent to normally until the service ends. Other SES-receiving domains typically feed an application rather than a person, so their addresses rarely belong on a marketing list, and one in a signup form typically indicates an automated integration or a test. Relays are the exception: an address that answers `is_relay: true` is a person's alias, so treat it like any other signup.
`mx_provider` alone cannot separate these cases, because WorkMail, SES receiving and the relays all report `amazon-ses`. Read `is_relay` and the verdict with it: WorkMail typically answers `deliverable` or `undeliverable`, where SES receiving answers `risky` with `reason: catch_all`. These answers are billed like any other: a `risky` catch-all answer costs 1 credit, with repeats of the same address free for seven days, and the domain intelligence endpoint costs 1 credit when the domain has mail servers.
For a live look at any domain, run it through the free MX lookup and the SPF and DMARC checker.
---
# Barracuda Email Gateway Defense: MX records and email verification
How domains behind Barracuda look in DNS, how the gateway answers a verification handshake, and which verdicts to expect for Barracuda-protected addresses.
Updated: 2026-10-07
Barracuda Email Gateway Defense (formerly Barracuda Essentials) is a hosted security gateway used mostly by small and mid-size organisations, often sold through IT service providers. Companies point their MX at Barracuda, which filters inbound mail and hands it to the mailbox host behind it. Spaw reports such domains as `mx_provider: "barracuda"`.
## How Barracuda shows up in DNS
Domains behind the service publish a pair of MX records under `ess.barracudanetworks.com`, such as `d160221a.ess.barracudanetworks.com` and `d160221b.ess.barracudanetworks.com`, with regional variants for Europe, Australia, Canada and other data centres. Spaw matches the `.barracudanetworks.com` suffix. The target is a filtering layer, so `mx_provider` identifies the gateway rather than where the mailboxes live.
## How Barracuda answers a verification handshake
Barracuda's gateway supports recipient verification: when the customer has connected the service to their directory (Microsoft 365 or Google Workspace synchronisation, or an LDAP lookup), unknown recipients are rejected at `RCPT TO`. That integration is common among Barracuda's customer base because the same connection drives the rest of the product, so clean answers are more frequent here than behind some other gateways. Without it, the gateway accepts every recipient and the domain answers as catch-all. Barracuda also applies sender reputation checks and rate limits before the recipient step, so a probe from an unfamiliar source is occasionally deferred.
## What that means for your verdicts
Domains with recipient verification answer `deliverable` with `mailbox_exists: true` or `undeliverable` with `reason: mailbox_not_found` and `smtp_reason: no_mailbox`. The rest answer `risky` with `reason: catch_all`; Spaw remembers that per domain for seven days and returns `mailbox_confidence` for each address, with the ten-point bonus for an established provider and adjustments for DMARC, SPF, the domain's age, the username's shape and the delivery outcomes every account has reported for the domain, once there are at least five. Deferred probes show `smtp_reason: greylisted`, and `callback_url` re-checks them after 5 and 20 minutes, billing 1 credit only if a re-check confirms the mailbox.
As with every gateway, your delivery feedback carries the most weight: a delivered report confirms the mailbox for later lookups (`smtp_reason: delivered_recently`), and bounces lower the confidence for the whole domain.
## SPF and DMARC at Barracuda
Companies that also send outbound mail through the service add `include:spf.ess.barracudanetworks.com` to their SPF record for the US instance, or a regional variant such as `include:spf.ess.uk.barracudanetworks.com`, `include:spf.ess.de.barracudanetworks.com`, `include:spf.ess.au.barracudanetworks.com` or `include:spf.ess.ca.barracudanetworks.com`, as Barracuda's documentation lists them. The inbound filter does not change the domain's own SPF and DMARC, which Spaw reports live in `has_spf` and `dmarc_policy`.
## Practical notes for sending to domains behind Barracuda
Barracuda's customer base skews to smaller organisations: schools, clinics, local government and firms with an outsourced IT provider. Their gateways are often tuned strictly, and their lists change with staff turnover, so re-verifying on a schedule pays off. A `mailbox_not_found` from a Barracuda gateway with recipient verification is as reliable as one from the mailbox host itself.
For a live look at any Barracuda-protected domain, run it through the free MX lookup and the SPF and DMARC checker.
---
# Fastmail: MX records and email verification
How Fastmail-hosted domains look in DNS, how Fastmail answers an SMTP verification handshake, and which verdicts to expect for Fastmail addresses.
Updated: 2026-09-03
Fastmail is an independent mailbox provider with a large base of custom-domain customers. Its infrastructure runs under the `messagingengine.com` name, which is why Spaw reports it as `mx_provider: "fastmail"` even though that hostname never appears in a Fastmail address.
## How Fastmail shows up in DNS
Fastmail's DNS guide asks custom domains to publish `in1-smtp.messagingengine.com` at priority 10 and `in2-smtp.messagingengine.com` at priority 20. Fastmail's own domains (fastmail.com, fastmail.fm and the many alternative domains it offers) use the same hosts. Spaw matches the `.messagingengine.com` suffix. Fastmail is a mailbox host: the MX target stores the mail, `mx_found` is true and `mx_implicit` is false.
## How Fastmail answers a verification handshake
Fastmail typically rejects unknown recipients at `RCPT TO`, so most mailbox checks settle on the first attempt. Two Fastmail features change that picture for some domains. Subdomain addressing lets any user receive mail at `anything@user.fastmail.com`, which is effectively a per-user catch-all on that subdomain. And custom domains can be configured with a catch-all address, in which case the inbound server accepts every recipient and the domain answers as catch-all.
## What that means for your verdicts
Fastmail addresses mostly come back `deliverable` with `mailbox_exists: true` or `undeliverable` with `reason: mailbox_not_found` and `smtp_reason: no_mailbox`. Catch-all domains answer `risky` with `reason: catch_all`; Spaw remembers the domain for seven days and returns `mailbox_confidence`, which includes the ten-point bonus for an established provider. Fastmail is not in the consumer username rule set, so `invalid_local_part` does not apply; plus-addressing is supported and reported as `is_alias: true`.
Fastmail users are often technical and privacy-conscious, and Fastmail's masked-email feature creates single-purpose addresses on domains such as `fastmail.com` subdomains that stop accepting mail once the user disables them. Such an address is real until it is not; a later lookup answers `mailbox_not_found` and the address joins your suppression list.
## SPF and DMARC at Fastmail
Fastmail's manual DNS guide publishes `v=spf1 include:spf.messagingengine.com ?all` for SPF, DKIM CNAME records for the `fm1`, `fm2` and `fm3` selectors, and suggests starting DMARC at `v=DMARC1; p=none;` so you can observe before enforcing. Note the neutral `?all` in Fastmail's default: it is a permissive record, and Spaw still reports `has_spf: true` for it because the record exists.
## Practical notes for sending to Fastmail users
Fastmail's filtering weighs authentication and reputation in the usual way and applies user-defined rules heavily, so a properly aligned sending domain is the main requirement. Expect masked and single-purpose addresses on lists collected from technical audiences and re-verify those lists on a schedule; a monitor in the Spaw dashboard does that for you and alerts when deliverability decays.
For a live look at any Fastmail-hosted domain, run it through the free MX lookup and the SPF and DMARC checker.
---
# GMX: MX records and email verification
How GMX domains look in DNS, how GMX answers an SMTP verification handshake, and which verdicts to expect for gmx.net, gmx.de and gmx.com addresses.
Updated: 2026-10-07
GMX is one of the two big German consumer mailbox providers, run by 1&1 Mail & Media alongside its sister brand WEB.DE. It serves gmx.net, gmx.de, gmx.at, gmx.ch and the international gmx.com, all on the same platform. Spaw reports the GMX domains as `mx_provider: "gmx"`.
## How GMX shows up in DNS
GMX domains publish MX records under `gmx.net`: gmx.net and gmx.de point at `mx00.emig.gmx.net` and `mx01.emig.gmx.net`, and gmx.com at `mx00.gmx.net` and `mx01.gmx.net`. Spaw matches the `.gmx.net` suffix. WEB.DE, despite sharing the infrastructure, uses hosts under `web.de` and classifies as `other` until the classifier learns that pattern. GMX is a mailbox host: `mx_found` is true and `mx_implicit` is false.
## How GMX answers a verification handshake
GMX rejects unknown recipients at `RCPT TO`, so a missing mailbox is confirmed in the session. The provider is strict about who it talks to at all: it enforces reverse DNS on the connecting host, applies reputation checks before the recipient step, and greylists or refuses connections from sources it does not trust. A probe from infrastructure GMX has not seen is therefore more likely than average to end with `smtp_reason: greylisted` or `no_connect` rather than an answer.
## What that means for your verdicts
Confirmed answers are `deliverable` with `mailbox_exists: true` or `undeliverable` with `reason: mailbox_not_found` and `smtp_reason: no_mailbox`. Unverified probes leave `smtp_checked: false` and `mailbox_exists: null`, the verdict rests on the free signals, ten risk points are added for the unverified state, and `mailbox_confidence` is returned; GMX is not on the established-provider list, so that estimate depends most on the username's shape and on the delivery outcomes every account has reported for the domain, which count once there are at least five. GMX has no consumer username rules in Spaw's rule set, so `invalid_local_part` does not apply. Catch-all does not occur on consumer GMX domains.
For unverified single lookups, pass `callback_url` so Spaw re-checks after 5 and 20 minutes; for lists, report delivery outcomes so confirmed mailboxes carry `smtp_reason: delivered_recently` on later lookups.
## SPF and DMARC at GMX
Consumer GMX users have nothing to configure. The GMX domains publish their own SPF and DMARC records, and GMX enforces DMARC on inbound mail, so a message with a gmx.de From address sent through another service fails alignment. Spaw's `dmarc_policy` field shows the live policy for any domain; for gmx.net it reflects whatever GMX currently publishes.
## Practical notes for sending to GMX users
GMX and WEB.DE together receive a large share of German consumer mail and are known for strict inbound policies: a valid reverse DNS entry for your sending IPs, aligned SPF and DKIM, and a working postmaster address are practical requirements, and 1&1 operates a postmaster programme with a certification for bulk senders. Plus-addressing is not a standard feature on consumer GMX accounts. Addresses on these domains are typically individuals, so role-word usernames are rare and `reason: role` is uncommon here.
For a live look at any GMX domain, run it through the free MX lookup and the SPF and DMARC checker.
---
# GoDaddy Workspace Email: MX records and email verification
How domains on GoDaddy's legacy Workspace Email look in DNS, how they answer a verification handshake, and why most GoDaddy email now classifies as Microsoft.
Updated: 2026-10-07
GoDaddy sold its own mailbox product, Workspace Email, for many years alongside domain registrations and hosting. Its servers run under the `secureserver.net` name, and Spaw reports domains that still point there as `mx_provider: "godaddy"`. GoDaddy has since moved its email business to Microsoft 365 from GoDaddy, so the classifier label is shrinking.
## How GoDaddy shows up in DNS
Legacy Workspace Email domains publish MX records under `secureserver.net`, historically `smtp.secureserver.net` at priority 0 and `mailstore1.secureserver.net` at priority 10. Spaw matches the `.secureserver.net` suffix. Note that GoDaddy's own domains, including secureserver.net itself, now resolve to `mail.protection.outlook.com` hosts and classify as `microsoft`, as do all customers on Microsoft 365 from GoDaddy. If a domain you expected to be GoDaddy-hosted comes back as `microsoft`, that migration is the reason, and the Microsoft page describes how it behaves.
## How GoDaddy answers a verification handshake
The legacy Workspace Email servers typically reject unknown recipients at `RCPT TO`, so most checks settle in the session. A catch-all mailbox could be configured per domain, and domains with one answer as catch-all. The platform also rate-limits unfamiliar sources, which occasionally defers a probe.
## What that means for your verdicts
For domains still on Workspace Email, expect `deliverable` with `mailbox_exists: true`, `undeliverable` with `reason: mailbox_not_found` and `smtp_reason: no_mailbox`, or `risky` with `reason: catch_all` on domains with a catch-all mailbox. For catch-all domains Spaw returns `mailbox_confidence` without the established-provider bonus, so the domain's own DMARC and SPF, its age and the shape of the username carry the estimate. There are no consumer username rules for GoDaddy domains.
The more important verdict pattern is around the migration itself. Mailboxes that were not carried over to Microsoft 365 stop existing, so a list collected years ago from small businesses on GoDaddy email tends to produce a wave of `mailbox_not_found` answers when re-verified now. Those answers are free within your account's email fair use and cost 1 credit each beyond it, and the addresses join your suppression list automatically.
## SPF and DMARC at GoDaddy
GoDaddy's help pages ask domain owners to publish `v=spf1 include:secureserver.net -all` for email on Professional Email, Microsoft 365 from GoDaddy, Linux hosting and their hosting products, adding other services' includes as needed and keeping to a single SPF record. Spaw reports the live state in `has_spf` and `dmarc_policy`; a small-business domain without SPF adds ten risk points to its addresses.
## Practical notes for sending to GoDaddy-hosted users
GoDaddy's email customers are overwhelmingly small businesses with domains registered years ago, and their lists age quickly. Re-verify before every campaign rather than once a year, and expect role addresses (`info@`, `contact@`) to dominate; Spaw flags those `risky` with `reason: role` even when they exist. Where a domain has moved to Microsoft 365, the catch-all behaviour common on Exchange Online tenants applies, and `mailbox_confidence` plus your own delivery feedback are the tools for ranking those addresses.
For a live look at any domain, run it through the free MX lookup and the SPF and DMARC checker.
---
# Google Workspace and Gmail: MX records and email verification
How Google-hosted domains look in DNS, how Gmail answers an SMTP verification handshake, and which verdicts and reasons to expect for Google addresses.
Updated: 2026-09-03
Google hosts more mailboxes than any other provider, split between consumer Gmail and Google Workspace on company domains. Both run on the same inbound infrastructure, so Spaw reports them under one `mx_provider` value: `google`.
## How Google shows up in DNS
A Google-hosted domain publishes MX records under `l.google.com` or, for newer Workspace setups, the single host `smtp.google.com`. Gmail itself resolves to `gmail-smtp-in.l.google.com` and its `alt1` to `alt4` siblings; Workspace domains set up before 2023 typically use `aspmx.l.google.com` with `alt1.aspmx.l.google.com` and so on, and Google's current setup guide recommends the one-record form, `smtp.google.com` at priority 1.
Spaw matches either pattern and returns `mx_provider: "google"`. Google is a mailbox host: the MX points at the servers that store mail, not at a filtering layer in front of somebody else's servers. `mx_found` is true, `mx_implicit` is false, and `has_spf` and `dmarc_policy` describe the domain's own records.
## How Google answers a verification handshake
Google answers the recipient check honestly and quickly. When the SMTP handshake reaches `RCPT TO` for an address that does not exist, Gmail and Workspace both reject it in the session, typically with a `550 5.1.1` reply that says the address was not found. A mailbox that exists is accepted at the same step. Greylisting and silent throttling are uncommon for a single well-formed probe, so an answer usually arrives on the first attempt.
Workspace domains can be configured as catch-all by an administrator, but that is the exception: the default rejects unknown recipients. Google also treats dots in Gmail usernames as insignificant and delivers plus-tagged addresses to the base mailbox, which is why Spaw folds both into `normalized_email` before it does anything else.
## What that means for your verdicts
For Google addresses you should mostly see clean answers: `deliverable` with `mailbox_exists: true`, or `undeliverable` with `reason: mailbox_not_found` and `smtp_reason: no_mailbox`. Catch-all answers are rare and, when they appear, come from a Workspace domain whose admin turned the option on; Spaw then remembers the domain as catch-all for seven days and returns `mailbox_confidence` instead of spending more probes on it.
Two things happen before any probe for the consumer domains gmail.com and googlemail.com. First, Google's own username rules apply: Gmail usernames contain only letters, digits and dots, and are at most 30 characters once the dots are removed. An address that breaks those rules is answered `undeliverable` with `reason: invalid_local_part` without a handshake. Second, dots and plus-tags are normalised, so `m.ia+news@gmail.com` and `mia@gmail.com` share one cache entry and one suppression entry.
A `google` provider also adds ten points to `mailbox_confidence` when a probe could not settle a mailbox, because an established provider with strict signup rules makes a plausible-looking address more likely to exist.
## SPF and DMARC at Google
Google Workspace tells domain owners to publish `v=spf1 include:_spf.google.com ~all` and to add a DKIM key from the admin console. Consumer Gmail users have nothing to configure; gmail.com itself publishes SPF and a DMARC policy. On a Workspace domain, a missing SPF record shows up in Spaw as `has_spf: false` and costs the address ten risk points; `dmarc_policy` reports `none`, `quarantine` or `reject` from the live record.
## Practical notes for sending to Google users
Google enforces sender authentication on the receiving side: since 2024 bulk senders to Gmail need aligned SPF or DKIM, a DMARC record, one-click unsubscribe and a low complaint rate, so a list full of unverified Gmail addresses is expensive in two ways. Role inboxes such as `info@` on Workspace domains exist and are probed like any other address; Spaw still flags them `risky` with `reason: role` because a shared inbox is not a person. Plus-tags are safe to send to, but Spaw reports `is_alias: true` so you can decide whether to keep the tagged form.
For a live look at any Google-hosted domain, run it through the free MX lookup and the SPF and DMARC checker.
---
# HEY: MX records and email verification
How HEY and HEY for Domains addresses look in DNS, how HEY answers a verification handshake, and which verdicts to expect for hey.com and custom domains.
Updated: 2026-09-03
HEY is the email service from 37signals, launched in 2020 with a screening-first design: mail from a new sender is held in a screener until the recipient lets it through. It serves personal accounts on hey.com and company domains through HEY for Domains. Spaw reports both as `mx_provider: "hey"`.
## How HEY shows up in DNS
hey.com publishes a single MX record, `home-mx.app.hey.com`, and HEY for Domains customers point their MX at hosts under the same `hey.com` zone, which HEY displays during setup. Spaw matches the `.hey.com` suffix. HEY is a mailbox host: the MX target is HEY's own inbound infrastructure, `mx_found` is true and `mx_implicit` is false.
## How HEY answers a verification handshake
HEY typically rejects unknown recipients at `RCPT TO`, so a missing hey.com mailbox is confirmed in the session. The screener is a feature of the inbox, not of the SMTP conversation: a message to an existing address is accepted by the server whether or not the recipient has approved the sender, and the screening happens afterwards. HEY for Domains does not expose a conventional catch-all setting, so catch-all answers are uncommon on HEY domains. Deferrals are rare but possible for sources HEY has not seen.
## What that means for your verdicts
HEY addresses mostly come back `deliverable` with `mailbox_exists: true` or `undeliverable` with `reason: mailbox_not_found` and `smtp_reason: no_mailbox`. Occasional unverified probes show `smtp_reason: greylisted` with `mailbox_confidence` returned; HEY is not on the established-provider list, so the estimate depends on the domain's own records and the username's shape. There are no consumer username rules for hey.com in Spaw's rule set, and hey.com usernames are short by design because HEY sold them on a first-come basis, so `is_gibberish` is more often true on real HEY addresses than elsewhere and should be read as a hint only.
A `deliverable` verdict on a HEY address is a statement about the mailbox, not about reach: a first message from an unknown sender lands in the screener and is only seen if the recipient chooses to see it.
## SPF and DMARC at HEY
HEY for Domains shows the SPF, DKIM and DMARC records to publish during domain setup and asks for the MX to be switched only at the final step, so existing mail keeps flowing until the switch. HEY does not publish a public DNS reference page for these values, so copy them from the setup screen rather than from third-party guides. hey.com itself publishes SPF and a DMARC policy. Spaw reports the live state of any domain in `has_spf` and `dmarc_policy`.
## Practical notes for sending to HEY users
Expect low engagement from cold outreach: the screener means unsolicited mail is rarely read, and HEY users can block a sender permanently with one click. Transactional mail from services the user signed up for is approved once and then delivered normally. HEY for Domains customers are small companies, so role addresses exist and are flagged `risky` with `reason: role` as usual. HEY supports plus-style addressing for personal accounts through its own aliasing, which a verifier sees as ordinary addresses.
For a live look at any HEY-hosted domain, run it through the free MX lookup and the SPF and DMARC checker.
---
# Mailgun inbound routes: MX records and email verification
How domains that route inbound mail through Mailgun look in DNS, why Mailgun routes accept every recipient, and which verdicts to expect for addresses on them.
Updated: 2026-10-07
Mailgun is a sending service for applications, but it also receives mail: a domain whose MX records point at Mailgun delivers every incoming message to Mailgun's inbound routes, which store it, forward it or post it to a webhook. Such domains are usually application domains (a support system, a reply-tracking subdomain, a notifications domain), not places where people read mail in an inbox. Spaw reports them as `mx_provider: "mailgun"`.
## How Mailgun shows up in DNS
Domains receiving through Mailgun publish two MX records, `mxa.mailgun.org` and `mxb.mailgun.org`, at priority 10. Mailgun's own product domain, mailgun.com, is on Microsoft 365; it is mailgun.net, a Mailgun-operated domain, that shows the pattern in practice. Spaw matches the `.mailgun.org` suffix. Mailgun is an inbound-routing service rather than a mailbox host: the MX target accepts mail on behalf of whatever the customer's routes do with it.
## How Mailgun answers a verification handshake
Inbound routes accept every recipient by design. Mailgun does not know which addresses "exist" because routes are pattern-based (`match_recipient(".*@support.example.com")` is the canonical example), so the SMTP handshake says yes to any username and the customer's route logic decides afterwards. To a verifier, every Mailgun-receiving domain is a catch-all, and no probe can prove that a specific mailbox exists.
## What that means for your verdicts
Every syntactically valid address on a Mailgun-receiving domain answers `risky` with `reason: catch_all` and `mailbox_exists: null`. Spaw remembers the catch-all per domain for seven days, so subsequent lookups on the same domain do not spend a probe, and returns `mailbox_confidence` for each address. Mailgun is not on the established-provider list, so the estimate is driven by the domain's own DMARC and SPF, its registration age, the shape of the username and the delivery outcomes every account has reported for the domain, once there are at least five; a name-shaped username on a young domain with no DMARC will score low, and that is the right answer for most application domains.
Consider what the address is for before you add it to a list. A reply-tracking address such as `reply+abc123@mail.example.com` is a machine endpoint, and sending marketing mail to it is pointless even though it will be accepted. `is_gibberish: true` on such addresses is a useful hint.
## SPF and DMARC at Mailgun
Mailgun's domain verification guide has customers add `include:mailgun.org` to their SPF record (`v=spf1 include:mailgun.org ~all` when Mailgun is the only sender) and publish the DKIM TXT record shown in the Mailgun dashboard, usually on a dedicated sending subdomain so the root domain's own MX and reputation stay untouched. Mailgun explicitly advises against putting its MX records on a root domain that already receives mail elsewhere. Spaw reports `has_spf` and `dmarc_policy` for whichever domain you look up, including subdomains.
## Practical notes for sending to Mailgun-receiving domains
Mail sent to a Mailgun inbound route reaches software, not a person, unless the customer forwards it to a mailbox. If you see these domains on a customer list, they usually came from automated signups, reply addresses harvested from email threads, or test data. Treat the `risky` catch-all verdict at face value and keep them out of human-facing campaigns; the feedback endpoint can confirm the few that do reach a person.
For a live look at any Mailgun-receiving domain, run it through the free MX lookup and the SPF and DMARC checker.
---
# Microsoft 365, Outlook.com and Exchange Online: MX records and email verification
How Microsoft-hosted domains look in DNS, why many Microsoft 365 tenants answer as catch-all, and which verdicts to expect for Outlook and Exchange addresses.
Updated: 2026-10-07
Microsoft runs two very different mail services on one inbound platform: consumer Outlook.com (with the older hotmail.com, live.com and msn.com domains) and Exchange Online, the mail service behind Microsoft 365 for companies. Spaw reports both as `mx_provider: "microsoft"`.
## How Microsoft shows up in DNS
Exchange Online tenants publish a single MX record shaped like `contoso-com.mail.protection.outlook.com`: the customer's domain with the dots replaced by dashes, under `mail.protection.outlook.com`. Consumer domains use the sibling `olc.protection.outlook.com` zone, for example `outlook-com.olc.protection.outlook.com`. Microsoft has begun rolling out a second naming scheme under `.mx.microsoft`, which Spaw matches as well.
Microsoft is a mailbox host, but the MX target is the Exchange Online Protection edge, a filtering layer that sits in front of every tenant's mailboxes. That edge, not the mailbox server, answers the SMTP handshake, and its behaviour depends on how the tenant is configured.
## How Microsoft answers a verification handshake
Consumer Outlook.com addresses are answered like Gmail: an unknown username is rejected at `RCPT TO`, an existing one is accepted.
Exchange Online tenants are the complicated case. Microsoft documents Directory-Based Edge Blocking, which rejects messages to recipients that are not in the tenant's directory at the network perimeter with `550 5.4.1 Recipient address rejected: Access denied`. It is in effect automatically when every recipient of the domain lives in Exchange Online. When a domain is set to Internal Relay instead, which is common during migrations and in hybrid setups with on-premises Exchange, the edge accepts every recipient and sorts the invalid ones out later with a bounce. From the outside that tenant is a catch-all, and it is a frequent configuration.
## What that means for your verdicts
Expect three patterns. Tenants with edge blocking answer cleanly: `deliverable` with `mailbox_exists: true`, or `undeliverable` with `reason: mailbox_not_found` and `smtp_reason: no_mailbox`. Internal-relay and hybrid tenants answer `risky` with `reason: catch_all`; Spaw remembers the domain as catch-all for seven days, spends no further probes on it, and returns `mailbox_confidence` instead, which for a Microsoft domain starts with the ten-point bonus for an established provider and then moves with DMARC, SPF, the shape of the username and the delivery outcomes every account has reported for the domain, once there are at least five. Disabled or de-licensed accounts commonly answer as missing, so a departed employee shows up as `mailbox_not_found` rather than `mailbox_disabled`.
For the consumer domains (outlook.com and its country variants, hotmail.com, live.com, msn.com), Microsoft's own username rules apply before any probe: letters, digits, dots, underscores and hyphens, at most 64 characters. Anything else is `undeliverable` with `reason: invalid_local_part` and costs nothing.
## SPF and DMARC at Microsoft
Microsoft's documentation for custom domains in Microsoft 365 is `v=spf1 include:spf.protection.outlook.com -all`, with separate includes for the government and 21Vianet clouds, and it recommends the hard-fail form together with DKIM and DMARC. Spaw shows the live result in `has_spf` and `dmarc_policy`; a company domain without SPF adds ten risk points to every address on it. Consumer Outlook.com users have nothing to configure.
## Practical notes for sending to Microsoft users
Because catch-all is so common on Microsoft 365, a list of business addresses often comes back with more `risky` answers than a list of consumer ones. That is not a defect of the list; it is the tenant declining to say which mailboxes exist. Use `mailbox_confidence` to rank those addresses, and report delivery outcomes back through the feedback endpoint: after a delivered report, later lookups of the same address answer `smtp_reason: delivered_recently` with a confirmed mailbox, and bounces lower the confidence for the whole domain. Plus-addressing works on Outlook.com and Exchange Online, and Spaw reports it as `is_alias: true`.
For a live look at any Microsoft-hosted domain, run it through the free MX lookup and the SPF and DMARC checker.
---
# Mimecast: MX records and email verification behind a security gateway
How domains behind Mimecast look in DNS, why a security gateway usually answers as catch-all, and which verdicts to expect for addresses protected by Mimecast.
Updated: 2026-10-07
Mimecast is an email security gateway: companies point their MX records at Mimecast, which filters inbound mail and forwards the clean messages to the real mailbox host, usually Microsoft 365 or Google Workspace. Spaw reports such domains as `mx_provider: "mimecast"`, and the gateway, not the mailbox host, is what a verification handshake talks to.
## How Mimecast shows up in DNS
Domains behind Mimecast publish MX records under `mimecast.com`, with regional inbound clusters such as `us-smtp-inbound-1.mimecast.com`, `eu-smtp-inbound-1.mimecast.com` or `service-alpha-inbound-a.mimecast.com`, normally as a pair at equal priority. Spaw matches the `.mimecast.com` suffix. Because the MX target is a filtering layer, `mx_provider` tells you who guards the domain, not where the mailboxes live; the domain intelligence endpoint reports the gateway and leaves the mailbox host unknown.
## How Mimecast answers a verification handshake
A gateway does not know which mailboxes exist unless the customer syncs its directory to it. Mimecast offers directory-based recipient validation, and customers who enable it get clean rejections for unknown recipients at `RCPT TO`. Many do not, and their gateways accept every recipient during the handshake, pass the message to the mailbox host, and only then discover that the mailbox is missing, producing a bounce after the fact. From the outside that domain is a catch-all, and it is the common answer behind Mimecast. Mimecast also greylists and rate-limits senders it has not seen, which leaves some probes unverified.
## What that means for your verdicts
Expect `risky` with `reason: catch_all` for most Mimecast-protected domains. Spaw remembers the catch-all answer per domain for seven days, spends no further probes on it, and returns `mailbox_confidence` for each address instead: Mimecast is on the established-provider list, so the estimate starts ten points up and then moves with the domain's DMARC and SPF, its registration age, whether the username looks like a name, and the delivery outcomes every account has reported for the domain, once there are at least five. Domains with recipient validation enabled answer cleanly, `deliverable` with `mailbox_exists: true` or `undeliverable` with `reason: mailbox_not_found`. Unverified probes show `smtp_reason: greylisted` or `timeout`; pass `callback_url` to have them re-checked after 5 and 20 minutes.
Because catch-all is the norm here, your own delivery data is the strongest signal available. A delivered report through the feedback endpoint turns later lookups of that address into a confirmed mailbox with `smtp_reason: delivered_recently`, and bounces lower the confidence for the whole domain.
## SPF and DMARC at Mimecast
Companies that send outbound mail through Mimecast add a regional include to their SPF record, such as `include:us._netblocks.mimecast.com` or `include:eu._netblocks.mimecast.com`; the global `include:_netblocks.mimecast.com` record exists too, but it expands to several regional includes and counts against the ten-lookup limit accordingly. Mimecast's documentation lists the correct include for each region. Inbound filtering does not change the domain's SPF; Spaw reports `has_spf` and `dmarc_policy` from the live records as usual.
## Practical notes for sending to domains behind Mimecast
Mimecast-protected companies are typically mid-size and enterprise organisations with strict policies: attachments, links and unauthenticated senders are inspected aggressively, and a sender without aligned SPF and DKIM is more likely to be held or rejected here than at a consumer provider. Treat a `risky` catch-all answer as "the gateway would not say", rank those addresses by `mailbox_confidence`, and feed outcomes back so the ranking improves.
For a live look at any Mimecast-protected domain, run it through the free MX lookup and the SPF and DMARC checker.
---
# Proofpoint: MX records and email verification behind a security gateway
How domains behind Proofpoint Essentials and Proofpoint Protection look in DNS, why the gateway usually answers as catch-all, and which verdicts to expect.
Updated: 2026-10-07
Proofpoint runs two hosted email security services: Proofpoint Protection for enterprises, whose MX hosts live under `pphosted.com`, and Proofpoint Essentials for smaller companies through partners, under `ppe-hosted.com`. Both sit in front of the real mailbox host, and Spaw reports both as `mx_provider: "proofpoint"`.
## How Proofpoint shows up in DNS
Enterprise customers publish MX records shaped like `mxa-00148501.gslb.pphosted.com` and `mxb-00148501.gslb.pphosted.com`, where the number identifies the customer; Essentials customers use regional hosts under `ppe-hosted.com`. Spaw matches both suffixes. The MX target is a filtering gateway, so `mx_provider` names the guard at the door and the mailbox host behind it, typically Microsoft 365 or Google Workspace, stays out of view.
## How Proofpoint answers a verification handshake
Proofpoint can validate recipients against the customer's directory when the customer sets that up, and those gateways reject unknown recipients in the session. Without recipient validation, the gateway accepts every recipient at `RCPT TO`, forwards the message and lets the mailbox host bounce it later, which makes the domain a catch-all to any verifier. In practice, catch-all is the common answer behind Proofpoint. Enterprise gateways also throttle unfamiliar sources and can drop a connection mid-session, which surfaces as `smtp_reason: timeout` or `no_connect`.
## What that means for your verdicts
Most Proofpoint-protected domains answer `risky` with `reason: catch_all`. Spaw remembers the catch-all per domain for seven days and returns `mailbox_confidence` per address: Proofpoint is on the established-provider list, so the estimate starts ten points up, then moves with DMARC and SPF, the domain's age, the shape of the username, and the delivery outcomes every account has reported for the domain, once there are at least five. Domains with recipient validation answer cleanly, `deliverable` with `mailbox_exists: true` or `undeliverable` with `reason: mailbox_not_found`. A timed-out probe leaves the address unverified with the free signals deciding the verdict; `callback_url` schedules re-checks after 5 and 20 minutes, billing 1 credit only if a re-check confirms the mailbox.
Feed delivery outcomes back. Behind a catch-all gateway, a delivered report is the only way to turn an address into a confirmed mailbox (`smtp_reason: delivered_recently`), and a bounce rate of half or more for the domain removes twenty points from every confidence estimate on it.
## SPF and DMARC at Proofpoint
Companies that also send through Proofpoint add the include their console shows. Proofpoint Essentials documents regional values such as `include:_spf-us.ppe-hosted.com` and `include:_spf-eu.ppe-hosted.com`; enterprise (pphosted.com) customers receive per-customer authorized hosts from Proofpoint rather than one universal include, so the exact value should be copied from the admin console. Inbound filtering leaves the domain's own SPF and DMARC untouched, and Spaw reports them live in `has_spf` and `dmarc_policy`.
## Practical notes for sending to domains behind Proofpoint
Proofpoint customers are mostly large organisations, and the gateway applies reputation, URL rewriting and attachment sandboxing before anything reaches a mailbox. Authentication alignment matters more than at any consumer provider. Expect a high share of `risky` catch-all answers on B2B lists, rank them by `mailbox_confidence`, and keep role addresses out of personalised sequences even when the gateway accepts them.
For a live look at any Proofpoint-protected domain, run it through the free MX lookup and the SPF and DMARC checker.
---
# Proton Mail: MX records and email verification
How Proton-hosted domains look in DNS, how Proton Mail answers a verification handshake, and which verdicts to expect for proton.me and custom domains.
Updated: 2026-10-07
Proton Mail is the encrypted mailbox service from Proton, used both on its own domains (proton.me, protonmail.com, protonmail.ch, pm.me) and on custom domains for paid plans. Spaw reports every Proton-hosted domain as `mx_provider: "proton"`.
## How Proton shows up in DNS
Proton domains publish two MX records, `mail.protonmail.ch` at priority 10 and `mailsec.protonmail.ch` at priority 20. Custom domains on Proton use the same two hosts. Spaw matches the `.protonmail.ch` suffix; Proton is a mailbox host, so the MX target is Proton's own inbound server and `mx_implicit` is false.
## How Proton answers a verification handshake
Proton typically rejects unknown recipients during the handshake, so unknown usernames on proton.me answer `mailbox_not_found` with `smtp_reason: no_mailbox` on the first attempt. Custom domains on Proton can have a catch-all address configured by the domain's administrator; those domains answer as catch-all. Proton's inbound servers are conservative with unfamiliar senders and occasionally defer a first connection, which shows up as an unverified answer with `smtp_reason: greylisted`.
## What that means for your verdicts
For Proton's own consumer domains, Spaw applies Proton's username rules before any probe: letters, digits, dots, underscores and hyphens. An address that breaks them is answered `undeliverable` with `reason: invalid_local_part` at no cost. Valid addresses are then probed and come back `deliverable` with `mailbox_exists: true` or `undeliverable` with `reason: mailbox_not_found`.
An address on proton.me, protonmail.com, protonmail.ch or pm.me is a real mailbox at a free provider: Spaw answers it with `free_provider: true`, `domain_category: "webmail"` and `is_relay: false`. The aliases Proton Pass hands out are different. They live on Proton Pass's own domains (passmail.net, passmail.com, passinbox.com and passfwd.com), and Spaw answers those, and pass2mail.com, which Proton registered for Pass too, with `is_relay: true` and `relay_service: "Proton Pass"`: a real person's mailbox behind a name they control, never a burner.
Custom domains behave like any business domain: a clean reject or accept when catch-all is off, `risky` with `reason: catch_all` and a `mailbox_confidence` estimate when it is on. Proton is not on Spaw's list of established providers for the confidence bonus, so the estimate leans on the domain's DMARC and SPF records, its registration age and the shape of the username.
## SPF and DMARC at Proton
Proton's anti-spoofing guide tells custom-domain owners to add `include:_spf.protonmail.ch` to their SPF record, to add the DKIM CNAME records from the Proton dashboard, and to publish DMARC starting with `p=quarantine`, moving to `p=reject` once legitimate mail passes. Proton's own domains publish strict policies. Spaw reports the live state of a custom domain in `has_spf` and `dmarc_policy`.
## Practical notes for sending to Proton users
Proton supports plus-addressing on its own domains, reported by Spaw as `is_alias: true`. Because Proton is chosen by privacy-conscious users, complaint rates on unsolicited mail tend to be high and Proton's filtering weighs sender reputation heavily, so authenticate your sending domain properly and avoid sending to addresses you have not verified. Proton's address book aliases (`pm.me` for paid users) resolve to the same account and are handled like any other Proton domain.
For a live look at any Proton-hosted domain, run it through the free MX lookup and the SPF and DMARC checker.
---
# Rackspace Email: MX records and email verification
How Rackspace-hosted domains look in DNS, how Rackspace Email answers an SMTP verification handshake, and which verdicts to expect for addresses on it.
Updated: 2026-10-07
Rackspace Email is a hosted mailbox service for small businesses, sold directly and through many web hosts and resellers. Its infrastructure runs under the `emailsrvr.com` name, so Spaw reports these domains as `mx_provider: "rackspace"` even though rackspace.com itself, the company's corporate domain, is hosted on Microsoft 365.
## How Rackspace shows up in DNS
Rackspace Email asks customers to publish exactly two MX records, `mx1.emailsrvr.com` at priority 10 and `mx2.emailsrvr.com` at priority 20, and no others. Spaw matches the `.emailsrvr.com` suffix. Rackspace is a mailbox host: the MX target stores the mail, `mx_found` is true and `mx_implicit` is false. Hosted Exchange customers at Rackspace use different records and are classified by whatever their MX points at.
## How Rackspace answers a verification handshake
Rackspace Email typically rejects unknown recipients at `RCPT TO`, so most checks settle on the first attempt. The admin control panel lets a domain owner configure a catch-all mailbox, and domains with one answer as catch-all. Rackspace's inbound servers also apply rate limits and reputation checks, and a probe from a source they have not seen is occasionally deferred.
## What that means for your verdicts
Expect `deliverable` with `mailbox_exists: true` or `undeliverable` with `reason: mailbox_not_found` and `smtp_reason: no_mailbox` for most domains, and `risky` with `reason: catch_all` for the ones with a catch-all mailbox. For catch-all domains Spaw returns `mailbox_confidence`; Rackspace is not on the established-provider list, so the estimate depends on the domain's DMARC and SPF, its age, the username's shape and the delivery outcomes every account has reported for the domain, once there are at least five. There are no consumer username rules for Rackspace domains, since every Rackspace domain is a customer's own domain.
Rackspace's customers are small businesses, and their address lists carry many role inboxes: `info@`, `office@`, `sales@`. Spaw flags them `risky` with `reason: role` even when the handshake confirms they exist. Staff turnover shows up as `mailbox_not_found` once the account is deleted, and the address then joins your suppression list.
## SPF and DMARC at Rackspace
Rackspace's documentation asks domain owners to publish `v=spf1 include:emailsrvr.com ~all` when Rackspace Email is the only sender, and to enable DKIM from the control panel; any other service sending as the domain needs its own include. Spaw reports the live state in `has_spf` and `dmarc_policy`, and a company domain without SPF adds ten risk points to its addresses.
## Practical notes for sending to Rackspace users
Because most Rackspace domains belong to small companies that manage DNS through a web host or reseller, SPF and DMARC are frequently missing or misconfigured on the receiving side, which affects their own deliverability but not yours. On your side, an aligned sending domain and a clean list are enough; Rackspace's filtering is conventional. Re-verify these lists on a schedule, since small-business staff and role inboxes change often.
For a live look at any Rackspace-hosted domain, run it through the free MX lookup and the SPF and DMARC checker.
---
# Tuta (Tutanota): MX records and email verification
How Tuta-hosted domains look in DNS, how Tuta answers a verification handshake, and which verdicts to expect for tuta.com, tutanota.com and custom domains.
Updated: 2026-09-03
Tuta, known as Tutanota until its 2023 rebrand, is an end-to-end encrypted mailbox service run by Tutao GmbH in Germany. It serves its own domains (tuta.com, tuta.io, tutanota.com, tutanota.de, tutamail.com, keemail.me) and custom domains on paid plans, all from infrastructure under the `tutanota.de` name. Spaw reports them as `mx_provider: "tutanota"`.
## How Tuta shows up in DNS
Every Tuta-hosted domain publishes a single MX record, `mail.tutanota.de`. Spaw matches the `.tutanota.de` suffix. Tuta is a mailbox host: the MX target is Tuta's own inbound server, which encrypts incoming mail for the recipient's key on arrival. `mx_found` is true and `mx_implicit` is false for a correctly configured domain.
## How Tuta answers a verification handshake
Tuta typically rejects unknown recipients at `RCPT TO`, so a missing mailbox is confirmed in the session. Custom domains on Tuta can configure a catch-all address from the settings, and those domains answer as catch-all. Tuta's inbound server is conservative with unfamiliar sources, so occasional deferrals show up as `smtp_reason: greylisted`.
## What that means for your verdicts
Most Tuta addresses come back `deliverable` with `mailbox_exists: true` or `undeliverable` with `reason: mailbox_not_found` and `smtp_reason: no_mailbox`. Custom domains with catch-all enabled answer `risky` with `reason: catch_all`; Spaw remembers the catch-all for seven days and returns `mailbox_confidence`, which without the established-provider bonus leans on the domain's own records, its age and the shape of the username. Tuta is not in the consumer username rule set, so `invalid_local_part` does not apply.
One point deserves care. Because Tuta offers free, anonymous, encrypted accounts, some disposable-domain lists include Tuta's public domains, and a lookup can then answer `risky` with `reason: disposable` for a real, long-lived mailbox. Spaw's disposable allowlist exists for exactly this kind of false positive; if you see Tuta's public domains flagged as disposable on your lists, treat the flag with suspicion and check the domain on the free disposable checker, which shows which list produced the match.
## SPF and DMARC at Tuta
Tuta supports SPF, DKIM and DMARC on custom domains and shows the exact records to publish in its domain setup wizard; the SPF value in common use is `v=spf1 include:spf.tutanota.de -all`, and the wizard also provides the DKIM CNAME records and a suggested DMARC record. Tuta's own domains publish strict policies. Spaw reports the live state of a custom domain in `has_spf` and `dmarc_policy`.
## Practical notes for sending to Tuta users
Tuta users chose an encrypted provider on purpose, and Tuta's spam filtering and its users' tolerance for marketing mail are both strict. Send only what was asked for, authenticate properly, and expect complaint-driven blocks to be quick. Tuta does not offer Gmail-style plus-addressing on its own domains; users create separate aliases instead, which look like ordinary addresses to a verifier and answer individually.
For a live look at any Tuta-hosted domain, run it through the free MX lookup and the SPF and DMARC checker.
---
# Yahoo Mail and AOL: MX records and email verification
How Yahoo and AOL domains look in DNS, why their servers often leave a verification handshake unverified, and which verdicts to expect for those addresses.
Updated: 2026-09-03
Yahoo Mail and AOL Mail run on the same infrastructure since Yahoo acquired AOL, and the merged platform also hosts a long list of country domains (yahoo.co.uk, yahoo.fr, ymail.com, rocketmail.com and more). Spaw reports all of them as `mx_provider: "yahoo"`.
## How Yahoo shows up in DNS
Yahoo domains publish MX records under `yahoodns.net`: yahoo.com points at hosts such as `mta5.am0.yahoodns.net`, `mta6.am0.yahoodns.net` and `mta7.am0.yahoodns.net`, and aol.com at `mx-aol.mail.gm0.yahoodns.net`. Spaw matches the `.yahoodns.net` suffix. Yahoo is a mailbox host, but a large consumer one with an aggressive perimeter, and that perimeter is what a verification handshake talks to.
## How Yahoo answers a verification handshake
Yahoo's inbound servers are the most frequent source of unverified answers among the big consumer providers. They commonly defer a first connection from an unfamiliar sender with a temporary failure, apply per-connection rate limits, and sometimes accept a recipient at `RCPT TO` only to decide later. A verification probe cannot wait the minutes a deferral asks for, so many Yahoo lookups end with `smtp_checked: false` and an `smtp_reason` of `greylisted` or `timeout`. Clear rejections do happen, and when they do they are reliable: an address Yahoo says does not exist really does not.
## What that means for your verdicts
Expect three kinds of answers. A confirmed mailbox gives `deliverable` with `mailbox_exists: true`. A confirmed missing mailbox gives `undeliverable` with `reason: mailbox_not_found`. And a large share of lookups gives `deliverable` with `smtp_checked: false`, `mailbox_exists: null` and a `mailbox_confidence` estimate; the unverified state adds ten risk points, so these addresses usually land in the low band rather than at zero.
Before any probe, Yahoo's username rules apply to yahoo.com, aol.com and their listed variants: letters, digits, dots, underscores and hyphens. Anything else is `undeliverable` with `reason: invalid_local_part` at no cost. Yahoo is not on Spaw's established-provider list for the confidence bonus, because a consumer perimeter that defers probes says little about individual mailboxes.
The most effective way to firm up Yahoo answers is your own delivery data. Report delivered and bounced outcomes through the feedback endpoint or an ESP webhook: a delivered report turns later lookups of that address into a confirmed mailbox with `smtp_reason: delivered_recently`, and the bounce rate of the domain feeds every confidence estimate on it. For an unverified single lookup, pass `callback_url` so Spaw re-checks after 5 and 20 minutes and pushes the settled verdict.
## SPF and DMARC at Yahoo
Consumer Yahoo and AOL users have nothing to configure; yahoo.com and aol.com publish their own SPF and a DMARC policy of `p=reject`. That policy has a practical consequence for senders: mail that claims a yahoo.com or aol.com From address but is sent through another service fails DMARC and is rejected. Spaw's `dmarc_policy` field shows the live policy for any domain.
## Practical notes for sending to Yahoo users
Yahoo joined Google in requiring aligned SPF or DKIM, a DMARC record, one-click unsubscribe and a low complaint rate from bulk senders in 2024, and it recycles abandoned addresses into spam traps more readily than most providers. Old Yahoo addresses on a list are therefore worth re-verifying before every campaign, and a `mailbox_not_found` answer on one should be treated as final. Yahoo does not support plus-addressing in the Gmail sense; it uses a separate disposable-address feature with a hyphen, which looks like a normal address to a verifier.
For a live look at any Yahoo-hosted domain, run it through the free MX lookup and the SPF and DMARC checker.
---
# Yandex 360 and Yandex Mail: MX records and email verification
How domains on Yandex 360 for Business look in DNS, how Yandex answers an SMTP verification handshake, and which verdicts to expect for Yandex-hosted addresses.
Updated: 2026-10-07
Yandex runs the largest mailbox service in the Russian-speaking world: consumer Yandex Mail on yandex.ru and its sister domains, and Yandex 360 for Business for company domains. Business domains point their MX at `mx.yandex.net` and Yandex's own consumer domains at `mx.yandex.ru`; Spaw matches both and reports `mx_provider: "yandex"`.
## How Yandex shows up in DNS
Yandex 360 for Business asks domain owners to publish one MX record, `mx.yandex.net` at priority 10, or to delegate the domain to Yandex's DNS, which configures it automatically. Spaw matches the `.yandex.net` suffix, and the `.yandex.ru` suffix that Yandex's own consumer domains, including yandex.com and yandex.ru, currently resolve to. Yandex is a mailbox host: `mx_found` is true and `mx_implicit` is false for a correctly configured business domain.
## How Yandex answers a verification handshake
Yandex typically rejects unknown recipients at `RCPT TO`, so most business-domain checks settle on the first attempt. Its inbound servers are cautious with senders they have not seen and can defer a first connection, which leaves some probes unverified with `smtp_reason: greylisted`. A Yandex 360 administrator can configure a catch-all mailbox per domain, and those domains answer as catch-all.
## What that means for your verdicts
Expect `deliverable` with `mailbox_exists: true`, `undeliverable` with `reason: mailbox_not_found` and `smtp_reason: no_mailbox`, or `risky` with `reason: catch_all` on domains with a catch-all mailbox. For catch-all domains Spaw returns `mailbox_confidence`, which without the established-provider bonus is driven by the domain's DMARC and SPF, its registration age, the shape of the username and the delivery outcomes every account has reported for the domain, once there are at least five. There are no consumer username rules for Yandex domains in Spaw's rule set, so `invalid_local_part` does not apply; the RFC syntax check still does, and Cyrillic domain names are converted to their punycode form before any lookup.
## SPF and DMARC at Yandex
Yandex 360's documentation gives `v=spf1 redirect=_spf.yandex.net` as the SPF record when Yandex is the only sender, or `v=spf1 ip4:... include:_spf.yandex.net ~all` when other servers send too, and it provides a DKIM key from the admin console. Delegated domains get these records automatically. Spaw reports the live state in `has_spf` and `dmarc_policy`; a business domain without SPF adds ten risk points to its addresses.
## Practical notes for sending to Yandex users
Yandex's filtering leans on sender authentication and on its own reputation data, and it expects List-Unsubscribe headers from bulk senders in the same way the large western providers do. Many Yandex-hosted business domains use Cyrillic or transliterated usernames; Spaw's name dictionary is English-centric, so `is_gibberish` is more likely to be wrong on these lists and should be treated as a hint rather than a rule. Plus-addressing is supported and reported as `is_alias: true`.
For a live look at any Yandex-hosted domain, run it through the free MX lookup and the SPF and DMARC checker.
---
# Zoho Mail: MX records and email verification
How Zoho-hosted domains look in DNS, how Zoho answers an SMTP verification handshake, and which verdicts to expect for Zoho Mail addresses.
Updated: 2026-09-03
Zoho Mail is the mailbox service inside the Zoho suite, popular with small companies that already run Zoho CRM or Books. Spaw reports it as `mx_provider: "zoho"`.
## How Zoho shows up in DNS
A Zoho-hosted domain publishes three MX records: `mx.zoho.com`, `mx2.zoho.com` and `mx3.zoho.com` at priorities 10, 20 and 50. Customers on the European data centre use `mx.zoho.eu` and its siblings, and Zoho also runs regional endpoints under `zoho.in`, `zoho.com.au` and others. Spaw matches the `.zoho.com` and `.zoho.eu` suffixes, which cover the two largest regions; a domain on another regional endpoint currently classifies as `other`, which the classifier will pick up in a later revision of its patterns.
Zoho is a mailbox host: the MX points at Zoho's own inbound servers, which store the mail. `mx_found` is true and `mx_implicit` is false for a correctly configured domain.
## How Zoho answers a verification handshake
Zoho typically rejects unknown recipients at `RCPT TO`, so the mailbox check settles on the first attempt for most domains. Administrators can enable a catch-all address per domain from the Zoho admin console, and when they do the inbound server accepts every recipient and the domain answers as catch-all. Greylisting is not the norm for a single probe, though Zoho does rate-limit senders it has not seen before, which occasionally leaves an answer unverified with `smtp_reason: greylisted` or `timeout`.
## What that means for your verdicts
Most Zoho addresses come back `deliverable` with `mailbox_exists: true` or `undeliverable` with `reason: mailbox_not_found`. Domains with a catch-all configured answer `risky` with `reason: catch_all`; Spaw remembers that for seven days and returns `mailbox_confidence`, which for Zoho includes the ten-point bonus for an established provider. Zoho has no consumer username rules in Spaw's rule set, so `invalid_local_part` does not apply to it; the RFC syntax check and the domain checks still run.
Small companies on Zoho often route several role inboxes to one person. Spaw still flags `info@`, `sales@` and similar as `risky` with `reason: role`, and the handshake usually confirms that the inbox exists, so `mailbox_exists` can be true on a risky answer. That combination means the address is real but shared.
## SPF and DMARC at Zoho
Zoho's SPF documentation asks domain owners to publish `v=spf1 include:zohomail.com -all`, or `v=spf1 include:one.zoho.com -all` when several Zoho services send on the domain's behalf, and to add the DKIM selector from the admin console. Older setups still carry `include:zoho.com`, which continues to resolve. Spaw reports the live state in `has_spf` and `dmarc_policy`; a Zoho-hosted company domain without SPF adds ten risk points to its addresses.
## Practical notes for sending to Zoho users
Zoho supports plus-addressing, which Spaw reports as `is_alias: true`, and it applies its own spam filtering aggressively to new senders, so a warmed-up sending domain with aligned SPF and DKIM matters more here than at the large consumer providers. Because Zoho customers are mostly businesses, expect a higher share of role addresses and treat `mailbox_exists: true` on a role address as "real but shared".
For a live look at any Zoho-hosted domain, run it through the free MX lookup and the SPF and DMARC checker.
---
# iCloud Mail: MX records and email verification
How iCloud Mail and iCloud custom domains look in DNS, how Apple's servers answer a verification handshake, and which verdicts to expect.
Updated: 2026-10-07
iCloud Mail serves Apple's consumer accounts on icloud.com, together with the legacy me.com and mac.com addresses that still deliver to the same mailboxes, and since 2021 it also hosts custom domains for iCloud+ subscribers. Spaw reports all of them as `mx_provider: "icloud"`.
## How iCloud shows up in DNS
Every iCloud-hosted domain publishes the same pair of MX records, `mx01.mail.icloud.com` and `mx02.mail.icloud.com`, both at priority 10. Apple's setup guide for custom domains uses exactly these two hosts. Spaw matches the `.mail.icloud.com` suffix. iCloud is a mailbox host: the MX target is Apple's own inbound server, `mx_found` is true and `mx_implicit` is false.
## How iCloud answers a verification handshake
Apple's inbound servers typically reject unknown recipients at `RCPT TO`, so a missing icloud.com mailbox is confirmed in the session. They are also strict about sender reputation and will defer or drop connections from sources they distrust, which occasionally leaves a probe unverified with `smtp_reason: greylisted` or `no_connect`. Custom domains on iCloud+ can enable a catch-all address from the iCloud settings, and those domains then answer as catch-all.
## What that means for your verdicts
For icloud.com, me.com and mac.com, Apple's username rules apply before any probe: letters, digits, dots, underscores and hyphens. Anything else is `undeliverable` with `reason: invalid_local_part` without a handshake. Valid addresses are probed and mostly come back `deliverable` with `mailbox_exists: true` or `undeliverable` with `reason: mailbox_not_found` and `smtp_reason: no_mailbox`.
An ordinary icloud.com, me.com or mac.com address is a real mailbox at a free provider, and Spaw answers it with `free_provider: true`, `domain_category: "webmail"` and `is_relay: false`.
Apple has two alias features, and they answer differently. When an app offers Sign in with Apple and the user chooses to hide their address, the app receives a random address on `privaterelay.appleid.com`, and Apple forwards only the mail sent from the domains and addresses the app's developer registered with Apple. Apple said in August 2026 that new addresses will be issued on `private.icloud.com` from later that year, and that existing ones keep working. Spaw flags both domains: `is_relay: true`, `relay_service: "Sign in with Apple private relay"` and `domain_category: "relay"`. The flag belongs to the name `private.icloud.com` alone, so an icloud.com address never inherits it. Hide My Email in iCloud+ is the other feature, and its aliases sit on icloud.com itself, beside the real mailboxes, where Apple's August 2026 announcement keeps them. Nothing in the address or the domain marks one as an alias, so Spaw cannot tell it from an ordinary iCloud mailbox and does not flag it: it answers like any other icloud.com address. Both kinds forward to a real inbox until the user turns them off.
Custom domains with catch-all enabled answer `risky` with `reason: catch_all` and a `mailbox_confidence` estimate; iCloud is not on the established-provider list, so the estimate relies on the domain's own records, its age and the shape of the username.
## SPF and DMARC at iCloud
For custom domains, Apple's guide publishes `v=spf1 include:icloud.com ~all` for SPF, a DKIM CNAME with the selector shown during setup, and a DMARC record. icloud.com itself publishes SPF and a DMARC policy, so From addresses at icloud.com sent through other services fail alignment. Spaw reports the live state of any domain in `has_spf` and `dmarc_policy`.
## Practical notes for sending to iCloud users
Apple's Mail Privacy Protection loads remote content through Apple's proxies, which makes open rates for iCloud users unreliable and makes engagement data a poor substitute for verification. Legacy me.com and mac.com addresses are still live for the accounts that had them, so do not discard them on age alone; verify them. iCloud does not use Gmail-style plus-addressing for consumer accounts, and role-style usernames on icloud.com are individual accounts rather than shared inboxes, though Spaw still applies its role-word list to them.
For a live look at any iCloud-hosted domain, run it through the free MX lookup and the SPF and DMARC checker.
---
# Check phone numbers in Firebase's beforeSmsSent function with Spaw
A beforeSmsSent blocking function that asks Spaw about the number and the visitor's IP, refuses codes your SMS policy would not send, and fails open.
Updated: 2026-09-27
Firebase Authentication runs a `beforeSmsSent` blocking function before it sends a phone sign-in or multi-factor code, and a function that throws an `HttpsError` stops that SMS. Asking Spaw about the number inside it means a code goes out only where your account's [SMS policy](/docs/phone-intelligence#policy) answers `ok_to_send: true`: not to a number an SMS-receiving website publishes, not to a range reserved for fiction, not to a number your own reports marked as abused. Firebase still sends the message itself; the function only decides whether it goes.
## What the function receives
These facts come from Firebase's blocking-functions pages (last updated 24 September 2026) and the type definitions in `firebase-functions` 7.4.0, both read on 27 September 2026.
| Item | What Firebase provides |
| --- | --- |
| Trigger | `beforeSmsSent` from `firebase-functions/v2/identity`, Node.js only |
| Prerequisite | The project upgraded to Firebase Authentication with Identity Platform |
| The number | `event.additionalUserInfo.phoneNumber` |
| The visitor's address | `event.ipAddress`, the address of the device the person is signing in from |
| The flow | `event.smsType`: `SIGN_IN_OR_SIGN_UP`, `MULTI_FACTOR_ENROLLMENT` or `MULTI_FACTOR_SIGN_IN` |
| Refusing | Throw an `HttpsError`; Firebase wraps it and returns it to the client app as an internal error (`auth/internal-error`), which the screen that requests the code should catch |
| Time limit | "Your function must respond within 7 seconds. After 7 seconds, Firebase Authentication returns an error, and the client operation fails." |
The last row matters most. A check that hangs does not let the code through; it breaks the sign-in. The sample gives Spaw 2.5 seconds and sends the code unchecked when no answer arrives in that time.
## Before you start
1. Create a secret API key on the API keys page of your Spaw dashboard. Scope it to phone and give it a daily credit cap: the cap bounds what a flood of attempts can spend on lookups. Once the cap is spent the function sends codes unchecked, so set it above a normal day's sign-ups.
2. Store it as a function secret with `firebase functions:secrets:set SPAW_KEY`.
3. Switch the SMS policy's opt-out rule off, on the SMS policy tab of the phone verification page or with one request. By default a number your suppression list holds as opted out is refused, and a person who once replied STOP to a marketing message could no longer sign in.
```bash
curl -X PUT https://spaw.co/api/v1/phone/policy \
-H "Authorization: Bearer sk_live_…" \
-H "Content-Type: application/json" \
-d '{"block_opted_out": false}'
```
An account has one policy, and every lookup it makes applies it. With the rule off, an opted-out number is decided like any other and the answer still says `meta.opted_out: true`, so a marketing flow on the same account reads that field itself.
## The function
```javascript
const { beforeSmsSent, HttpsError } = require("firebase-functions/v2/identity");
const { defineSecret } = require("firebase-functions/params");
const logger = require("firebase-functions/logger");
const SPAW_KEY = defineSecret("SPAW_KEY");
// Codes that attach a number to an account. A multi-factor sign-in code goes
// to a number the user enrolled earlier, so it is logged and never refused.
const REFUSE_ON = new Set(["SIGN_IN_OR_SIGN_UP", "MULTI_FACTOR_ENROLLMENT"]);
exports.checknumberbeforesms = beforeSmsSent({ secrets: [SPAW_KEY] }, async (event) => {
const phone = event.additionalUserInfo?.phoneNumber;
if (!phone) {
return;
}
let answer;
try {
const response = await fetch("https://spaw.co/api/v1/phone", {
method: "POST",
headers: {
Authorization: `Bearer ${SPAW_KEY.value()}`,
"Content-Type": "application/json",
Accept: "application/json",
},
body: JSON.stringify(event.ipAddress ? { phone, client_ip: event.ipAddress } : { phone }),
signal: AbortSignal.timeout(2500),
});
if (!response.ok) {
logger.warn(`Spaw answered ${response.status}; the code goes out unchecked`);
return;
}
answer = (await response.json()).data;
} catch (error) {
logger.warn("Spaw did not answer in time; the code goes out unchecked", error);
return;
}
// opted_out is decided before every rule except invalid, so it says nothing
// about the rest: switch block_opted_out off rather than refuse a code on it.
if (answer.ok_to_send || answer.blocked_by === "opted_out") {
return;
}
logger.info("Spaw would refuse this code", { blocked_by: answer.blocked_by, sms_type: event.smsType });
if (REFUSE_ON.has(event.smsType)) {
throw new HttpsError("permission-denied", "This number cannot receive a verification code.");
}
});
```
Deploy it with `firebase deploy --only functions`. The request carries two fields, the number and the visitor's address, and nothing else about the person. `client_ip` is counted, never looked up and never billed; the [privacy policy](/privacy) describes how the count is kept. The sample follows Firebase's documented signature; run it in your own project before you rely on it.
## Which codes it refuses, and which it only logs
`REFUSE_ON` holds the flows where a number is being attached to an account: a phone sign-up or sign-in, which Firebase reports as one type, and a multi-factor enrolment. A multi-factor sign-in sends a code to a number the user enrolled earlier, and refusing it would lock an existing customer out, so the function logs what Spaw said and lets the code go. Add `MULTI_FACTOR_SIGN_IN` to the set only if you would rather send such a user to support. Because sign-in and sign-up share a type, a returning user whose number has since appeared on an SMS-receiving website is refused as well; anyone can read the codes such a site publishes, so that is usually the side to err on.
Under the published defaults a refusal names one of these in `blocked_by`: `invalid`, `fictional` (a range reserved for fiction, such as the North American 555-01XX block), `disposable` (published by an SMS-receiving website), `premium_rate`, `reported_abuse` (your own abuse reports), `reported_abuse_widely` (three or more accounts within 30 days), or `risk_score` (a score above 60). Rules you add answer `country` or `line_type`. The [gate-SMS guide](/guides/gate-sms-verification-codes-with-the-sms-policy) explains each rule and the order they apply in.
The visitor's address adds what the number alone cannot show. Five distinct numbers tried from one address within 60 minutes add [`numbers_per_client`](/docs/signals/phone/numbers_per_client) (+30), unless Spaw's IP data places the address on a mobile carrier or a corporate proxy, where many people share one address: those addresses still answer the count but never add the signal, and on a sign-in screen many visitors are on mobile data. Five distinct numbers your account looks up in one 1,000-number range within 60 minutes add `range_burst` (+40), whatever the address. Neither passes the default ceiling of 60 on its own. Together they score 70, and so does `numbers_per_client` beside a number in a block allocated to a virtual-number wholesaler (`virtual`, +40): the answer is then `ok_to_send: false` with `blocked_by: "risk_score"`. The [SMS pumping guide](/guides/sms-pumping-what-the-default-sms-policy-lets-through) shows what the defaults let through and the request that tightens them.
## Why it fails open
Every outcome that is not an answer ends in `return`: a timeout, a network error, `402 INSUFFICIENT_CREDITS` when the balance is empty, `429` when the key's daily cap is spent or your phone requests exceed 50 a second, a `5xx`. The code then goes out exactly as it did before the function existed. Failing closed would turn a Spaw outage or an empty balance into an outage of your sign-in. The price is that an attack arriving during one of those failures is not filtered, which is why each one is logged as a warning: alert on them.
Firebase's own controls stack with this check. Its phone-auth guide asks for an SMS region policy, the countries Firebase may send codes to ("Setting an SMS region policy can help protect your apps from SMS abuse. For new projects, the default policy allows no regions."), and when reCAPTCHA Enterprise protects the project the event carries `additionalUserInfo.recaptchaScore`, a reading of the request rather than the number. The Spaw policy can hold a country allow-list too; keep the two lists the same, or leave `allowed_countries` unset and let Firebase's region policy be the one source of truth.
## Report the codes people entered
A code that was delivered and never entered is what SMS pumping looks like from the sender's side. Report every code that was entered as a `verified` outcome: it cancels an earlier failure reported for the number and clears a suppression entry your failure reports added. Firebase checks the code itself, so the report comes from your backend: an ID token whose `firebase.sign_in_provider` is `phone` was issued after the code was entered, and its `phone_number` claim holds the number.
```javascript
const { getAuth } = require("firebase-admin/auth");
async function reportVerifiedSignIn(idToken) {
const token = await getAuth().verifyIdToken(idToken);
if (token.firebase.sign_in_provider !== "phone" || !token.phone_number) {
return;
}
try {
await fetch("https://spaw.co/api/v1/phone/feedback", {
method: "POST",
headers: {
Authorization: `Bearer ${process.env.SPAW_KEY}`,
"Content-Type": "application/json",
},
body: JSON.stringify({ items: [{ phone: token.phone_number, outcome: "verified" }] }),
signal: AbortSignal.timeout(2500),
});
} catch {
// A missed report only means one less outcome on your account.
}
}
module.exports = { reportVerifiedSignIn };
```
Call it once, when your server creates its session from a new sign-in, not on every request the token authorises. A multi-factor sign-in is marked by `firebase.sign_in_second_factor: "phone"`; that token names the factor's uid rather than its number, and the Admin SDK's `getUser()` lists the number under `multiFactor.enrolledFactors`. Firebase sends the SMS through its own provider, so there are no delivery receipts to forward: `verified` is the one outcome this setup can report. With `verified` alone the [feedback summary](/docs/api/phone-feedback-summary) reads `verified_share: 1` and lists no blocks, because both are worked out against delivery reports. To read a conversion rate, compare the verified count with the number of codes the function let through, which you can log on its way out. The [report endpoint](/docs/api/report-phone-feedback) is free.
## Try the branches without Firebase
The documented test numbers answer a fixed result each, cost nothing and are never counted in your traffic, so each branch can be seen with curl before the function is deployed:
```bash
curl -X POST https://spaw.co/api/v1/phone \
-H "Authorization: Bearer sk_live_…" \
-H "Content-Type: application/json" \
-d '{"phone": "+12025550103", "client_ip": "203.0.113.7"}'
```
Under the default policy `+1 202 555 0103` answers `ok_to_send: false` with `blocked_by: "disposable"`, `+1 202 555 0105` answers `blocked_by: "fictional"`, and `+1 202 555 0102` (the test number that answers the `virtual` signal, score 40) and `+1 202 555 0100` answer `ok_to_send: true`.
## What to leave out
Leave `hlr` out of the request. The live carrier-network check (`hlr: true`) is not enabled on the service: `hlr_checked` answers false and nothing extra is charged. Leave the policy's `require_reachable` rule off for the same reason: while the check is not enabled, no lookup answers `ok_to_send: true` under it: every number that passes the other rules answers `blocked_by: "unconfirmed"`, so the function would refuse every phone sign-in, sign-up and enrolment code. Send nothing but the number and the address: not the user's email, uid or display name.
`ok_to_send` is a fraud and deliverability decision. It is not consent and not a do-not-call scrub, and whether the person agreed to hear from you stays yours to record.
## Cost
A lookup costs 1 credit when the number is valid. Invalid numbers, the test numbers and a repeat of the same number within seven days are free, so a resent code costs nothing. Outcome reports are free. Phone endpoints accept 50 requests a second. Spaw is not affiliated with Google or Firebase.
---
# Feed Postmark bounces into Spaw's suppression list
Point a Postmark webhook at your Spaw feedback URL and every hard bounce, delivery and spam complaint updates your suppression list and measured accuracy.
Updated: 2026-09-03
A verification verdict is a prediction; what your mail server sees afterwards is the truth. Spaw accepts Postmark's bounce, delivery and spam-complaint webhooks directly, so every hard bounce lands on your suppression list without a line of code, every delivery counts as a confirmed mailbox, and the History tab shows how often the verdicts you paid for were right.
## Set up the webhook
1. Open the API keys page in the Spaw dashboard and generate a feedback key. The page shows the ready-made URL for each supported provider; the Postmark one looks like this:
```text
https://spaw.co/api/v1/email/feedback/postmark/fb_…
```
2. In Postmark, open the server and message stream you send from, go to its Webhooks section, and add a webhook with that URL.
3. Tick the Bounce, Delivery and SpamComplaint events. Other events are accepted and ignored.
4. Send Postmark's test payload from the webhook's settings. Spaw answers `200` with how many outcomes it recorded, and the entry appears on your suppression list a moment later.
Postmark cannot send a bearer token, which is why the feedback key rides in the URL. Treat the URL as a secret: anyone who has it can add outcomes to your account. Rotating the key on the API keys page invalidates every old URL at once, and a rotated URL answers `401 INVALID_FEEDBACK_KEY`.
## What Spaw reads from the payload
Postmark posts one JSON object per event. Spaw reads `RecordType` and the recipient in `Email` or `Recipient`, and nothing else in the payload:
| Postmark event | Recorded as |
| --- | --- |
| `Bounce` with `Type` HardBounce, BadEmailAddress, ManuallyDeactivated, Blocked or DnsError | bounced |
| `Bounce` with any other type, for example a transient or soft bounce | ignored |
| `Delivery` | delivered |
| `SpamComplaint` | complained |
The `Description` or `Details` field is stored as the reason, and `BouncedAt`, `DeliveredAt` or `ReceivedAt` as the time it happened. A payload like this records one bounce:
```json
{
"RecordType": "Bounce",
"Type": "HardBounce",
"Email": "mia@acme.com",
"Description": "The server was unable to deliver your message (ex. unknown user, mailbox not found).",
"BouncedAt": "2026-09-03T10:12:44Z"
}
```
## What Spaw does with the events
A bounce or complaint adds the address to your suppression list with source `feedback`. From then on, batch, bulk and monitor runs answer it from your list at no cost, with reason `suppressed` and a risk score of 100; a single lookup still re-verifies it but leaves the entry in place. A bounce comes off the list when a later delivery to that address is reported; a spam complaint stays until you remove it. An address you imported stays until you remove it too: a complaint about it is recorded on your import, and no delivery clears it.
A delivery removes an entry Spaw had added automatically, and for the next 90 days the address counts as a confirmed mailbox for your own lookups: the answer carries `smtp_reason: "delivered_recently"` and no mailbox probe runs. Only your account's deliveries are used for your lookups.
Every outcome is matched against the verdict you were given for that address in the previous 90 days. The feedback summary endpoint and the History tab report the delivery rate among addresses answered `deliverable` and the bounce rate among addresses answered `undeliverable`, which are the two numbers that say how right Spaw was for you. Outcomes are kept for 180 days.
## Cost
Feedback is free and is never logged as a lookup. The webhook endpoint is throttled at 120 requests per minute per IP, which is far above what a single Postmark server sends.
---
# Import the Spaw OpenAPI document into Postman or a generator
An OpenAPI 3.1 document for every endpoint, with examples and error schemas, at /openapi.json: import it into Postman, Insomnia or Bruno, or generate a client.
Updated: 2026-10-08
Every Spaw endpoint is described in one OpenAPI 3.1 document, which is also the source the reference pages are rendered from, so the two can never disagree. It is served as JSON at `https://spaw.co/openapi.json` and as YAML at `https://spaw.co/openapi.yaml`, with no authentication. If your tool understands OpenAPI, it understands Spaw.
## What the document contains
The document covers every endpoint the API serves: single, batch and bulk verification for email, phone, IP and postal addresses, business verification and the cross-signal consistency check, the browser endpoints for publishable keys, domain intelligence, the suppression lists, the monitored lists, delivery feedback and the accuracy summary, the provider webhook targets, and the account endpoints. A count is deliberately not quoted here, because it changes with every launch and this page is written to be read months later. Each operation carries a request example, a response example, and the named error responses it can return. Three vendor extensions add what a generated client cannot infer: `x-spaw-auth` says whether the operation takes a bearer key, a publishable key or a feedback key, `x-spaw-billing` states in one sentence what the operation costs, and `x-spaw-errors` lists the stable error codes.
The `EmailResult` schema documents every field of a verification with its meaning, and the `ErrorEnvelope` schema is the shape of every failure: `{ "success": false, "error": { "code", "message", "request_id" } }`, with `error.errors` keyed by field on a validation failure.
## Import into an API client
1. Create a secret key on the API keys page.
2. In Postman, choose Import and paste `https://spaw.co/openapi.yaml`; Postman builds a collection with one request per operation. Insomnia and Bruno both import an OpenAPI URL or file the same way.
3. Set the collection's authorization to bearer token and paste the key. The document declares the `bearerAuth` scheme on every operation, so the tools apply it automatically.
4. Send the first request against a test address, which costs nothing:
```json
POST https://spaw.co/api/v1/email
{ "email": "deliverable@spaw.test" }
```
The six fixed addresses on `spaw.test` answer canonical verdicts, are never billed, and never appear in your history, so a shared collection can be exercised freely.
## Generate a client
Any generator that reads OpenAPI 3.1 works; recent releases of openapi-generator do. For example:
```bash
openapi-generator-cli generate \
-i https://spaw.co/openapi.yaml \
-g typescript-fetch \
-o ./spaw-client
```
Before generating, check whether an official client already exists for your language: Spaw ships hand-written Python, Node.js and PHP clients built from this same document, with the error classes already mapped to the API's codes. A generated client is the right choice for Go, Ruby, Java, C# and everything else.
## Use it with AI tools
The document is also the fastest way to teach a coding assistant the API. Point it at `https://spaw.co/openapi.yaml`, or at `https://spaw.co/llms-full.txt`, which concatenates every documentation page, the endpoint reference and the verdict reasons into one Markdown file. Each endpoint page under `/docs/api/{operationId}` has a Markdown twin at the same URL with `.md` appended.
## What to do with the verdict
Whatever the tool, the answer is the same 46-field object. Branch on `deliverable`: keep `deliverable`, drop `undeliverable`, and route `risky` by `reason`. The `sources` array names the dataset and version behind every answer, which is what to quote when a verdict surprises you.
## Cost
Importing or generating costs nothing. Lookups follow the usual rules: one credit per fresh deliverable or risky verdict, one credit per five IP lookups, nothing for unverified answers, invalid input, repeats within seven days or the test addresses, and nothing for undeliverable verdicts within a fair-use allowance. Every account gets 100 free credits a month, and `GET /api/v1/account` reads the live balance for free.
---
# Use Spaw from Claude Code, Cursor, VS Code and Codex over MCP
Spaw's MCP server at https://spaw.co/mcp gives AI agents twelve verification tools with your existing API key, the same billing and the same answers.
Updated: 2026-10-04
Spaw speaks the Model Context Protocol, so an AI agent can verify email addresses, phone numbers, IPs, postal addresses and business identifiers, and compare them against each other, as tools instead of you pasting curl output into a chat — one at a time, or a whole list in one call. The server lives at `https://spaw.co/mcp`, uses streamable HTTP, and authenticates with the same secret API key as the REST API. Agent usage shows up on your Usage page as `mcp · key name`, so you can see what the agent spent. The sections below connect Claude Code, Cursor, Visual Studio Code and OpenAI's Codex; any other client that can send a request header connects the same way.
## Connect Claude Code
1. Create a secret key on the API keys page. Use a dedicated key for the agent so you can revoke it without touching your servers, and export it as `SPAW_KEY` in the shell you run Claude Code from.
2. Register the server once:
```bash
claude mcp add --transport http spaw https://spaw.co/mcp \
--header "Authorization: Bearer $SPAW_KEY"
```
The shell expands `$SPAW_KEY` when the command runs (in bash or zsh; in PowerShell write `$env:SPAW_KEY`, or use the single-quoted form that follows), so Claude Code stores the key itself; single-quote the header as `'Authorization: Bearer ${SPAW_KEY}'` to store only the reference, which Claude Code reads from its environment each time it connects. The server is added to the current project only; add `--scope user` to use it in every project.
3. Ask for a verification in plain language, for example "check whether mia@acme.com can receive mail and tell me the reason if not". Claude Code calls the `verify-email` tool and reads the answer back.
## Connect Cursor
[Add to Cursor](cursor://anysphere.cursor-deeplink/mcp/install?name=spaw&config=eyJ1cmwiOiJodHRwczovL3NwYXcuY28vbWNwIiwiaGVhZGVycyI6eyJBdXRob3JpemF0aW9uIjoiQmVhcmVyICR7ZW52OlNQQVdfS0VZfSJ9fQ%3D%3D) opens Cursor with the server filled in. Cursor fills in `${env:SPAW_KEY}` from its own environment when it connects (some earlier releases sent it literally, so update Cursor if the server answers 401); export `SPAW_KEY` in your shell profile and restart Cursor after changing it. If nothing happens, Cursor may not be installed or the browser blocked the link; add the server by hand, in `~/.cursor/mcp.json` for every project or `.cursor/mcp.json` for one:
```json
{
"mcpServers": {
"spaw": {
"url": "https://spaw.co/mcp",
"headers": { "Authorization": "Bearer ${env:SPAW_KEY}" }
}
}
}
```
## Connect VS Code
[Install in VS Code](vscode:mcp/install?%7B%22name%22%3A%22spaw%22%2C%22type%22%3A%22http%22%2C%22url%22%3A%22https%3A%2F%2Fspaw.co%2Fmcp%22%2C%22headers%22%3A%7B%22Authorization%22%3A%22Bearer%20%24%7Binput%3Aspaw-key%7D%22%7D%2C%22inputs%22%3A%5B%7B%22type%22%3A%22promptString%22%2C%22id%22%3A%22spaw-key%22%2C%22description%22%3A%22Spaw%20secret%20API%20key%22%2C%22password%22%3Atrue%7D%5D%7D) opens Visual Studio Code with the server filled in. VS Code asks for the key the first time the server starts and keeps it in its secret storage, so nothing needs exporting. If nothing opens, VS Code may not be installed (VS Code Insiders takes `vscode-insiders:` links) or the browser blocked the link; add the server by hand, in your user `mcp.json` (run MCP: Open User Configuration from the Command Palette) or in `.vscode/mcp.json` for one workspace. VS Code's file lists servers under `servers`, not `mcpServers`, and the header takes the key from the input declared under `inputs`:
```json
{
"servers": {
"spaw": {
"type": "http",
"url": "https://spaw.co/mcp",
"headers": { "Authorization": "Bearer ${input:spaw-key}" }
}
},
"inputs": [
{
"type": "promptString",
"id": "spaw-key",
"description": "Spaw secret API key",
"password": true
}
]
}
```
## Connect Codex
OpenAI's Codex adds the server with one command:
```bash
codex mcp add spaw --url https://spaw.co/mcp \
--bearer-token-env-var SPAW_KEY
```
It writes this to `~/.codex/config.toml`, which the Codex CLI, the Codex IDE extension and the ChatGPT desktop app share:
```toml
[mcp_servers.spaw]
url = "https://spaw.co/mcp"
bearer_token_env_var = "SPAW_KEY"
```
Codex stores the variable's name, not the key, and reads it each time it connects. If `SPAW_KEY` is unset or empty, Codex refuses to start the server and names the variable. A desktop app started from the Dock or the Start menu sees `SPAW_KEY` only if it is set in the environment the app launches with.
## Other clients
Any other client that connects to remote MCP servers over streamable HTTP and can send a custom header works the same way: send `Authorization: Bearer` followed by your secret key to `https://spaw.co/mcp`, wherever that client keeps its configuration. ChatGPT on the web adds MCP servers as apps that sign in with OAuth or use no authentication; this server offers no OAuth, so it cannot be added there, while the ChatGPT desktop app reads the Codex configuration above. claude.ai's custom connectors expect OAuth too, except in Anthropic's request-headers beta for a limited set of organizations: choose No sign-in and add an Authorization request header whose value is Bearer followed by a secret key, which everyone in the organization then shares.
A missing or wrong key answers 401, and some clients, Cursor and VS Code among them, then try an OAuth sign-in and report an OAuth or registration error rather than a bad key, so check the key, or the variable that holds it, first. The full setup for each client is in the [MCP server documentation](/docs/mcp#connect).
## The twelve tools
| Tool | Arguments | What it does |
| --- | --- | --- |
| `verify-email` | `email` | One full verification: verdict, reason, risk score and every documented field. |
| `verify-email-batch` | `emails`, up to 50 | Compact per-address rows under the same billing; suppressed addresses answer free. |
| `domain-intelligence` | `domain` | MX records, mail provider, SPF and DMARC, registration age, disposable and free-provider flags for a whole domain. |
| `lookup-ip` | `ip`, `privacy?` | Locate and risk-score an IPv4 or IPv6 address: country and network, operator type, datacenter, Tor, VPN and relay flags, and the published risk score. Pass `me` for the calling address. |
| `lookup-ip-batch` | `ips`, up to 1,000, `privacy?` | The same answer for a whole list, in input order, each row carrying its position and the address as submitted. Five answered addresses cost one credit; reserved and unallocated ranges, unknown addresses and repeats are free. |
| `verify-phone` | `phone`, `country?`, `hlr?`, `client_ip?`, `call_window?` | Validate and enrich a number: E.164 and display forms, line type and carrier with the source that settled them, the regulator's block status where a table is installed, and the fictional, virtual and disposable flags. |
| `verify-phone-batch` | `phones`, up to 1,000, `country?`, `hlr?`, `call_window?` | The same answer for a whole list, in input order. An item is a number or an object of `phone` and `country`; suppressed numbers answer free. |
| `verify-address` | `address` or fields, `country`, `deliverability?` | Read a postal address against its own country's format: the parts, the postcode against the national directory, the delivery point named, the register's answer with its coverage, and how many companies are registered there. |
| `verify-address-batch` | `addresses`, up to 100, `country?`, `deliverability?` | The same answer for a whole list, in input order. An item is a written address or an object of the separated fields; suppressed addresses answer free. |
| `verify-entity` | `identifier`, `type`, `country?` | Read a Legal Entity Identifier or a UK company number against locally synced registers: legal name, whether the entity is still live, jurisdiction, legal form and incorporation date. |
| `check-consistency` | any of `email`, `phone`, `ip`, `address` | Run several signals in one call and name where they agree and where they do not. The per-signal answers bill as usual; the comparison on top is free. |
| `account-balance` | none | The credit balance and monthly grant. Free, useful before a batch. |
The fields and reasons are exactly the REST API's, so a `reason` of `catch_all` or an `smtp_reason` of `greylisted` means the same thing the reference pages describe. An agent that runs out of credits receives a clear tool error naming `INSUFFICIENT_CREDITS`, never a half answer — except part-way down a list, where the batch tools answer the rows they could pay for and report `meta.stopped_reason` (`insufficient_credits`, or `key_spend_cap` when the key's own daily cap is what stopped it), so nothing already billed is thrown away.
## What to do with the verdict
The useful pattern is to let the agent do the reading. Ask it to verify a list of addresses from a file, keep the ones answered `deliverable`, set aside `risky` ones with their reasons for a human to look at, and drop the `undeliverable` ones. Because the batch tool honours your suppression list, addresses that already bounced come back free and marked, and the agent can skip them without a lookup. For a domain the agent has never seen, `domain-intelligence` is the cheaper first question: it costs one credit only when the domain accepts mail, and it tells the agent whether the domain is a burner or a catch-all before any address is checked.
## Limits and safety
Requests share the API's rate limit of 5 per second per account. Use a secret key, never a publishable one: the agent runs on your behalf from a server, and publishable keys only work from a browser on an allowed domain. Keys are shown once at creation and stored hashed; rotate the agent's key from the API keys page if a configuration file leaks.
Everything a secret key was limited to at creation holds here too. A key scoped to a product family can only call that family's tools — an agent reaching for another one gets a tool error naming `KEY_SCOPE_DENIED`, which it can read, rather than a failed connection it can only report. The daily credit cap counts what the agent's tool calls bill and refuses further calls with `KEY_SPEND_CAP_REACHED` once it is spent, so an agent left running overnight cannot spend more than you decided. A key with an address allowlist is refused from anywhere else before a tool runs at all. Give the agent its own key with the narrowest scopes it needs and a cap you would not mind paying: it is the cheapest way to bound what an autonomous loop can do.
## Cost
Identical to the REST API. A fresh deliverable or risky verdict costs one credit; unverified answers, invalid input, repeats within seven days and the `spaw.test` test addresses are free, undeliverable verdicts are free within a fair-use allowance, one credit covers five IP lookups, and the balance tool is always free. Every account gets 100 free credits a month.
---
# Verify Shopify customer emails and order shipping addresses
Check new Shopify customers' emails and each order's shipping address through Zapier or Make, then tag them. Why checkout is out of reach and there is no app.
Updated: 2026-10-14
Two things are worth saying plainly before the steps. The Spaw form helper is a script for pages you control, and Shopify checkout is not one of them: checkout does not load storefront scripts, so the helper cannot check the email field there. And Spaw does not offer a Shopify app. What works is verifying customers after Shopify has created them, through a Zap or a Make scenario, and tagging them so your email tool and your staff know which addresses are real. The same route checks the shipping address on each order once it is placed. Order confirmations still go out the moment the order is placed; the verdict protects everything that comes after.
## Verify new customers
1. Create a secret API key on the API keys page of your Spaw dashboard.
2. In Zapier or Make, start with the Shopify trigger for a new customer. A new-order trigger works too if you only care about buyers rather than account sign-ups.
3. Add the verification step: a Webhooks by Zapier POST or a Make HTTP request to `https://spaw.co/api/v1/email` with the header `Authorization: Bearer sk_live_…` and this body, mapping the customer's email:
```json
{
"email": "mia@acme.com"
}
```
The [Zapier page](/integrations/zapier) and the [Make page](/integrations/make) show the step field by field. When the Spaw apps for those platforms are listed, it becomes a Verify Email action with a saved connection.
4. Add the Shopify action that updates a customer and set tags from `data.deliverable`, for example `email-deliverable`, `email-risky` or `email-undeliverable`. A customer note can carry `data.reason` and `data.did_you_mean` for staff.
5. Test by creating a customer with `deliverable@spaw.test`, a free test address that answers a fixed result and never appears in your history.
If your marketing runs on Klaviyo, the [Klaviyo page](/integrations/klaviyo) puts the verdict on the profile instead, where segments and flows can use it directly.
## Check the shipping address after the order
The same route checks the address an order will ship to. It runs after checkout, never inside it: the order is already placed by the time the answer arrives, so the result is a tag for whoever packs the parcel, not a gate the customer sees. The decision behind the tag is your account's [shipping policy](/guides/gate-a-checkout-with-the-shipping-policy) — which signals block, how high the score may go, whether a register must confirm the building — set once on the Spaw dashboard or through the API, so the Zap or scenario only reads the answer and never decides anything itself.
1. In Zapier or Make, start with the Shopify trigger for a new order.
2. Add a Webhooks by Zapier POST or a Make HTTP request to `https://spaw.co/api/v1/address` with the same `Authorization: Bearer sk_live_…` header, and map the order's shipping address into the separated parts:
| Shopify shipping address | Spaw field |
| --- | --- |
| `address1` | `address_line1` |
| `address2` | `address_line2` |
| `company` | `organization` |
| `city` | `locality` |
| `province` | `administrative_area` |
| `zip` | `postal_code` |
| `country_code` | `country` |
```json
{
"address_line1": "1 Spaw Test Street",
"address_line2": "",
"locality": "Springfield",
"administrative_area": "Illinois",
"postal_code": "99999",
"country": "US"
}
```
Send the parts only, with no `address` key: when a written line is present the API reads the line and sets `address_line1` and `address_line2` aside. Leave out the customer's name and phone number as well, because nothing in the check needs them and nothing in the answer is about who lives at the address. The country is required, and Shopify's two-letter `country_code` is exactly the form the API takes. The region is the other way round: map `province`, the name, rather than `province_code`, because Shopify's codes for some countries — `JP-13` for Tokyo, `MH` for Maharashtra — are not a form the postcode directory publishes, and a region it cannot match adds `region_mismatch` to the score.
3. Add the Shopify action that updates the order and set its tags from `data.ok_to_ship` and `data.blocked_by`: `address-ok` when `ok_to_ship` is true, and when it is false `address-hold` plus `address-hold-` followed by `blocked_by`, for example `address-hold-placeholder` for a street typed as "asdf" or `address-hold-invalid` for an address that cannot stand as written. In Zapier the split is a Paths step or two Filters on `ok_to_ship`; in Make it is a router with a filter on each route. An order note can carry `data.formatted`, the address the way its country writes it, for the person checking the label.
4. Test with the order above. `1 Spaw Test Street, Springfield, IL 99999` is one of five test addresses that answer a fixed result under your policy, cost no credits and leave no repeat marker, so the ok branch can be wired before any credit is spent.
When the Spaw apps for Zapier and Make are listed, the request step becomes the Validate Postal Address action in Zapier or the Validate a Postal Address module in Make. Both take the street as one written line, so map `address1` and `address2` into it and the city, province and postcode into their own inputs.
Three things the tag does not say. It does not confirm that post arrives: that answer is licensed and not enabled, so `deliverability_checked` comes back false and the tag reflects only the open data and your policy. It is not a verdict on the customer: a mailbox shop is a legitimate delivery point for many people, which is why `mail_drop` passes under the default policy unless you add it. And an address missing from a register is not evidence that it does not exist, except under a register whose coverage is complete. The [address lookup](/docs/api/validate-address) documents every field the answer carries.
## Clean the existing customer list
Export customers to CSV, run the email column through a bulk verification on the Spaw dashboard or with the [bulk endpoint](/docs/api/create-bulk-job), and import the result back with the verdict column turned into tags. The result CSV keeps your original columns. Undeliverable rows also join your Spaw suppression list, so future runs answer them without a probe.
## Marketing consent and the storefront
Shopify's newsletter sign-up forms on the storefront are theme pages, and the form helper can watch those inputs if your theme allows a script tag; the same applies to a contact page. Checkout is the exception. Customers who mistype at checkout are best caught by the tag flow above and a follow-up through another channel, such as SMS or a phone call, with the corrected address.
## What to do with the verdict
Use the tags in customer segments. `email-deliverable` customers get the full marketing program. For `email-risky`, read the reason: [`catch_all`](/docs/reasons/catch_all) means a business domain that accepts every address and the mailbox is unconfirmed, `role` means a shared inbox, `disposable` means a throwaway address that will not survive to the next campaign. Exclude `email-undeliverable` customers from marketing sends rather than deleting the record, because the order history matters and `data.did_you_mean` often names the address the customer meant.
## Cost
Each new customer that answers deliverable or risky spends 1 credit; unverified answers and repeats within seven days are free, and undeliverable answers are free within a fair-use allowance. Each order address that answers valid spends 1 credit; invalid addresses, the test addresses and repeats of the same address within seven days are free. Zapier and Make bill their own tasks. Bulk runs bill per processed row and verify duplicates once.
---
# Verify emails and phone numbers in HubSpot with Spaw
Verify HubSpot contacts' emails and phone numbers with a workflow webhook action that calls the Spaw API and writes answers to properties, or through Zapier.
Updated: 2026-09-27
HubSpot workflows can call an external URL and use the answer in later actions, which is all a verification step needs. The webhook action is part of Data Hub Professional and Enterprise; on other tiers, the same result comes from a Zap or a Make scenario that triggers on a new contact. No HubSpot app is required in either case, and Spaw does not ask for access to your portal. The same kind of workflow validates the contact's phone number, as the phone section below shows.
## Verify new contacts with a workflow webhook
1. Create a secret API key on the API keys page of your Spaw dashboard.
2. Create three contact properties to hold the answer: a single-line text property for the verdict, one for the reason, and a number property for the risk score. Internal names such as `spaw_verdict`, `spaw_reason` and `spaw_risk_score` keep them easy to find.
3. Create a contact-based workflow whose enrollment trigger is a contact whose email is known, re-enrolling when the email changes.
4. Add the webhook action. Set the method to POST and the URL to `https://spaw.co/api/v1/email`.
5. Choose API-key authentication for the request. The header name is `Authorization` and the value is `Bearer sk_live_…`; HubSpot stores the value as a secret so it never appears in the workflow itself.
6. Customize the request body so it contains only the contact's email under the key `email`:
```json
{
"email": "mia@acme.com"
}
```
7. Turn on the option that includes the response as data. After the action runs once in a test, the response fields become available to later actions under `data`, for example `data.deliverable`, `data.reason`, `data.risk_score` and `data.did_you_mean`.
8. Add a property-setting action that copies `data.deliverable` into your verdict property, and two more for the reason and the score.
9. Add a branch on the verdict property. Test the workflow by enrolling a contact whose email is `deliverable@spaw.test`; it answers a canonical result, costs nothing and never appears in your Spaw history.
The full list of fields the webhook receives is on the [single-address endpoint page](/docs/api/verify-email). Everything in it can be written to a property the same way.
## Other tiers: Zapier or Make
Without the webhook action, use a new-contact trigger from HubSpot in Zapier or Make, a Spaw verification step (Webhooks by Zapier or Make's HTTP module today, the Spaw apps once they are listed), and an update-contact action that writes the three properties. The [Zapier page](/integrations/zapier) has the exact request. The workflow branches described below then run on the properties as usual.
## Existing contacts
For the contacts already in the portal, export the emails, run them through a bulk verification on the Spaw dashboard or the API, and import the result CSV back into HubSpot with the verdict column mapped to the property. The [list cleaning checklist](/guides/email-list-cleaning-checklist) covers what to do with each verdict on an old list.
## What to do with the verdict
Branch the workflow on the verdict property. `deliverable` contacts go on to marketing emails and sequences. For `risky` contacts, look at the reason: [`catch_all`](/docs/reasons/catch_all) means the domain accepts everything and the mailbox is unconfirmed, [`role`](/docs/reasons/role) means a shared inbox such as info@; both are usually worth keeping out of cold sequences but fine for transactional mail. `undeliverable` contacts should be set to not marketing-eligible or handed to a review list, and when `data.did_you_mean` is filled, an internal notification with the suggested address is the fastest way to recover the lead.
## Phone numbers
A second contact-based workflow validates the phone number with the same webhook action. Its enrollment trigger is a contact whose phone number is known, re-enrolling when the number changes, so an edited number is checked again.
1. Create four single-line text contact properties for the answer, for example `spaw_phone_e164`, `spaw_line_type`, `spaw_ok_to_send` and `spaw_blocked_by`.
2. Add the webhook action with the method POST, the URL `https://spaw.co/api/v1/phone` and the same API-key authentication as the email step.
3. Customize the request body so it carries the contact's Phone Number under `phone` and its Country/Region Code under `country`. Nothing else about the contact is needed.
```json
{
"phone": "(202) 555-0100",
"country": "US"
}
```
4. Turn on the option that includes the response as data, and add property-setting actions that copy `data.e164`, `data.line_type`, `data.ok_to_send` and `data.blocked_by` into the four properties. `data.e164` is the form to store and dedupe by; it is also kept for an invalid number in a block nobody holds, so read it together with `spaw_ok_to_send`, which is false for every invalid number.
5. Branch on `spaw_ok_to_send` before any step that texts the contact. Contacts where it is false go to a review list with `spaw_blocked_by` as the reason: `invalid` for a number that is not valid, `disposable` for one published on an SMS-receiving website, and so on.
6. Test the workflow with +1 202 555 0100 and +1 202 555 0103, two documented test numbers that cost nothing: the first answers a valid mobile with `ok_to_send` true, the second `ok_to_send` false with `blocked_by` `disposable`, under the default SMS policy.
Map Country/Region Code, not Country/Region. HubSpot describes Country/Region Code as "the two letter country code where the contact is located (e.g., US)" (HubSpot's list of default contact properties, read 27 September 2026), which is the kind of code the API expects: ISO 3166-1 alpha-2. HubSpot describes Country/Region only as the contact's country of residence, and it can hold a name such as "United States"; a name, or `UK` (the United Kingdom's code is `GB`), is refused with a `422` and the error code `VALIDATION_FAILED`. An empty country is read as none. A number stored with its + and calling code needs no country at all; a national number sent without one answers `valid: false` with the reason `missing_country`, free.
`ok_to_send` is your account's [SMS policy](/guides/gate-sms-verification-codes-with-the-sms-policy) applied to the answer: the countries and line types you send to, the signals you refuse, a ceiling on the score and, by default, the numbers your suppression list holds as opted out, set once on the account so every workflow, Zap, batch and bulk run applies the same rules. It is a fraud and deliverability decision, not consent and not a do-not-call scrub. `line_type` comes from the numbering plan or, for a North American number, from the holder of its block in the regulator's table; it describes how the range was allocated, never the network that serves the number after a port, and an ordinary US number answers `fixed_line_or_mobile` unless its block holder settles the type ([why](/guides/why-us-numbers-cannot-be-split-into-mobile-and-landline)). Leave the complaint counts (`complaints_90d`, `complaints_last_at`) off contact records: they count the complaints consumers sent the FTC naming a number as the caller, which is not the Do Not Call Registry and says nothing about whether you may call or text the number, and the reports are unverified, with caller ID often spoofed. Every field is on the [phone endpoint page](/docs/api/validate-phone).
On tiers without the webhook action, run the phone check as a second Zap or scenario on the same new-contact trigger: a filter that continues only when the phone number is filled (an empty `phone` is refused with a `422`), a step that posts the same body to `https://spaw.co/api/v1/phone`, and an update-contact action of its own that writes the four properties. Kept apart from the email flow, a contact without a phone number still gets its email verdict. The [Zapier page](/integrations/zapier) covers the request.
## Cost
Each enrollment that answers deliverable or risky spends 1 credit; unverified answers and repeats of the same address within seven days are free, and undeliverable answers are free within a fair-use allowance. A re-enrollment after an email edit is a new address and bills normally. A phone lookup costs 1 credit when the number is valid; invalid numbers, unassigned blocks and repeats of the same number within seven days are free. Email lookups are limited to 5 requests per second and phone lookups to 50, in separate buckets; HubSpot spreads workflow actions out enough that this rarely matters, but a bulk enrollment of thousands of contacts should be done through a bulk run instead.
---
# Verify emails for Mailchimp with Spaw
Verify new Mailchimp subscribers through Zapier or Make, tag them by verdict, clean an existing audience with a bulk run, and feed bounces back as suppressions.
Updated: 2026-09-03
A Mailchimp audience is only as good as the addresses that enter it, and a high bounce rate on a campaign can get sending paused. Spaw does not connect to your Mailchimp account directly. Instead, a Zap or a Make scenario verifies each new subscriber and tags the record, and a bulk run cleans what is already there. Both keep the decision in Mailchimp, where segments and automations can use it.
## Verify new subscribers
1. Create a secret API key on the API keys page of your Spaw dashboard.
2. In Zapier or Make, start with the Mailchimp trigger for a new subscriber in the audience you care about.
3. Add the verification step: a Webhooks by Zapier POST or a Make HTTP request to `https://spaw.co/api/v1/email` with the header `Authorization: Bearer sk_live_…` and this body, mapping the subscriber's address:
```json
{
"email": "mia@acme.com"
}
```
The [Zapier page](/integrations/zapier) and the [Make page](/integrations/make) show every field of the step. When the Spaw apps for those platforms are listed, the step becomes a Verify Email action with a saved connection.
4. Add the Mailchimp action that adds or updates a subscriber, match on the same email, and set a tag from the verdict: for example `verified` when `data.deliverable` is `deliverable`, `risky` when it is `risky`, and `undeliverable` otherwise. A merge field can hold the reason if you want it visible on the profile.
5. Test with `deliverable@spaw.test`, which answers a fixed result at no cost and never appears in your history.
Mailchimp double opt-in already keeps most typos out of an audience. Verification adds what confirmation cannot: it flags throwaway domains, role inboxes and catch-all domains before the first campaign, and it catches addresses that confirmed once and died later.
## Clean an existing audience
Export the audience to CSV, run the email column through a bulk verification on the Spaw dashboard or the [bulk endpoint](/docs/api/create-bulk-job), and import the result CSV back with the verdict column mapped to a merge field or used to build tags on import. The result keeps your original columns. Rows that answer undeliverable also land on your Spaw suppression list, so future bulk runs answer them for free.
## Send bounces back to Spaw
Mailchimp cleans hard bounces from the audience on its own. Export those cleaned addresses now and then and post them to the [suppression import endpoint](/docs/api/add-suppressions) as `{ "emails": [...] }`, up to 10,000 per request. Every later batch or bulk run then answers them from the list at no cost, which keeps them from re-entering through another form.
## What to do with the verdict
Segment on the tags. Send campaigns to `verified`, and to `risky` subscribers only after a look at the reason: `catch_all` domains accept every address and are often real business inboxes, `role` inboxes such as newsletter@ rarely open campaigns, and `disposable` domains are gone by the time you send. Do not send to `undeliverable`; archive or unsubscribe those, and when the response's `data.did_you_mean` is filled, an internal notification lets someone fix the record before it is lost. The [bounce rate guide](/guides/email-bounce-rate-benchmarks) shows what these groups do to deliverability if they are left in.
## Cost
Each new subscriber that answers deliverable or risky spends 1 credit; unverified answers and repeats within seven days are free, and undeliverable answers are free within a fair-use allowance. Zapier and Make bill their own tasks. A bulk run bills per processed row and verifies duplicates once, and suppression imports are free.
---
# Verify emails on WordPress and WooCommerce forms with Spaw
The Spaw WordPress plugin checks form emails as visitors type, can reject addresses that cannot receive mail, and can block Tor, VPN or datacenter visitors.
Updated: 2026-09-26
The Spaw Email Verification plugin connects a WordPress site to Spaw in two layers. In the browser it loads the Spaw form helper on the comment form, the registration form and the WooCommerce classic checkout, so a visitor who mistypes an address sees a "Did you mean …?" correction and a notice when the address cannot receive mail. On the server, with a secret key, it verifies the address again when the form is submitted and can refuse addresses Spaw reports as undeliverable, and it can look up the address the submission came from to refuse Tor, VPN or datacenter visitors. Nothing is sent to Spaw until you save a key.
The plugin has been submitted to the WordPress.org directory. Until the listing is live, install it from the plugin zip: ask support@spaw.co for the current build.
## Install and configure
1. Create a free account at https://spaw.co and open the API keys page.
2. Create a publishable key locked to your site's domain. A publishable key is safe in page source: it can only run single lookups through the four browser endpoints (email, phone, IP and postal address), only from the domains you list, and this plugin uses it for email lookups. Add a Cloudflare Turnstile site key and secret to the key if your forms are open to the public, so bots cannot spend your credits.
3. In WordPress, go to Plugins, choose Add New, then Upload Plugin, and upload the zip. Activate it.
4. Open Settings, then Spaw. Paste the publishable key and tick the forms to verify: comments, registration, and WooCommerce checkout if WooCommerce is active. The "Extra email inputs" field takes a CSS selector for any other inputs, for example a newsletter form or the WooCommerce block checkout's email field.
5. To also reject undeliverable addresses on the server, paste a secret key and tick "Block undeliverable addresses". Prefer the publishable key alone unless you need blocking; the secret key is stored in the WordPress options table like any other plugin setting.
6. To refuse submissions from Tor exits, known VPN exits or datacenter ranges, tick the switches under "Visitor address check". Each is independent and off by default.
## What happens on each form
The browser helper runs when a visitor leaves the email field. It never blocks a submission: it only renders a hint under the field, and every result is also emitted as a `spaw:result` event on the input in case your theme wants to do more.
The server check runs on `preprocess_comment` for comments, `registration_errors` for registrations, and `woocommerce_checkout_process` for the classic checkout. It posts the address to `POST /api/v1/email` with a five-second timeout, caches the verdict for seven days in a transient keyed by the hashed address, and rejects only verdicts on the blocked list. By default that list is `undeliverable` alone. If the API cannot be reached, answers an error, or is slow, the submission goes through as normal.
## Block Tor, VPN and datacenter visitors
With the same secret key, the plugin can look up the address a comment, registration or checkout comes from through the [IP lookup endpoint](/docs/api/lookup-ip) and refuse the submission when a flag you ticked is true: `is_tor` for Tor exit nodes, `is_vpn` for known VPN exits, `is_datacenter` for datacenter and cloud ranges, which is traffic that is not a person on a home connection. Visitors on iCloud Private Relay are never blocked, because relay users are ordinary Apple customers even though the aggregate lists mark the relay's egress as hosting. Answers are cached for six hours in a transient keyed by the hashed address, and a lookup that fails or is slow never blocks anyone. Each answered lookup costs one credit; reserved or private addresses and repeats within seven days are free.
The plugin reads `REMOTE_ADDR`. Behind a proxy or CDN that is the proxy's address, so map the real client address with the `spaw_visitor_ip` filter, for example from the header your CDN sets. Two more filters cover the rest: `spaw_block_ip` receives the matched flag and the answer and can veto a block, and `spaw_blocked_ip_message` rewrites the rejection text.
## Adjust the behaviour with filters
Three filters cover the usual customisations. This example also refuses risky addresses, which covers disposable domains, role inboxes and catch-all domains, and rewrites the rejection message:
```php
add_filter('spaw_blocked_verdicts', function (array $verdicts, string $email): array {
return ['undeliverable', 'risky'];
}, 10, 2);
add_filter('spaw_blocked_message', function (string $message): string {
return 'Please use a work email address that can receive mail.';
});
add_filter('spaw_email_selectors', function (array $selectors): array {
$selectors[] = '.newsletter-form input[type="email"]';
return $selectors;
});
```
Logged-in users are never checked on the comment form, because their address was verified when their account was created.
## What to do with the verdict
Blocking `undeliverable` is safe: the address has invalid syntax, a domain with no working mail servers, or a mailbox the receiving server rejected. Blocking `risky` is a policy choice. It stops burner signups and shared inboxes, but it also refuses catch-all domains, which many companies use for perfectly good addresses. Read the `reason` values before widening the list.
## Cost
Each answered email lookup costs one credit from your Spaw balance, and one credit covers five answered visitor address lookups. Unverified answers cost nothing, undeliverable answers cost nothing within a fair-use allowance, a repeat of the same address within seven days costs nothing, and the plugin's own seven-day cache means a returning visitor is not looked up twice. Every account gets 100 free credits a month; packs start at $9 for 1,000 credits.
---
# Verify emails, phones, IPs and postal addresses in Google Sheets
Paste-in Apps Script: =SPAW_VERIFY(A2) for an email verdict, =SPAW_PHONE(D2), =SPAW_IP(C2) for an IP's risk and =SPAW_ADDRESS(F2, "GB") for a postal address.
Updated: 2026-10-14
A column of addresses becomes a column of verdicts without leaving the sheet. The script below adds six custom functions: `SPAW_VERIFY` checks one cell or a whole range against the Spaw API and returns any field of an email verification, `SPAW_DOMAIN` inspects a domain's mail setup, `SPAW_PHONE` validates and enriches phone numbers, `SPAW_IP` locates and risk-scores IP addresses, `SPAW_ADDRESS` reads and checks postal addresses, and `SPAW_ENTITY` reads a business identifier against company registers. It is plain Apps Script with no dependencies, it asks for no authorization because it only defines custom functions, and no message is ever sent to the addresses it checks.
## Install
1. Open the spreadsheet and choose Extensions, then Apps Script.
2. Replace the contents of `Code.gs` with the script at the bottom of this page and save the project.
3. Create a secret API key on the API keys page of your dashboard (https://spaw.co/api-keys). Paste it into the `SPAW_API_KEY` constant at the top of the script, or store it as the script property `SPAW_API_KEY` under Project Settings. The property wins when both are set.
4. Back in the sheet, type `=SPAW_VERIFY("deliverable@spaw.test")`. It answers `deliverable` and costs nothing, because the six `spaw.test` addresses are free test fixtures.
Anyone with edit access to the spreadsheet can open the script and read the key. Create a key just for this sheet, and revoke it on the API keys page when you want to rotate it. Publishable `pk_` keys are browser keys and do not work from Apps Script.
## Formulas
| Formula | Answer |
| --- | --- |
| `=SPAW_VERIFY(A2)` | `deliverable`, `risky` or `undeliverable` |
| `=SPAW_VERIFY(A2, "reason")` | why the verdict is not deliverable, such as `role` or `catch_all` |
| `=SPAW_VERIFY(A2, "deliverable", "refresh")` | a fresh answer past the seven-day repeat window, billed as fresh when answered; `max_age=86400` and `timeout=10` work too, comma-separated |
| `=SPAW_VERIFY(A2, "deliverable,reason,risk_score")` | one row with a column per field |
| `=SPAW_VERIFY(A2:A500, "deliverable,reason")` | one row per address, sent to the API 50 at a time |
| `=SPAW_VERIFY(A2, "did_you_mean")` | a suggested correction for a typo, or an empty cell |
| `=SPAW_DOMAIN(B2)` | whether the domain can receive mail |
| `=SPAW_DOMAIN("acme.com", "mx_provider,has_spf,dmarc_policy")` | who runs the domain's mail and what it publishes |
| `=SPAW_PHONE(D2)` | whether a phone number is valid: `TRUE` or `FALSE` |
| `=SPAW_PHONE(D2, "US", "e164,line_type,carrier")` | a number written without a calling code, parsed for the country, with the normalized form and what the numbering plan and the regulator's table say |
| `=SPAW_PHONE(D2:D500, "", "valid,reason,is_disposable")` | one row per number, sent to the API 1,000 at a time |
| `=SPAW_PHONE(D2:E500, "", "e164,carrier")` | the same with a country per row in the second column, which wins over the second argument |
| `=SPAW_IP(C2)` | the risk level of an IP address: `low`, `medium` or `high` |
| `=SPAW_IP(C2, "country,city,is_vpn,privacy_service")` | where the address is, and the VPN or relay service when one is named |
| `=SPAW_IP(C2:C2000, "risk_level,is_datacenter,hosting_provider")` | one row per address, sent to the API 1,000 at a time |
| `=SPAW_IP(C2, "risk_level", "privacy")` | a lookup Spaw keeps nothing about, not even the repeat marker |
| `=SPAW_ADDRESS(F2, "GB")` | whether a postal address stands up in its country: `TRUE` or `FALSE` |
| `=SPAW_ADDRESS(F2, "GB", "formatted,postcode_status,address_type")` | the tidied address, whether its postcode is still live, and what kind of delivery point it is |
| `=SPAW_ADDRESS(F2:F500, "GB", "valid,reason,risk_level")` | one row per address, sent to the API 100 at a time |
| `=SPAW_ADDRESS(F2:G500, "", "valid,country")` | the same with a country per row in the second column, which wins over the second argument |
| `=SPAW_ADDRESS(F2, "GB", "confirmation,changes,street_suggestion")` | what stood behind each part and what the parser set aside, as JSON, and the street the register most likely meant |
| `=SPAW_ENTITY(H2, "lei")` | whether a register carries a business identifier: `TRUE` or `FALSE` |
| `=SPAW_ENTITY(H2, "lei", "", "name,status,registration_status")` | the legal name, whether the entity is still live, and whether its LEI registration is current |
| `=SPAW_ENTITY(H2, "company_number", "GB", "name,status_detail")` | a company number read against the register of the country given |
| `=SPAW_ENTITY(H2:H500, "lei", "", "found,name,status")` | one row per identifier, sent to the API 500 at a time |
The field argument (the second for `SPAW_VERIFY`, `SPAW_DOMAIN` and `SPAW_IP`, the third for `SPAW_PHONE` and `SPAW_ADDRESS`, whose second is the country, the fourth for `SPAW_ENTITY`) accepts any field of the [email result](/docs/api/verify-email), the [domain result](/docs/api/domain-intelligence), the [phone result](/docs/api/validate-phone), the [IP result](/docs/api/lookup-ip), the [address result](/docs/api/validate-address) or the [business identifier result](/docs/api/verify-entity), and a comma-separated list returns the fields side by side. Blank cells and cells holding anything but text answer an empty cell and never reach the API, except a whole number, which `SPAW_PHONE` and `SPAW_ENTITY` accept because a sheet stores an unformatted phone or company number that way. Field names are checked before any request, so a typo there shows an error instead of spending a credit. `SPAW_DOMAIN` also accepts an email address and uses its domain.
## What to do with the verdict
Filter on the first column: `deliverable` addresses go out, `undeliverable` addresses are dropped, and `risky` addresses deserve a look at `reason` and `risk_score` before you decide. A `catch_all` domain accepts mail for every address, so the mailbox could not be confirmed; `role` marks a shared inbox such as `info@`; `disposable` marks a burner. The `did_you_mean` field catches `gmail.con` and its relatives, so add a column for it and fix the typos before sending. The [list cleaning checklist](/guides/email-list-cleaning-checklist) walks through the whole pass, and every reason has its own page under [verdict reasons](/docs/reasons).
## Phone numbers
`SPAW_PHONE` takes the number a form collected, in any notation, and answers whether it is valid, with a typed `reason` when it is not: `missing_country` for a national number without the optional second argument, `unassigned_block` for a number in a block the regulator has not allocated, `emergency_number` or `short_number` for numbers that are not subscriber numbers. The other fields carry the E.164 form to store and dedupe by, the national and international forms, the country and its facts, `line_type` and `carrier` with the source that settled them (`numbering_plan`, or `block_allocation` from the regulators' tables, which say who the numbers were allocated to and never who serves them after a port), `block_status`, the `is_fictional`, `is_virtual` and `is_disposable` flags, and a published `risk_score` with its `risk_signals`. A plain numeric cell is accepted as a number. A range is sent to the [batch endpoint](/docs/api/lookup-phone-batch) 1,000 numbers per request, and a second column in the range gives each row its own country, which wins over the second argument. Numbers on your suppression list are answered free. To decide which rows get a code, ask for the decision rather than the evidence: `=SPAW_PHONE(B2:B500, "GB", "ok_to_send,blocked_by")` puts your account's [SMS policy](/guides/gate-sms-verification-codes-with-the-sms-policy) beside every number, `TRUE` or `FALSE`, with the rule that refused a row in the second column (`invalid`, `opted_out` for a single cell holding a number your suppression list holds as opted out, `country`, `line_type`, a signal such as `disposable`, `risk_score`, or `suppressed` for a number on your suppression list: a range is sent as a batch and honours that list, while a single cell is one lookup and never does). The policy is set once on the account, not in the sheet, so a sheet, a batch call and a bulk run apply the same rules. It is a fraud and deliverability decision, not consent and not a do-not-call scrub. A [phone lookup](/docs/api/validate-phone) costs 1 credit when the number is valid; invalid numbers, unassigned blocks and repeats within seven days are free. The script deliberately has no function for reporting SMS outcomes: custom functions must stay free of side effects, because Sheets recalculates them whenever it likes, so a report would be sent again and again; use the [feedback endpoint](/docs/api/report-phone-feedback) or a connector for that.
## IP addresses
`SPAW_IP` takes the address a signup, order or log line came from and answers where it is (country, city, timezone, currency), what network it belongs to and what kind of operator runs it, and tri-state flags for datacenter and cloud ranges, Tor exits, VPN exits and iCloud Private Relay, with `hosting_provider` and `privacy_service` naming the provider when its own published list contains the address. `is_anonymous` is true when any of Tor, VPN or relay is, `risk_score` is a published sum and `risk_signals` lists the weights applied, so a filter on the risk level needs no interpretation. A range is sent 1,000 addresses per request. The optional third argument `privacy` asks Spaw to keep nothing about the lookup, not even the seven-day repeat marker, and skips the script cache, so every such lookup is fresh and billed. One credit covers five [IP lookups](/docs/api/lookup-ip) that resolve a location, a network or a positive flag: the lookup that opens a block reports `credits_used` 1, the next four 0, and `meta.ip_lookups_prepaid` counts what is left. Reserved or private ranges, unallocated space, unknown addresses and repeats within seven days are free.
## Postal addresses
`SPAW_ADDRESS` takes the address a form or an order collected, on one line, and reads it the way its country writes addresses. The second argument is the two-letter country, and it is not optional: it decides which format the address is read under, which parts that country requires and which postcode directory answers, and it is never guessed. A single cell without one is refused before any request; a row inside a range without one comes back `valid` `FALSE` with reason `missing_country`, free. A range may carry a country per row in its second column, which wins over the argument, and every row is sent with its own country, 100 addresses per request.
The answer carries the components and the `formatted` address, whether the postcode matches the country's pattern and whether the directory still lists it (`postcode_status`), whether the town written on the address is the town the directory publishes for that postcode, what kind of delivery point it is (`address_type`: street, PO box, private mailbox, general delivery, rural route, military, parcel locker, bulk mail), how close a national address register comes to it (`match_level` against the `register` named), the postcode centroid, how many companies are registered there, and a published `risk_score` with the `risk_signals` behind it.
To decide which rows ship, ask for the decision rather than the evidence: `=SPAW_ADDRESS(F2:F500, "GB", "ok_to_ship,blocked_by")` puts your account's [shipping policy](/guides/gate-a-checkout-with-the-shipping-policy) beside every address, `TRUE` or `FALSE`, with the rule that refused a row in the second column (`invalid`, a signal such as `placeholder`, `risk_score` or `unconfirmed`, or `suppressed` for an address on your suppression list: a range is sent as a batch and honours that list, while a single cell is one lookup and never does). The policy is set once on the account, not in the sheet, so a sheet, a batch call and a bulk run apply the same rules.
An empty cell means the signal was not evaluated, never "no". `postcode_status` and the coordinates are empty where no postcode directory covers the country. `exists` is `TRUE` only on a premise match and `FALSE` only under a register whose coverage is complete: a register whose coverage is partial, such as the US National Address Database or the French address base, cannot make an address's absence mean anything, so it answers empty. `is_mail_drop` is a heuristic and not a postal service's own agency flag: it fires when a mailbox operator's own published branch list names the address, or when so many companies are registered there that it is one in practice, and `mail_drop_source` says which of the two.
Two answers are licensed and not enabled: whether post actually arrives (`deliverability_checked` comes back `FALSE`, and `is_deliverable`, `is_cmra`, `is_vacant` and `is_residential` stay empty) and the list of addresses inside a postcode, which has no formula here at all. A valid address costs 1 credit; invalid addresses and repeats within seven days are free.
## Business identifiers
`SPAW_ENTITY` reads the identifier a supplier, a customer or a counterparty gave you against the registers Spaw syncs. The second argument says which register it belongs to, `lei` or `company_number`, and it is never guessed: reading a company number as a Legal Entity Identifier would answer a confident "no". A company number also needs the country of its register as the third argument, because company numbers are only unique inside their own register; a LEI is global by construction and the country is not sent with one. `GB`, the UK register, is the one installed today, and another country answers `unsupported_register`, free. A column is read as one type and sent to the [batch endpoint](/docs/api/lookup-entity-batch) 500 identifiers per request, so put each register in its own column.
The answer carries the legal name, whether the entity is still live (`status`, with the register's own wording in `status_detail`, which separates a strike-off from a liquidation), the country and jurisdiction it was formed in, its category and legal form, when it was registered and when the register last updated the record, and for a LEI `checksum_valid`, `registration_status` and `next_renewal_at`. Where a LEI names its entry in a national register Spaw also holds, `flags` names what the two disagree about and `agreements` quotes both sides, so a disagreement is something you can read rather than take on trust. `dissolved_in_national_register` is the strongest signal the product carries: a dissolved company no longer exists as a legal person and cannot enter a contract.
The one caveat, and it matters more than anything else here: the UK company file is a snapshot of the **live** register, so a company that is struck off is **removed from the file** rather than marked dissolved — the statuses that file carries are `Active`, `Active - Proposal to Strike off`, `Liquidation`, `In Administration` and similar, and never `Dissolved`. A UK dissolution therefore reaches you as `found` `false` with `reason` `unknown_identifier` rather than as that flag, which never fires on the only national register installed today. Check `found` before you trust the flag, and read `status_detail` for the earlier warning: `Active - Proposal to Strike off` folds to a `status` of `active`, because the company is still on the register. A LEI lookup of the same company is quieter still: GLEIF keeps its record, so `found` stays `true` and no flag fires. What moves is `agreements`, which comes back empty while `other_register` still names `uk-companies-house` — the counterpart row is gone. That pair is the signal on this path, and `lei_registration_not_current` follows later, when the lapsed registration is not renewed. A [monitored list](/docs/business-verification) turns the disappearance into an alert of its own, `left_register`.
`checksum_valid` is a typing check and nothing more — it catches a mistyped or transposed character, while a perfectly formed LEI can never have been issued, which is what the register lookup answers. `registration_status` is about the identifier rather than the entity: a LEI must be reconfirmed each year and a great many are not, so `LAPSED` means the reference data has not been rechecked recently, not that the company has gone.
No registered address is returned, ever, and none is stored: a registered address is very often somebody's home, and an identifier alone does not say whether a company or a sole trader stands behind it. Both registers are bulk files held on the Spaw server, so no third party sits in the request path. The LEI reference data is published under a CC0 public-domain dedication and refreshed daily; the UK company data is used under the Open Government Licence v3.0 and republished monthly, so an answer can be up to five weeks behind the live register and `sources` carries the date of the copy it was read from. Spaw is not affiliated with, endorsed by or connected to either publisher, and an answer here is Spaw's rather than theirs. A [business lookup](/docs/api/verify-entity) costs 1 credit when a register carried the identifier; unknown, malformed and unsupported identifiers and repeats within seven days are free.
## Cost and limits
Sheets recalculates custom functions more often than you might expect, so the script keeps every answer in the Apps Script cache for six hours, keyed by the lowercased address, and serves repeats from there. Beyond that, the API answers a repeat of a charged lookup free for seven days. A fresh `deliverable` or `risky` verdict costs 1 credit; an `undeliverable` answer is free.
A range is sent [50 addresses per request](/docs/api/verify-email-batch). Each key may make 5 requests per second, and a column of single-cell formulas recalculates all at once, so prefer one range formula per column; the script retries once after a rate-limit answer. If the balance runs out part-way through a range, the API answers the rows it could pay for, the script caches them, and the cell shows an error saying so. Top up on the Billing page and recalculate: the cached rows are not fetched or charged again. A custom function has 30 seconds to run, which fits a few hundred addresses per formula; for lists of thousands, use the [bulk endpoint](/docs/api/create-bulk-job) instead.
## The script
Copy all of it into `Code.gs`. The same file, with its tests, lives in the Spaw integrations repository.
```javascript
/**
* Spaw email verification, phone validation, IP intelligence, postal
* address and business identifier checks for Google Sheets.
*
* Custom functions that verify email addresses, inspect domains, validate
* phone numbers, look up IP addresses, read postal addresses and check
* business identifiers with the Spaw API (https://spaw.co) straight from a
* formula:
*
* =SPAW_VERIFY(A2) the verdict for one address
* =SPAW_VERIFY(A2, "reason") one field of the answer
* =SPAW_VERIFY(A2:A500, "deliverable,reason") a range, several columns
* =SPAW_VERIFY(A2, "deliverable", "refresh") a fresh, billable answer
* =SPAW_DOMAIN("acme.com", "mx_provider") domain-level signals
* =SPAW_PHONE(D2) whether a phone number is valid
* =SPAW_PHONE(D2, "US", "e164,line_type") a national number, several columns
* =SPAW_PHONE(D2:E500, "", "valid,reason") a range with a country column, 1,000 per request
* =SPAW_IP(C2) the risk level of an IP address
* =SPAW_IP(C2:C500, "country,is_vpn,risk_score") a range of addresses, several columns
* =SPAW_ADDRESS(F2, "GB") whether a postal address stands up
* =SPAW_ADDRESS(F2, "GB", "formatted,postcode_status") the tidied address and its postcode
* =SPAW_ADDRESS(F2:G500, "", "valid,reason") a range with a country column, 100 per request
* =SPAW_ENTITY(H2, "lei") whether a register carries a business identifier
* =SPAW_ENTITY(H2, "company_number", "GB", "name,status") a company number and its register
* =SPAW_ENTITY(H2:H500, "lei", "", "status,flags") a column of identifiers, 500 per request
*
* Install
* 1. In your spreadsheet open Extensions > Apps Script.
* 2. Replace the contents of Code.gs with this file and save the project.
* 3. Create a secret API key at https://spaw.co/api-keys and paste it into
* SPAW_API_KEY below, or store it as the script property SPAW_API_KEY
* (Project Settings > Script Properties). The script property wins when
* both are set.
* 4. Back in the sheet, try =SPAW_VERIFY("deliverable@spaw.test").
*
* Anyone with edit access to the spreadsheet can open the script and read the
* key, so create a key just for this sheet and revoke it on the API keys page
* to rotate it. Publishable pk_ keys are browser keys and do not work here.
*
* Every answered lookup (a deliverable or risky verdict) costs 1 credit,
* and one credit covers five IP lookups; repeats within seven days and the
* spaw.test test addresses are free, and undeliverable answers are free
* within a fair-use allowance. A mailbox the handshake could not confirm
* answers "risky" with reason "unverified", free. Answers are kept in the script cache for
* six hours so a recalculation does not call the API again. A range is
* verified 50 addresses per request, which is the fastest and cheapest way to
* check a column.
*
* The optional third argument asks for freshness: "refresh" sets the earlier
* answer aside and bills the lookup as fresh when answered, "max_age=86400"
* does so only when the earlier answer is older than that many seconds, and
* "timeout=10" caps the mailbox handshake at that many seconds. Combine them
* with commas: "max_age=3600,timeout=10".
*
* SPAW_PHONE validates and enriches phone numbers: the E.164 and display
* forms, the country and its facts, the line type and carrier with the source
* that settled them (the numbering plan, or the block the regulator allocated,
* which is never port-aware), the block status, the fictional, virtual and
* disposable flags, and a published risk score. Pass a two-letter country as
* the second argument for numbers written without a calling code, or give the
* range a second column holding a country per row. A valid number costs 1
* credit; invalid numbers, unassigned blocks and repeats within seven days are
* free. A range is sent 1,000 numbers per request; numbers on your suppression
* list are answered free. The script never reports SMS outcomes: custom
* functions must stay free of side effects, so use the API or a connector for
* that.
*
* SPAW_IP locates and risk-scores IPv4 and IPv6 addresses: country, city,
* timezone and currency, the network and its operator type, tri-state flags
* for datacenter, Tor, VPN and iCloud Private Relay with the provider named,
* and a published risk score. One credit covers five lookups that resolve a
* location, a network or a positive flag; reserved and unallocated ranges,
* unknown addresses and repeats within seven days are free. A range is sent 1,000 addresses per
* request. Its optional third argument "privacy" keeps nothing about the
* lookup on Spaw's side (not even the repeat marker) and skips the script
* cache, so every such lookup is fresh and billed.
*
* SPAW_ADDRESS reads a postal address the way its country writes them and
* checks it against open data: the components and the formatted address,
* whether the postcode exists and is still live, its centroid, whether the
* town matches it, what kind of delivery point it is, how close a national
* address register comes to it, how many companies are registered there,
* and a published risk score. The country decides which format the address
* is read under and is never guessed: pass a two-letter code as the second
* argument, or give the range a second column holding one per row. A single
* cell without a country is refused before any request; a row in a range
* without one is answered "missing_country", free. A range is sent 100
* addresses per request, each row carrying its own country. A valid address
* costs 1 credit; invalid addresses and repeats within seven days are free.
*
* Two address answers are licensed and not enabled: whether post actually
* arrives (deliverability_checked comes back false, and is_deliverable,
* is_cmra, is_vacant and is_residential stay empty) and the list of
* addresses inside a postcode, which has no function here at all. An empty
* cell always means the signal was not evaluated, never "no".
*
* SPAW_ENTITY reads a business identifier against the registers Spaw syncs:
* a Legal Entity Identifier, or a company number against the register of the
* country named. The second argument says which of the two the identifier is
* and is never guessed; a company number also needs the country of its
* register as the third argument, because company numbers are only unique
* inside their own register, while a LEI is global by construction and
* ignores it. GB is the register installed today; another country answers
* "unsupported_register", free. Both registers are bulk files held on the
* Spaw server, so no third party sits in the request path. The answer gives
* the legal name, whether the entity is still live and the register's own
* wording for it, where it was formed, and for a LEI its check digit and
* registration status, which is about the identifier rather than the entity:
* a LEI must be reconfirmed each year and a great many are not. Where a LEI
* names its entry in a national register Spaw also holds, both records are
* compared and both sides are quoted in "agreements". A range goes 500
* identifiers per request, all read as the same type. 1 credit when a
* register answers; unknown, malformed and unsupported identifiers and
* repeats within seven days are free.
*
* No registered address is ever returned, by design: a registered address is
* very often somebody's home, and an identifier alone does not say whether a
* company or a sole trader stands behind it. The LEI reference data is
* published under a CC0 public-domain dedication and the UK company data
* under the Open Government Licence v3.0; Spaw is not affiliated with,
* endorsed by or connected to either publisher, and an answer here is Spaw's
* rather than theirs. The UK file is republished monthly, so an answer can be
* five weeks behind the live register: "sources" carries the date of the copy
* it was read from.
*/
const SPAW_API_KEY = 'sk_live_REPLACE_ME';
const SPAW_API_BASE = 'https://spaw.co/api/v1';
const SPAW_BATCH_SIZE = 50;
const SPAW_IP_BATCH_SIZE = 1000;
const SPAW_PHONE_BATCH_SIZE = 1000;
const SPAW_ADDRESS_BATCH_SIZE = 100;
const SPAW_ENTITY_BATCH_SIZE = 500;
const SPAW_CACHE_SECONDS = 21600;
const SPAW_CACHE_PREFIX = 'spaw:v1:';
const SPAW_CACHE_KEY_LIMIT = 250;
const SPAW_EMAIL_FIELDS = [
'email', 'normalized_email', 'is_alias', 'is_gibberish', 'looks_generated',
'local_part_pattern', 'first_name', 'last_name', 'deliverable', 'reason',
'risk_score', 'risk_level', 'risk_signals', 'syntax_valid', 'domain', 'domain_category',
'mx_found', 'mx_implicit', 'mx_provider', 'mx_provider_type', 'mx_blocklisted',
'mx_network_type', 'has_spf', 'dmarc_policy', 'domain_registered_at', 'domain_age_days',
'domain_expires_at', 'parked', 'disposable', 'is_relay', 'relay_service', 'role',
'role_type', 'free_provider', 'smtp_checked', 'smtp_checked_at', 'mailbox_exists',
'previously_invalid', 'previously_invalid_at', 'catch_all', 'smtp_reason',
'mailbox_confidence', 'widely_seen', 'did_you_mean', 'ok_to_send', 'blocked_by', 'sources',
];
const SPAW_DOMAIN_FIELDS = [
'domain', 'valid', 'domain_category', 'mx_found', 'mx_implicit', 'catch_all', 'mx_provider',
'mx_provider_type', 'mx_blocklisted', 'mx_network_type', 'has_spf', 'spf_all',
'spf_lookup_count', 'dmarc_policy', 'dmarc_pct', 'dmarc_rua', 'dkim_selectors', 'mta_sts',
'tls_rpt', 'bimi', 'domain_registered_at', 'domain_age_days', 'domain_expires_at',
'domain_updated_at', 'registrar', 'registrant_redacted', 'parked', 'disposable', 'is_relay',
'relay_service', 'free_provider', 'sources',
];
const SPAW_PHONE_FIELDS = [
'valid', 'reason', 'e164', 'national', 'international', 'rfc3966', 'country',
'country_name', 'calling_code', 'continent', 'is_eu', 'currency', 'line_type',
'line_type_source', 'carrier', 'carrier_source', 'block_status', 'region',
'is_geographic', 'timezones', 'extension', 'is_fictional', 'is_virtual',
'is_disposable', 'disposable_seen_at', 'risk_score', 'risk_level',
'risk_signals', 'hlr_checked', 'reachable', 'ported', 'current_carrier',
'roaming', 'carrier_code', 'carrier_brand', 'block_pooled', 'block_assigned_at',
'region_level', 'local_time', 'utc_offsets', 'did_you_mean',
'range_seen_by_you', 'client_velocity', 'destination_new_for_you',
'complaints_90d', 'complaints_last_at', 'ok_to_send', 'blocked_by', 'sources',
];
const SPAW_IP_FIELDS = [
'ip', 'version', 'reason', 'network', 'continent', 'country', 'country_name', 'is_eu', 'is_eea',
'privacy_regime', 'sanctions', 'fatf_list', 'region', 'city', 'latitude', 'longitude',
'location_source', 'location_confidence', 'timezone', 'timezone_source', 'currency', 'calling_code',
'asn', 'org', 'asn_type', 'connection_type', 'is_mobile', 'is_satellite', 'registry',
'registry_country', 'allocated_at', 'is_datacenter', 'hosting_provider', 'is_corporate_proxy',
'corporate_proxy', 'is_crawler', 'crawler', 'crawler_kind', 'is_blocklisted', 'blocklist',
'is_attack_source', 'attack_source', 'is_tor', 'tor_last_seen', 'is_vpn', 'is_relay',
'privacy_service', 'is_anonymous', 'account_list',
'risk_score', 'risk_level', 'risk_signals', 'seen_by_you', 'sources',
];
const SPAW_ADDRESS_FIELDS = [
'valid', 'reason', 'country', 'country_name', 'continent', 'is_eu', 'is_eea', 'privacy_regime',
'formatted', 'formatted_lines', 'formatted_local', 'organization', 'building', 'street', 'house_number',
'unit', 'po_box', 'dependent_locality', 'locality', 'administrative_area', 'postal_code',
'postal_code_formatted', 'postal_code_valid', 'postal_code_type', 'postcode_status',
'locality_matches_postcode', 'administrative_area_matches_postcode', 'address_type',
'address_type_source', 'is_po_box', 'is_private_mailbox', 'exists', 'match_level', 'register',
'register_coverage', 'premise_key', 'building_use', 'is_multi_unit', 'unit_check', 'street_suggestion',
'latitude', 'longitude', 'location_source', 'companies_registered', 'is_mail_drop', 'mail_drop_provider',
'mail_drop_source', 'deliverability_checked', 'is_deliverable', 'is_cmra', 'is_vacant',
'is_residential', 'confirmation', 'changes', 'looks_placeholder', 'seen_by_you', 'risk_score', 'risk_level',
'risk_signals', 'ok_to_ship', 'blocked_by', 'sources',
];
const SPAW_ENTITY_FIELDS = [
'identifier', 'identifier_type', 'found', 'reason', 'name', 'status', 'status_detail',
'country', 'jurisdiction', 'category', 'legal_form', 'registered_at', 'record_updated_at',
'checksum_valid', 'registration_status', 'next_renewal_at', 'other_register',
'other_identifier', 'flags', 'agreements', 'sources',
];
const SPAW_ENTITY_TYPES = ['lei', 'company_number'];
/**
* Verifies one email address, or every address in a range, with Spaw.
*
* @param {string|string[][]} value A cell holding an email address, or a one-column range of them.
* @param {string} [fields] The field to return, "deliverable" by default. A comma-separated list such as "deliverable,reason,risk_score" returns one column per field.
* @param {string} [options] Freshness controls: "refresh", "max_age=SECONDS" and/or "timeout=SECONDS", comma-separated.
* @return {string|boolean|number|Array} The requested field, or one row per input row.
* @customfunction
*/
function SPAW_VERIFY(value, fields, options) {
const names = spawFieldNames_(fields, 'deliverable', SPAW_EMAIL_FIELDS);
const freshness = spawOptions_(options);
if (!Array.isArray(value)) {
const address = spawCleanInput_(value);
const values = spawValues_(address === '' ? null : spawVerifyOne_(address, freshness), names);
return names.length === 1 ? values[0] : [values];
}
const addresses = value.map(function (row) {
return spawCleanInput_(Array.isArray(row) ? row[0] : row);
});
const results = spawVerifyMany_(addresses, freshness);
return addresses.map(function (address, index) {
return spawValues_(address === '' ? null : results[index], names);
});
}
/**
* Inspects a domain's mail setup with Spaw: MX records, provider, SPF and
* DMARC, registration age, disposable and free-provider flags. An email
* address works too; its domain is used.
*
* @param {string|string[][]} domain A cell holding a domain (or an address), or a one-column range of them.
* @param {string} [fields] The field to return, "mx_found" by default. A comma-separated list such as "mx_provider,has_spf,dmarc_policy" returns one column per field.
* @return {string|boolean|number|Array} The requested field, or one row per input row.
* @customfunction
*/
function SPAW_DOMAIN(domain, fields) {
const names = spawFieldNames_(fields, 'mx_found', SPAW_DOMAIN_FIELDS);
if (!Array.isArray(domain)) {
const name = spawCleanDomain_(domain);
const values = spawValues_(name === '' ? null : spawDomainOne_(name), names);
return names.length === 1 ? values[0] : [values];
}
return domain.map(function (row) {
const name = spawCleanDomain_(Array.isArray(row) ? row[0] : row);
return spawValues_(name === '' ? null : spawDomainOne_(name), names);
});
}
/**
* Validates and enriches one phone number, or every number in a range, with
* Spaw: the E.164 and display forms, country facts, line type and carrier with
* the source that settled them, the regulator's block status, the fictional,
* virtual and disposable flags, and the risk score. A range may carry a
* second column with a country per row, which wins over the country argument.
*
* @param {string|string[][]} value A cell holding a phone number, or a range of them; a second column holds a two-letter country per row.
* @param {string} [country] A two-letter ISO 3166-1 code (US, GB, PL) for numbers written without a calling code; leave empty for numbers in international format.
* @param {string} [fields] The field to return, "valid" by default. A comma-separated list such as "e164,line_type,carrier" returns one column per field.
* @return {string|boolean|number|Array} The requested field, or one row per input row.
* @customfunction
*/
function SPAW_PHONE(value, country, fields) {
const names = spawFieldNames_(fields, 'valid', SPAW_PHONE_FIELDS);
const region = spawCleanCountry_(country);
if (!Array.isArray(value)) {
const number = spawCleanPhone_(value);
const values = spawValues_(number === '' ? null : spawPhoneOne_(number, region), names);
return names.length === 1 ? values[0] : [values];
}
const entries = value.map(function (row) {
const cells = Array.isArray(row) ? row : [row];
const rowRegion = cells.length > 1 ? spawCleanCountry_(cells[1]) : '';
return { phone: spawCleanPhone_(cells[0]), region: rowRegion === '' ? region : rowRegion };
});
const results = spawPhoneMany_(entries);
return entries.map(function (entry, index) {
return spawValues_(entry.phone === '' ? null : results[index], names);
});
}
/**
* Locates and risk-scores one IP address, or every address in a range, with
* Spaw: location, network, operator type, datacenter / Tor / VPN / relay
* flags with the provider named, and the risk score.
*
* @param {string|string[][]} value A cell holding an IPv4 or IPv6 address, or a one-column range of them.
* @param {string} [fields] The field to return, "risk_level" by default. A comma-separated list such as "country,is_vpn,risk_score" returns one column per field.
* @param {string} [options] "privacy" to keep nothing about the lookup on Spaw's side and skip the cache; every such lookup is fresh and billed.
* @return {string|boolean|number|Array} The requested field, or one row per input row.
* @customfunction
*/
function SPAW_IP(value, fields, options) {
const names = spawFieldNames_(fields, 'risk_level', SPAW_IP_FIELDS);
const privacy = spawIpOptions_(options);
if (!Array.isArray(value)) {
const address = spawCleanInput_(value);
const values = spawValues_(address === '' ? null : spawIpOne_(address, privacy), names);
return names.length === 1 ? values[0] : [values];
}
const addresses = value.map(function (row) {
return spawCleanInput_(Array.isArray(row) ? row[0] : row);
});
const results = spawIpMany_(addresses, privacy);
return addresses.map(function (address, index) {
return spawValues_(address === '' ? null : results[index], names);
});
}
/**
* Reads one postal address, or every address in a range, the way its
* country writes them: the components and the formatted address, the
* postcode's status and centroid, the delivery-point type, how close a
* national address register comes to it, the companies registered there,
* and the risk score. A range may carry a second column with a country per
* row, which wins over the country argument.
*
* An empty cell means the signal was not evaluated, never "no": the
* postcode status is empty where no directory covers the country, and
* `exists` is empty unless the building was found or a register whose
* coverage is complete does not carry it. `is_mail_drop` is a heuristic
* from mailbox operators' own published branch lists and the number of
* companies registered at the address, not a postal service's own flag.
*
* @param {string|string[][]} value A cell holding a postal address on one line, or a range of them; a second column holds a two-letter country per row.
* @param {string} [country] A two-letter ISO 3166-1 code (GB, US, PL). It decides which country's format the address is read under, and one cell needs it.
* @param {string} [fields] The field to return, "valid" by default. A comma-separated list such as "formatted,postcode_status,risk_level" returns one column per field.
* @return {string|boolean|number|Array} The requested field, or one row per input row.
* @customfunction
*/
function SPAW_ADDRESS(value, country, fields) {
const names = spawFieldNames_(fields, 'valid', SPAW_ADDRESS_FIELDS);
const region = spawCleanCountry_(country);
if (!Array.isArray(value)) {
const address = spawCleanInput_(value);
if (address !== '' && region === '') {
throw new Error('Spaw: SPAW_ADDRESS needs a two-letter country as its second argument, for example =SPAW_ADDRESS(F2, "GB") (MISSING_COUNTRY)');
}
const values = spawValues_(address === '' ? null : spawAddressOne_(address, region), names);
return names.length === 1 ? values[0] : [values];
}
const entries = value.map(function (row) {
const cells = Array.isArray(row) ? row : [row];
const rowRegion = cells.length > 1 ? spawCleanCountry_(cells[1]) : '';
return { address: spawCleanInput_(cells[0]), region: rowRegion === '' ? region : rowRegion };
});
const results = spawAddressMany_(entries);
return entries.map(function (entry, index) {
return spawValues_(entry.address === '' ? null : results[index], names);
});
}
/**
* Reads one business identifier against the registers Spaw syncs, or every
* identifier in a column: a Legal Entity Identifier, or a company number
* against the register of the country named. The answer gives the legal
* name, whether the entity is still live, where it was formed, and for a
* LEI its check digit and registration status, plus what the two registers
* agree and disagree about. It never carries a registered address.
*
* @param {string|string[][]} value A cell holding a LEI or a company number, or a one-column range of them.
* @param {string} type Which register the identifier belongs to: "lei" or "company_number". It is never guessed.
* @param {string} [country] A two-letter ISO 3166-1 code naming the register a company number belongs to. Required for a company number, ignored for a LEI.
* @param {string} [fields] The field to return, "found" by default. A comma-separated list such as "name,status,registration_status" returns one column per field.
* @return {string|boolean|number|Array} The requested field, or one row per input row.
* @customfunction
*/
function SPAW_ENTITY(value, type, country, fields) {
const names = spawFieldNames_(fields, 'found', SPAW_ENTITY_FIELDS);
const register = spawEntityType_(type);
const region = spawCleanCountry_(country);
// A company number without its register has nothing to be looked up in,
// so it is refused here rather than spending a request on a 422.
if (register === 'company_number' && region === '') {
throw new Error('Spaw: a company number needs the country of its register as the third argument, for example =SPAW_ENTITY(H2, "company_number", "GB") (MISSING_COUNTRY)');
}
if (!Array.isArray(value)) {
const identifier = spawCleanIdentifier_(value);
const values = spawValues_(identifier === '' ? null : spawEntityOne_(identifier, register, region), names);
return names.length === 1 ? values[0] : [values];
}
const identifiers = value.map(function (row) {
return spawCleanIdentifier_(Array.isArray(row) ? row[0] : row);
});
const results = spawEntityMany_(identifiers, register, region);
return identifiers.map(function (identifier, index) {
return spawValues_(identifier === '' ? null : results[index], names);
});
}
function spawVerifyOne_(address, freshness) {
const cache = spawCache_();
const key = spawCacheKey_('email', address.toLowerCase());
const cached = freshness.bypassCache ? null : spawCacheGet_(cache, key);
if (cached !== null) {
return cached;
}
const body = spawRequest_('POST', '/email', spawMerge_({ email: address }, freshness.body));
const data = body.data || null;
spawCachePutAll_(cache, key === null ? {} : spawCacheEntry_(key, data));
return data;
}
function spawVerifyMany_(addresses, freshness) {
const results = addresses.map(function () {
return null;
});
const cache = spawCache_();
const positions = {};
const normalized = [];
addresses.forEach(function (address, index) {
if (address === '') {
return;
}
const norm = address.toLowerCase();
if (!positions[norm]) {
positions[norm] = [];
normalized.push(norm);
}
positions[norm].push(index);
});
const keys = normalized.map(function (norm) {
return spawCacheKey_('email', norm);
});
const cached = freshness.bypassCache ? {} : spawCacheGetAll_(cache, keys);
const missing = [];
normalized.forEach(function (norm, position) {
const key = keys[position];
if (key !== null && cached[key]) {
spawAssign_(results, positions[norm], JSON.parse(cached[key]));
} else {
missing.push(norm);
}
});
for (let start = 0; start < missing.length; start += SPAW_BATCH_SIZE) {
const chunk = missing.slice(start, start + SPAW_BATCH_SIZE);
const emails = chunk.map(function (norm) {
return addresses[positions[norm][0]];
});
const body = spawRequest_('POST', '/email/batch', spawMerge_({ emails: emails }, freshness.body));
const items = (body.data && body.data.results) || [];
const entries = {};
items.forEach(function (item, position) {
const norm = chunk[position];
const data = (item && item.data) || null;
const key = spawCacheKey_('email', norm);
spawAssign_(results, positions[norm], data);
if (key !== null && data !== null) {
entries[key] = JSON.stringify(data);
}
});
spawCachePutAll_(cache, entries);
if (items.length < chunk.length) {
const reason = (body.meta && body.meta.stopped_reason) || 'incomplete_batch';
const detail = reason === 'insufficient_credits'
? ' because your credit balance ran out; the verified rows are cached, so top up on the Billing page and recalculate'
: '';
throw new Error('Spaw: the batch stopped after ' + items.length + ' of ' + chunk.length + ' addresses' + detail + ' (' + reason.toUpperCase() + ')');
}
}
return results;
}
function spawIpOne_(address, privacy) {
const cache = spawCache_();
const key = spawCacheKey_('ip', address.toLowerCase());
const cached = privacy.bypassCache ? null : spawCacheGet_(cache, key);
if (cached !== null) {
return cached;
}
const body = spawRequest_('POST', '/ip', spawMerge_({ ip: address }, privacy.body));
const data = body.data || null;
if (!privacy.bypassCache) {
spawCachePutAll_(cache, key === null ? {} : spawCacheEntry_(key, data));
}
return data;
}
function spawIpMany_(addresses, privacy) {
const results = addresses.map(function () {
return null;
});
const cache = spawCache_();
const positions = {};
const normalized = [];
addresses.forEach(function (address, index) {
if (address === '') {
return;
}
const norm = address.toLowerCase();
if (!positions[norm]) {
positions[norm] = [];
normalized.push(norm);
}
positions[norm].push(index);
});
const keys = normalized.map(function (norm) {
return spawCacheKey_('ip', norm);
});
const cached = privacy.bypassCache ? {} : spawCacheGetAll_(cache, keys);
const missing = [];
normalized.forEach(function (norm, position) {
const key = keys[position];
if (key !== null && cached[key]) {
spawAssign_(results, positions[norm], JSON.parse(cached[key]));
} else {
missing.push(norm);
}
});
for (let start = 0; start < missing.length; start += SPAW_IP_BATCH_SIZE) {
const chunk = missing.slice(start, start + SPAW_IP_BATCH_SIZE);
const ips = chunk.map(function (norm) {
return addresses[positions[norm][0]];
});
const body = spawRequest_('POST', '/ip/batch', spawMerge_({ ips: ips }, privacy.body));
const items = (body.data && body.data.results) || [];
const entries = {};
items.forEach(function (item, position) {
const norm = chunk[position];
const data = (item && item.data) || null;
const key = spawCacheKey_('ip', norm);
spawAssign_(results, positions[norm], data);
if (key !== null && data !== null && !privacy.bypassCache) {
entries[key] = JSON.stringify(data);
}
});
spawCachePutAll_(cache, entries);
if (items.length < chunk.length) {
const reason = (body.meta && body.meta.stopped_reason) || 'incomplete_batch';
const detail = reason === 'insufficient_credits'
? ' because your credit balance ran out; the answered rows are cached, so top up on the Billing page and recalculate'
: '';
throw new Error('Spaw: the batch stopped after ' + items.length + ' of ' + chunk.length + ' addresses' + detail + ' (' + reason.toUpperCase() + ')');
}
}
return results;
}
function spawPhoneOne_(number, region) {
const cache = spawCache_();
const key = spawPhoneCacheKey_(number, region);
const cached = spawCacheGet_(cache, key);
if (cached !== null) {
return cached;
}
const payload = { phone: number };
if (region !== '') {
payload.country = region;
}
const body = spawRequest_('POST', '/phone', payload);
const data = body.data || null;
spawCachePutAll_(cache, key === null ? {} : spawCacheEntry_(key, data));
return data;
}
/**
* A range goes to the batch endpoint 1,000 numbers per request, served
* from the cache where it can be. Each entry carries its own region (the
* row's country column, else the country argument); a number is sent as
* an object with its country when it has one, so every row parses under
* the right numbering plan.
*/
function spawPhoneMany_(entries) {
const results = entries.map(function () {
return null;
});
const cache = spawCache_();
const positions = {};
const unique = [];
entries.forEach(function (entry, index) {
if (entry.phone === '') {
return;
}
const norm = entry.region + ':' + entry.phone.toLowerCase();
if (!positions[norm]) {
positions[norm] = [];
unique.push(norm);
}
positions[norm].push(index);
});
const keys = unique.map(function (norm) {
const entry = entries[positions[norm][0]];
return spawPhoneCacheKey_(entry.phone, entry.region);
});
const cached = spawCacheGetAll_(cache, keys);
const missing = [];
unique.forEach(function (norm, position) {
const key = keys[position];
if (key !== null && cached[key]) {
spawAssign_(results, positions[norm], JSON.parse(cached[key]));
} else {
missing.push(norm);
}
});
for (let start = 0; start < missing.length; start += SPAW_PHONE_BATCH_SIZE) {
const chunk = missing.slice(start, start + SPAW_PHONE_BATCH_SIZE);
const phones = chunk.map(function (norm) {
const entry = entries[positions[norm][0]];
return entry.region === '' ? entry.phone : { phone: entry.phone, country: entry.region };
});
const body = spawRequest_('POST', '/phone/batch', { phones: phones });
const items = (body.data && body.data.results) || [];
const cacheEntries = {};
items.forEach(function (item, position) {
const norm = chunk[position];
const entry = entries[positions[norm][0]];
const data = (item && item.data) || null;
const key = spawPhoneCacheKey_(entry.phone, entry.region);
spawAssign_(results, positions[norm], data);
if (key !== null && data !== null) {
cacheEntries[key] = JSON.stringify(data);
}
});
spawCachePutAll_(cache, cacheEntries);
if (items.length < chunk.length) {
const reason = (body.meta && body.meta.stopped_reason) || 'incomplete_batch';
const detail = reason === 'insufficient_credits'
? ' because your credit balance ran out; the answered rows are cached, so top up on the Billing page and recalculate'
: '';
throw new Error('Spaw: the batch stopped after ' + items.length + ' of ' + chunk.length + ' numbers' + detail + ' (' + reason.toUpperCase() + ')');
}
}
return results;
}
function spawPhoneCacheKey_(number, region) {
return spawCacheKey_('phone', region + ':' + number.toLowerCase());
}
function spawAddressOne_(address, region) {
const cache = spawCache_();
const key = spawAddressCacheKey_(address, region);
const cached = spawCacheGet_(cache, key);
if (cached !== null) {
return cached;
}
const payload = { address: address };
if (region !== '') {
payload.country = region;
}
const body = spawRequest_('POST', '/address', payload);
const data = body.data || null;
spawCachePutAll_(cache, key === null ? {} : spawCacheEntry_(key, data));
return data;
}
/**
* A range goes to the batch endpoint 100 addresses per request, served
* from the cache where it can be. Each entry carries its own region (the
* row's country column, else the country argument); an address is sent as
* an object with its country when it has one, so every row is read under
* the right country's format.
*/
function spawAddressMany_(entries) {
const results = entries.map(function () {
return null;
});
const cache = spawCache_();
const positions = {};
const unique = [];
entries.forEach(function (entry, index) {
if (entry.address === '') {
return;
}
const norm = entry.region + ':' + entry.address.toLowerCase();
if (!positions[norm]) {
positions[norm] = [];
unique.push(norm);
}
positions[norm].push(index);
});
const keys = unique.map(function (norm) {
const entry = entries[positions[norm][0]];
return spawAddressCacheKey_(entry.address, entry.region);
});
const cached = spawCacheGetAll_(cache, keys);
const missing = [];
unique.forEach(function (norm, position) {
const key = keys[position];
if (key !== null && cached[key]) {
spawAssign_(results, positions[norm], JSON.parse(cached[key]));
} else {
missing.push(norm);
}
});
for (let start = 0; start < missing.length; start += SPAW_ADDRESS_BATCH_SIZE) {
const chunk = missing.slice(start, start + SPAW_ADDRESS_BATCH_SIZE);
const addresses = chunk.map(function (norm) {
const entry = entries[positions[norm][0]];
return entry.region === '' ? entry.address : { address: entry.address, country: entry.region };
});
const body = spawRequest_('POST', '/address/batch', { addresses: addresses });
const items = (body.data && body.data.results) || [];
const cacheEntries = {};
items.forEach(function (item, position) {
const norm = chunk[position];
const entry = entries[positions[norm][0]];
const data = (item && item.data) || null;
const key = spawAddressCacheKey_(entry.address, entry.region);
spawAssign_(results, positions[norm], data);
if (key !== null && data !== null) {
cacheEntries[key] = JSON.stringify(data);
}
});
spawCachePutAll_(cache, cacheEntries);
if (items.length < chunk.length) {
const reason = (body.meta && body.meta.stopped_reason) || 'incomplete_batch';
const detail = reason === 'insufficient_credits'
? ' because your credit balance ran out; the answered rows are cached, so top up on the Billing page and recalculate'
: '';
throw new Error('Spaw: the batch stopped after ' + items.length + ' of ' + chunk.length + ' addresses' + detail + ' (' + reason.toUpperCase() + ')');
}
}
return results;
}
function spawAddressCacheKey_(address, region) {
return spawCacheKey_('address', region + ':' + address.toLowerCase());
}
function spawEntityOne_(identifier, register, region) {
const cache = spawCache_();
const key = spawEntityCacheKey_(identifier, register, region);
const cached = spawCacheGet_(cache, key);
if (cached !== null) {
return cached;
}
const body = spawRequest_('POST', '/entity', spawEntityItem_(identifier, register, region));
const data = body.data || null;
spawCachePutAll_(cache, key === null ? {} : spawCacheEntry_(key, data));
return data;
}
/**
* A column goes to the batch endpoint 500 identifiers per request, served
* from the cache where it can be. Every row is read as the same type, so
* the register and its country are the arguments rather than a column.
*/
function spawEntityMany_(identifiers, register, region) {
const results = identifiers.map(function () {
return null;
});
const cache = spawCache_();
const positions = {};
const unique = [];
identifiers.forEach(function (identifier, index) {
if (identifier === '') {
return;
}
const norm = identifier.toLowerCase();
if (!positions[norm]) {
positions[norm] = [];
unique.push(norm);
}
positions[norm].push(index);
});
const keys = unique.map(function (norm) {
return spawEntityCacheKey_(identifiers[positions[norm][0]], register, region);
});
const cached = spawCacheGetAll_(cache, keys);
const missing = [];
unique.forEach(function (norm, position) {
const key = keys[position];
if (key !== null && cached[key]) {
spawAssign_(results, positions[norm], JSON.parse(cached[key]));
} else {
missing.push(norm);
}
});
for (let start = 0; start < missing.length; start += SPAW_ENTITY_BATCH_SIZE) {
const chunk = missing.slice(start, start + SPAW_ENTITY_BATCH_SIZE);
const sent = chunk.map(function (norm) {
return spawEntityItem_(identifiers[positions[norm][0]], register, region);
});
const body = spawRequest_('POST', '/entity/batch', { identifiers: sent });
const items = (body.data && body.data.results) || [];
const cacheEntries = {};
items.forEach(function (item, position) {
const norm = chunk[position];
const identifier = identifiers[positions[norm][0]];
const data = (item && item.data) || null;
const key = spawEntityCacheKey_(identifier, register, region);
spawAssign_(results, positions[norm], data);
if (key !== null && data !== null) {
cacheEntries[key] = JSON.stringify(data);
}
});
spawCachePutAll_(cache, cacheEntries);
if (items.length < chunk.length) {
const reason = (body.meta && body.meta.stopped_reason) || 'incomplete_batch';
const detail = reason === 'insufficient_credits'
? ' because your credit balance ran out; the answered rows are cached, so top up on the Billing page and recalculate'
: '';
throw new Error('Spaw: the batch stopped after ' + items.length + ' of ' + chunk.length + ' identifiers' + detail + ' (' + reason.toUpperCase() + ')');
}
}
return results;
}
/**
* One item as both endpoints take it. The country is sent only with a
* company number: a LEI is global by construction and the API ignores it.
*/
function spawEntityItem_(identifier, register, region) {
const item = { identifier: identifier, type: register };
if (register === 'company_number') {
item.country = region;
}
return item;
}
function spawEntityCacheKey_(identifier, register, region) {
return spawCacheKey_('entity', register + ':' + region + ':' + identifier.toLowerCase());
}
function spawDomainOne_(name) {
const cache = spawCache_();
const key = spawCacheKey_('domain', name);
const cached = spawCacheGet_(cache, key);
if (cached !== null) {
return cached;
}
const body = spawRequest_('GET', '/email/domain/' + encodeURIComponent(name));
const data = body.data || null;
spawCachePutAll_(cache, key === null ? {} : spawCacheEntry_(key, data));
return data;
}
function spawRequest_(method, path, payload) {
const options = {
method: method,
contentType: 'application/json',
headers: {
Authorization: 'Bearer ' + spawApiKey_(),
Accept: 'application/json',
},
muteHttpExceptions: true,
};
if (payload !== undefined) {
options.payload = JSON.stringify(payload);
}
let response = UrlFetchApp.fetch(SPAW_API_BASE + path, options);
if (response.getResponseCode() === 429) {
Utilities.sleep(spawRetryAfterMs_(response));
response = UrlFetchApp.fetch(SPAW_API_BASE + path, options);
}
return spawParse_(response);
}
function spawParse_(response) {
const status = response.getResponseCode();
let body = null;
try {
body = JSON.parse(response.getContentText());
} catch (error) {
body = null;
}
if (body === null || typeof body !== 'object') {
throw new Error('Spaw: unexpected answer from the API (HTTP_' + status + ')');
}
if (body.success === false || status >= 400) {
const failure = body.error || {};
const code = failure.code || 'HTTP_' + status;
throw new Error('Spaw: ' + spawErrorMessage_(code, failure.message) + ' (' + code + ')');
}
return body;
}
function spawErrorMessage_(code, message) {
switch (code) {
case 'INSUFFICIENT_CREDITS':
return 'your credit balance is empty; buy a credit pack on the Billing page of your Spaw dashboard';
case 'RATE_LIMITED':
return 'too many requests at once; verify the column as one range, or recalculate in a moment (answered rows are cached)';
case 'UNAUTHENTICATED':
return 'the API key was rejected; paste a secret key from https://spaw.co/api-keys';
default:
return message || 'the request failed';
}
}
function spawRetryAfterMs_(response) {
let headers = {};
try {
headers = response.getHeaders() || {};
} catch (error) {
headers = {};
}
const seconds = parseInt(headers['Retry-After'] || headers['retry-after'] || '1', 10);
if (isNaN(seconds) || seconds < 1) {
return 1000;
}
return Math.min(seconds, 3) * 1000;
}
function spawApiKey_() {
let key = '';
try {
const stored = PropertiesService.getScriptProperties().getProperty('SPAW_API_KEY');
if (stored) {
key = String(stored).trim();
}
} catch (error) {
key = '';
}
if (key === '') {
key = String(SPAW_API_KEY || '').trim();
}
if (key.indexOf('pk_') === 0) {
throw new Error('Spaw: publishable pk_ keys only work in the browser; paste a secret sk_live_ key from https://spaw.co/api-keys (NO_API_KEY)');
}
if (key === '' || key === 'sk_live_REPLACE_ME' || key.indexOf('sk_') !== 0) {
throw new Error('Spaw: set your secret API key in SPAW_API_KEY or in the SPAW_API_KEY script property; keys come from https://spaw.co/api-keys (NO_API_KEY)');
}
return key;
}
/**
* Parses the freshness options: "refresh", "max_age=SECONDS" and
* "timeout=SECONDS", comma-separated. refresh and max_age also skip the
* script cache, because the point is a newer answer than the one on hand.
*/
function spawOptions_(options) {
const parsed = { body: {}, bypassCache: false };
const raw = options === undefined || options === null ? '' : String(options).trim();
if (raw === '') {
return parsed;
}
raw.split(',').forEach(function (token) {
const cleaned = token.trim().toLowerCase();
if (cleaned === '') {
return;
}
const separator = cleaned.search(/[=:]/);
const name = separator === -1 ? cleaned : cleaned.slice(0, separator).trim();
const value = separator === -1 ? '' : cleaned.slice(separator + 1).trim();
if (name === 'refresh') {
if (value === '' || value === 'true' || value === 'yes' || value === '1') {
parsed.body.refresh = true;
parsed.bypassCache = true;
}
return;
}
if (name === 'max_age' || name === 'timeout') {
const seconds = parseInt(value, 10);
if (isNaN(seconds) || seconds <= 0) {
throw new Error('Spaw: "' + name + '" needs a number of seconds, for example "' + name + '=' + (name === 'max_age' ? '86400' : '10') + '" (UNKNOWN_OPTION)');
}
parsed.body[name] = seconds;
if (name === 'max_age') {
parsed.bypassCache = true;
}
return;
}
throw new Error('Spaw: unknown option "' + token.trim() + '"; use refresh, max_age=SECONDS or timeout=SECONDS (UNKNOWN_OPTION)');
});
return parsed;
}
/**
* Parses the SPAW_IP options: "privacy" asks the API to keep nothing about
* the lookup and skips the script cache, because the point is that no copy
* of the answer is kept anywhere.
*/
function spawIpOptions_(options) {
const parsed = { body: {}, bypassCache: false };
const raw = options === undefined || options === null ? '' : String(options).trim();
if (raw === '') {
return parsed;
}
raw.split(',').forEach(function (token) {
const cleaned = token.trim().toLowerCase();
if (cleaned === '') {
return;
}
const separator = cleaned.search(/[=:]/);
const name = separator === -1 ? cleaned : cleaned.slice(0, separator).trim();
const value = separator === -1 ? '' : cleaned.slice(separator + 1).trim();
if (name === 'privacy') {
if (value === '' || value === 'true' || value === 'yes' || value === '1') {
parsed.body.privacy = true;
parsed.bypassCache = true;
}
return;
}
throw new Error('Spaw: unknown option "' + token.trim() + '"; SPAW_IP accepts privacy (UNKNOWN_OPTION)');
});
return parsed;
}
function spawMerge_(target, extra) {
Object.keys(extra).forEach(function (name) {
target[name] = extra[name];
});
return target;
}
function spawFieldNames_(fields, fallback, known) {
const raw = fields === undefined || fields === null || String(fields).trim() === ''
? fallback
: String(fields);
const names = raw.split(',').map(function (name) {
return name.trim().toLowerCase();
}).filter(function (name) {
return name !== '';
});
names.forEach(function (name) {
if (known.indexOf(name) === -1) {
throw new Error('Spaw: unknown field "' + name + '"; use one of ' + known.join(', ') + ' (UNKNOWN_FIELD)');
}
});
return names.length === 0 ? [fallback] : names;
}
function spawCleanInput_(value) {
return typeof value === 'string' ? value.trim() : '';
}
/**
* A phone number cell may hold text or a plain number (Sheets stores
* 4155550142 as a number); anything else answers an empty cell.
*/
function spawCleanPhone_(value) {
if (typeof value === 'number') {
return isFinite(value) && Math.floor(value) === value ? String(value) : '';
}
return spawCleanInput_(value);
}
/**
* A company number cell often holds a plain number, because Sheets stores
* 1234567 that way and the register pads it back to eight characters. A
* LEI is always text.
*/
function spawCleanIdentifier_(value) {
if (typeof value === 'number') {
return isFinite(value) && Math.floor(value) === value ? String(value) : '';
}
return spawCleanInput_(value);
}
/**
* Which register the identifier belongs to. There is no default: an
* identifier does not say which register issued it, and reading a company
* number as a LEI would answer a confident "no".
*/
function spawEntityType_(value) {
const cleaned = value === undefined || value === null ? '' : String(value).trim().toLowerCase();
if (SPAW_ENTITY_TYPES.indexOf(cleaned) === -1) {
throw new Error('Spaw: the second argument must be one of ' + SPAW_ENTITY_TYPES.join(', ') + ', for example =SPAW_ENTITY(H2, "lei") (UNKNOWN_TYPE)');
}
return cleaned;
}
function spawCleanCountry_(value) {
const cleaned = value === undefined || value === null ? '' : String(value).trim().toUpperCase();
if (cleaned !== '' && !/^[A-Z]{2}$/.test(cleaned)) {
throw new Error('Spaw: the country must be a two-letter ISO 3166-1 code such as US or GB (UNKNOWN_COUNTRY)');
}
return cleaned;
}
function spawCleanDomain_(value) {
const cleaned = spawCleanInput_(value);
const at = cleaned.lastIndexOf('@');
return (at === -1 ? cleaned : cleaned.slice(at + 1)).toLowerCase();
}
function spawValues_(data, names) {
return names.map(function (name) {
return spawScalar_(data === null || data === undefined ? null : data[name]);
});
}
function spawScalar_(value) {
if (value === null || value === undefined) {
return '';
}
if (typeof value === 'object') {
return JSON.stringify(value);
}
return value;
}
function spawAssign_(results, indexes, data) {
indexes.forEach(function (index) {
results[index] = data;
});
}
function spawCache_() {
try {
return CacheService.getScriptCache();
} catch (error) {
return null;
}
}
function spawCacheKey_(kind, normalized) {
const key = SPAW_CACHE_PREFIX + kind + ':' + normalized;
return key.length <= SPAW_CACHE_KEY_LIMIT ? key : null;
}
function spawCacheEntry_(key, data) {
const entry = {};
if (data !== null) {
entry[key] = JSON.stringify(data);
}
return entry;
}
function spawCacheGet_(cache, key) {
if (cache === null || key === null) {
return null;
}
try {
const hit = cache.get(key);
return hit ? JSON.parse(hit) : null;
} catch (error) {
return null;
}
}
function spawCacheGetAll_(cache, keys) {
const wanted = keys.filter(function (key) {
return key !== null;
});
if (cache === null || wanted.length === 0) {
return {};
}
try {
return cache.getAll(wanted) || {};
} catch (error) {
return {};
}
}
function spawCachePutAll_(cache, entries) {
if (cache === null || Object.keys(entries).length === 0) {
return;
}
try {
cache.putAll(entries, SPAW_CACHE_SECONDS);
} catch (error) {
// The cache is a convenience: a failed write only costs a repeat call.
}
}
```
---
# Verify emails, phones, IPs and postal addresses in Zapier
Call the Spaw API from any Zap with Webhooks by Zapier to verify emails, phone numbers, IPs and postal addresses, and what the Spaw app adds once listed.
Updated: 2026-10-14
Zapier is where most teams first wire a verification step between a form, a CRM and a mailing list. Spaw fits in two ways: today, any Zap can call the REST API through Zapier's own Webhooks app; soon, a Spaw app built by us will offer the same lookups as ordinary actions with a saved connection. This page covers both, in that order.
## What works today: Webhooks by Zapier
Webhooks by Zapier is a premium app, so it needs a paid Zapier plan. Everything else is a plain HTTPS call.
1. Create a secret API key on the API keys page of your Spaw dashboard. It starts with `sk_live_`. Publishable `pk_` keys are locked to browser origins and will not work from Zapier.
2. Open the Zap that receives the address, for example a new form entry or a new CRM contact, and add an action step using Webhooks by Zapier.
3. Choose the POST event. Set the URL to `https://spaw.co/api/v1/email` and the payload type to JSON.
4. Under the data fields, add one key named `email` and map it to the address from the trigger step.
5. Under headers, add `Authorization` with the value `Bearer sk_live_…` using your key.
6. Test the step. A good first test address is `deliverable@spaw.test`, which answers a canonical result, costs nothing and never appears in your history.
The request Zapier sends looks like this:
```json
{
"email": "mia@acme.com"
}
```
The answer is the full verification result. Zapier flattens nested JSON, so the verdict shows up in later steps as the data field `deliverable` (the raw key is `data__deliverable`), the reason as `reason`, the risk score as `risk_score`, and the typo suggestion as `did_you_mean`. Every field of the [single-address response](/docs/api/verify-email) is available for mapping.
For a Zap that handles a list rather than one address at a time, point the same step at `https://spaw.co/api/v1/email/batch` and send `{ "emails": [...] }` with up to 50 addresses. The [batch endpoint](/docs/api/verify-email-batch) answers in input order and counts as one request against the rate limit.
## The Spaw app for Zapier
A Spaw app for Zapier is built and is going through Zapier's publishing process; it is not in the Zapier app directory yet. When it is, this page will carry the link. It replaces the manual webhook step with these pieces:
- A connection that takes your secret API key once and labels itself with your account email.
- Actions: Verify Email, Verify Emails (up to 50 addresses, one per line), Check Domain, Add to Suppression List, Look Up IP Address, Report IP Outcome (abuse, fraud, bot or chargeback, free; it scores your own later lookups of the address), Validate Phone Number, Validate Phone Numbers (up to 1,000, with a shared Country and a Live carrier check input), Report SMS Outcome (delivered, undelivered, abused, verified or opted out, free), Validate Postal Address, and Verify Business Identifier (a Legal Entity Identifier or a company number, read against the registers Spaw syncs).
- A search step, Find Email Verdict, so a lookup can sit in a path or a filter the way Zapier expects lookups to.
- Errors that name the API code, so an empty balance reads `INSUFFICIENT_CREDITS` in the Zap history rather than a generic failure, and rate limits are retried automatically.
Nothing about the data changes between the two paths. The app calls the same endpoints with the same fields, so a Zap built on Webhooks by Zapier today can be moved over field by field later.
## IP lookups
The same webhook step also looks up the address a signup or order came from. Point it at `https://spaw.co/api/v1/ip` with the body `{ "ip": "203.0.113.9" }`, mapping the address your form tool or CRM records for the submission. The answer places the address on the map, names the network and its operator type, and carries tri-state flags for datacenter and cloud ranges, Tor exits, VPN exits and iCloud Private Relay, with the provider named when its own published list contains the address. `risk_score` is a published sum (Tor 70, datacenter 40, VPN 30, relay 10) and `risk_signals` lists the weights applied, so a Filter step on `is_anonymous` or on `risk_level` needs no interpretation. In the Spaw app the same call is the Look Up IP Address action, with a Privacy mode input that keeps nothing about the lookup on Spaw's side, not even the seven-day repeat marker. One credit covers five [IP lookups](/docs/api/lookup-ip) that resolve a location, a network or a positive flag: the lookup that opens a block reports `credits_used` 1, the next four 0, and `meta.ip_lookups_prepaid` counts what is left. Reserved or private ranges, unallocated space, unknown addresses and repeats within seven days are free, and IP endpoints have their own limit of 50 requests per second.
## Phone numbers
The same webhook step validates the phone number a form collected. Point it at `https://spaw.co/api/v1/phone` with the body `{ "phone": "+44 7911 012345" }`, adding a `country` key (an ISO 3166-1 alpha-2 code such as `US`) when the numbers arrive without a calling code. The answer carries `valid` and a typed `reason`, the E.164 form to store and dedupe by, the national and international forms, the country and its facts, `line_type` and `carrier` with the source that settled them (the numbering plan, or the block allocation published by the regulator, which says who the numbers were allocated to and never who serves them after a port), `block_status`, the `is_fictional`, `is_virtual` and `is_disposable` flags, and a published `risk_score` with its `risk_signals`. A Filter on `valid`, or on `is_disposable` before an SMS step, needs no interpretation. Better still, filter on `ok_to_send` before the SMS step: it is your account's [SMS policy](/guides/gate-sms-verification-codes-with-the-sms-policy) applied to this answer (the countries and line types you send to, the signals you refuse, a ceiling on the score and, by default, the numbers your suppression list holds as opted out), and when it is false `blocked_by` names the rule that decided it (`invalid`, `opted_out`, `country`, `line_type`, a signal such as `disposable`, or `risk_score`; `suppressed` for a number on your suppression list, which batch, bulk and monitored runs honour and a single lookup never does, while a single lookup of a number the list holds as opted out answers `opted_out` unless the policy's `block_opted_out` is off), the reason to write on the record or send down the review path of a Paths step. The policy is set once on the account, not in the Zap, so every Zap, batch and bulk run applies the same rules. It is a fraud and deliverability decision, not consent and not a do-not-call scrub. In the Spaw app the same call is the Validate Phone Number action. A [phone lookup](/docs/api/validate-phone) costs 1 credit when the number is valid; invalid numbers, unassigned blocks and repeats within seven days are free.
A Zap that handles a list points the same step at `https://spaw.co/api/v1/phone/batch` with `{ "phones": [...] }`, up to 1,000 numbers with an optional `country`; the app's Validate Phone Numbers action does the same from a line-item field and adds a Live carrier check input (the API's `hlr`). The live carrier-network check (`hlr: true`) is not enabled on the service: `hlr_checked` answers false and nothing extra is charged. After your SMS step, a second webhook (or the app's Report SMS Outcome action) posts `{ "items": [{ "phone": "...", "outcome": "delivered" }] }` to `https://spaw.co/api/v1/phone/feedback`, mapping `undelivered` or `abused` from your SMS provider's status; the report is free and feeds only your own later lookups of that number. An `undelivered` item can carry the `failure_class` your provider's error code names (`sender_side` for a carrier filter or an unregistered campaign, `permanent` for a number that no longer exists), and only failures about the number count against it.
## Postal addresses
The same webhook step checks the postal address on an order or a signup. Point it at `https://spaw.co/api/v1/address` with the body `{ "address": "10 Downing Street, London SW1A 2AA", "country": "GB" }`, or send the parts separately as `address_line1`, `building`, `locality`, `administrative_area`, `postal_code` and the rest. The country is not optional in practice: it decides which format the address is read under, which parts that country requires and which postcode directory answers, and it is never guessed. The answer carries the components and the `formatted` address, whether the postcode matches the country's pattern and whether the directory still lists it, whether the town matches the postcode, what kind of delivery point it is (`address_type`), how close a national address register comes to it (`match_level` against the `register` named), the postcode centroid, how many companies are registered there, and a published `risk_score` with its `risk_signals`. Before a shipping step, filter on `ok_to_ship` rather than on the evidence: it is your account's [shipping policy](/guides/gate-a-checkout-with-the-shipping-policy) applied to this answer, and when it is false `blocked_by` names the rule that decided it (`invalid`, a signal such as `placeholder`, `risk_score` or `unconfirmed`; `suppressed` for an address on your suppression list, which batch, bulk and monitored runs honour and a single lookup never does), which is the reason to write on the record or send down the review path of a Paths step. The policy is set once on the account, not in the Zap, so every Zap, batch and bulk run applies the same rules. In the Spaw app the same call is the Validate Postal Address action, whose optional inputs are sent as separate fields and win over anything read out of the line. For a list, the same step points at `https://spaw.co/api/v1/address/batch` with `{ "addresses": [...] }`, up to 100 per call.
The answer says how each part was confirmed (`confirmation`: the postcode, street, house number, town and region each mapped to the dataset that stood behind it, or `unconfirmed`) and what the parser set aside on the way (`changes`: a country line dropped, a care-of line removed, a building name separated from the street, …). An empty field means the signal was not evaluated, never "no". `exists` is true only on a premise match and false only under a register whose coverage is complete; a register whose coverage is partial, because it covers part of the country or cannot vouch for every address, cannot make an address's absence mean anything. `is_mail_drop` is a heuristic and not a postal service's own agency flag: an operator's own published branch list names the address, or so many companies are registered there that it is one in practice, and `mail_drop_source` says which. Two answers are licensed and not enabled — whether post actually arrives (`deliverability_checked` comes back false with `is_deliverable`, `is_cmra`, `is_vacant` and `is_residential` empty) and the list of addresses inside a postcode, which answers a typed `501` rather than a guess. An address lookup costs 1 credit when the address is valid; invalid addresses and repeats within seven days are free.
## Business identifiers
The same webhook step checks the company behind a signup, an invoice or a supplier record. Point it at `https://spaw.co/api/v1/entity` with the body `{ "identifier": "213800QILIUD4ROSUO03", "type": "lei" }`, or `{ "identifier": "01234567", "type": "company_number", "country": "GB" }` for a number from a national register. The country names which register the number belongs to, because company numbers are only unique inside their own; a Legal Entity Identifier is global by construction and ignores it. The answer carries the legal name, whether the entity is still live (`status`, with the register's own wording in `status_detail`), the country and jurisdiction it was formed in, its category and legal form, when it was registered, and for a LEI `checksum_valid`, `registration_status` and `next_renewal_at`. Where a LEI names its entry in a national register Spaw also holds, `agreements` quotes both sides field by field and `flags` names what differs — including `dissolved_in_national_register`, the strongest signal the product carries, because a dissolved company no longer exists as a legal person and cannot enter a contract. In the Spaw app the same call is the Verify Business Identifier action, whose Identifier type input picks the register and whose Country input is required for a company number. For a list, the same step points at `https://spaw.co/api/v1/entity/batch` with `{ "identifiers": [...] }`, up to 500 per call.
The one caveat, and it matters more than anything else here: the UK company file is a snapshot of the **live** register, so a company that is struck off is **removed from the file** rather than marked dissolved — the statuses that file carries are `Active`, `Active - Proposal to Strike off`, `Liquidation`, `In Administration` and similar, and never `Dissolved`. A UK dissolution therefore reaches you as `found` `false` with `reason` `unknown_identifier` rather than as that flag, which never fires on the only national register installed today. Check `found` before you trust the flag, and read `status_detail` for the earlier warning: `Active - Proposal to Strike off` folds to a `status` of `active`, because the company is still on the register. A LEI lookup of the same company is quieter still: GLEIF keeps its record, so `found` stays `true` and no flag fires. What moves is `agreements`, which comes back empty while `other_register` still names `uk-companies-house` — the counterpart row is gone. That pair is the signal on this path, and `lei_registration_not_current` follows later, when the lapsed registration is not renewed. A [monitored list](/docs/business-verification) turns the disappearance into an alert of its own, `left_register`.
`checksum_valid` is a typing check and nothing more: it catches a mistyped or transposed character, while a perfectly formed LEI can never have been issued, which is what the register lookup answers. `registration_status` is about the identifier rather than the entity — a LEI must be reconfirmed each year and a great many are not, so `LAPSED` means the reference data has not been rechecked recently, not that the company has gone. Nothing in the answer is a registered address, and nothing will be: a registered address is very often somebody's home, and an identifier alone does not say whether a company or a sole trader stands behind it.
Both registers are bulk files synced onto the Spaw server, so no third party sits in the request path. The LEI reference data is published under a CC0 public-domain dedication and refreshed daily; the UK company data is used under the Open Government Licence v3.0 and republished monthly, so an answer can be up to five weeks behind the live register and every answer names the date of the copy in `sources`. Spaw is not affiliated with, endorsed by or connected to either publisher, and the answer is Spaw's rather than theirs. A [business lookup](/docs/api/verify-entity) costs 1 credit when a register carried the identifier; unknown, malformed and unsupported identifiers and repeats within seven days are free.
## What to do with the verdict
Add a Filter or a Paths step right after the verification step and branch on `deliverable`. A `deliverable` answer continues to the CRM or mailing list as usual. A `risky` answer deserves a look at `reason`: `catch_all` and `role` addresses are often fine to keep with a lower priority, while `disposable` rarely is. An `undeliverable` answer should stop the Zap or route the record to a review list, and when `did_you_mean` is filled, for example `mia@gmail.com` for `mia@gmail.con`, you can write the correction back instead of discarding the lead. Teams that prefer one number to branch on use `risk_score`, which is a published sum of weights rather than a black box.
## Cost
Each Zap run that verifies an address spends 1 credit when the answer is deliverable or risky. Unverified answers, invalid input, and repeats of the same address within seven days cost nothing, and undeliverable answers cost nothing within a fair-use allowance. The Zapier task itself counts against your Zapier plan as any action does. A key may make 5 requests per second; a Zap that fires faster than that receives `RATE_LIMITED` with a `Retry-After` header, and Zapier's replay handles the rest.
---
# Check phone numbers in the Supabase Send SMS Hook with Spaw
A Send SMS Hook on an Edge Function that asks Spaw about the number, refuses codes your SMS policy would not send, then sends the rest itself.
Updated: 2026-09-27
Supabase's Send SMS Hook replaces Supabase's built-in SMS sending: once it is on, Supabase Auth hands each SMS one-time code to your endpoint and sends nothing itself. That makes the hook the place to ask Spaw about the number first. The code goes to your SMS provider only when your account's [SMS policy](/docs/phone-intelligence#policy) answers `ok_to_send: true`, and a refused number never costs you a message. Because the hook now owns delivery, the recipe below checks the number and then sends the SMS; there is no setting that checks and hands the code back to Supabase.
## What the hook receives
From Supabase's auth-hooks documentation and the source of Supabase Auth, both read on 27 September 2026:
| Item | What Supabase provides |
| --- | --- |
| The payload | `user` (the Supabase user record) and `sms.otp`, signed under the Standard Webhooks specification |
| The number | `sms.phone`, the number the code is for, and `user.phone`; both come without the leading `+` |
| The visitor's address | `metadata.ip_address`, added to every hook payload in Supabase Auth 2.187.0 (23 February 2026); the Send SMS Hook page does not list it yet |
| Refusing | An `error` object with `http_code` and `message` in the JSON answer is returned to your app with that status, except on a multi-factor challenge, where Supabase answers a generic 500 |
| Time limit | "We have a time budget of 5s for the entire webhook invocation, including retry requests." |
Two consequences follow. The number needs its `+` back before it goes to Spaw: `12025550103` without it answers `valid: false` with reason `missing_country`, and so would every other number sent that way, unless the key has a default country, which reads the digits as a national number of that country and so misreads numbers from anywhere else. And the whole hook, the Spaw call and the SMS provider's call together, has five seconds, so the sample gives Spaw two.
`metadata.ip_address` is the address Supabase Auth saw, the same one its rate limits use. When a server-side framework calls Supabase Auth for the visitor, that is your server unless you forward the visitor's address in the `Sb-Forwarded-For` header. Supabase's rate-limits page says the header is honoured only on requests made with a secret API key, and that a new project has to switch IP Address Forwarding on under Authentication, Rate Limits first. If you do not forward it, pass nothing to `checkNumber()` as the address rather than your server's: a public server address puts every visitor in one count and would soon add [`numbers_per_client`](/docs/signals/phone/numbers_per_client) to all of them, while a private or reserved address is counted but never adds the signal.
## Before you start
1. Create a secret API key on the API keys page of your Spaw dashboard, scoped to phone, with a daily credit cap above a normal day's sign-ups. Generate a feedback key on the same page; the sample puts it in the delivery-receipt URL.
2. Switch the SMS policy's opt-out rule off, so a person who once replied STOP to a marketing message can still receive a sign-in code:
```bash
curl -X PUT https://spaw.co/api/v1/phone/policy \
-H "Authorization: Bearer sk_live_…" \
-H "Content-Type: application/json" \
-d '{"block_opted_out": false}'
```
An account has one policy, applied to every lookup it makes. With the rule off, an opted-out number is decided like any other and the answer still says `meta.opted_out: true`, so a marketing flow on the same account reads that field itself.
3. Set the secrets: `supabase secrets set SPAW_KEY=sk_live_… SPAW_FEEDBACK_KEY=fb_… TWILIO_ACCOUNT_SID=… TWILIO_AUTH_TOKEN=… TWILIO_PHONE_NUMBER=…`, and `SEND_SMS_HOOK_SECRET` once the hook has generated one.
4. Deploy the function below with `supabase functions deploy send-sms --no-verify-jwt`. Supabase asks for the flag because the hook runs before any JWT exists; the Standard Webhooks signature protects the payload instead.
5. In the dashboard, under Authentication, Hooks, add the Send SMS hook with the HTTP type, point it at the function's URL and generate the secret.
## The function
The sample sends through Twilio's Messages API, the provider Supabase's own Send SMS Hook example uses; any provider's HTTP call fits in the same place.
```javascript
import { Webhook } from "https://esm.sh/standardwebhooks@1.0.0";
const hook = new Webhook(Deno.env.get("SEND_SMS_HOOK_SECRET").replace("v1,whsec_", ""));
const spawKey = Deno.env.get("SPAW_KEY");
const spawFeedbackKey = Deno.env.get("SPAW_FEEDBACK_KEY");
const twilioSid = Deno.env.get("TWILIO_ACCOUNT_SID");
const twilioToken = Deno.env.get("TWILIO_AUTH_TOKEN");
const twilioFrom = Deno.env.get("TWILIO_PHONE_NUMBER");
// Supabase Auth hands every answer back as JSON, errors included.
const reply = (body, status = 200) =>
new Response(JSON.stringify(body), { status, headers: { "Content-Type": "application/json" } });
// Supabase stores numbers without the leading plus; Spaw needs it.
const e164 = (value) => "+" + String(value ?? "").replace(/^\+/, "");
async function checkNumber(phone, clientIp) {
try {
const response = await fetch("https://spaw.co/api/v1/phone", {
method: "POST",
headers: {
Authorization: `Bearer ${spawKey}`,
"Content-Type": "application/json",
Accept: "application/json",
},
body: JSON.stringify(clientIp ? { phone, client_ip: clientIp } : { phone }),
signal: AbortSignal.timeout(2000),
});
return response.ok ? (await response.json()).data : null;
} catch {
return null;
}
}
Deno.serve(async (req) => {
let event;
try {
event = hook.verify(await req.text(), Object.fromEntries(req.headers));
} catch {
return reply({ error: { http_code: 401, message: "Invalid hook signature" } }, 401);
}
const { user, sms, metadata } = event;
const to = e164(sms.phone ?? user.phone);
// Never refuse a multi-factor code, or a confirmed number signing in again.
const isNewNumber = sms.sms_type !== "mfa" && (!user.phone_confirmed_at || e164(user.phone) !== to);
const answer = await checkNumber(to, metadata?.ip_address);
// null means Spaw did not answer in time: the code goes out unchecked.
if (answer && !answer.ok_to_send && answer.blocked_by !== "opted_out") {
console.log(JSON.stringify({ spaw_blocked_by: answer.blocked_by, new_number: isNewNumber }));
if (isNewNumber) {
return reply({ error: { http_code: 400, message: "This number cannot receive a verification code." } });
}
}
const sent = await fetch(`https://api.twilio.com/2010-04-01/Accounts/${twilioSid}/Messages.json`, {
method: "POST",
headers: {
Authorization: "Basic " + btoa(`${twilioSid}:${twilioToken}`),
"Content-Type": "application/x-www-form-urlencoded",
},
body: new URLSearchParams({
To: to,
From: twilioFrom,
Body: `Your code is ${sms.otp}`,
StatusCallback: `https://spaw.co/api/v1/phone/feedback/twilio/${spawFeedbackKey}`,
}),
});
if (!sent.ok) {
return reply({ error: { http_code: 502, message: "The SMS provider did not accept the message." } });
}
return reply({});
});
```
The request to Spaw carries the number and the visitor's address and nothing else: never the code, the message or anything from the user record. The code goes only to your SMS provider. The sample follows Supabase's documented payload; run it in your own project before you rely on it.
The refusal is returned with status 200 on purpose. Supabase Auth reads the `error` object in a 200 answer and returns its `http_code` and `message` to your app, while a bare `400` or `403` from an HTTP hook reaches the app as a `500`, as Supabase's hooks page says. Your app's `signInWithOtp()` or `updateUser()` call receives the error, and the screen can say that this number cannot receive a code and offer another way in.
## Which codes it refuses, and which it only logs
The payload names one flow: `sms.sms_type` is `"mfa"` for a multi-factor code, at enrolment and at every later sign-in alike, and is absent on the phone sign-up, sign-in, phone-change and reauthentication codes (Supabase Auth source, read 27 September 2026). For those the sample decides from the user record. A number with no `phone_confirmed_at`, or one that differs from the confirmed `user.phone` (a phone change sends the code to the new number), is being attached to the account: those are refused. A confirmed number signing in again is only logged, because refusing it would lock an existing customer out. A multi-factor code is only logged too, enrolment included, since the payload cannot tell the two apart: `user.phone` says nothing about an enrolled factor, and Supabase answers a refused MFA challenge with a bare 500 rather than your message.
A refusal names the rule in `blocked_by`; the [gate-SMS guide](/guides/gate-sms-verification-codes-with-the-sms-policy) explains each rule and the order they apply in. With the visitor's address, five distinct numbers from one address within 60 minutes add `numbers_per_client` (+30). An address Spaw's IP data places on a mobile carrier or a corporate proxy, where many people share one address, still answers the count but never adds the signal. Five distinct numbers in one 1,000-number range within 60 minutes add `range_burst` (+40). Either alone stays under the default ceiling of 60; the two together score 70 and answer `blocked_by: "risk_score"`.
Supabase's own controls, CAPTCHA protection and the SMS rate limits under Authentication, Rate Limits, act on the request before the hook is called. Keep them on; the Spaw check acts on the number.
## Why it fails open
When Spaw does not answer within two seconds, or answers with an error (`402 INSUFFICIENT_CREDITS` on an empty balance, `429` when the key's daily cap is spent or your phone requests exceed 50 a second, a `5xx`), `checkNumber()` returns null and the code is sent unchecked. Failing closed would turn a Spaw outage into an outage of your sign-in. The price is that an attack arriving during one of those failures is not filtered; log the nulls if you want to alert on them.
## Delivery receipts and verified codes
The `StatusCallback` in the sample points Twilio's delivery receipts at your [SMS delivery-receipt URL](/docs/api/sms-status-webhook). Spaw reads `delivered`, `undelivered` and `failed`, classifies the error code, and records an opt-out code such as Twilio 21610 as `opted_out`; receipts are free and feed only your own later lookups.
The outcome that separates real people from pumped traffic is `verified`: the person entered the code. Report it from server code that runs after `supabase.auth.verifyOtp()` returns a session, with the number in E.164 form (the `+` again: a number without it is skipped, not recorded):
```bash
curl -X POST https://spaw.co/api/v1/phone/feedback \
-H "Authorization: Bearer sk_live_…" \
-H "Content-Type: application/json" \
-d '{"items": [{"phone": "+12025550100", "outcome": "verified"}]}'
```
The [feedback summary](/docs/api/phone-feedback-summary) then reads `verified_share`, the share of numbers you reported delivered or verified that were verified, overall and for each of the ten blocks with the most failed deliveries. Pumped traffic is delivered and never verified, so a block whose share sits far under yours is the one to look at.
## Try the branches without Supabase
The documented test numbers answer a fixed result each, cost nothing and are never counted in your traffic. Under the default policy `+1 202 555 0103` answers `ok_to_send: false` with `blocked_by: "disposable"`, `+1 202 555 0105` answers `blocked_by: "fictional"`, and `+1 202 555 0102` (the test number that answers the `virtual` signal, score 40) and `+1 202 555 0100` answer `ok_to_send: true`. Post them to `https://spaw.co/api/v1/phone` with curl to see each branch before the hook is live.
## What to leave out
Leave `hlr` out of the request. The live carrier-network check (`hlr: true`) is not enabled on the service: `hlr_checked` answers false and nothing extra is charged. Leave the policy's `require_reachable` rule off for the same reason: while the check is not enabled, no lookup answers `ok_to_send: true` under it: every number that passes the other rules answers `blocked_by: "unconfirmed"`.
`ok_to_send` is a fraud and deliverability decision. It is not consent and not a do-not-call scrub, and whether the person agreed to hear from you stays yours to record.
## Cost
A lookup costs 1 credit when the number is valid. Invalid numbers, the test numbers and a repeat of the same number within seven days are free, so a resent code costs nothing. Delivery receipts and outcome reports are free. The SMS itself is billed by your provider. Spaw is not affiliated with Supabase.
---
# Send Amazon SES bounces to Spaw through SNS
Subscribe an SNS topic to your Spaw feedback URL so SES permanent bounces, complaints and deliveries update your suppression list and measured accuracy.
Updated: 2026-09-03
Amazon SES reports what happened to a message through Amazon SNS: SES publishes a notification to a topic, and SNS delivers it to every subscriber. Spaw is built to be one of those subscribers. Subscribe your Spaw feedback URL to the topic and permanent bounces land on your suppression list, deliveries count as confirmed mailboxes, and the History tab measures how accurate the verdicts were for your own sending.
## Set up the subscription
1. In the Spaw dashboard, open the API keys page and generate a feedback key. Copy the SES URL it shows:
```text
https://spaw.co/api/v1/email/feedback/ses/fb_…
```
2. In the AWS console, create an SNS topic, or reuse the one your SES notifications already publish to.
3. Point SES at the topic. Either add an event destination of type SNS to the configuration set you send with and select the Bounce, Complaint and Delivery event types, or set the topic on the verified identity's notification settings for the same three types.
4. Add a subscription to the topic with protocol HTTPS and the Spaw URL as the endpoint.
5. SNS sends a `SubscriptionConfirmation` message to the URL. Spaw confirms it by fetching the `SubscribeURL` in that message, but only when it is an https URL on an `amazonaws.com` host, so nothing else can make Spaw fetch arbitrary addresses. The subscription shows as confirmed within a few seconds; if AWS retries the confirmation, that is handled the same way.
Raw message delivery can be on or off. With it off, the notification arrives as an SNS envelope whose `Message` field is a JSON string, and Spaw decodes it; with it on, the bare SES notification arrives and is read directly. SNS posts JSON with a `text/plain` content type, and Spaw decodes the body as JSON regardless.
## What Spaw reads from the notification
Spaw looks at `notificationType`, or `eventType` for configuration-set events, and nothing else in the payload:
| SES notification | Recorded as |
| --- | --- |
| `Bounce` with `bounceType` Permanent | bounced, one outcome per address in `bouncedRecipients` |
| `Bounce` with `bounceType` Transient or Undetermined | ignored, SES retries these itself |
| `Complaint` | complained, one outcome per address in `complainedRecipients` |
| `Delivery` | delivered, one outcome per address in `delivery.recipients` |
For a bounce the recipient's `diagnosticCode` is stored as the reason, falling back to the `bounceSubType`; for a complaint the `complaintFeedbackType`. The notification's timestamp becomes the time of the outcome. A permanent bounce looks like this once unwrapped:
```json
{
"notificationType": "Bounce",
"bounce": {
"bounceType": "Permanent",
"bounceSubType": "General",
"timestamp": "2026-09-03T10:12:44.000Z",
"bouncedRecipients": [
{ "emailAddress": "mia@acme.com", "diagnosticCode": "smtp; 550 5.1.1 user unknown" }
]
}
}
```
## What Spaw does with the events
A permanent bounce or a complaint adds the address to your suppression list with source `feedback`. Batch, bulk and monitor runs then answer it from your list at no cost, with reason `suppressed` and a risk score of 100; a single lookup still re-verifies it but leaves the entry in place. A permanent bounce comes off the list when a later delivery to that address is reported; a complaint stays until you remove it. An address you imported stays until you remove it too: a complaint about it is recorded on your import, and no delivery clears it.
A delivery removes any entry Spaw had added automatically, and for 90 days the address counts as a confirmed mailbox for your own lookups, reported as `smtp_reason: "delivered_recently"` without a probe.
Each outcome is matched against the verdict you were given for the address in the previous 90 days, which is what the feedback summary endpoint and the History tab report: the delivery rate among addresses answered `deliverable` and the bounce rate among addresses answered `undeliverable`. Outcomes are kept for 180 days.
## Keep the URL private
SNS cannot send a bearer token, so the feedback key in the URL is the credential. Anyone holding the URL can add outcomes to your account. If it leaks, rotate the key on the API keys page: every old URL stops working at once and answers `401 INVALID_FEEDBACK_KEY`, and you update the SNS subscription with the new one.
## Cost
Feedback is free and never logged as a lookup. The endpoint is throttled at 120 requests per minute per IP; SNS retries a request that is throttled or times out on its own schedule.
---
# Spaw Python SDK: email, phone, IP and postal address checks
The official spaw-sdk package wraps every Spaw endpoint with typed results and one exception class per API code. Standard library only, Python 3.9 or newer.
Updated: 2026-10-14
The Python client is a thin wrapper over the REST API: one method per endpoint, the API's `data` and `meta` blocks returned exactly as documented, and a typed exception for every error code. It has no dependencies beyond the standard library, ships type hints, and works on Python 3.9 and newer.
The package is built and will be published on PyPI as `spaw-sdk`; the distribution name differs from the import name because `spaw` was already taken. Until the listing is live, the plain REST calls in the API reference do the same job with a few more lines.
## Install and verify one address
1. Create a secret key on the API keys page.
2. Install the package once it is published:
```bash
pip install spaw-sdk
```
3. Verify an address. The import name is `spaw`:
```python
from spaw import Client
client = Client("sk_live_…")
result = client.verify_email("mia@acme.com")
print(result.data["deliverable"]) # "deliverable", "risky" or "undeliverable"
print(result.data["reason"]) # e.g. "catch_all", or None when deliverable
print(result.data["risk_score"]) # 0–100, a sum of published weights
print(result.meta["credits_used"]) # 1 for a fresh answered lookup, 0 otherwise
```
Every `reason` and `smtp_reason` value is explained on the verdict reasons pages, and `result.data` carries all 46 documented fields, including `did_you_mean`, `mx_provider` and `smtp_checked_at`.
## Everything the client does
| Method | Endpoint |
| --- | --- |
| `verify_email(email, callback_url=None, callback_secret=None)` | `POST /email` |
| `verify_emails(emails)` | `POST /email/batch`, up to 50 |
| `domain(domain)` | `GET /email/domain/{domain}` |
| `list_bulk_jobs(page=1, status=None, since=None, until=None)`, `preview_bulk_job(emails=None, csv=None, webhook_url=None)`, `start_bulk_job(draft_id, mapping=None, idempotency_key=None)`, `create_bulk_job(emails, webhook_url=None)`, `get_bulk_job(id)`, `cancel_bulk_job(id)`, `resume_bulk_job(id)`, `redeliver_bulk_webhook(id)`, `download_bulk_results(id, variant=None, format=None)` | the bulk endpoints, up to 100,000 addresses; a run that stopped on an empty balance is resumed from the top of its stored list |
| `list_suppressions(page=1, value=None, source=None, reason=None, since=None, until=None, per_page=None)`, `add_suppressions(emails)`, `remove_suppression(id)`, `remove_suppressions(values)` | the suppression list, by id or by the address itself (up to 1,000 per call) |
| `report_feedback(items)`, `feedback_summary()` | delivery outcomes and measured accuracy |
| `account()`, `ledger(since=None, until=None, reason=None, via=None, api_token_id=None, page=None, per_page=None)`, `request_log(request_id)`, `data_status()` | the live credit balance, the credit ledger, and one request read back by the id it answered with |
| `send_test_webhook(url, event)` | `POST /webhooks/test`, a real signed delivery to a URL you name, free, so a signature check can be written before a paid run depends on it |
| `verify_webhook(raw_body, header, secret, tolerance=…)` | checks a delivery you received: the digest AND the timestamp in `X-Spaw-Signature-V2`, compared in constant time. A module-level function, not an API call |
| `validate_phone(phone, country=None, hlr=None, client_ip=None, call_window=None)`, `validate_phones(phones, country=None, hlr=None, call_window=None)` | `POST /phone` and `POST /phone/batch`, up to 1,000 numbers |
| `phone_block_report(country, prefix)` | `GET /phone/block/{country}/{prefix}`, the regulator's own allocation table for one range |
| `list_phone_bulk_jobs(page=1, …)`, `preview_phone_bulk_job(phones=None, csv=None, country=None, hlr=None)`, `start_phone_bulk_job(draft_id, mapping=None)`, `create_phone_bulk_job(phones, country=None, webhook_url=None)`, `get_phone_bulk_job(id)`, `cancel_phone_bulk_job(id)`, `resume_phone_bulk_job(id)`, `redeliver_phone_bulk_webhook(id)`, `download_phone_bulk_results(id, variant=None, format=None)` | the bulk phone endpoints, up to 100,000 numbers |
| `list_phone_suppressions(page=1, value=None, country=None, …)`, `add_phone_suppressions(items, country=None)`, `remove_phone_suppression(id)`, `remove_phone_suppressions(values, country=None)` | the phone suppression list; a value is read to E.164 before it is matched |
| `get_phone_policy()`, `update_phone_policy(max_risk_score=…, block_signals=…, allowed_countries=…, blocked_countries=…, blocked_line_types=…, require_reachable=…, block_opted_out=…)`, `reset_phone_policy()` | the account's SMS policy behind `ok_to_send` and `blocked_by` on every phone answer (`GET`, `PUT`, `DELETE /phone/policy`); a fraud and deliverability decision, not consent and not a do-not-call scrub |
| `lookup_ip(ip, privacy=None, abuse_contact=None, network_details=None, hostname=None, browser_timezone=None, fields=None)`, `lookup_my_ip()`, `lookup_ips(ips)` | `POST /ip`, `GET /ip/me`, `POST /ip/batch`, up to 1,000 addresses; `browser_timezone` fills `timezone_matches`, `fields` keeps only the named keys |
| `lookup_asn(asn)`, `lookup_ip_range(prefix)` | `GET /ip/asn/{asn}` (every prefix of one autonomous system) and `GET /ip/range?prefix=…` (what the feeds say about a whole prefix) |
| `list_ip_lists()`, `create_ip_list(name, kind, entries)`, `get_ip_list(id)`, `update_ip_list(id, …)`, `delete_ip_list(id)` | your own allow and deny lists, account-wide, applied to every lookup: an allow-list hit zeroes the score, a deny-list hit tops it |
| `list_ip_bulk_jobs(page=1, …)`, `preview_ip_bulk_job(ips=None, csv=None, privacy=None)`, `start_ip_bulk_job(draft_id, mapping=None)`, `create_ip_bulk_job(ips, webhook_url=None, privacy=None)`, `get_ip_bulk_job(id)`, `cancel_ip_bulk_job(id)`, `resume_ip_bulk_job(id)`, `redeliver_ip_bulk_webhook(id)`, `download_ip_bulk_results(id, variant=None, format=None)` | the bulk IP endpoints, up to 100,000 addresses |
| `report_ip_feedback(items)`, `ip_feedback_summary()` | IP outcomes (abuse, fraud, bot, chargeback), free |
| `get_email_policy()`, `update_email_policy(max_risk_score=…, block_signals=…, min_mailbox_confidence=…)`, `reset_email_policy()` | the account's sending policy behind `ok_to_send` and `blocked_by` on every email answer (`GET`, `PUT`, `DELETE /email/policy`) |
| `list_domain_monitors()`, `create_domain_monitor(name, domains, cadence, webhook_url=None, webhook_events=None)`, `get_domain_monitor(id)`, `update_domain_monitor(id, …)`, `run_domain_monitor(id)`, `delete_domain_monitor(id)` | monitored domain lists: up to 500 domains re-checked daily, weekly or monthly for mail-setup drift (`/email/domain-monitors`) |
| `list_ip_monitors()`, `create_ip_monitor(name, ips, cadence, webhook_url=None, webhook_events=None)`, `get_ip_monitor(id)`, `update_ip_monitor(id, …)`, `run_ip_monitor(id)`, `delete_ip_monitor(id)` | monitored IP lists with blocklist alerts, changeable in place and postable to a webhook |
| `verify_address(address, country=None, deliverability=None, fields=None)`, `verify_addresses(addresses, country=None)` | `POST /address` and `POST /address/batch`, up to 100 addresses; the separated fields include `building`, `fields` keeps only the named keys, and every answer carries the 61 fields incl. `unit_check`, `looks_placeholder`, `seen_by_you`, `ok_to_ship` and `blocked_by` |
| `list_address_bulk_jobs(page=1, …)`, `preview_address_bulk_job(addresses=None, csv=None, country=None, deliverability=None)`, `start_address_bulk_job(draft_id, mapping=None)`, `create_address_bulk_job(addresses, country=None, deliverability=None, webhook_url=None, idempotency_key=None)`, `get_address_bulk_job(id)`, `cancel_address_bulk_job(id)`, `resume_address_bulk_job(id)`, `redeliver_address_bulk_webhook(id)`, `download_address_bulk_results(id, variant=None, format=None)` | the bulk address endpoints, up to 100,000 addresses |
| `address_streets(country, postal_code)` | `GET /address/streets`, every street a national register lists inside a postcode, 1 credit |
| `addresses_at_postcode(postcode, country=None)` | `GET /address/postcode/{postcode}`: the same streets from an open register, or a licensed partner's delivery points where one is configured |
| `get_address_policy()`, `update_address_policy(max_risk_score=…, block_signals=…, require_exists=…)`, `reset_address_policy()` | the account's shipping policy behind `ok_to_ship` and `blocked_by` on every address answer (`GET`, `PUT`, `DELETE /address/policy`) |
| `list_address_monitors()`, `create_address_monitor(name, addresses, cadence, country=None, deliverability=None, webhook_url=None, webhook_events=None)`, `get_address_monitor(id)`, `update_address_monitor(id, …)`, `run_address_monitor(id)`, `delete_address_monitor(id)` | monitored address lists, alerting on newly flagged addresses |
| `report_address_feedback(items, country=None)`, `address_feedback_summary()` | postal outcomes (delivered, returned, moved, refused), free and never shared between accounts |
| `list_address_suppressions(page=1, value=None, country=None, …)`, `add_address_suppressions(items, country=None)`, `remove_address_suppression(id)`, `remove_address_suppressions(values, country=None)` | the address suppression list, up to 10,000 per import; a value is folded against its country before it is matched |
| `verify_entity(identifier, type, country=None)`, `verify_entities(identifiers)` | `POST /entity` and `POST /entity/batch`, up to 500 identifiers read against locally synced registers |
| `list_entity_monitors()`, `create_entity_monitor(name, identifiers, cadence, webhook_url=None, webhook_events=None)`, `get_entity_monitor(id)`, `update_entity_monitor(id, …)`, `run_entity_monitor(id)`, `delete_entity_monitor(id)` | monitored identifier lists, alerting when an entity is dissolved or its registration lapses |
| `list_monitors()`, `create_monitor(name, emails, cadence, webhook_url=None, webhook_events=None)`, `get_monitor(id)`, `update_monitor(id, …)`, `run_monitor(id)`, `delete_monitor(id)`, and the phone equivalents | monitored email and phone lists; an update sends only the fields you name, and `webhook_url=None` removes the webhook |
| `check_consistency(email=None, phone=None, country=None, ip=None, address=None)` | `POST /consistency`, any subset of the four signals compared against each other |
| `list_consistency_bulk_jobs()`, `create_consistency_bulk_job(items, country=None, webhook_url=None)`, `preview_consistency_bulk_job(items=None, csv=None, …)`, `start_consistency_bulk_job(draft_id, mapping=None)`, `get_consistency_bulk_job(id)`, `cancel_consistency_bulk_job(id)`, `resume_consistency_bulk_job(id)`, `redeliver_consistency_bulk_webhook(id)`, `download_consistency_bulk_results(id, variant=None)` | the same check over a whole list, queued: a row costs the sum of the legs it ran and the comparison is free |
A batch keeps the API's shape: `data["results"]` holds one `{index, input, data, meta}` per address in input order — `index` is its place in the list you sent and `input` the value as you sent it, because the answer carries the normalized form, and `meta["stopped_reason"]` is set instead of an exception when the balance runs out part-way, so paid results are never lost.
A bulk job or a monitor returns its `webhook_secret` once; keep it, because it signs the deliveries. `spaw.verify_webhook(raw_body, header, secret)` is the other half: pass the raw body and the `X-Spaw-Signature-V2` header and it answers whether the delivery is ours, unmodified and recent. It checks the timestamp as well as the digest, which is the point of the header — a signature over the body alone never goes stale, so a captured delivery could be posted back at you a year later and still verify. The original `X-Spaw-Signature`, with no timestamp in it, is sent beside it until 1 March 2027 and is deliberately not accepted by this call.
A delivery that failed is not lost: ``redeliver_bulk_webhook(job_id)`` (and its phone, IP, address and cross-signal twins) posts that run's completion webhook to your URL again, for the day your endpoint was down for the half-minute the three attempts cover. The body is byte-for-byte the one the first delivery carried, rebuilt from the run's own frozen row, so the two reconcile — but the signature is stamped afresh, and has to be, because `verify_webhook` refuses anything whose timestamp is more than five minutes old. Verify a redelivery with the same call you verify everything else with. Nothing deduplicates it for you: a receiver that gets both sees the event twice, and `job.id` is the key to deduplicate on. A run created without a `webhook_url` raises `ConflictError` with code `WEBHOOK_NOT_CONFIGURED` and one that has not settled `JOB_NOT_FINISHED`; a run whose last delivery succeeded is not refused, because a receiver can lose a message it acknowledged. The delivery is queued rather than made while you wait — one attempt takes half a minute against an endpoint that is down — so the call answers `202` and the run it hands back reads `webhook_status: "pending"` until a worker has an answer; read the outcome with `get_bulk_job(job_id)`. It is free and capped at ten a minute per signed-in user, neither per key nor per account.
## Addresses
`verify_address` takes the address as one written line or as a mapping of the separated fields (`address_line1`, `locality`, `postal_code` and the rest), always with a `country`: that is what decides which country's format it is read under, and it is never guessed. The answer splits the address into components, formats it the way that country writes addresses, and says what open data can establish — the postcode's status and centroid, whether the town matches it, what kind of delivery point it is, how close a national address register comes to it, and how many companies are registered there.
```python
address = client.verify_address("10 Downing Street, London SW1A 2AA", country="GB")
address.data["formatted"] # the address as that country writes it
address.data["postcode_status"] # "live", "terminated", or None where no directory covers the country
address.data["address_type"] # street, po_box, private_mailbox, general_delivery, …
address.data["risk_signals"] # the weights behind risk_score
client.verify_addresses(addresses, country="GB") # up to 100 per call; an item's own country wins
```
For a whole list, queue a bulk run instead of looping: up to 100,000 rows in the background, billed per row on exactly the single-lookup rules, with in-list repeats free and counted in `duplicate_count`.
Read the list before you start it: the `preview_bulk_job()` call (and its phone, IP and address twins) answers what we made of it — which column filled which field and whether a heading named it or we guessed, the first rows as parsed, the repeats, and `credits_upper_bound` — and queues nothing. That figure is a CEILING, never a quote: a row can be free for reasons no preview can know in advance, so a run usually costs less and never more. Start it with the `start_bulk_job()` call, correcting a column we read wrongly with `mapping`.
```python
job = client.create_address_bulk_job(
addresses, country="GB", idempotency_key="addresses-2026-09-05"
).data["job"]
client.get_address_bulk_job(job["id"]) # status, counts, credits_used
client.download_address_bulk_results(job["id"]) # or variant="invalid" / "high"
```
A field answers `None` when its dataset does not cover the country, which means "not evaluated" and never "no". `exists` is `True` only on a premise match and `False` only under a register whose coverage is complete: a register whose coverage is partial, because it covers part of the country or cannot vouch for every address, cannot make an address's absence mean anything, so it answers `None`. `is_mail_drop` is a heuristic built from mailbox operators' own published branch lists and from how many companies are registered at an address, never a postal service's own agency flag; `mail_drop_source` says which of the two fired.
One answer is licensed and not enabled. `deliverability=True` would ask a partner whether post actually arrives and whether the delivery point is a mail-receiving agency, vacant or residential; until one is configured `deliverability_checked` comes back False, the four fields stay None and the premium credits are charged only where a partner answered. The streets inside a postcode are an open answer wherever a national register with a street listing is installed: `address_streets(country, postal_code)` lists them for 1 credit, `addresses_at_postcode` answers the same streets as one entry each (a licensed partner's delivery points, 2 credits, where one is configured), and a country neither covers (Italy, for one), and a postcode the listing cannot speak for — a Northern Irish BT, forces BFPO or non-geographic BX code, or a German code outside Berlin — raises `NotEnabledError` (`ADDRESS_LIST_UNAVAILABLE`), free; an ordinary British postcode answers its district's streets from Ordnance Survey's Open Names, never its buildings. A postcode the register lists no street at raises `NotFoundError` (`ADDRESS_STREETS_NOT_FOUND`), free.
A saved list of up to 500 addresses is re-read on a `weekly` or `monthly` cadence — there is no daily one, because the registers and postcode directories an address is measured against are republished weekly at best. Every run answers each address `ok`, one of the flagged `invalid`, `not_in_register`, `terminated_postcode`, `mail_drop` and `high_risk`, or `unverifiable`, which means the address stood as written but no dataset installed on this server covers it well enough to check — a limit of the data, never a property of the address, and never an alert. An email goes out only for an address that was already known and unflagged and became flagged; a first run and a newly added address only set the baseline, and one that stays flagged never alerts twice. Runs bill per address exactly like a single lookup.
```python
monitor = client.create_address_monitor("Supplier sites", addresses, "monthly", country="GB").data["monitor"]
client.get_address_monitor(monitor["id"]).data["monitor"]["results"] # {address: {status, flags, changed, …}}
client.run_address_monitor(monitor["id"]) # re-check now
```
Tell the service what happened to the post you sent and it keeps your own suppression list. Outcomes are `delivered`, `returned`, `moved` and `refused`. A `moved` or `refused` report suppresses the address at once; a second `returned` not followed by a later `delivered` suppresses it; a `delivered` report clears an entry the feedback put there, and never touches one you imported. Batch, bulk and monitor runs answer a suppressed address free with `meta["suppressed"]` true, a single lookup never consults the list, and drops an entry your reports added once they no longer stand behind it (a delivery since, or reports aged out); an import leaves only when you remove it; and an entry your reports added is re-verified rather than served once it is older than 90 days, while an import never goes stale.
```python
client.report_address_feedback([ # free, and never logged as a lookup
{"address": "10 Downing Street, London SW1A 2AA", "outcome": "returned"},
{"address_line1": "Postfach 10 01 01", "country": "DE", "outcome": "moved"},
], country="GB")
client.address_feedback_summary().data["outcomes"] # your own counts, by outcome
client.add_address_suppressions(dead_addresses, country="GB") # up to 10,000, free
client.list_address_suppressions().data["suppressions"]
client.remove_address_suppression(suppression_id) # read fresh next time
```
Unlike the IP and phone products, address feedback is never shared between accounts. An IP is a network endpoint and a phone number is a handset, but a postal address is where somebody lives, and a shared "post fails here" database would be a database about residences. Your reports raise the risk of your own later lookups of an address and nobody else's, through `reported_returned` (+50, dataset `address-feedback`), which is scored while your most recent report for the address is `returned`, `moved` or `refused` rather than `delivered`.
## Businesses
Business verification reads one identifier — a Legal Entity Identifier or a UK company number — against the registers this server holds, which is the check you want before signing with a counterparty or when a supplier list needs going over again. Both registers are bulk files synced locally, so no third party sits in the request path and nothing about a lookup leaves the server; the LEI reference data is published under a CC0 public-domain dedication, and Spaw is not affiliated with its publisher. A company the national register records as dissolved is the strongest signal the product carries: a dissolved company no longer exists as a legal person and cannot enter a contract. A monitor that stops being run is deleted and the identifiers it stored go with it — `prunes_after_days` on every monitor payload says how long that takes — because an identifier can belong to a sole trader as easily as to a company.
## Cross-signal consistency
`check_consistency` runs any subset of the four signals in one call and reports where their answers agree and where they do not: it is built for signup, onboarding and marketplace vetting, the moment before there is an order or a card and the contact details are all you have. Each signal bills exactly as its own single lookup, and the analysis on top is free, because every comparison is arithmetic over fields the four pipelines already computed. There is no score and no verdict — you get named flags and both sides of every comparison quoted, because a difference between two fields is a fact about the fields and not a judgement about a person.
`check_consistency` is one record at a time. `preview_consistency_bulk_job` and `start_consistency_bulk_job` run the same check over a list of up to 100,000 rows, so scoring a signups export is one upload rather than four and a spreadsheet join afterwards. One header row is read as an email address, a phone number, an IP address and the address fields at once, and a row that resolves only two of the four is a perfectly good consistency check of those two.
The preview prices the run against the SIGNALS the columns feed rather than one credit a row — a file reaching only an email address and a phone number answers `credits_per_row` 2, not 4 — and it is still a ceiling and never a quote.
```python
draft = client.preview_consistency_bulk_job(csv=open("signups.csv").read(), country="GB")
draft.data["draft"]["mapping"] # {"1": "email", "2": "phone", "3": "ip", "4": "address_line1", …}
draft.data["draft"]["credits_upper_bound"] # the most it can cost, never what it will cost
job = client.start_consistency_bulk_job(draft.data["draft"]["id"])
rows = client.download_consistency_bulk_results(job.data["job"]["id"], variant="flagged")
```
A result row is the comparison, not four lookups pasted together: `signals_requested`, `signals_answered`, `country_agreement`, `flags`, one cell per comparison, `distance_km`, and the few per-product fields those comparisons read. A comparison cell reads `agree`, `differ`, `not_compared`, `missing_value`, `low_confidence`, or nothing at all — an empty cell is a comparison never attempted, because the row did not carry both of its signals.
```python
report = client.check_consistency(
email="ada@example.com",
phone="+442079460018",
country="GB", # the region hint for the phone number only
ip="81.2.69.142",
address={"address": "10 Downing Street, London SW1A 2AA", "country": "GB"},
)
report.data["consistency"]["flags"] # e.g. ["ip_country_differs_from_address_country"]
report.data["consistency"]["country_agreement"] # "agree", "differ", or None when none could be made
report.data["signals"]["ip"]["data"] # each product's own result, unchanged
report.meta["credits_used"] # the sum of the per-signal costs
```
## Handle errors
Every failure raises a subclass of `SpawError` carrying `code`, `message`, `http_status` and `request_id`; no credits are spent on a failed request.
```python
from spaw import InsufficientCreditsError, RateLimitedError, SpawError
try:
result = client.verify_email("mia@acme.com")
except InsufficientCreditsError:
... # nothing ran and nothing was billed
except RateLimitedError as error:
... # error.retry_after seconds, from the Retry-After header
except SpawError as error:
print(error.code, error.http_status, error.request_id)
```
`AuthenticationError` maps `UNAUTHENTICATED`, `ValidationError` maps `VALIDATION_FAILED` with `errors` keyed by field, `NotFoundError` covers a bulk job or suppression entry that is not on the account, and `SpawConnectionError` means the API could not be reached. Pass `max_retries=2` to the client to retry `429` and `5xx` answers automatically, honouring `Retry-After`.
## What to do with the verdict
Branch on `deliverable`. Keep `deliverable` addresses, drop `undeliverable` ones, and route `risky` ones by `reason`: `disposable` and `likely_typo` are usually refused, `role` depends on your use case, and `catch_all` is often a fine address whose mailbox simply cannot be confirmed, in which case `mailbox_confidence` gives you a number to threshold. Offer `did_you_mean` back to the user when it is set.
## Cost
One credit per fresh deliverable or risky verdict, and one per five IP lookups. Unverified answers, invalid input, repeats within seven days and the `spaw.test` test addresses cost nothing, undeliverable verdicts cost nothing within a fair-use allowance, and every account gets 100 free credits a month. Use `deliverable@spaw.test` and its siblings in your test suite: they answer fixed results, cost nothing and never appear in your history.
---
# Verify emails and phone numbers for Klaviyo with Spaw
Verify Klaviyo profiles' emails and SMS phone numbers through Zapier or Make, write the E.164 number and the SMS policy answer back, and clean a list in bulk.
Updated: 2026-09-27
Klaviyo profiles arrive from storefront sign-ups, pop-ups and checkout, and a welcome flow fires within minutes. Verifying the address before that first send keeps bounces out of the account's reputation. Spaw has no Klaviyo app; the flow below uses Klaviyo's trigger in Zapier or Make, a verification step, and a profile update that stores the verdict as a custom property your segments can read. A second flow does the same for the phone number an SMS flow will use, and hands Klaviyo the E.164 form it requires.
## Verify new profiles
1. Create a secret API key on the API keys page of your Spaw dashboard.
2. In Zapier or Make, start with the Klaviyo trigger for a new profile or a new list subscription.
3. Add the verification step: a Webhooks by Zapier POST or a Make HTTP request to `https://spaw.co/api/v1/email` with the header `Authorization: Bearer sk_live_…` and this body, mapping the profile's email:
```json
{
"email": "mia@acme.com"
}
```
The [Zapier page](/integrations/zapier) and the [Make page](/integrations/make) list every setting. Once the Spaw apps for those platforms are listed, the step becomes a Verify Email action with a saved connection.
4. Add the Klaviyo action that updates a profile, match on the email, and write custom properties from the response: `spaw_verdict` from `data.deliverable`, `spaw_reason` from `data.reason`, and `spaw_risk_score` from `data.risk_score`.
5. Build a segment in Klaviyo where `spaw_verdict` equals `undeliverable` and exclude it from your flows and campaigns, or invert it and send only to profiles whose verdict equals `deliverable`.
6. Test by subscribing `deliverable@spaw.test`, a free test address that answers a fixed result and is never logged.
Because the welcome flow may fire before the Zap finishes, add a short delay at the start of the flow, or make the first email conditional on the property being set. A few minutes is enough.
## Clean an existing list
Export the list or segment to CSV, run the email column through a bulk verification on the Spaw dashboard or with the [bulk endpoint](/docs/api/create-bulk-job), and import the result CSV back into Klaviyo with the verdict columns mapped to the same custom properties. The result keeps your original columns. Undeliverable rows also join your Spaw suppression list, so the next run answers them without a probe.
## What to do with the verdict
Keep the decision in segments so it applies to every flow at once. `deliverable` profiles get the full program. `risky` profiles need the reason: [`disposable`](/docs/reasons/disposable) addresses are throwaway inboxes that will never convert, `catch_all` addresses are unconfirmed but often real, and `role` inboxes such as info@ are shared. `undeliverable` profiles should be suppressed in Klaviyo rather than deleted, because a filled `data.did_you_mean` is a customer who mistyped at checkout and can be reached through another channel, for example SMS, with the corrected address. For stores on Shopify, the [Shopify page](/integrations/shopify) covers the customer-side flow.
## Phone numbers for SMS
Klaviyo's profile API takes the phone number "in E.164 format", and its help centre adds that for API calls "Klaviyo only accepts the E.164 format" (Klaviyo's developer reference and its article on accepted phone number formats for SMS, both read 27 September 2026). A Zap or scenario that carries a number from a form, a CRM or a store into a profile therefore needs the E.164 form first, and the phone step returns it together with the checks.
1. Give the phone step a Zap or scenario of its own, with a filter that continues only when a phone number is present, so the email flow is left as it is and a profile without a number skips the step (an empty `phone` is refused with a `422`). Place the step before the Klaviyo action when the number comes from another app, or after the new-profile trigger when it is already in Klaviyo: a Webhooks by Zapier POST or a Make HTTP request to `https://spaw.co/api/v1/phone` with the same header and this body, mapping the number and, for numbers written without a calling code, the two-letter country:
```json
{
"phone": "(202) 555-0100",
"country": "US"
}
```
`country` is an ISO 3166-1 alpha-2 code such as `US` or `GB`. A country name, or `UK`, is refused with a `422`, and a number written with its + and calling code needs no country. Send the number and its country only, nothing else about the person. Once the Spaw apps are listed, the step becomes the Validate Phone Number action in Zapier and Validate a Phone Number in Make.
2. Add a Paths step in Zapier or a router in Make on `data.valid`. On the valid path, the Klaviyo action writes the profile's phone number from `data.e164`; on the other, it keeps what was typed in a custom property for someone to review. A filled `data.did_you_mean` there is a suggestion for that person to confirm, never a value to write over the number.
3. On both paths, write `spaw_phone_ok_to_send` from `data.ok_to_send` and `spaw_phone_blocked_by` from `data.blocked_by` as custom properties.
4. In the SMS flow, add a conditional split on `spaw_phone_ok_to_send` after the short delay described above, so a profile the Zap has not reached yet waits instead of receiving the message.
5. Test with +1 202 555 0100 and +1 202 555 0103, two documented test numbers that cost nothing: the first answers a valid mobile with `ok_to_send` true, the second `ok_to_send` false with `blocked_by` `disposable`, under the default SMS policy.
The step writes the phone number and two properties, nothing else. It never subscribes a profile to SMS or changes its consent: that stays with the form or checkout that collected the number. `ok_to_send` is your account's [SMS policy](/guides/gate-sms-verification-codes-with-the-sms-policy) applied to the answer, set once on the account so every Zap, batch and bulk run applies the same rules. It is a fraud and deliverability decision, not consent and not a do-not-call scrub. When it is false, `blocked_by` names the rule that decided it: `invalid`, `opted_out` (by default) for a number your suppression list holds as opted out, `country`, `line_type`, a signal such as `disposable` for a number published by an SMS-receiving website, where anyone can read a code sent to it, or `risk_score`. Every field is on the [phone endpoint page](/docs/api/validate-phone).
## Cost
Each new profile that answers deliverable or risky spends 1 credit; unverified answers and repeats within seven days are free, and undeliverable answers are free within a fair-use allowance. A phone lookup costs 1 credit when the number is valid; invalid numbers, unassigned blocks and repeats of the same number within seven days are free. Zapier and Make bill their own tasks. Bulk runs bill per processed row and verify duplicates once.
---
# Verify emails and phone numbers in Airtable with Spaw
Verify the emails and phone numbers of new Airtable records through Zapier, Make or an automation script that calls the Spaw API, and write the answers back.
Updated: 2026-09-27
Airtable bases collect addresses from forms, imports and interfaces, and a verdict column next to the email column is the simplest way to keep them honest. There are three ways to fill it: a Zap or Make scenario that reacts to new records, an Airtable automation that runs a short script, and a bulk run for the rows already there. All three write the same fields. A phone column works the same way with its own fields, as the phone section shows.
## Prepare the table
1. Add three fields: a single select or text field named Verdict, a text field named Reason, and a number field named Risk score. A fourth text field for the suggested correction is worth having.
2. Create a view that shows records whose Verdict is empty; the flows below use it as their trigger and their to-do list.
3. Create a secret API key on the API keys page of your Spaw dashboard.
## Verify new records through Zapier or Make
1. Start the flow with the Airtable trigger for a new record in the view from step 2.
2. Add the verification step: a Webhooks by Zapier POST or a Make HTTP request to `https://spaw.co/api/v1/email` with the header `Authorization: Bearer sk_live_…` and this body, mapping the record's email field:
```json
{
"email": "mia@acme.com"
}
```
The [Zapier page](/integrations/zapier) and the [Make page](/integrations/make) show the step in full. When the Spaw apps for those platforms are listed, it becomes a Verify Email action with a saved connection.
3. Add the Airtable action that updates a record, match on the record id from the trigger, and map `data.deliverable`, `data.reason`, `data.risk_score` and `data.did_you_mean` into the four fields. Filling Verdict removes the record from the view, so it is not processed twice.
## Verify inside an Airtable automation
Airtable automations can run a script with network access, which removes the need for a third tool. Create an automation triggered when a record enters the empty-Verdict view, add a script action, pass the record id and email as input variables, and use this script:
```js
const { recordId, email } = input.config();
const table = base.getTable('Leads');
const response = await fetch('https://spaw.co/api/v1/email', {
method: 'POST',
headers: { 'Authorization': 'Bearer sk_live_…', 'Content-Type': 'application/json' },
body: JSON.stringify({ email }),
});
const result = await response.json();
if (result.success === false) {
throw new Error(`Spaw: ${result.error.code} ${result.error.message}`);
}
await table.updateRecordAsync(recordId, {
'Verdict': result.data.deliverable,
'Reason': result.data.reason ?? '',
'Risk score': result.data.risk_score,
'Suggestion': result.data.did_you_mean ?? '',
});
```
The key sits in the script, so anyone who can edit the automation can read it; give the base its own key and revoke it from the API keys page if the base is shared widely. Test with `deliverable@spaw.test`, a free test address that answers a fixed result and is never logged.
## Clean a whole table
Export the table to CSV, run the email column through a bulk verification on the Spaw dashboard or with the [bulk endpoint](/docs/api/create-bulk-job), and import the result back with the verdict columns mapped to the four fields, matching on the primary field. The result keeps your original columns. For a spreadsheet rather than a base, the [Google Sheets page](/integrations/google-sheets) offers a custom function instead.
## What to do with the verdict
Filter views on Verdict. `deliverable` records are ready for outreach. `risky` records need the Reason: `catch_all` means an unconfirmed mailbox on a domain that accepts everything, `role` means a shared inbox, `disposable` means a throwaway address. `undeliverable` records should be kept out of sends, and a filled Suggestion field is usually a person worth one correction.
## Phone numbers
A phone column gets its own fields and its own view, and either path above can fill them.
1. Add four fields: Phone E.164 and Line type as text, OK to send as a checkbox, and Blocked by as text.
2. Create a view that shows records whose phone field is filled and whose Phone E.164 and Blocked by are both empty. Every answer fills one of the two, so a record leaves the view once it has been checked.
3. Through Zapier or Make, start a second flow on a new record in that view, add a Webhooks by Zapier POST or a Make HTTP request to `https://spaw.co/api/v1/phone` with the same header and a body of `{ "phone": "(202) 555-0100", "country": "US" }`, mapping the phone field, and map `data.e164`, `data.line_type`, `data.ok_to_send` and `data.blocked_by` into the four fields. An invalid number in a block nobody holds still comes back with its parsed E.164 form, so read Phone E.164 together with OK to send, which is false for every invalid number. Once the Spaw apps are listed, the step becomes the Validate Phone Number action in Zapier and Validate a Phone Number in Make.
Inside Airtable, create a second automation triggered when a record enters the view, pass the record id, the phone number and, if the table has one, a two-letter country field as input variables, and use this script:
```js
const { recordId, phone, country } = input.config();
const table = base.getTable('Leads');
const response = await fetch('https://spaw.co/api/v1/phone', {
method: 'POST',
headers: { 'Authorization': 'Bearer sk_live_…', 'Content-Type': 'application/json' },
body: JSON.stringify({ phone, country: country || null }),
});
const result = await response.json();
if (result.success === false) {
throw new Error(`Spaw: ${result.error.code} ${result.error.message}`);
}
await table.updateRecordAsync(recordId, {
'Phone E.164': result.data.valid ? result.data.e164 : '',
'Line type': result.data.line_type ?? '',
'OK to send': result.data.ok_to_send,
'Blocked by': result.data.blocked_by ?? '',
});
```
`country` must be an ISO 3166-1 alpha-2 code such as `US` or `GB`, and it matters only for numbers written without their + and calling code; a national number sent without one answers `valid: false` with the reason `missing_country`, free. A country name, or `UK`, is refused with a `422`, which the script turns into an error on the automation run. The script keeps Phone E.164 empty for an invalid number, because an invalid number in a block nobody holds still carries its parsed form. Test with +1 202 555 0100 and +1 202 555 0103, two documented test numbers that cost nothing: the first answers a valid mobile with `ok_to_send` true, the second `ok_to_send` false with `blocked_by` `disposable`, under the default SMS policy.
Filter the views that feed a text message on OK to send. It is your account's [SMS policy](/guides/gate-sms-verification-codes-with-the-sms-policy) applied to the answer, set once on the account so every automation, Zap, batch and bulk run applies the same rules, and Blocked by names the rule that decided a false: `invalid`, `opted_out` (by default) for a number your suppression list holds as opted out, `country`, `line_type`, a signal such as `disposable` for a number published by an SMS-receiving website, where anyone can read a code sent to it, or `risk_score`. It is a fraud and deliverability decision, not consent and not a do-not-call scrub. `line_type` comes from the numbering plan or, for a North American number, from the holder of its block in the regulator's table; it describes how the range was allocated, never the network that serves the number after a port, and an ordinary US number answers `fixed_line_or_mobile` unless its block holder settles the type ([why](/guides/why-us-numbers-cannot-be-split-into-mobile-and-landline)). Every field is on the [phone endpoint page](/docs/api/validate-phone).
## Cost
Each record that answers deliverable or risky spends 1 credit; unverified answers and repeats within seven days are free, and undeliverable answers are free within a fair-use allowance. A phone lookup costs 1 credit when the number is valid; invalid numbers, unassigned blocks and repeats of the same number within seven days are free. Zapier and Make bill their own tasks; Airtable automation runs count against your Airtable plan. Bulk runs bill per processed row and verify duplicates once.
---
# Verify emails and phone numbers in Pipedrive with Spaw
Verify each new Pipedrive person's email and phone number through Zapier or Make, write the answers to custom fields, and clean the database in bulk.
Updated: 2026-09-27
Pipedrive is a sales CRM, and the addresses in it come from web forms, imports and reps typing fast. Spaw does not offer a Pipedrive marketplace app, because a Zap or a Make scenario gives the same result without asking for access to your account: a new-person trigger, a verification step, and an update-person action that writes the verdict to a custom field. The person's phone number takes a second flow of the same kind.
## Set up the fields
1. In Pipedrive, add custom fields on the person: a single-option or text field for the verdict, a text field for the reason, and a numeric field for the risk score. Fields on the person rather than the deal keep the verdict attached to the address.
2. Create a secret API key on the API keys page of your Spaw dashboard.
## Verify new people as they arrive
1. In Zapier or Make, start the flow with the Pipedrive trigger for a new person.
2. Add the verification step. Until the Spaw apps for Zapier and Make are listed, this is a Webhooks by Zapier POST or a Make HTTP request to `https://spaw.co/api/v1/email` with the header `Authorization: Bearer sk_live_…` and this body, mapping the address from the trigger:
```json
{
"email": "mia@acme.com"
}
```
The [Zapier page](/integrations/zapier) and the [Make page](/integrations/make) show the exact configuration.
3. Add a filter so the flow continues only when the person has an email.
4. Add the Pipedrive action that updates a person, and map the response's `data.deliverable`, `data.reason` and `data.risk_score` into the three custom fields. Zapier presents nested fields with a `data` prefix; Make shows them under the parsed response.
5. Turn the flow on and test it by creating a person with the address `deliverable@spaw.test`, which answers a fixed deliverable result at no cost and never appears in your history.
A person can carry more than one email in Pipedrive. Verify the primary one, or add a second verification step for the other addresses if reps rely on them.
## Clean the existing database
For people already in Pipedrive, export them to a spreadsheet, and run the email column through a bulk verification on the Spaw dashboard or with the [bulk endpoint](/docs/api/create-bulk-job). The result CSV keeps your original columns and adds the verdict, so it imports straight back with the person id as the match key. Rows that answer undeliverable are added to your suppression list automatically, and later batch and bulk runs answer them without a probe. The [list cleaning checklist](/guides/email-list-cleaning-checklist) explains what to do with each group.
## What to do with the verdict
Use the verdict field in Pipedrive filters and automations. A `deliverable` person is ready for sequences. A `risky` person needs a glance at the reason: `catch_all` means the company's mail server accepts every address, so the mailbox is unconfirmed but often real, `role` means a shared inbox such as sales@, and `disposable` means a throwaway domain. An `undeliverable` person should not enter a sequence; when the response's `data.did_you_mean` is filled, the address was a near miss and the suggested spelling can be written back as a note for the rep. `risk_score` is a 0 to 100 sum of published weights for teams that prefer one threshold.
## Phone numbers
Pipedrive keeps a list of phone numbers on each person, each with a label and a flag for the primary one (Pipedrive's API reference for persons, read 27 September 2026). Validate the primary number, as with email.
1. Add four text custom fields on the person: the E.164 number, the line type, whether sending is allowed, and the reason when it is not.
2. Start a second Zap or scenario on the same new-person trigger, with a filter that continues only when the person has a phone number (an empty `phone` is refused with a `422`). In it, add a Webhooks by Zapier POST or a Make HTTP request to `https://spaw.co/api/v1/phone` with the same header and this body, mapping the primary phone number:
```json
{
"phone": "(202) 555-0100",
"country": "US"
}
```
A number stored with its + and calling code needs no `country`. For numbers reps type in national format, send the ISO 3166-1 alpha-2 code of their country: typed into the step when your people are all in one country, or mapped from a custom field that holds the two-letter code. A country name, or `UK` (the code is `GB`), is refused with a `422`, and a national number sent without a country answers `valid: false` with the reason `missing_country`, free. Send the number and its country only, nothing else about the person. Once the Spaw apps are listed, the step becomes the Validate Phone Number action in Zapier and Validate a Phone Number in Make.
3. Give that flow an update-person action of its own that maps `data.e164`, `data.line_type`, `data.ok_to_send` and `data.blocked_by` into the four fields. Kept separate, a person without a phone number still gets its email verdict, and a person without an email still gets its phone number checked.
4. Test with +1 202 555 0100 and +1 202 555 0103, two documented test numbers that cost nothing: the first answers a valid mobile with `ok_to_send` true, the second `ok_to_send` false with `blocked_by` `disposable`, under the default SMS policy.
`data.e164` is the form to dedupe people by. It is also kept for an invalid number in a block nobody holds, so read it together with the sending field, which is false for every invalid number. Before a step that texts the person, filter on that field: `ok_to_send` is your account's [SMS policy](/guides/gate-sms-verification-codes-with-the-sms-policy) applied to the answer, set once on the account so every Zap, batch and bulk run applies the same rules, and `blocked_by` names the rule that decided a false. It is a fraud and deliverability decision, not consent and not a do-not-call scrub. `line_type` comes from the numbering plan or, for a North American number, from the holder of its block in the regulator's table; it describes how the range was allocated, never the network that serves the number after a port, and an ordinary US number answers `fixed_line_or_mobile` unless its block holder settles the type ([why](/guides/why-us-numbers-cannot-be-split-into-mobile-and-landline)). Leave the complaint counts (`complaints_90d`, `complaints_last_at`) off person records: they count the complaints consumers sent the FTC naming a number as the caller, which is not the Do Not Call Registry and says nothing about whether you may call or text the number, and the reports are unverified, with caller ID often spoofed. Every field is on the [phone endpoint page](/docs/api/validate-phone).
## Cost
Each new person that answers deliverable or risky spends 1 credit; unverified answers and repeats within seven days are free, and undeliverable answers are free within a fair-use allowance. A phone lookup costs 1 credit when the number is valid; invalid numbers, unassigned blocks and repeats of the same number within seven days are free. Zapier and Make count their own tasks or operations. A bulk run bills each row the same way as it is processed, and duplicates inside the file are verified once.
---
# Verify emails, phones and addresses on any website form
One script tag adds live email, phone and address checks to Webflow, Framer, Squarespace, Carrd or plain HTML forms: hints only, never blocking.
Updated: 2026-09-10
The Spaw form helper is a small script that watches the email, phone and address inputs you mark, verifies the value when the visitor leaves the field, and renders a hint: for an email address a "Did you mean …?" correction when it looks like a typo and a notice when it cannot receive mail; for a phone number its international format when it is valid, a note when it is fictional, shared on SMS-receiving sites or virtual, and the reason when it is not valid; for a shipping or billing address the address as the country writes it, a note when it is a box or a locker rather than a front door, and the reason when it cannot stand as written. It never blocks a submit and never waits for the API, so a slow network or an empty credit balance only means the hint does not appear. It works on any page where you can add custom code, which covers Webflow, Framer, Squarespace, Carrd and a hand-written HTML form alike.
## Add it to a form
1. On the API keys page, create a publishable key and lock it to the domains your form runs on. A publishable key can only run single lookups from those origins, so it is safe in page source.
2. Give the key a daily credit cap. Once the cap is spent the browser endpoint answers `429 KEY_SPEND_CAP_REACHED` until the next day, and the helper stays quiet.
3. Add the script and mark the input:
```html
```
4. For forms open to the public, store a Cloudflare Turnstile site key and secret on the publishable key and add `data-turnstile-site-key` to the script tag. The helper then fetches a fresh single-use token before every lookup and the endpoint refuses lookups without one, so a bot scripting your form cannot spend your credits. Turnstile only shows a challenge when Cloudflare needs one.
## Phone fields
Mark the phone input `data-spaw-phone`. Numbers typed without a calling code need a country: fix it with `data-spaw-country="GB"` on the input, or name a country select on the same form with `data-spaw-country-field` and the helper reads the ISO code from it on every check.
```html
```
The hint shows the international format of a valid number, a note for fictional, disposable (shared on SMS-receiving websites) and virtual numbers, and a plain-words reason for an invalid one; a number in a block the regulator has not allocated reads "not in service". The live carrier check is never run from the browser.
## Address fields
Mark the address input `data-spaw-address`. An address only means something measured against one country's postal format, so this mode always needs a country: `data-spaw-country="GB"` on the input, or a country select on the same form named by `data-spaw-country-field`. Until a country is there the helper stays quiet rather than spending a round trip on a lookup that would be refused, and the address is checked as soon as the select has an answer: the helper listens to that select as well as to the address field, so the ordinary checkout order — address typed, country picked afterwards — still gets the address checked, and picking the country again for a different one re-checks it.
```html
```
Mark the one field that holds the whole address as it would be written on an envelope — house number, street, town and postcode together, on one line or several. That written line is what is sent, under `address`. The separated `address_line1` and `address_line2` fields are ignored whenever a written address is present, so a form that spreads the address over several inputs is a job for a server-side call rather than for this helper.
There is no single verdict word for an address, so the hint only says what the answer says. An address that cannot stand as written shows its `reason` in plain words: add the street and house number, add the town or city, that postcode is not the right shape for this country, that postcode has the right shape but the national directory does not list it. An address that can stand shows a note when `address_type` says the place is somewhere post is collected rather than a front door — a post-office box, a private mailbox at a mail-receiving agency, a parcel locker, a counter for general delivery — and otherwise the address as the country writes it, which is where a transposed house number or a dropped town shows itself. `postal_code_valid` is never false on an address that stands — a postcode of the wrong shape comes back as the reason instead — so it is true where a postcode was checked and null where there was none to check. The licensed deliverability check is never run from the browser.
## When you cannot set an attribute on the input
Some builders let you add custom code to the page but not attributes to a form field. Mark the field from a short script placed before the helper's script tag. Listeners registered earlier run first, so the input is marked before the helper looks for it:
```html
```
In Webflow and Framer, paste both tags into the site's custom code section that lands in the head; Squarespace and Carrd take them in the page or site code injection settings. On a plain HTML page, put them anywhere in the document.
## Keep your secret key on the server instead
If you would rather not use a publishable key, point `data-endpoint` at a route you host. The helper posts `{ "email": "…" }` there and expects the API's JSON back unchanged; the route adds your secret key and forwards the request to `POST /api/v1/email`. Phone inputs proxy through `data-phone-endpoint` on the script tag (or `data-endpoint` on the phone input itself): that route receives `{ "phone": "…", "country": "GB" }` and forwards it to `POST /api/v1/phone`. Address inputs use `data-address-endpoint` the same way, receive `{ "address": "…", "country": "GB" }` and forward to `POST /api/v1/address`. A five-line proxy in any framework does it; the product page shows Laravel and Node versions. Rate-limit the routes, because they spend your credits.
## Enforce your own policy
Every verdict is emitted as a bubbling `spaw:result` event on the input, with the full API response in `event.detail` plus `type` (`email`, `phone` or `address`) and the checked `input`. The helper itself only hints; the event is where you decide, and `type` is what tells the three signals apart:
```html
```
The email, phone and address responses are the same answers the API gives a server, field for field, each minus the billing block, so page visitors never see your balance. Branch on `deliverable`, use `did_you_mean` to offer the fix, and read `reason` before refusing anything marked `risky`: a catch-all domain is risky but often a fine address. For an address, `risk_score` and `risk_signals` spell out why a place looks like somewhere post is collected rather than a home, and refusing a post-office box is a shipping policy, not a judgement about the person typing it.
## Cost
One credit per answered lookup (an IP credit covers five), charged to the key's owner. Unverified email answers, undeliverable ones within the fair-use allowance, invalid numbers, addresses that cannot stand as written, repeats within seven days and the free test addresses on `spaw.test` cost nothing and never count toward the key's daily cap. Every account gets 100 free credits a month.
---
# Verify emails, phones, IPs and postal addresses in Make
Call the Spaw API from Make's HTTP module and route on email verdicts, phone and IP flags or a postal address's ok_to_ship. The Spaw app for Make awaits review.
Updated: 2026-10-14
Make (formerly Integromat) scenarios are a natural place for a verification step, because a router can send each address down a different branch depending on the verdict. Spaw has no special requirements: the API speaks JSON over HTTPS with a bearer token, which is exactly what Make's HTTP module sends. A dedicated Spaw app for Make is built and waiting for Make's review; until it is available, the HTTP module gives you every field the app will.
## What works today: the HTTP module
1. Create a secret API key on the API keys page of your Spaw dashboard. Keep it out of scenario names and notes; it starts with `sk_live_`.
2. In your scenario, add the HTTP module and pick the action that makes a request (it is the one that lets you set the method, headers and body by hand).
3. Set the URL to `https://spaw.co/api/v1/email` and the method to POST.
4. Add two headers: `Authorization` with the value `Bearer sk_live_…`, and `Content-Type` with the value `application/json`.
5. Set the body type to raw, the content type to JSON, and the request content to the JSON below, mapping the address from the module before it.
6. Turn on response parsing so the answer's fields become mappable in the modules that follow.
```json
{
"email": "{{1.email}}"
}
```
Run the scenario once with `deliverable@spaw.test` as the address. It answers a canonical deliverable result, costs no credits and is never logged, so you can wire the whole flow before spending anything.
The parsed response carries `data` with every field of a [single-address lookup](/docs/api/verify-email) and `meta` with `credits_used`, `credits_remaining` and `cache_hit`. For a scenario that processes a list, use an array aggregator to collect up to 50 addresses, send them as `{ "emails": [...] }` to `https://spaw.co/api/v1/email/batch`, and iterate over `data.results`. The [batch endpoint](/docs/api/verify-email-batch) answers in input order and counts as one request against the rate limit.
To check only a domain, for example before enriching a company record, call `GET https://spaw.co/api/v1/email/domain/acme.com` with the same header. The [domain endpoint](/docs/api/domain-intelligence) answers whether the domain receives mail, who runs its mail, its SPF and DMARC status and its registration age.
## The Spaw app for Make
The app packages the same calls as modules with a saved connection, so nobody has to paste headers:
- A connection named Spaw API key that stores the secret key and verifies it against your account.
- Modules: Verify an Email, Verify Emails (up to 50), Get Domain Intelligence, Add to Suppression List, Look Up an IP Address, Validate a Postal Address, Verify a Business Identifier, Validate a Phone Number, Validate Phone Numbers (up to 1,000, with a shared Country and a Live carrier check parameter), Report an SMS Outcome (delivered, undelivered, abused, verified or opted out, free), and Report an IP Outcome (abuse, fraud, bot or chargeback, free).
- Errors mapped to Make's own types: a rejected key stops the scenario with an authentication error, a rate limit raises a rate-limit error Make can retry, and everything else reports the API's message.
It is not in Make's app catalogue yet. This page will link the app when Make has finished its review. Scenarios built on the HTTP module keep working unchanged either way.
## IP lookups
The HTTP module looks up the address behind a signup or an order the same way: URL `https://spaw.co/api/v1/ip`, method POST, the same two headers, and the request content `{"ip": "{{1.ip}}"}` mapped from the module that recorded the visitor's address. The parsed answer's `data` block carries the location, the network and its operator type, tri-state flags for datacenter and cloud ranges, Tor exits, VPN exits and iCloud Private Relay with the provider named when its own list contains the address, `is_anonymous`, and a published `risk_score` with its `risk_signals`, so a router filter on `is_anonymous` or on `risk_level` needs no interpretation. In the Spaw app the same call is the Look Up an IP Address module, with a Privacy mode parameter that keeps nothing about the lookup on Spaw's side. One credit covers five [IP lookups](/docs/api/lookup-ip) that resolve a location, a network or a positive flag: the lookup that opens a block reports `credits_used` 1, the next four 0, and `meta.ip_lookups_prepaid` counts what is left. Reserved or private ranges, unallocated space, unknown addresses and repeats within seven days are free, and IP endpoints have their own limit of 50 requests per second.
## Phone numbers
The HTTP module validates a phone number the same way: URL `https://spaw.co/api/v1/phone`, method POST, the same two headers, and the request content `{"phone": "{{1.phone}}"}`, with a `country` key (an ISO 3166-1 alpha-2 code such as `US`) when the numbers are written without a calling code. The parsed answer's `data` block carries `valid` and a typed `reason`, the E.164, national, international and `tel:` forms, the country and its facts, `line_type` and `carrier` with the source that settled them (the numbering plan, or the block allocation published by the regulator, which says who the numbers were allocated to and never who serves them after a port), `block_status`, the `is_fictional`, `is_virtual` and `is_disposable` flags, and a published `risk_score` with its `risk_signals`, so a router filter on `valid` or on `is_disposable` needs no interpretation. Route on `ok_to_send` before the SMS module rather than on the evidence: it is your account's [SMS policy](/guides/gate-sms-verification-codes-with-the-sms-policy) applied to this answer (the countries and line types you send to, the signals you refuse, a ceiling on the score and, by default, the numbers your suppression list holds as opted out), and on the false route `blocked_by` names the rule that decided it (`invalid`, `opted_out`, `country`, `line_type`, a signal such as `disposable`, or `risk_score`; `suppressed` for a number on your suppression list, which batch, bulk and monitored runs honour and a single lookup never does, while a single lookup of a number the list holds as opted out answers `opted_out` unless the policy's `block_opted_out` is off). The policy is set once on the account, not in the scenario, so every scenario, batch and bulk run applies the same rules. It is a fraud and deliverability decision, not consent and not a do-not-call scrub. In the Spaw app the same call is the Validate a Phone Number module. A [phone lookup](/docs/api/validate-phone) costs 1 credit when the number is valid; invalid numbers, unassigned blocks and repeats within seven days are free.
A scenario that processes a list aggregates up to 1,000 numbers and sends `{"phones": [...]}` to `https://spaw.co/api/v1/phone/batch`, with an optional `country`; the app's Validate Phone Numbers module is a search over the same call that iterates one bundle per number and adds a Live carrier check parameter (the API's `hlr`). The live carrier-network check (`hlr: true`) is not enabled on the service: `hlr_checked` answers false and nothing extra is charged. After the SMS module, the HTTP module (or the app's Report an SMS Outcome module) posts `{"items": [{"phone": "{{1.phone}}", "outcome": "delivered"}]}` to `https://spaw.co/api/v1/phone/feedback`, with `undelivered` or `abused` mapped from the provider's status; the report is free and feeds only your own later lookups of that number. An `undelivered` item can carry the `failure_class` your provider's error code names (`sender_side` for a carrier filter or an unregistered campaign, `permanent` for a number that no longer exists), and only failures about the number count against it.
## Postal addresses
The HTTP module checks the postal address on an order the same way: URL `https://spaw.co/api/v1/address`, method POST, the same two headers, and the request content `{"address": "{{1.address}}", "country": "GB"}`, or the parts separately as `address_line1`, `building`, `locality`, `administrative_area`, `postal_code` and the rest. The country is not optional in practice: it decides which format the address is read under, which parts that country requires and which postcode directory answers, and it is never guessed. The parsed answer's `data` block carries the components and the `formatted` address, `postal_code_valid` and `postcode_status`, whether the town matches the postcode, `address_type` with the source that settled it, `match_level` against the `register` named, the postcode centroid, `companies_registered`, and a published `risk_score` with its `risk_signals`. Route on `ok_to_ship` rather than on the evidence: it is your account's [shipping policy](/guides/gate-a-checkout-with-the-shipping-policy) applied to this answer, so a router with one filter on `ok_to_ship` true and one on false needs no interpretation, and on the false route `blocked_by` names the rule that decided it (`invalid`, a signal such as `placeholder`, `risk_score` or `unconfirmed`; `suppressed` for an address on your suppression list, which batch, bulk and monitored runs honour and a single lookup never does) — the reason to write back to the order or hand to whoever reviews it. The policy is set once on the account, not in the scenario, so every scenario, batch and bulk run applies the same rules. In the Spaw app the same call is the Validate a Postal Address module, whose optional parameters are sent as separate fields and win over anything read out of the line. A scenario that processes a list aggregates up to 100 addresses and sends `{"addresses": [...]}` to `https://spaw.co/api/v1/address/batch`.
The answer says how each part was confirmed (`confirmation`: the postcode, street, house number, town and region each mapped to the dataset that stood behind it, or `unconfirmed`) and what the parser set aside on the way (`changes`: a country line dropped, a care-of line removed, a building name separated from the street, …). An empty field means the signal was not evaluated, never "no". `exists` is true only on a premise match and false only under a register whose coverage is complete; a register whose coverage is partial, because it covers part of the country or cannot vouch for every address, cannot make an address's absence mean anything. `is_mail_drop` is a heuristic and not a postal service's own agency flag: an operator's own published branch list names the address, or so many companies are registered there that it is one in practice, and `mail_drop_source` says which. Two answers are licensed and not enabled — whether post actually arrives (`deliverability_checked` comes back false with `is_deliverable`, `is_cmra`, `is_vacant` and `is_residential` empty) and the list of addresses inside a postcode, which answers a typed `501` rather than a guess. An address lookup costs 1 credit when the address is valid; invalid addresses and repeats within seven days are free.
## Business identifiers
The HTTP module checks a company the same way: URL `https://spaw.co/api/v1/entity`, method POST, the same two headers, and the request content `{"identifier": "{{1.lei}}", "type": "lei"}` — or `{"identifier": "{{1.company_number}}", "type": "company_number", "country": "GB"}`, because a company number is only unique inside its own register and the country says which one, while a Legal Entity Identifier is global by construction and ignores it. The parsed answer's `data` block carries the legal name, `status` with the register's own `status_detail`, the country and jurisdiction of formation, the category and legal form, `registered_at` and `record_updated_at`, and for a LEI `checksum_valid`, `registration_status` and `next_renewal_at`. Where a LEI names its entry in a national register Spaw also holds, `agreements` quotes both sides field by field and `flags` names what differs, so a router filter on `dissolved_in_national_register` needs no interpretation: a dissolved company no longer exists as a legal person and cannot enter a contract. In the Spaw app the same call is the Verify a Business Identifier module. A scenario that processes a list aggregates up to 500 identifiers and sends `{"identifiers": [...]}` to `https://spaw.co/api/v1/entity/batch`.
The one caveat, and it matters more than anything else here: the UK company file is a snapshot of the **live** register, so a company that is struck off is **removed from the file** rather than marked dissolved — the statuses that file carries are `Active`, `Active - Proposal to Strike off`, `Liquidation`, `In Administration` and similar, and never `Dissolved`. A UK dissolution therefore reaches you as `found` `false` with `reason` `unknown_identifier` rather than as that flag, which never fires on the only national register installed today. Check `found` before you trust the flag, and read `status_detail` for the earlier warning: `Active - Proposal to Strike off` folds to a `status` of `active`, because the company is still on the register. A LEI lookup of the same company is quieter still: GLEIF keeps its record, so `found` stays `true` and no flag fires. What moves is `agreements`, which comes back empty while `other_register` still names `uk-companies-house` — the counterpart row is gone. That pair is the signal on this path, and `lei_registration_not_current` follows later, when the lapsed registration is not renewed. A [monitored list](/docs/business-verification) turns the disappearance into an alert of its own, `left_register`.
`checksum_valid` is a typing check and nothing more: a perfectly formed LEI can never have been issued, which is what the register lookup answers. `registration_status` is about the identifier rather than the entity — a LEI must be reconfirmed each year and a great many are not, so `LAPSED` means the reference data has not been rechecked recently, not that the company has gone. Nothing in the answer is a registered address, and nothing will be: a registered address is very often somebody's home, and an identifier alone does not say whether a company or a sole trader stands behind it.
Both registers are bulk files synced onto the Spaw server, so no third party sits in the request path. The LEI reference data is published under a CC0 public-domain dedication and refreshed daily; the UK company data is used under the Open Government Licence v3.0 and republished monthly, so an answer can be up to five weeks behind the live register and every answer names the date of the copy in `sources`. Spaw is not affiliated with, endorsed by or connected to either publisher. A [business lookup](/docs/api/verify-entity) costs 1 credit when a register carried the identifier; unknown, malformed and unsupported identifiers and repeats within seven days are free.
## What to do with the verdict
Put a router after the verification module with one filter per outcome. Route `deliverable` answers straight on. Send `risky` answers through a second filter on `reason`: a `catch_all` domain or a `role` inbox is usually worth keeping with a note, a `disposable` domain usually is not. Stop `undeliverable` answers, or write `did_you_mean` back to the source when it is filled, since a corrected typo is a recovered lead rather than a lost one. If you would rather compare a single number, `risk_score` is 0 to 100 and its weights are documented in the [risk score guide](/guides/how-spaw-computes-the-risk-score).
## Cost
Every verified address that answers deliverable or risky spends 1 credit; unverified answers, invalid input and repeats within seven days are free, and undeliverable answers are free within a fair-use allowance. Make counts its own operations separately. The API accepts 5 requests per second per key, so a scenario that bursts through a large list should use the batch endpoint or a short sleep between iterations.
---
# Check phone numbers in Cognito's Pre sign-up and SMS sender triggers
Check a phone number with Spaw in Cognito's Pre sign-up trigger and in the custom SMS sender Lambda, which sends every code itself. Fails open.
Updated: 2026-09-27
Amazon Cognito gives a phone check two places to run: the Pre sign-up trigger, which can deny a sign-up before any code exists, and the custom SMS sender trigger, which replaces Cognito's own SMS sending so that your Lambda function delivers every code. Asking Spaw about the number in both means a code is sent only where your account's [SMS policy](/docs/phone-intelligence#policy) answers `ok_to_send: true`. The custom SMS sender is a delivery hook, not a filter: once it is assigned, Cognito sends no SMS at all, and a code your function does not send simply never arrives.
## What the triggers receive
From Amazon Cognito's developer guide (the custom SMS sender, custom sender, Pre sign-up and Lambda trigger pages) and the AWS Lambda guide, read on 27 September 2026:
| Item | Pre sign-up | Custom SMS sender |
| --- | --- | --- |
| The number | `request.userAttributes.phone_number` | `request.userAttributes.phone_number` |
| The code | none yet | `request.code`, encrypted with your KMS key |
| The flow | `triggerSource` `PreSignUp_SignUp`, the one the sample checks; the trigger also runs as `PreSignUp_AdminCreateUser` and `PreSignUp_ExternalProvider` | `triggerSource`: `CustomSMSSender_SignUp`, `_ResendCode`, `_VerifyUserAttribute`, `_UpdateUserAttribute`, `_Authentication`, `_ForgotPassword`, `_AdminCreateUser` |
| Your app's extra data | `request.clientMetadata`, from SignUp, AdminCreateUser, AdminRespondToAuthChallenge and ForgotPassword | `request.clientMetadata`, from RespondToAuthChallenge and AdminRespondToAuthChallenge only |
| Refusing | Throw an error: the sign-up is denied | No refusal exists: Cognito "doesn't expect any additional return information in the response" |
| Invocation | Synchronous, "must respond within 5 seconds" | Not synchronous: "Except for Custom sender Lambda triggers, Amazon Cognito invokes Lambda functions synchronously"; the function's own timeout applies (3 seconds by default) |
Neither event carries the visitor's IP address. Cognito passes on only what your app puts in `ClientMetadata`, so the samples read `clientMetadata.client_ip` and work without it. Two limits apply: a client-side `SignUp` call from a browser does not know the visitor's public address, so only a backend that makes the call can fill it in; and anyone calling Cognito's public sign-up API directly can put any address there, or none. Without an address the answer's `client_velocity` is null and [`numbers_per_client`](/docs/signals/phone/numbers_per_client) cannot fire, while [`range_burst`](/docs/signals/phone/range_burst), five distinct numbers your account looked up in one 1,000-number range within 60 minutes (+40), still counts.
## Before you start
1. Create a secret API key on the API keys page of your Spaw dashboard, scoped to phone, with a daily credit cap above a normal day's sign-ups, and store it in each function's `SPAW_KEY` environment variable (or read it from Secrets Manager).
2. Switch the SMS policy's opt-out rule off, so a person who once replied STOP to a marketing message can still receive a code:
```bash
curl -X PUT https://spaw.co/api/v1/phone/policy \
-H "Authorization: Bearer sk_live_…" \
-H "Content-Type: application/json" \
-d '{"block_opted_out": false}'
```
An account has one policy, applied to every lookup it makes. With the rule off, an opted-out number is decided like any other and the answer still says `meta.opted_out: true`, so a marketing flow on the same account reads that field itself.
3. For the custom SMS sender, follow Cognito's guide to activating custom sender triggers: a symmetric KMS key, permission for Cognito to invoke the function, and an `aws cognito-idp update-user-pool` call with `CustomSMSSender={LambdaVersion=V1_0,LambdaArn=…}` and the `KMSKeyID`. The console cannot set it. `UpdateUserPool` resets every parameter you leave out to its default, so the call has to repeat the pool's existing configuration.
4. Raise the sender function's timeout above the Lambda default of 3 seconds. It decrypts the code, waits up to 2.5 seconds for Spaw and then calls SNS.
## Deny a sign-up in the Pre sign-up trigger
A sign-up is the flow where refusing is cleanest: the trigger runs before any code is sent, and an error it throws denies the sign-up, so your app's `SignUp` call receives the error and can say so.
```javascript
import { isIP } from "node:net";
export const handler = async (event) => {
const phone = event.request.userAttributes.phone_number;
// Only self-service sign-up: an administrator creating a user, or a first
// sign-in through an identity provider, is never denied here.
if (event.triggerSource !== "PreSignUp_SignUp" || !phone) {
return event;
}
// ClientMetadata is whatever the caller sent: pass it on only when it is an address.
const clientIp = event.request.clientMetadata?.client_ip ?? "";
let answer = null;
try {
const response = await fetch("https://spaw.co/api/v1/phone", {
method: "POST",
headers: {
Authorization: `Bearer ${process.env.SPAW_KEY}`,
"Content-Type": "application/json",
Accept: "application/json",
},
body: JSON.stringify(isIP(clientIp) ? { phone, client_ip: clientIp } : { phone }),
signal: AbortSignal.timeout(2500),
});
answer = response.ok ? (await response.json()).data : null;
} catch {
// Fail open: the sign-up continues unchecked.
}
if (answer && !answer.ok_to_send && answer.blocked_by !== "opted_out") {
throw new Error("This number cannot receive a verification code.");
}
return event;
};
```
Pass the visitor's address from your backend as `ClientMetadata: { client_ip: "…" }` on the `SignUp` call and the lookup counts it. The samples pass the value on only when it parses as an IP address: Spaw answers a malformed `client_ip` with `422`, which a fail-open check would read as no answer, so a caller could otherwise skip the check by sending junk there. The code the custom SMS sender then sends for this sign-up looks the same number up again within minutes; a repeat within seven days is free.
## Check every code in the custom SMS sender
```javascript
import { KmsKeyringNode, buildClient, CommitmentPolicy } from "@aws-crypto/client-node";
import { SNSClient, PublishCommand } from "@aws-sdk/client-sns";
import { isIP } from "node:net";
const { decrypt } = buildClient(CommitmentPolicy.REQUIRE_ENCRYPT_ALLOW_DECRYPT);
const keyring = new KmsKeyringNode({ generatorKeyId: process.env.KEY_ID, keyIds: [process.env.KEY_ARN] });
const sns = new SNSClient({});
// Codes that attach a number to an account. Sign-in, password-reset and
// admin-created-user messages go to a number the account already holds.
const REFUSE_ON = new Set([
"CustomSMSSender_SignUp",
"CustomSMSSender_ResendCode",
"CustomSMSSender_VerifyUserAttribute",
"CustomSMSSender_UpdateUserAttribute",
]);
async function checkNumber(phone, clientIp) {
try {
const response = await fetch("https://spaw.co/api/v1/phone", {
method: "POST",
headers: {
Authorization: `Bearer ${process.env.SPAW_KEY}`,
"Content-Type": "application/json",
Accept: "application/json",
},
body: JSON.stringify(isIP(clientIp ?? "") ? { phone, client_ip: clientIp } : { phone }),
signal: AbortSignal.timeout(2500),
});
return response.ok ? (await response.json()).data : null;
} catch {
return null;
}
}
export const handler = async (event) => {
const phone = event.request.userAttributes.phone_number;
if (!phone || !event.request.code) {
return;
}
// Only present when your app passed it through RespondToAuthChallenge.
const answer = await checkNumber(phone, event.request.clientMetadata?.client_ip);
if (answer && !answer.ok_to_send && answer.blocked_by !== "opted_out") {
console.log(JSON.stringify({ trigger: event.triggerSource, spaw_blocked_by: answer.blocked_by }));
if (REFUSE_ON.has(event.triggerSource)) {
// Nothing is sent and Cognito is not told: the person receives no code.
return;
}
}
const { plaintext } = await decrypt(keyring, Buffer.from(event.request.code, "base64"));
await sns.send(new PublishCommand({
PhoneNumber: phone,
Message: `Your code is ${Buffer.from(plaintext).toString("utf-8")}`,
}));
};
```
`@aws-crypto/client-node` goes into the deployment package, as Cognito's own example does; `@aws-sdk/client-sns` is part of the AWS SDK for JavaScript v3 that the Node.js Lambda runtimes include. The number is checked before the code is decrypted, so a refused code is never decrypted at all, and Spaw receives the number and, when present, the address: never the code, the message or the other attributes. The samples follow the documented event shapes; run them in your own account before you rely on them.
## Which codes it refuses, and what the person sees
`REFUSE_ON` holds the flows where a number is being attached to an account: a sign-up, its resent code, and adding or changing a phone number. Sign-in codes (`_Authentication`), password resets and the temporary password of a user you created go to a number the account already holds, and refusing them would lock an existing customer out, so they are logged and sent.
A refusal in the sender is silent. Cognito does not wait for this function, so your app moves on to its code-entry step and the code never arrives. Say on that screen what to do when no code comes, and keep the Pre sign-up check in place, because a sign-up refused there gets a clear error while one refused in the sender leaves an unconfirmed user behind.
A refusal names the rule in `blocked_by`; the [gate-SMS guide](/guides/gate-sms-verification-codes-with-the-sms-policy) explains each rule and the order they apply in.
## Why it fails open, and why it never throws over Spaw
A timeout, a network error, `402 INSUFFICIENT_CREDITS` on an empty balance, `429` when the key's daily cap is spent or your phone requests exceed 50 a second, or a `5xx` leaves `answer` null, and the code is sent as if the check did not exist. Failing closed would turn a Spaw outage into an outage of your sign-up. In the sender there is a second reason: Cognito does not wait for its answer, so an error thrown over the Spaw call would reach nobody who could show it to the person, and the code would simply not be sent.
## Report the codes people entered
The Post confirmation trigger runs on ConfirmSignUp, AdminConfirmSignUp and ConfirmForgotPassword. When a sign-up was confirmed and the phone number is a verified attribute, report it as `verified`. A pool whose Pre sign-up trigger sets `autoVerifyPhone` marks numbers verified without a code, so skip the report there:
```javascript
export const handler = async (event) => {
const { phone_number: phone, phone_number_verified: verified } = event.request.userAttributes;
if (event.triggerSource === "PostConfirmation_ConfirmSignUp" && phone && verified === "true") {
try {
await fetch("https://spaw.co/api/v1/phone/feedback", {
method: "POST",
headers: {
Authorization: `Bearer ${process.env.SPAW_KEY}`,
"Content-Type": "application/json",
},
body: JSON.stringify({ items: [{ phone, outcome: "verified" }] }),
signal: AbortSignal.timeout(1500),
});
} catch {
// A missed report only means one less outcome on your account.
}
}
return event;
};
```
Reports are free. Spaw has no delivery-receipt adapter for SNS, so `verified` is the outcome this setup reports, and with `verified` alone the [feedback summary](/docs/api/phone-feedback-summary) reads `verified_share: 1` and lists no blocks. For a conversion figure, turn on delivery status logging in the SNS text-messaging preferences, which writes each SMS's result to CloudWatch Logs (Amazon SNS developer guide, read 27 September 2026), and forward those results to `POST /api/v1/phone/feedback` as `delivered` or `undelivered`. Otherwise compare the verified count with the codes the sender function sent.
## Try the branches without Cognito
The documented test numbers answer a fixed result each, cost nothing and are never counted in your traffic. Under the default policy `+1 202 555 0103` answers `ok_to_send: false` with `blocked_by: "disposable"`, `+1 202 555 0105` answers `blocked_by: "fictional"`, and `+1 202 555 0102` (the test number that answers the `virtual` signal, score 40) and `+1 202 555 0100` answer `ok_to_send: true`. Post them to `https://spaw.co/api/v1/phone` with curl, or put one in a Lambda console test event, to see each branch.
## What to leave out
Leave `hlr` out of the request. The live carrier-network check (`hlr: true`) is not enabled on the service: `hlr_checked` answers false and nothing extra is charged. Leave the policy's `require_reachable` rule off for the same reason: while the check is not enabled, no lookup answers `ok_to_send: true` under it: every number that passes the other rules answers `blocked_by: "unconfirmed"`.
`ok_to_send` is a fraud and deliverability decision. It is not consent and not a do-not-call scrub, and whether the person agreed to hear from you stays yours to record.
## Cost
A lookup costs 1 credit when the number is valid. Invalid numbers, the test numbers and a repeat of the same number within seven days are free, so the sender's second look at a number the Pre sign-up trigger checked costs nothing, and neither does a resent code. Outcome reports are free. SNS bills the SMS. Spaw is not affiliated with Amazon Web Services.
---
# Feed Mailgun events into Spaw's suppression list
Add your Spaw feedback URL as a Mailgun webhook for delivered, permanent failure and complaint events, and your suppression list keeps itself current.
Updated: 2026-09-03
Mailgun tells you about every message it could not deliver, and Spaw can listen. Register your Spaw feedback URL as a webhook on the sending domain and each permanent failure adds the address to your suppression list, each delivery confirms a mailbox for your own later lookups, and the History tab measures the verdicts against what actually happened.
## Set up the webhooks
1. Open the API keys page in the Spaw dashboard, generate a feedback key, and copy the Mailgun URL:
```text
https://spaw.co/api/v1/email/feedback/mailgun/fb_…
```
2. In Mailgun, open the sending domain and go to its Webhooks settings.
3. Add the URL for the Delivered Messages, Permanent Failure and Spam Complaints events. Mailgun lets each event type post to more than one URL, so this sits alongside anything you already have.
4. Use Mailgun's test button for each event. Spaw answers `200` with the number of outcomes it recorded; a test that carries no recipient records nothing and still answers `200`.
Temporary failures are not needed. Mailgun keeps retrying those on its own, and a temporary failure says nothing definitive about the address, so Spaw ignores the event even if you register it.
## What Spaw reads from the payload
Mailgun posts one event per request under `event-data`. Spaw reads the `event` name, the `recipient`, the `severity`, the delivery status, and the timestamp, and nothing else:
| Mailgun event | Recorded as |
| --- | --- |
| `delivered` | delivered |
| `failed` with `severity` permanent | bounced |
| `failed` with `severity` temporary | ignored |
| `complained` | complained |
The reason is taken from the first non-empty of `delivery-status.description`, `delivery-status.message` and `reason`, because Mailgun fills whichever it has. The Unix `timestamp` becomes the time of the outcome. A permanent failure, trimmed to the fields Spaw uses:
```json
{
"event-data": {
"event": "failed",
"severity": "permanent",
"recipient": "mia@acme.com",
"reason": "bounce",
"delivery-status": { "message": "550 5.1.1 The email account that you tried to reach does not exist" },
"timestamp": 1788782400
}
}
```
## What Spaw does with the events
A permanent failure or a complaint adds the address to your suppression list with source `feedback`. Batch, bulk and monitor runs answer it from your list at no cost from then on, with reason `suppressed` and a risk score of 100. A single lookup still re-verifies it but leaves the entry in place. A permanent failure comes off the list when a later delivery to that address is reported; a complaint stays until you remove it. An address you imported stays until you remove it too: a complaint about it is recorded on your import, and no delivery clears it.
A delivery removes any entry Spaw had added automatically, and for 90 days the address counts as a confirmed mailbox for your own lookups: the verification answers `smtp_reason: "delivered_recently"` and skips the mailbox probe. Every outcome is matched against the verdict you were given for the address in the previous 90 days, and the feedback summary reports the delivery rate among addresses answered `deliverable` and the bounce rate among addresses answered `undeliverable`. Outcomes are kept for 180 days.
If you already have a bounce export from before the webhook existed, import it once through the suppression import endpoint; imported entries are honoured from day one.
## Keep the URL private
Mailgun signs its webhooks, but Spaw does not need the signing key: the feedback key in the URL is the credential, and anyone holding the URL can add outcomes to your account. If it leaks, rotate the key on the API keys page; every old URL then answers `401 INVALID_FEEDBACK_KEY` and you paste the new one into Mailgun.
## Cost
Feedback is free and never logged as a lookup. The endpoint is throttled at 120 requests per minute per IP.
---
# Spaw Node.js SDK: email, phone, IP and postal address checks
The official spaw package for Node.js 18 and newer wraps every Spaw endpoint with typed results, TypeScript declarations and one error class per API code.
Updated: 2026-10-14
The Node.js client is a thin wrapper over the REST API: one method per endpoint, the API's `data` and `meta` blocks returned exactly as documented, TypeScript declarations, and an error class for every code. It has no dependencies, because it uses the `fetch` built into Node.js 18 and newer, and it is an ES module.
The package is built and will be published on npm as `spaw`. Until the listing is live, a plain `fetch` against the API reference does the same job. For code that runs in a browser, use the form helper with a publishable key instead; the secret key this client needs must never ship to a page.
## Install and verify one address
1. Create a secret key on the API keys page and put it in an environment variable.
2. Install the package once it is published:
```bash
npm install spaw
```
3. Verify an address:
```js
import Spaw from 'spaw';
const spaw = new Spaw({ apiKey: process.env.SPAW_API_KEY });
const result = await spaw.verifyEmail('mia@acme.com');
console.log(result.data.deliverable); // "deliverable", "risky" or "undeliverable"
console.log(result.data.reason); // e.g. "catch_all", or null when deliverable
console.log(result.data.risk_score); // 0–100, a sum of published weights
console.log(result.meta.credits_used); // 1 for a fresh answered lookup, 0 otherwise
```
`result.data` carries all 46 documented fields, including `did_you_mean`, `mx_provider` and `smtp_checked_at`, and every `reason` value is explained on the verdict reasons pages.
## Everything the client does
| Method | Endpoint |
| --- | --- |
| `verifyEmail(email, options)` | `POST /email`, with `callbackUrl` and `callbackSecret` for async settling |
| `verifyEmails(emails)` | `POST /email/batch`, up to 50 |
| `domain(domain)` | `GET /email/domain/{domain}` |
| `listBulkJobs({ page, status, since, until })`, `previewBulkJob(emails, { csv, webhookUrl })`, `startBulkJob(draftId, { mapping, idempotencyKey })`, `createBulkJob(emails, { webhookUrl })`, `getBulkJob(id)`, `cancelBulkJob(id)`, `resumeBulkJob(id)`, `redeliverBulkWebhook(id)`, `downloadBulkResults(id, { variant, format })` | the bulk endpoints, up to 100,000 addresses; a run that stopped on an empty balance is resumed from the top of its stored list |
| `listSuppressions({ page, value, source, reason, since, until, perPage })`, `addSuppressions(emails)`, `removeSuppression(id)`, `removeSuppressions(values)` | the suppression list, by id or by the address itself (up to 1,000 per call) |
| `reportFeedback(items)`, `feedbackSummary()` | delivery outcomes and measured accuracy |
| `account()`, `ledger({ since, until, reason, via, apiTokenId, page, perPage })`, `requestLog(requestId)`, `dataStatus()` | the live credit balance, the credit ledger, and one request read back by the id it answered with |
| `sendTestWebhook(url, event)` | `POST /webhooks/test`, a real signed delivery to a URL you name, free, so a signature check can be written before a paid run depends on it |
| `verifyWebhook(rawBody, header, secret, { toleranceSeconds })` | checks a delivery you received: the digest AND the timestamp in `X-Spaw-Signature-V2`, compared in constant time. Not an API call |
| `validatePhone(phone, { country, hlr, clientIp, callWindow })`, `validatePhones(phones, { country, hlr, callWindow })` | `POST /phone` and `POST /phone/batch`, up to 1,000 numbers |
| `phoneBlockReport(country, prefix)` | `GET /phone/block/{country}/{prefix}`, the regulator's own allocation table for one range |
| `listPhoneBulkJobs({ page, … })`, `previewPhoneBulkJob(phones, { csv, country, hlr })`, `startPhoneBulkJob(draftId, { mapping })`, `createPhoneBulkJob(phones, { country, webhookUrl })`, `getPhoneBulkJob(id)`, `cancelPhoneBulkJob(id)`, `resumePhoneBulkJob(id)`, `redeliverPhoneBulkWebhook(id)`, `downloadPhoneBulkResults(id, { variant, format })` | the bulk phone endpoints, up to 100,000 numbers |
| `listPhoneSuppressions({ page, value, country, … })`, `addPhoneSuppressions(items, { country })`, `removePhoneSuppression(id)`, `removePhoneSuppressions(values, { country })` | the phone suppression list; a value is read to E.164 before it is matched |
| `getPhonePolicy()`, `updatePhonePolicy({ maxRiskScore, blockSignals, allowedCountries, blockedCountries, blockedLineTypes, requireReachable, blockOptedOut })`, `resetPhonePolicy()` | the account's SMS policy behind `ok_to_send` and `blocked_by` on every phone answer (`GET`, `PUT`, `DELETE /phone/policy`); a fraud and deliverability decision, not consent and not a do-not-call scrub |
| `lookupIp(ip, { privacy, abuseContact, networkDetails, hostname, browserTimezone, fields })`, `lookupMyIp()`, `lookupIps(ips)` | `POST /ip`, `GET /ip/me`, `POST /ip/batch`, up to 1,000 addresses; `browserTimezone` fills `timezone_matches`, `fields` keeps only the named keys |
| `lookupAsn(asn)`, `lookupIpRange(prefix)` | `GET /ip/asn/{asn}` (every prefix of one autonomous system) and `GET /ip/range?prefix=…` (what the feeds say about a whole prefix) |
| `listIpLists()`, `createIpList({ name, kind, entries })`, `getIpList(id)`, `updateIpList(id, changes)`, `deleteIpList(id)` | your own allow and deny lists, account-wide, applied to every lookup: an allow-list hit zeroes the score, a deny-list hit tops it |
| `listIpBulkJobs({ page, … })`, `previewIpBulkJob(ips, { csv, privacy })`, `startIpBulkJob(draftId, { mapping })`, `createIpBulkJob(ips, { webhookUrl, privacy })`, `getIpBulkJob(id)`, `cancelIpBulkJob(id)`, `resumeIpBulkJob(id)`, `redeliverIpBulkWebhook(id)`, `downloadIpBulkResults(id, { variant, format })` | the bulk IP endpoints, up to 100,000 addresses |
| `reportIpFeedback(items)`, `ipFeedbackSummary()` | IP outcomes (abuse, fraud, bot, chargeback), free |
| `getEmailPolicy()`, `updateEmailPolicy({ maxRiskScore, blockSignals, minMailboxConfidence })`, `resetEmailPolicy()` | the account's sending policy behind `ok_to_send` and `blocked_by` on every email answer (`GET`, `PUT`, `DELETE /email/policy`) |
| `listDomainMonitors()`, `createDomainMonitor({ name, domains, cadence, webhookUrl, webhookEvents })`, `getDomainMonitor(id)`, `updateDomainMonitor(id, changes)`, `runDomainMonitor(id)`, `deleteDomainMonitor(id)` | monitored domain lists: up to 500 domains re-checked daily, weekly or monthly for mail-setup drift (`/email/domain-monitors`) |
| `listIpMonitors()`, `createIpMonitor({ name, ips, cadence, webhookUrl, webhookEvents })`, `getIpMonitor(id)`, `updateIpMonitor(id, changes)`, `runIpMonitor(id)`, `deleteIpMonitor(id)` | monitored IP lists with blocklist alerts, changeable in place and postable to a webhook |
| `verifyAddress(address, { country, deliverability, fields })`, `verifyAddresses(addresses, { country })` | `POST /address` and `POST /address/batch`, up to 100 addresses; the separated fields include `building`, `fields` keeps only the named keys, and every answer carries the 61 fields incl. `unit_check`, `looks_placeholder`, `seen_by_you`, `ok_to_ship` and `blocked_by` |
| `listAddressBulkJobs({ page, … })`, `previewAddressBulkJob(addresses, { csv, country, deliverability })`, `startAddressBulkJob(draftId, { mapping })`, `createAddressBulkJob(addresses, { country, deliverability, webhookUrl, idempotencyKey })`, `getAddressBulkJob(id)`, `cancelAddressBulkJob(id)`, `resumeAddressBulkJob(id)`, `redeliverAddressBulkWebhook(id)`, `downloadAddressBulkResults(id, { variant, format })` | the bulk address endpoints, up to 100,000 addresses |
| `addressStreets(country, postalCode)` | `GET /address/streets`, every street a national register lists inside a postcode, 1 credit |
| `addressesAtPostcode(postcode, { country })` | `GET /address/postcode/{postcode}`: the same streets from an open register, or a licensed partner's delivery points where one is configured |
| `getAddressPolicy()`, `updateAddressPolicy({ maxRiskScore, blockSignals, requireExists })`, `resetAddressPolicy()` | the account's shipping policy behind `ok_to_ship` and `blocked_by` on every address answer (`GET`, `PUT`, `DELETE /address/policy`) |
| `listAddressMonitors()`, `createAddressMonitor({ name, addresses, cadence, country, deliverability, webhookUrl, webhookEvents })`, `getAddressMonitor(id)`, `updateAddressMonitor(id, changes)`, `runAddressMonitor(id)`, `deleteAddressMonitor(id)` | monitored address lists, alerting on newly flagged addresses |
| `reportAddressFeedback(items, { country })`, `addressFeedbackSummary()` | postal outcomes (delivered, returned, moved, refused), free and never shared between accounts |
| `listAddressSuppressions({ page, value, country, … })`, `addAddressSuppressions(items, { country })`, `removeAddressSuppression(id)`, `removeAddressSuppressions(values, { country })` | the address suppression list, up to 10,000 per import; a value is folded against its country before it is matched |
| `verifyEntity(identifier, { type, country })`, `verifyEntities(identifiers)` | `POST /entity` and `POST /entity/batch`, up to 500 identifiers read against locally synced registers |
| `listEntityMonitors()`, `createEntityMonitor({ name, identifiers, cadence, webhookUrl, webhookEvents })`, `getEntityMonitor(id)`, `updateEntityMonitor(id, changes)`, `runEntityMonitor(id)`, `deleteEntityMonitor(id)` | monitored identifier lists, alerting when an entity is dissolved or its registration lapses |
| `listMonitors()`, `createMonitor({ name, emails, cadence, webhookUrl, webhookEvents })`, `getMonitor(id)`, `updateMonitor(id, changes)`, `runMonitor(id)`, `deleteMonitor(id)`, and the phone equivalents | monitored email and phone lists; an update sends only the keys you pass, and `{ webhookUrl: null }` removes the webhook |
| `checkConsistency({ email, phone, country, ip, address })` | `POST /consistency`, any subset of the four signals compared against each other |
| `listConsistencyBulkJobs()`, `createConsistencyBulkJob(items, { country, webhookUrl })`, `previewConsistencyBulkJob(items, { csv, country })`, `startConsistencyBulkJob(draftId, { mapping })`, `getConsistencyBulkJob(id)`, `cancelConsistencyBulkJob(id)`, `resumeConsistencyBulkJob(id)`, `redeliverConsistencyBulkWebhook(id)`, `downloadConsistencyBulkResults(id, { variant })` | the same check over a whole list, queued: a row costs the sum of the legs it ran and the comparison is free |
A batch keeps the API's shape: `data.results` holds one `{ index, input, data, meta }` per address in input order — `index` is its place in the list you sent and `input` the value as you sent it, because the answer carries the normalized form, and `meta.stopped_reason` is set instead of throwing when the balance runs out part-way, so paid results are never lost. A bulk job returns its `webhook_secret` once; keep it, because it signs the completion webhook. `verifyWebhook(rawBody, header, secret)` is the other half: pass the raw body and the `X-Spaw-Signature-V2` header and it answers whether the delivery is ours, unmodified and recent. It checks the timestamp as well as the digest, which is the point of the header — a signature over the body alone never goes stale, so a captured delivery could be posted back at you a year later and still verify. The original `X-Spaw-Signature`, with no timestamp in it, is sent beside it until 1 March 2027 and is deliberately not accepted by this call.
A delivery that failed is not lost: ``redeliverBulkWebhook(jobId)`` (and its phone, IP, address and cross-signal twins) posts that run's completion webhook to your URL again, for the day your endpoint was down for the half-minute the three attempts cover. The body is byte-for-byte the one the first delivery carried, rebuilt from the run's own frozen row, so the two reconcile — but the signature is stamped afresh, and has to be, because `verifyWebhook` refuses anything whose timestamp is more than five minutes old. Verify a redelivery with the same call you verify everything else with. Nothing deduplicates it for you: a receiver that gets both sees the event twice, and `job.id` is the key to deduplicate on. A run created without a `webhook_url` raises `ConflictError` with code `WEBHOOK_NOT_CONFIGURED` and one that has not settled `JOB_NOT_FINISHED`; a run whose last delivery succeeded is not refused, because a receiver can lose a message it acknowledged. The delivery is queued rather than made while you wait — one attempt takes half a minute against an endpoint that is down — so the call resolves `202` and the run it hands back reads `webhook_status: "pending"` until a worker has an answer; read the outcome with `getBulkJob(jobId)`. It is free and capped at ten a minute per signed-in user, neither per key nor per account.
## Addresses
`verifyAddress` takes the address as one written line or as an object of the separated fields (`address_line1`, `locality`, `postal_code` and the rest), always with a `country`: that is what decides which country's format it is read under, and it is never guessed. The answer splits the address into components, formats it the way that country writes addresses, and says what open data can establish — the postcode's status and centroid, whether the town matches it, what kind of delivery point it is, how close a national address register comes to it, and how many companies are registered there. `AddressResult` types every field.
```js
const address = await spaw.verifyAddress('10 Downing Street, London SW1A 2AA', { country: 'GB' });
address.data.formatted; // the address as that country writes it
address.data.postcode_status; // "live", "terminated", or null where no directory covers the country
address.data.address_type; // street, po_box, private_mailbox, general_delivery, …
address.data.risk_signals; // the weights behind risk_score
await spaw.verifyAddresses(addresses, { country: 'GB' }); // up to 100 per call; an item's own country wins
```
For a whole list, queue a bulk run instead of looping: up to 100,000 rows in the background, billed per row on exactly the single-lookup rules, with in-list repeats free and counted in `duplicate_count`.
Read the list before you start it: the `previewBulkJob()` call (and its phone, IP and address twins) answers what we made of it — which column filled which field and whether a heading named it or we guessed, the first rows as parsed, the repeats, and `credits_upper_bound` — and queues nothing. That figure is a CEILING, never a quote: a row can be free for reasons no preview can know in advance, so a run usually costs less and never more. Start it with the `startBulkJob()` call, correcting a column we read wrongly with `mapping`.
```js
const { job } = (
await spaw.createAddressBulkJob(addresses, { country: 'GB', idempotencyKey: 'addresses-2026-09-05' })
).data;
await spaw.getAddressBulkJob(job.id); // status, counts, credits_used
await spaw.downloadAddressBulkResults(job.id); // or { variant: 'invalid' } / 'high'
```
A field is `null` when its dataset does not cover the country, which means "not evaluated" and never "no". `exists` is true only on a premise match and false only under a register whose coverage is complete: a register whose coverage is partial, because it covers part of the country or cannot vouch for every address, cannot make an address's absence mean anything, so it answers null. `is_mail_drop` is a heuristic built from mailbox operators' own published branch lists and from how many companies are registered at an address, never a postal service's own agency flag; `mail_drop_source` says which of the two fired.
One answer is licensed and not enabled. `deliverability: true` would ask a partner whether post actually arrives and whether the delivery point is a mail-receiving agency, vacant or residential; until one is configured `deliverability_checked` comes back false, the four fields stay null and the premium credits are charged only where a partner answered. The streets inside a postcode are an open answer wherever a national register with a street listing is installed: `addressStreets(country, postalCode)` lists them for 1 credit, `addressesAtPostcode` answers the same streets as one entry each (a licensed partner's delivery points, 2 credits, where one is configured), and a country neither covers (Italy, for one), and a postcode the listing cannot speak for — a Northern Irish BT, forces BFPO or non-geographic BX code, or a German code outside Berlin — rejects with `NotEnabledError` (`ADDRESS_LIST_UNAVAILABLE`), free; an ordinary British postcode answers its district's streets from Ordnance Survey's Open Names, never its buildings. A postcode the register lists no street at rejects with `NotFoundError` (`ADDRESS_STREETS_NOT_FOUND`), free.
A saved list of up to 500 addresses is re-read on a `weekly` or `monthly` cadence — there is no daily one, because the registers and postcode directories an address is measured against are republished weekly at best. Every run answers each address `ok`, one of the flagged `invalid`, `not_in_register`, `terminated_postcode`, `mail_drop` and `high_risk`, or `unverifiable`, which means the address stood as written but no dataset installed on this server covers it well enough to check — a limit of the data, never a property of the address, and never an alert. An email goes out only for an address that was already known and unflagged and became flagged; a first run and a newly added address only set the baseline, and one that stays flagged never alerts twice. Runs bill per address exactly like a single lookup.
```js
const { monitor } = (await spaw.createAddressMonitor({ name: 'Supplier sites', addresses, cadence: 'monthly', country: 'GB' })).data;
(await spaw.getAddressMonitor(monitor.id)).data.monitor.results; // { address: { status, flags, changed, … } }
await spaw.runAddressMonitor(monitor.id); // re-check now
```
Tell the service what happened to the post you sent and it keeps your own suppression list. Outcomes are `delivered`, `returned`, `moved` and `refused`. A `moved` or `refused` report suppresses the address at once; a second `returned` not followed by a later `delivered` suppresses it; a `delivered` report clears an entry the feedback put there, and never touches one you imported. Batch, bulk and monitor runs answer a suppressed address free with `meta.suppressed` true, a single lookup never consults the list, and drops an entry your reports added once they no longer stand behind it (a delivery since, or reports aged out); an import leaves only when you remove it; and an entry your reports added is re-verified rather than served once it is older than 90 days, while an import never goes stale.
```js
await spaw.reportAddressFeedback([ // free, and never logged as a lookup
{ address: '10 Downing Street, London SW1A 2AA', outcome: 'returned' },
{ address_line1: 'Postfach 10 01 01', country: 'DE', outcome: 'moved' },
], { country: 'GB' });
(await spaw.addressFeedbackSummary()).data.outcomes; // your own counts, by outcome
await spaw.addAddressSuppressions(deadAddresses, { country: 'GB' }); // up to 10,000, free
(await spaw.listAddressSuppressions()).data.suppressions;
await spaw.removeAddressSuppression(suppressionId); // read fresh next time
```
Unlike the IP and phone products, address feedback is never shared between accounts. An IP is a network endpoint and a phone number is a handset, but a postal address is where somebody lives, and a shared "post fails here" database would be a database about residences. Your reports raise the risk of your own later lookups of an address and nobody else's, through `reported_returned` (+50, dataset `address-feedback`), which is scored while your most recent report for the address is `returned`, `moved` or `refused` rather than `delivered`.
## Businesses
Business verification reads one identifier — a Legal Entity Identifier or a UK company number — against the registers this server holds, which is the check you want before signing with a counterparty or when a supplier list needs going over again. Both registers are bulk files synced locally, so no third party sits in the request path and nothing about a lookup leaves the server; the LEI reference data is published under a CC0 public-domain dedication, and Spaw is not affiliated with its publisher. A company the national register records as dissolved is the strongest signal the product carries: a dissolved company no longer exists as a legal person and cannot enter a contract. A monitor that stops being run is deleted and the identifiers it stored go with it — `prunes_after_days` on every monitor payload says how long that takes — because an identifier can belong to a sole trader as easily as to a company.
## Cross-signal consistency
`checkConsistency` runs any subset of the four signals in one call and reports where their answers agree and where they do not: it is built for signup, onboarding and marketplace vetting, the moment before there is an order or a card and the contact details are all you have. Each signal bills exactly as its own single lookup, and the analysis on top is free, because every comparison is arithmetic over fields the four pipelines already computed. There is no score and no verdict — you get named flags and both sides of every comparison quoted, because a difference between two fields is a fact about the fields and not a judgement about a person.
`checkConsistency` is one record at a time. `previewConsistencyBulkJob` and `startConsistencyBulkJob` run the same check over a list of up to 100,000 rows, so scoring a signups export is one upload rather than four and a spreadsheet join afterwards. One header row is read as an email address, a phone number, an IP address and the address fields at once, and a row that resolves only two of the four is a perfectly good consistency check of those two.
The preview prices the run against the SIGNALS the columns feed rather than one credit a row — a file reaching only an email address and a phone number answers `credits_per_row` 2, not 4 — and it is still a ceiling and never a quote.
```js
const draft = await spaw.previewConsistencyBulkJob(undefined, { csv, country: 'GB' });
draft.data.draft.mapping; // { '1': 'email', '2': 'phone', '3': 'ip', '4': 'address_line1', … }
draft.data.draft.credits_upper_bound; // the most it can cost, never what it will cost
const job = await spaw.startConsistencyBulkJob(draft.data.draft.id);
const rows = await spaw.downloadConsistencyBulkResults(job.data.job.id, { variant: 'flagged' });
```
A result row is the comparison, not four lookups pasted together: `signals_requested`, `signals_answered`, `country_agreement`, `flags`, one cell per comparison, `distance_km`, and the few per-product fields those comparisons read. A comparison cell reads `agree`, `differ`, `not_compared`, `missing_value`, `low_confidence`, or nothing at all — an empty cell is a comparison never attempted, because the row did not carry both of its signals.
```js
const report = await spaw.checkConsistency({
email: 'ada@example.com',
phone: '+442079460018',
country: 'GB', // the region hint for the phone number only
ip: '81.2.69.142',
address: { address: '10 Downing Street, London SW1A 2AA', country: 'GB' },
});
report.data.consistency.flags; // e.g. ["ip_country_differs_from_address_country"]
report.data.consistency.country_agreement; // "agree", "differ", or null when none could be made
report.data.signals.ip.data; // each product's own result, unchanged
report.meta.credits_used; // the sum of the per-signal costs
```
## Handle errors
Every failure throws a subclass of `SpawError` carrying `code`, `message`, `httpStatus` and `requestId`; no credits are spent on a failed request.
```js
import Spaw, { InsufficientCreditsError, RateLimitedError, SpawError } from 'spaw';
try {
const result = await spaw.verifyEmail('mia@acme.com');
} catch (error) {
if (error instanceof InsufficientCreditsError) {
// nothing ran and nothing was billed
} else if (error instanceof RateLimitedError) {
// error.retryAfter seconds, from the Retry-After header
} else if (error instanceof SpawError) {
console.error(error.code, error.httpStatus, error.requestId);
}
}
```
`AuthenticationError` maps `UNAUTHENTICATED`, `ValidationError` maps `VALIDATION_FAILED` with `errors` keyed by field, `NotFoundError` covers a bulk job or suppression entry that is not on the account, and `SpawConnectionError` means the API could not be reached. Pass `maxRetries: 2` to the constructor to retry `429` and `5xx` answers automatically, honouring `Retry-After`.
## What to do with the verdict
Branch on `deliverable`. Keep `deliverable` addresses, drop `undeliverable` ones, and route `risky` ones by `reason`: refuse `disposable` and `likely_typo`, decide `role` per use case, and treat `catch_all` as an address whose mailbox cannot be confirmed rather than a bad one, using `mailbox_confidence` as the threshold. When `did_you_mean` is set, offer it back to the user.
## Cost
One credit per fresh deliverable or risky verdict, and one per five IP lookups. Unverified answers, invalid input, repeats within seven days and the `spaw.test` test addresses cost nothing, undeliverable verdicts cost nothing within a fair-use allowance, and every account gets 100 free credits a month. Point your test suite at `deliverable@spaw.test` and its siblings: fixed answers, no credits, never logged.
---
# Verify emails and phone numbers for ActiveCampaign with Spaw
Verify new ActiveCampaign contacts' emails and phone numbers through Zapier or Make, tag them by the answer so automations react, and clean a list in bulk.
Updated: 2026-09-27
ActiveCampaign automations start from tags and custom fields, so a verification step only has to put the verdict on the contact and the rest is ordinary automation work. Spaw does not offer an ActiveCampaign app; the flow uses ActiveCampaign's new-contact trigger in Zapier or Make, a verification step, and an update that adds a tag and fills a field. The contact's phone number takes a second flow of the same kind.
## Verify new contacts
1. Create a secret API key on the API keys page of your Spaw dashboard.
2. In ActiveCampaign, add a custom text field for the verdict reason and a number field for the risk score. Tags carry the verdict itself.
3. In Zapier or Make, start with the ActiveCampaign trigger for a new contact, or for a contact added to a specific list if you only want to verify subscribers.
4. Add the verification step: a Webhooks by Zapier POST or a Make HTTP request to `https://spaw.co/api/v1/email` with the header `Authorization: Bearer sk_live_…` and this body, mapping the contact's email:
```json
{
"email": "mia@acme.com"
}
```
The [Zapier page](/integrations/zapier) and the [Make page](/integrations/make) describe the step field by field. Once the Spaw apps for those platforms are listed, it becomes a Verify Email action with a saved connection.
5. Add the ActiveCampaign action that creates or updates a contact, match on the email, add a tag built from `data.deliverable` (for example `spaw-deliverable`, `spaw-risky`, `spaw-undeliverable`), and map `data.reason` and `data.risk_score` into the custom fields.
6. In ActiveCampaign, build an automation that starts when the `spaw-undeliverable` tag is added and unsubscribes the contact from marketing lists, and another that gates your welcome series on the `spaw-deliverable` tag.
7. Test by creating a contact with `deliverable@spaw.test`, a free test address that answers a fixed result and never appears in your history.
If a welcome automation starts on list subscription, add a wait step of a few minutes before the first send so the tag has time to arrive.
## Clean an existing list
Export the list to CSV, run the email column through a bulk verification on the Spaw dashboard or with the [bulk endpoint](/docs/api/create-bulk-job), and import the result CSV back with the verdict column mapped to tags on import and the reason and score mapped to the custom fields. The result keeps your original columns. Undeliverable rows also join your Spaw suppression list, so later runs answer them for free. The [list cleaning checklist](/guides/email-list-cleaning-checklist) explains what to do with each group.
## What to do with the verdict
Let the tags drive the automations. `spaw-deliverable` contacts get the full program. `spaw-risky` contacts need the reason field: [`role`](/docs/reasons/role) inboxes such as info@ rarely engage with marketing mail, `catch_all` domains are unconfirmed but often real, and `disposable` domains are gone before the second email. `spaw-undeliverable` contacts should be unsubscribed rather than deleted; when the response's `data.did_you_mean` is filled, a task for a teammate with the suggested address recovers the record. The risk score field gives a single threshold for teams that prefer one number.
## Phone numbers
1. In ActiveCampaign, add a custom text field for the phone answer's blocking reason. Tags carry the decision itself.
2. Start a second Zap or scenario on the same new-contact trigger, with a filter that continues only when the contact has a phone number (an empty `phone` is refused with a `422`, which stops the run). Kept apart from the email flow, a contact without a phone number still gets its email tag. In it, add a Webhooks by Zapier POST or a Make HTTP request to `https://spaw.co/api/v1/phone` with the same header and this body, mapping the contact's phone:
```json
{
"phone": "(202) 555-0100",
"country": "US"
}
```
3. Add a Paths step in Zapier or a router in Make on `data.ok_to_send`. The true path adds the tag `spaw-phone-ok` and writes `data.e164` into the contact's phone field, so every number is stored in one form. The false path adds `spaw-phone-blocked` and writes `data.blocked_by` into the reason field.
4. If your automations send text messages, start their SMS steps with a condition on the `spaw-phone-ok` tag, after the same short wait the welcome series uses.
5. Test with +1 202 555 0100 and +1 202 555 0103, two documented test numbers that cost nothing: the first answers a valid mobile with `ok_to_send` true, the second `ok_to_send` false with `blocked_by` `disposable`, under the default SMS policy.
A number stored with its + and calling code needs no `country`. For numbers written in national format, send the ISO 3166-1 alpha-2 code of their country, typed into the step when your contacts are all in one country or mapped from a field that holds the two-letter code; a country name, or `UK` (the code is `GB`), is refused with a `422`, and a national number sent without a country answers `valid: false` with the reason `missing_country`, free. Send the number and its country only, nothing else about the contact. Once the Spaw apps are listed, the step becomes the Validate Phone Number action in Zapier and Validate a Phone Number in Make.
The phone step only tags and fills fields; it never subscribes a contact to anything. `ok_to_send` is your account's [SMS policy](/guides/gate-sms-verification-codes-with-the-sms-policy) applied to the answer, set once on the account so every Zap, batch and bulk run applies the same rules, and `blocked_by` names the rule that decided a false: `invalid`, `opted_out` (by default) for a number your suppression list holds as opted out, `country`, `line_type`, a signal such as `disposable` for a number published by an SMS-receiving website, where anyone can read a code sent to it, or `risk_score`. It is a fraud and deliverability decision, not consent and not a do-not-call scrub. Every field is on the [phone endpoint page](/docs/api/validate-phone).
## Cost
Each new contact that answers deliverable or risky spends 1 credit; unverified answers and repeats within seven days are free, and undeliverable answers are free within a fair-use allowance. A phone lookup costs 1 credit when the number is valid; invalid numbers, unassigned blocks and repeats of the same number within seven days are free. Zapier and Make bill their own tasks. Bulk runs bill per processed row and verify duplicates once.
---
# Verify emails and phone numbers from Typeform with Spaw
Verify the email and phone answers of every Typeform response through Zapier or Make before they reach your CRM or sheet, and recover email typos.
Updated: 2026-09-27
Typeform checks that an email answer looks like an address, and nothing more. A response with `mia@gmail.con` passes and lands in your CRM as a lead nobody can reach. Because Typeform hosts the form and does not run your scripts, the Spaw form helper cannot watch the field the way it does on your own pages; verification happens right after submission instead, in the Zap or Make scenario that already carries the response onward. A phone-number question gets the same treatment in a second step, covered below.
## Verify each response
1. Create a secret API key on the API keys page of your Spaw dashboard.
2. In Zapier or Make, start with the Typeform trigger for a new response and pick the form.
3. Add the verification step: a Webhooks by Zapier POST or a Make HTTP request to `https://spaw.co/api/v1/email` with the header `Authorization: Bearer sk_live_…` and this body, mapping the email question's answer:
```json
{
"email": "mia@acme.com"
}
```
The [Zapier page](/integrations/zapier) and the [Make page](/integrations/make) show the step in detail. When the Spaw apps for those platforms are listed, it becomes a Verify Email action with a saved connection.
4. Add a filter or router on `data.deliverable` from the response.
5. Send `deliverable` and `risky` responses on to their destination as before, for example a CRM contact or a sheet row, and map `data.deliverable`, `data.reason` and `data.did_you_mean` into extra fields there.
6. Route `undeliverable` responses to a review sheet or a notification instead of the CRM.
7. Test by submitting the form with `deliverable@spaw.test`, a free test address that answers a fixed result and is never logged.
The response also contains every field of the [single-address result](/docs/api/verify-email), including whether the domain is a free provider, whether the address is a role inbox, and the risk score, so the destination can hold as much or as little as you need.
## Forms on your own site
If the form is a page you control rather than a hosted Typeform, the [form helper](/integrations/website-forms) checks the address while the visitor is still typing and offers the correction before they submit, which is the better moment, and it checks a phone input marked `data-spaw-phone` as well, showing the international format of a valid number or the reason one is not valid. The two approaches combine well: the helper on your own pages, the Zap for hosted forms.
## What to do with the verdict
The most valuable field here is `data.did_you_mean`. A form typo is usually a real person who wants to hear from you, so when the suggestion is filled, for example `mia@gmail.com` for `mia@gmail.con`, create the record with the corrected address and note the original. For `risky` responses, look at `data.reason`: `disposable` addresses were entered to get past the form and can be dropped, `role` inboxes and `catch_all` domains are usually worth keeping with lower priority. For a single threshold, `data.risk_score` is 0 to 100 with published weights, as the [regex guide](/guides/email-validation-regex-is-not-enough) explains.
## Phone numbers
A phone-number question gets its own step in the same Zap or scenario, placed before any filter on the email verdict so an undeliverable email does not stop it: a Webhooks by Zapier POST or a Make HTTP request to `https://spaw.co/api/v1/phone` with the same header and this body, mapping the phone answer:
```json
{
"phone": "+1 202 555 0100"
}
```
A number that arrives with its + and calling code needs nothing else. If the form collects numbers from one country without the code, add `country` with that country's ISO 3166-1 alpha-2 code, such as `US`. If a separate question asks for the country, map it only when its answers are two-letter codes: a country name, or `UK` (the code is `GB`), is refused with a `422`. A national number sent without a country answers `valid: false` with the reason `missing_country`, free. `phone` itself is required, so an unanswered question is refused with a `422` and stops the run; if the question is optional, put the step on a Paths branch in Zapier or a router route in Make that runs only when the answer is filled. Once the Spaw apps are listed, the step becomes the Validate Phone Number action in Zapier and Validate a Phone Number in Make.
Send `data.e164` on to the destination as the number to store: it is the one [E.164](/glossary/e164) form of the number however the respondent typed it, so records dedupe on it. Before any step that texts the respondent, filter or route on `data.ok_to_send`. It is your account's [SMS policy](/guides/gate-sms-verification-codes-with-the-sms-policy) applied to the answer, set once on the account so every Zap, batch and bulk run applies the same rules, and when it is false `data.blocked_by` names the rule that decided it: `invalid`, `opted_out` (by default) for a number your suppression list holds as opted out, `country`, `line_type`, a signal such as `disposable` for a number published by an SMS-receiving website, where anyone can read a code sent to it, or `risk_score`. It is a fraud and deliverability decision, not consent and not a do-not-call scrub. For an invalid answer sent with a country, `data.did_you_mean` may hold the one valid reading of the digits, for example a number typed without its +; unlike the email suggestion, keep it as a note for a person to confirm rather than writing it over the answer. Every field is on the [phone endpoint page](/docs/api/validate-phone).
Test with +1 202 555 0100 and +1 202 555 0103, two documented test numbers that cost nothing: the first answers a valid mobile with `ok_to_send` true, the second `ok_to_send` false with `blocked_by` `disposable`, under the default SMS policy.
## Cost
Each response that answers deliverable or risky spends 1 credit; unverified answers, the test addresses, and repeats of the same address within seven days are free, and undeliverable answers are free within a fair-use allowance. A phone lookup costs 1 credit when the number is valid; invalid numbers, unassigned blocks, the test numbers and repeats of the same number within seven days are free. Zapier and Make bill their own tasks. A form with a few hundred responses a month stays well inside a small credit pack.
---
# Verify emails and phone numbers in Salesforce with Spaw
Verify the emails and phone numbers of new Salesforce leads and contacts through Zapier or Make, store the answers in custom fields, and clean in bulk.
Updated: 2026-09-27
Salesforce orgs collect addresses through web-to-lead forms, list imports and integrations, and a bad address on a lead costs a rep's time before it bounces. Spaw does not publish an AppExchange package. The recommended path is a Zap or a Make scenario that triggers on a new lead or contact, verifies the address, and writes the verdict into custom fields that your reports, list views and flows can use. The record's phone number takes a second flow of the same kind.
## Prepare the org
1. Add three custom fields to Lead, and the same three to Contact if you verify both: a picklist or text field for the verdict, a text field for the reason, and a number field for the risk score. Names such as `Email_Verdict__c`, `Email_Verdict_Reason__c` and `Email_Risk_Score__c` read well in reports.
2. Create a secret API key on the API keys page of your Spaw dashboard.
## Verify new records as they are created
1. In Zapier or Make, start with the Salesforce trigger for a new record and choose the Lead object (or Contact).
2. Add the verification step: a Webhooks by Zapier POST or a Make HTTP request to `https://spaw.co/api/v1/email` with the header `Authorization: Bearer sk_live_…` and this body, mapping the record's email:
```json
{
"email": "mia@acme.com"
}
```
The [Zapier page](/integrations/zapier) and the [Make page](/integrations/make) walk through the exact settings. When the Spaw apps for both platforms are listed, the step becomes a Verify Email action with a saved connection; the fields do not change.
3. Add a filter so records without an email are skipped.
4. Add the Salesforce action that updates a record, pick the same object, match on the record id from the trigger, and map `data.deliverable`, `data.reason` and `data.risk_score` into the custom fields.
5. Test by creating a lead with the address `deliverable@spaw.test`, one of six free test addresses that answer a fixed result and never appear in your history.
Teams that prefer to stay inside Salesforce can call the same endpoint from Apex with a Named Credential holding the bearer token; the [OpenAPI document](/integrations/openapi) describes every request and response for that work. This page keeps to the no-code path.
## Clean existing leads and contacts
Export the addresses with their record ids, run the file through a bulk verification on the Spaw dashboard or with the [bulk endpoint](/docs/api/create-bulk-job), and load the result CSV back with a data loader, matching on the id. The result keeps your original columns and adds the verdict, reason, risk score and suggestion columns, so the mapping is one to one. Undeliverable rows also land on your Spaw suppression list, which later batch and bulk runs answer for free.
## What to do with the verdict
Build list views and assignment rules on the verdict field. `deliverable` leads route to reps as usual. `risky` leads deserve a look at the reason before a sequence: `catch_all` means the domain accepts every address and the mailbox is unconfirmed, `role` means a shared inbox, `disposable` means a throwaway domain. `undeliverable` leads should be flagged rather than deleted, because a filled `data.did_you_mean` value, for example `mia@acme.com` for `mia@acme.cmo`, is often a lead worth one correction. A validation rule or flow can block a lead from converting while the verdict is undeliverable.
## Phone numbers
A second Zap or scenario on the same trigger validates the lead's or contact's phone number.
1. Add four more custom fields to the same objects: text fields for the E.164 number, the line type and the blocking reason, and a checkbox for whether sending is allowed. Names such as `Phone_E164__c`, `Phone_Line_Type__c`, `Phone_Blocked_By__c` and `Phone_OK_To_Send__c` sit next to the email fields in reports.
2. Start that Zap or scenario with the same new-record trigger and object, and a filter that continues only when the record's Phone (or Mobile) field is filled (an empty `phone` is refused with a `422`). In it, add a Webhooks by Zapier POST or a Make HTTP request to `https://spaw.co/api/v1/phone` with the same header and this body, mapping that field:
```json
{
"phone": "(202) 555-0100",
"country": "US"
}
```
3. Give that flow an update-record action of its own on the same object, matched on the record id from the trigger, that maps `data.e164`, `data.line_type`, `data.blocked_by` and `data.ok_to_send` into the four fields. Kept separate, a record without a phone number still gets its email verdict, and a record without an email still gets its phone number checked.
4. Test by creating a lead with the phone number +1 202 555 0100, then +1 202 555 0103: two documented test numbers that cost nothing, the first answering a valid mobile with `ok_to_send` true, the second `ok_to_send` false with `blocked_by` `disposable`, under the default SMS policy.
`country` is optional and must be an ISO 3166-1 alpha-2 code. A number stored with its + and calling code needs none, and a national number sent without one answers `valid: false` with the reason `missing_country`, free. Do not map the address's Country field as it stands when it holds names: "United States", or `UK` (the code is `GB`), is refused with a `422`. Map a field that holds the two-letter code, add a formula field that converts your org's names, or type the one country your records come from into the step. Send the number and its country only, nothing else about the person. Once the Spaw apps are listed, the step becomes the Validate Phone Number action in Zapier and Validate a Phone Number in Make.
`Phone_E164__c` is the form to dedupe and match records by. It is also kept for an invalid number in a block nobody holds, so read it together with the checkbox, which is false for every invalid number. Before a flow step that texts the record, check the checkbox: `ok_to_send` is your account's [SMS policy](/guides/gate-sms-verification-codes-with-the-sms-policy) applied to the answer, set once on the account so every Zap, batch and bulk run applies the same rules, and `blocked_by` names the rule that decided a false. It is a fraud and deliverability decision, not consent and not a do-not-call scrub. `line_type` comes from the numbering plan or, for a North American number, from the holder of its block in the regulator's table; it describes how the range was allocated, never the network that serves the number after a port, and an ordinary US number answers `fixed_line_or_mobile` unless its block holder settles the type ([why](/guides/why-us-numbers-cannot-be-split-into-mobile-and-landline)). Leave the complaint counts (`complaints_90d`, `complaints_last_at`) off leads and contacts: they count the complaints consumers sent the FTC naming a number as the caller, which is not the Do Not Call Registry and says nothing about whether you may call or text the number, and the reports are unverified, with caller ID often spoofed. Every field is on the [phone endpoint page](/docs/api/validate-phone).
## Cost
Each new record that answers deliverable or risky spends 1 credit; unverified answers and repeats within seven days are free, as are the test addresses, and undeliverable answers are free within a fair-use allowance. A phone lookup costs 1 credit when the number is valid; invalid numbers, unassigned blocks, the test numbers and repeats of the same number within seven days are free. Zapier and Make bill their own tasks. A bulk run bills per row as it is processed and verifies duplicates once.
---
# Verify emails, phones, IPs and postal addresses in n8n
Call the Spaw API from n8n's HTTP Request node, branch on email verdicts, phone and IP flags or postal address checks, and what the n8n-nodes-spaw node adds.
Updated: 2026-10-14
n8n runs on n8n Cloud or on your own server, and either way its HTTP Request node can talk to Spaw with nothing installed. A community node, `n8n-nodes-spaw`, is built and will be published to npm; it wraps the same endpoints as resources and operations with a saved credential. Both paths are described here.
## What works today: the HTTP Request node
1. Create a secret API key on the API keys page of your Spaw dashboard.
2. In n8n, create a credential of the generic Header Auth type. Set the name to `Authorization` and the value to `Bearer sk_live_…`. The key is stored encrypted and never shows in the workflow JSON.
3. Add an HTTP Request node after the node that produces the address. Set the method to POST and the URL to `https://spaw.co/api/v1/email`.
4. Set authentication to the generic credential type, pick Header Auth, and choose the credential from step 2.
5. Turn on sending a body, choose JSON, and use an expression for the content:
```json
{
"email": "{{ $json.email }}"
}
```
6. Execute the node once with `deliverable@spaw.test` in the incoming item. It is one of six free test addresses that answer a fixed result and never touch your history.
The node outputs the API's envelope as one item: `data` holds every field of the [single-address response](/docs/api/verify-email), `meta` holds `credits_used`, `credits_remaining`, `cache_hit` and `request_id`.
For many items at once, send them to the [batch endpoint](/docs/api/verify-email-batch) instead. Set the URL to `https://spaw.co/api/v1/email/batch`, enable the node's option to execute once, and build the body with an expression that gathers every incoming item:
```json
{{ { "emails": $input.all().map(item => item.json.email).slice(0, 50) } }}
```
The answer's `data.results` array is in input order; a Split Out node turns it back into one item per address. A batch counts as one request against the 5 requests per second limit, which matters for workflows fed by large lists.
## The community node
`n8n-nodes-spaw` is a declarative community node written in TypeScript with no runtime dependencies, which is what n8n asks of nodes it verifies. Once it is on npm, self-hosted instances install it from the community nodes settings by package name, and n8n Cloud shows it after n8n's verification. It offers:
- A Spaw API credential holding the secret key, tested against your account endpoint when saved.
- Email resource: Verify, and Verify Many for up to 50 addresses per call.
- Domain resource: Get, the same signals as the domain endpoint.
- Suppression resource: Add, Get Many, and Remove, so bounce lists from other systems can be kept in sync.
- IP resource: Look Up, and Look Up Many for up to 1,000 addresses per call, both with a Privacy Mode option.
- Address resource: Validate, with the address on one line, a required Country, and an Options collection for the parts you would rather send separately (a named Building among them) and for the licensed deliverability check. The item carries every field of the answer, `confirmation` (what stood behind each part) and `changes` (what the parser set aside on the way) included.
- Entity resource: Verify, with an Identifier, an Identifier Type of LEI or Company Number, and a Country field that appears for a company number, which needs the register it belongs to.
- Phone resource: Validate, Validate Many for up to 1,000 numbers per call with a shared Country and a Live Carrier Check option, and Report Outcome to tell Spaw what happened after an SMS (delivered, undelivered, abused, verified or opted out), which is free. The IP resource has its own Report Outcome (abuse, fraud, bot or chargeback), free as well: your own reports score your own later lookups of the address.
It is not published yet. The HTTP Request node above uses the same endpoints, so a workflow built today can swap nodes later without changing what is stored.
## IP lookups
The HTTP Request node looks up addresses the same way: method POST, URL `https://spaw.co/api/v1/ip`, the same Header Auth credential, and a JSON body of `{ "ip": "{{ $json.ip }}" }` mapped from whatever recorded the visitor's address. The answer's `data` block carries the location, the network and its operator type, tri-state flags for datacenter and cloud ranges, Tor exits, VPN exits and iCloud Private Relay with the provider named when its own list contains the address, `is_anonymous`, and a published `risk_score` with its `risk_signals`. An IF node on `{{ $json.data.is_anonymous }}` or a Switch on `{{ $json.data.risk_level }}` covers most screening rules. For lists, `https://spaw.co/api/v1/ip/batch` takes `{ "ips": [...] }` with up to 1,000 addresses per call; in the community node these are the IP resource's Look Up and Look Up Many operations, whose Privacy Mode option keeps nothing about the lookup on Spaw's side. One credit covers five [IP lookups](/docs/api/lookup-ip) that resolve a location, a network or a positive flag: the lookup that opens a block reports `credits_used` 1, the next four 0, and `meta.ip_lookups_prepaid` counts what is left. Reserved or private ranges, unallocated space, unknown addresses and repeats within seven days are free, and IP endpoints have their own limit of 50 requests per second.
## Phone numbers
The HTTP Request node validates a phone number the same way: method POST, URL `https://spaw.co/api/v1/phone`, the same Header Auth credential, and a JSON body of `{ "phone": "{{ $json.phone }}" }`, with a `country` key (an ISO 3166-1 alpha-2 code such as `US`) when the numbers are written without a calling code. The answer's `data` block carries `valid` and a typed `reason`, the E.164, national, international and `tel:` forms, the country and its facts, `line_type` and `carrier` with the source that settled them (the numbering plan, or the block allocation published by the regulator, which says who the numbers were allocated to and never who serves them after a port), `block_status`, the `is_fictional`, `is_virtual` and `is_disposable` flags, and a published `risk_score` with its `risk_signals`. An IF node on `{{ $json.data.valid }}` gates the signup; a Switch on `{{ $json.data.risk_level }}` or `{{ $json.data.is_disposable }}` separates numbers worth an SMS from numbers that only receive codes for strangers. An IF node on `{{ $json.data.ok_to_send }}` is the shorter gate: it is your account's [SMS policy](/guides/gate-sms-verification-codes-with-the-sms-policy) applied to this answer (the countries and line types you send to, the signals you refuse, a ceiling on the score and, by default, the numbers your suppression list holds as opted out), and on the false branch `{{ $json.data.blocked_by }}` names the rule that decided it (`invalid`, `opted_out`, `country`, `line_type`, a signal such as `disposable`, or `risk_score`; `suppressed` for a number on your suppression list, which batch, bulk and monitored runs honour and a single lookup never does, while a single lookup of a number the list holds as opted out answers `opted_out` unless the policy's `block_opted_out` is off). The policy is set once on the account, not in the workflow, so every workflow, batch and bulk run applies the same rules. It is a fraud and deliverability decision, not consent and not a do-not-call scrub. In the community node this is the Phone resource's Validate operation. A [phone lookup](/docs/api/validate-phone) costs 1 credit when the number is valid; invalid numbers, unassigned blocks and repeats within seven days are free.
For a list, point the same node at `https://spaw.co/api/v1/phone/batch` with `{ "phones": [...] }` (up to 1,000 numbers, each a string or `{ "phone", "country" }`) and an optional list-level `country`; the answer's `data.results` is in input order and numbers on your suppression list come back free. In the community node this is Validate Many, whose Options collection carries Live Carrier Check (the API's `hlr`). The live carrier-network check (`hlr: true`) is not enabled on the service: `hlr_checked` answers false and nothing extra is charged. After you send an SMS, tell Spaw what happened with `POST https://spaw.co/api/v1/phone/feedback` and `{ "items": [{ "phone": "...", "outcome": "delivered" }] }` (`undelivered`, `abused`, `verified` and `opted_out` are the other outcomes): the report is free, feeds only your own later lookups of that number and your own phone suppression list, and is the Phone resource's Report Outcome operation in the community node. An `undelivered` item can carry the `failure_class` your provider's error code names (`sender_side` for a carrier filter or an unregistered campaign, `permanent` for a number that no longer exists), and only failures about the number count against it.
## Postal addresses
The HTTP Request node checks a postal address the same way: method POST, URL `https://spaw.co/api/v1/address`, the same Header Auth credential, and a JSON body of `{ "address": "{{ $json.address }}", "country": "GB" }`, or the parts separately as `address_line1`, `locality`, `administrative_area`, `postal_code` and the rest. The country is not optional in practice: it decides which format the address is read under, which parts that country requires and which postcode directory answers, and it is never guessed. The answer's `data` block carries the components and the `formatted` address, `postal_code_valid` and `postcode_status`, whether the town matches the postcode, `address_type` with the source that settled it, `match_level` against the `register` named, the postcode centroid, `companies_registered`, and a published `risk_score` with its `risk_signals`. An IF node on `{{ $json.data.ok_to_ship }}` gates the order: it is your account's [shipping policy](/guides/gate-a-checkout-with-the-shipping-policy) applied to this answer, and on the false branch `{{ $json.data.blocked_by }}` names the rule that decided it (`invalid`, a signal such as `placeholder`, `risk_score` or `unconfirmed`; `suppressed` for an address on your suppression list, which batch, bulk and monitored runs honour and a single lookup never does), the reason to write back to the order or pass to whoever reviews it. The policy is set once on the account, not in the workflow, so every workflow, batch and bulk run applies the same rules; a Switch on `{{ $json.data.address_type }}` is still there when a workflow needs to route a PO box or a private mailbox differently rather than refuse it. In the community node this is the Address resource's Validate operation; for a list, `https://spaw.co/api/v1/address/batch` takes `{ "addresses": [...] }` with up to 100 per call.
An empty field means the signal was not evaluated, never "no". `exists` is true only on a premise match and false only under a register whose coverage is complete; a register whose coverage is partial, because it covers part of the country or cannot vouch for every address, cannot make an address's absence mean anything. `is_mail_drop` is a heuristic and not a postal service's own agency flag: an operator's own published branch list names the address, or so many companies are registered there that it is one in practice, and `mail_drop_source` says which. Two answers are licensed and not enabled — whether post actually arrives (`deliverability_checked` comes back false with `is_deliverable`, `is_cmra`, `is_vacant` and `is_residential` empty) and the list of addresses inside a postcode, which answers a typed `501` rather than a guess. An address lookup costs 1 credit when the address is valid; invalid addresses and repeats within seven days are free.
## Business identifiers
The HTTP Request node checks a company the same way: method POST, URL `https://spaw.co/api/v1/entity`, the same Header Auth credential, and a JSON body of `{ "identifier": "{{ $json.lei }}", "type": "lei" }` — or `{ "identifier": "{{ $json.company_number }}", "type": "company_number", "country": "GB" }`, because a company number is only unique inside its own register and the country says which one, while a LEI is global by construction and ignores it. The answer's `data` block carries the legal name, `status` with the register's own `status_detail`, the country and jurisdiction of formation, the category and legal form, `registered_at` and `record_updated_at`, and for a LEI `checksum_valid`, `registration_status` and `next_renewal_at`. Where a LEI names its entry in a national register Spaw also holds, `agreements` quotes both sides field by field and `flags` names what differs. An IF node on `{{ $json.data.found }}` separates identifiers no register carried from the rest; a Switch on `{{ $json.data.status }}` or a filter on `dissolved_in_national_register` in `flags` stops a workflow before it signs with a company that no longer exists. In the community node this is the Entity resource's Verify operation, whose Country field only appears once Company Number is picked. For a list, `https://spaw.co/api/v1/entity/batch` takes `{ "identifiers": [...] }` with up to 500 per call, each a LEI string or an object of `identifier`, `type` and `country`.
The one caveat, and it matters more than anything else here: the UK company file is a snapshot of the **live** register, so a company that is struck off is **removed from the file** rather than marked dissolved — the statuses that file carries are `Active`, `Active - Proposal to Strike off`, `Liquidation`, `In Administration` and similar, and never `Dissolved`. A UK dissolution therefore reaches you as `found` `false` with `reason` `unknown_identifier` rather than as that flag, which never fires on the only national register installed today. Check `found` before you trust the flag, and read `status_detail` for the earlier warning: `Active - Proposal to Strike off` folds to a `status` of `active`, because the company is still on the register. A LEI lookup of the same company is quieter still: GLEIF keeps its record, so `found` stays `true` and no flag fires. What moves is `agreements`, which comes back empty while `other_register` still names `uk-companies-house` — the counterpart row is gone. That pair is the signal on this path, and `lei_registration_not_current` follows later, when the lapsed registration is not renewed. A [monitored list](/docs/business-verification) turns the disappearance into an alert of its own, `left_register`.
`checksum_valid` is a typing check and nothing more: a perfectly formed LEI can never have been issued, which is what the register lookup answers. `registration_status` is about the identifier rather than the entity — a LEI must be reconfirmed each year and a great many are not, so `LAPSED` means the reference data has not been rechecked recently, not that the company has gone. Nothing in the answer is a registered address, and nothing will be: a registered address is very often somebody's home, and an identifier alone does not say whether a company or a sole trader stands behind it.
Both registers are bulk files synced onto the Spaw server, so no third party sits in the request path. The LEI reference data is published under a CC0 public-domain dedication and refreshed daily; the UK company data is used under the Open Government Licence v3.0 and republished monthly, so an answer can be up to five weeks behind the live register and every answer names the date of the copy in `sources`. Spaw is not affiliated with, endorsed by or connected to either publisher. A [business lookup](/docs/api/verify-entity) costs 1 credit when a register carried the identifier; unknown, malformed and unsupported identifiers and repeats within seven days are free.
## What to do with the verdict
Add an IF node after the request and compare `{{ $json.data.deliverable }}` with `deliverable`. The true branch continues to your CRM or mailing tool. On the false branch, a Switch node on `{{ $json.data.reason }}` separates `catch_all` and `role`, which are often worth keeping, from `disposable` and `mailbox_not_found`, which are not. When `{{ $json.data.did_you_mean }}` is filled, the address was a near miss, and writing the suggestion back to the source recovers the record. For a numeric threshold use `data.risk_score`, whose weights are published.
Workflows that verify at scale should also read the suppression list occasionally through the [list endpoint](/docs/api/list-suppressions): every undeliverable verdict lands there, and batch and bulk runs answer suppressed addresses for free.
## Cost
A fresh deliverable or risky answer spends 1 credit; unverified answers, invalid input, repeats within seven days and the test addresses are free, and undeliverable answers are free within a fair-use allowance. n8n executions are counted by n8n separately. The node handles `429 RATE_LIMITED` like any HTTP error, so enable the HTTP Request node's retry option for bursty workflows.
---
# Check phone numbers in an Auth0 custom phone provider Action with Spaw
A custom-phone-provider Action that asks Spaw about the number and the visitor's IP, drops codes your SMS policy would not send and sends the rest.
Updated: 2026-09-27
Auth0 sends one-time codes through the tenant's phone provider, and a Custom provider is an Action on the `custom-phone-provider` trigger that delivers each message itself. That Action is where Spaw's check goes: it asks about the number and the visitor's address, drops the message with `api.notification.drop()` when your account's [SMS policy](/docs/phone-intelligence#policy) answers `ok_to_send: false`, and otherwise sends it. Auth0 sends nothing on its own once the Custom provider is selected, so the recipe below includes the SMS call.
## What the Action receives
From Auth0's documentation of the custom phone provider and its event and API objects, read on 27 September 2026:
| Item | What Auth0 provides |
| --- | --- |
| Where | Branding, Phone Provider, Custom; one provider per tenant for MFA and passwordless (Auth0's Unified Phone Experience, where new tenants are enrolled automatically) |
| The number | `event.notification.recipient`, in E.164 form |
| The visitor's address | `event.request.ip`, "the originating IP address of the request"; Auth0's own passwordless example allows for `event.request` being undefined, so the sample does too |
| The flow | `event.notification.message_type`: `otp_verify`, `otp_enroll`, `blocked_account`, `change_password` or `password_breach` |
| The message | `event.notification.as_text`, ready to send; `code` on the OTP types |
| Refusing | `api.notification.drop(reason)`: logged as failed and never retried; `retry(reason)` asks for up to five more attempts |
| Time limit | Each Action execution must complete in 20 seconds |
The older `send-phone-message` trigger runs only for MFA by SMS or voice, and Auth0 says it "should not be used for configuring a custom phone provider". A tenant still on it can use the same check: its event carries the number as `event.message_options.recipient`, the address as `event.request.ip`, and its API object has no drop method, so a refusal there means returning without sending.
## Before you start
1. Create a secret API key on the API keys page of your Spaw dashboard, scoped to phone, with a daily credit cap above a normal day's sign-ins, and a feedback key on the same page for the delivery-receipt URL.
2. Switch the SMS policy's opt-out rule off, so a person who once replied STOP to a marketing message can still receive a code:
```bash
curl -X PUT https://spaw.co/api/v1/phone/policy \
-H "Authorization: Bearer sk_live_…" \
-H "Content-Type: application/json" \
-d '{"block_opted_out": false}'
```
An account has one policy, applied to every lookup it makes. With the rule off, an opted-out number is decided like any other and the answer still says `meta.opted_out: true`, so a marketing flow on the same account reads that field itself.
3. In the Auth0 Dashboard open Branding, Phone Provider, select Custom and the Text delivery method, paste the Action below into Provider Configuration, and add the secrets `SPAW_KEY`, `SPAW_FEEDBACK_KEY`, `TWILIO_ACCOUNT_SID`, `TWILIO_AUTH_TOKEN` and `TWILIO_PHONE_NUMBER`. Save deploys it, and Send Test Message sends one through it.
## The Action
The sample sends through Twilio's Messages API; any provider's HTTP call fits in the same place.
```javascript
/**
* Handler to be executed while sending a phone notification.
*
* @param {Event} event - Details about the user and the context of the message.
* @param {CustomPhoneProviderAPI} api - Methods to drop or retry the notification.
*/
exports.onExecuteCustomPhoneProvider = async (event, api) => {
const { recipient, as_text, message_type } = event.notification;
// Add "otp_verify" if passwordless SMS sign-up creates accounts on your tenant.
const refuseOn = new Set(["otp_enroll"]);
if (message_type.startsWith("otp")) {
let answer = null;
try {
const response = await fetch("https://spaw.co/api/v1/phone", {
method: "POST",
headers: {
Authorization: `Bearer ${event.secrets.SPAW_KEY}`,
"Content-Type": "application/json",
Accept: "application/json",
},
body: JSON.stringify(event.request?.ip ? { phone: recipient, client_ip: event.request.ip } : { phone: recipient }),
signal: AbortSignal.timeout(2500),
});
answer = response.ok ? (await response.json()).data : null;
} catch {
// Fail open: the code goes out unchecked.
}
if (answer && !answer.ok_to_send && answer.blocked_by !== "opted_out") {
console.log(JSON.stringify({ message_type, spaw_blocked_by: answer.blocked_by }));
if (refuseOn.has(message_type)) {
api.notification.drop(`Spaw: ${answer.blocked_by}`);
return;
}
}
}
const { TWILIO_ACCOUNT_SID, TWILIO_AUTH_TOKEN, TWILIO_PHONE_NUMBER, SPAW_FEEDBACK_KEY } = event.secrets;
const sent = await fetch(`https://api.twilio.com/2010-04-01/Accounts/${TWILIO_ACCOUNT_SID}/Messages.json`, {
method: "POST",
headers: {
Authorization: "Basic " + Buffer.from(`${TWILIO_ACCOUNT_SID}:${TWILIO_AUTH_TOKEN}`).toString("base64"),
"Content-Type": "application/x-www-form-urlencoded",
},
body: new URLSearchParams({
To: recipient,
From: TWILIO_PHONE_NUMBER,
Body: as_text,
StatusCallback: `https://spaw.co/api/v1/phone/feedback/twilio/${SPAW_FEEDBACK_KEY}`,
}),
});
if (!sent.ok) {
api.notification.retry(`The SMS provider answered ${sent.status}`);
}
};
```
Spaw receives the number and the visitor's address and nothing else: not the code, the message text or anything from `event.user`. `client_ip` is counted, never looked up and never billed; the [privacy policy](/privacy) describes how the count is kept. Only the OTP types are checked: the account notices (`blocked_account`, `change_password`, `password_breach`) go to people who already have an account and are always sent. The sample handles text delivery; a tenant set to voice replaces the Messages call with its provider's call API. It follows Auth0's documented event and API objects; run it in your own tenant before you rely on it.
## Which codes it refuses, and which it only logs
`otp_enroll` is a number being attached to an account for multi-factor authentication, and the sample refuses it. `otp_verify` is logged and sent by default, because it covers both an enrolled user's second factor and a passwordless sign-in, and refusing an enrolled user locks a customer out. On a tenant where passwordless SMS creates accounts, add `otp_verify` to `refuseOn`: Auth0 creates a new passwordless user's profile only after the first code is entered, so for a sign-up `otp_verify` is the first code the number ever gets. The cost is that a returning user whose number is later refused needs support to get back in.
A dropped message is logged by Auth0 as failed and not retried, and the trigger's API object has no method that shows an error on the sign-in screen: the person waits for a code that does not come. Say on that screen what to do when no code arrives.
A refusal names the rule in `blocked_by`; the [gate-SMS guide](/guides/gate-sms-verification-codes-with-the-sms-policy) explains each rule and the order they apply in. With the visitor's address, five distinct numbers tried from one address within 60 minutes add [`numbers_per_client`](/docs/signals/phone/numbers_per_client) (+30). An address Spaw's IP data places on a mobile carrier or a corporate proxy, where many people share one address, still answers the count but never adds the signal. Five distinct numbers in one 1,000-number range within 60 minutes add `range_burst` (+40). Either alone stays under the default ceiling of 60; the two together score 70 and answer `blocked_by: "risk_score"`.
## Why it fails open
A timeout, a network error, `402 INSUFFICIENT_CREDITS` on an empty balance, `429` when the key's daily cap is spent or your phone requests exceed 50 a second, or a `5xx` leaves `answer` null, and the message is sent as if the check did not exist. Failing closed would turn a Spaw outage into an outage of your sign-in. The 2.5-second limit keeps the Spaw call well inside Auth0's 20 seconds with room for the provider's call.
## If the code goes through Twilio Verify
Auth0's own guide to a custom phone provider sends the code through Twilio Verify from this same trigger, calling `verifications.create` with the code Auth0 generated. The Spaw check belongs in the same place in that Action, before the `verifications.create` call, exactly where it sits before the Messages call above.
The same two calls wrap any backend that uses Verify directly, without Auth0: check the number before `verifications.create`, and report `verified` when `verificationChecks.create` answers `status: "approved"`. Verify runs its own SMS fraud controls: Twilio's documentation (updated 9 March 2026) says Fraud Guard "is on by default for all Verify customers" and that Geo Permissions adjust it country by country. They act on Twilio's side of the send and know nothing of your SMS policy, your own outcome reports or your own traffic, so the two checks complement each other rather than overlap.
## Delivery receipts and verified codes
The `StatusCallback` in the sample points Twilio's delivery receipts at your [SMS delivery-receipt URL](/docs/api/sms-status-webhook). Spaw reads `delivered`, `undelivered` and `failed`, classifies the error code, and records an opt-out code such as Twilio 21610 as `opted_out`; receipts are free and feed only your own later lookups.
A passwordless SMS login where the person has just entered a code can be reported as `verified` from a post-login Action. Auth0's login-trigger page says the trigger also runs "when a Refresh Token is exchanged", and a login that reuses an existing session runs it too, with no code entered in either case. `event.authentication.methods` keeps every method completed during the session, each with a timestamp, and Auth0 names an SMS one-time code `sms`. The sample therefore reports only when that `sms` entry is less than five minutes old:
```javascript
exports.onExecutePostLogin = async (event, api) => {
// Refresh-token exchanges and logins that reuse a session run this trigger too.
const sms = (event.authentication?.methods ?? []).find((method) => method.name === "sms");
if (
event.connection.strategy !== "sms" ||
!event.user.phone_number ||
event.transaction?.protocol === "oauth2-refresh-token" ||
!sms ||
Date.now() - new Date(sms.timestamp).getTime() > 5 * 60 * 1000
) {
return;
}
try {
await fetch("https://spaw.co/api/v1/phone/feedback", {
method: "POST",
headers: {
Authorization: `Bearer ${event.secrets.SPAW_KEY}`,
"Content-Type": "application/json",
},
body: JSON.stringify({ items: [{ phone: event.user.phone_number, outcome: "verified" }] }),
signal: AbortSignal.timeout(1500),
});
} catch {
// A missed report only means one less outcome on your account.
}
};
```
The post-login event object does not document the number of an MFA factor, so a multi-factor code is reported from wherever your app keeps that number, or not at all. The [feedback summary](/docs/api/phone-feedback-summary) then reads `verified_share`, the share of the numbers you reported delivered or verified that were verified, overall and for each of the ten blocks with the most failed deliveries; pumped traffic is delivered and never verified.
## Try the branches without Auth0
The documented test numbers answer a fixed result each, cost nothing and are never counted in your traffic. Under the default policy `+1 202 555 0103` answers `ok_to_send: false` with `blocked_by: "disposable"`, `+1 202 555 0105` answers `blocked_by: "fictional"`, and `+1 202 555 0102` (the test number that answers the `virtual` signal, score 40) and `+1 202 555 0100` answer `ok_to_send: true`. Post them to `https://spaw.co/api/v1/phone` with curl to see each branch before the Action is live.
## What to leave out
Leave `hlr` out of the request. The live carrier-network check (`hlr: true`) is not enabled on the service: `hlr_checked` answers false and nothing extra is charged. Leave the policy's `require_reachable` rule off for the same reason: while the check is not enabled, no lookup answers `ok_to_send: true` under it: every number that passes the other rules answers `blocked_by: "unconfirmed"`.
`ok_to_send` is a fraud and deliverability decision. It is not consent and not a do-not-call scrub, and whether the person agreed to hear from you stays yours to record.
## Cost
A lookup costs 1 credit when the number is valid. Invalid numbers, the test numbers and a repeat of the same number within seven days are free, so a resent code costs nothing. Delivery receipts and outcome reports are free. The SMS itself is billed by your provider. Spaw is not affiliated with Okta, Auth0 or Twilio.
---
# Feed SendGrid events into Spaw's suppression list
Point the SendGrid Event Webhook at your Spaw feedback URL so bounces, drops, deliveries and spam reports keep your suppression list current.
Updated: 2026-09-03
SendGrid's Event Webhook posts a batch of delivery events to a URL you choose. Give it your Spaw feedback URL and the loop closes without code: bounced and dropped addresses go straight onto your suppression list, deliveries confirm mailboxes for your own later lookups, and the History tab shows how the verdicts you paid for compared with what SendGrid actually saw.
## Set up the Event Webhook
1. In the Spaw dashboard, open the API keys page, generate a feedback key, and copy the SendGrid URL:
```text
https://spaw.co/api/v1/email/feedback/sendgrid/fb_…
```
2. In SendGrid, open the Event Webhook settings under Mail Settings and add a webhook with that URL as the HTTP POST destination.
3. Select the Delivered, Bounced, Dropped and Spam Reports events. Leave Deferred off, or on; Spaw ignores it either way, because a deferral says nothing definitive about the address.
4. Save, then use SendGrid's test feature. Spaw answers `200` with how many outcomes it recorded.
## What Spaw reads from the payload
SendGrid posts a JSON array with one object per event, often several events per request. For each object Spaw reads `event`, `email`, `reason` and `timestamp`, and nothing else:
| SendGrid event | Recorded as |
| --- | --- |
| `delivered` | delivered |
| `bounce` | bounced |
| `dropped` | bounced, the address was refused before sending |
| `spamreport` | complained |
| `deferred`, `processed`, `open`, `click` and the rest | ignored |
The `reason` field, which carries the receiving server's reply for a bounce or SendGrid's explanation for a drop, is stored as the reason, and the Unix `timestamp` becomes the time of the outcome. A request with two events, trimmed to the fields Spaw uses:
```json
[
{ "event": "bounce", "email": "mia@acme.com", "reason": "550 5.1.1 user unknown", "timestamp": 1788782400 },
{ "event": "delivered", "email": "sam@example.org", "timestamp": 1788782460 }
]
```
Treating `dropped` as a bounce is deliberate. SendGrid drops a message when the address is already on one of its own suppression lists or is malformed, and in both cases the address should not be sent to again.
## What Spaw does with the events
A bounce, a drop or a spam report adds the address to your suppression list with source `feedback`. Batch, bulk and monitor runs then answer it from your list at no cost, with reason `suppressed` and a risk score of 100; a single lookup still re-verifies it but leaves the entry in place. A bounce or a drop comes off the list when a later delivery to that address is reported; a spam report stays until you remove it. An address you imported stays until you remove it too: a spam report about it is recorded on your import, and no delivery clears it.
A delivery removes any entry Spaw had added automatically, and for the next 90 days the address counts as a confirmed mailbox for your own lookups, reported as `smtp_reason: "delivered_recently"` with no mailbox probe. Every outcome is matched against the verdict you were given for the address in the previous 90 days, and the feedback summary endpoint reports the delivery rate among addresses answered `deliverable` and the bounce rate among addresses answered `undeliverable`. Outcomes are kept for 180 days.
## Keep the URL private
SendGrid can sign its webhook posts, but Spaw does not verify that signature: the feedback key in the URL is the credential. Anyone holding the URL can add outcomes to your account, so if it leaks, rotate the key on the API keys page. Every old URL then answers `401 INVALID_FEEDBACK_KEY`, and you paste the new URL into SendGrid.
## Cost
Feedback is free and never logged as a lookup. The endpoint is throttled at 120 requests per minute per IP; SendGrid batches events, so even busy senders stay well under it.
---
# Spaw PHP SDK: email, phone, IP and postal address checks
The official spaw/spaw-php package wraps every Spaw endpoint in a dependency-free client with one exception per API code. PHP 8.2 with curl and json.
Updated: 2026-10-14
The PHP client is a thin, dependency-free wrapper over the REST API described by the OpenAPI document: one method per endpoint, a `Spaw\Response` carrying the envelope's `data` and `meta` arrays exactly as the API sends them, and a typed exception for every error code. It needs PHP 8.2 with the curl and json extensions, and its HTTP transport is an interface you can replace to route requests through your own stack or fake the API in tests.
The package is built and will be published on Packagist as `spaw/spaw-php`. Until the listing is live, Laravel's `Http::withToken()` against the API reference does the same job in a few lines.
## Install and verify one address
1. Create a secret key on the API keys page and put it in your environment.
2. Install the package once it is published:
```bash
composer require spaw/spaw-php
```
3. Verify an address:
```php
use Spaw\Client;
$spaw = new Client(getenv('SPAW_API_KEY'));
$result = $spaw->verifyEmail('mia@acme.com');
$result->data['deliverable']; // "deliverable" | "risky" | "undeliverable"
$result->data['reason']; // null, or e.g. "disposable", "mailbox_not_found"
$result->data['risk_score']; // 0–100, an auditable sum of published weights
$result->creditsUsed(); // 1 for a deliverable or risky answer, 0 otherwise
$result->requestId(); // quote this when writing to support
```
In Laravel, bind the client as a singleton in a service provider and type-hint it where you need it:
```php
$this->app->singleton(Client::class, fn () => new Client(config('services.spaw.key'), maxRetries: 2));
```
## Everything the client does
| Method | Endpoint |
| --- | --- |
| `verifyEmail($email, $callbackUrl = null, $callbackSecret = null)` | `POST /email` |
| `verifyEmails($emails)` | `POST /email/batch`, up to 50 |
| `domain($domain)` | `GET /email/domain/{domain}` |
| `listBulkJobs($page = 1, $status = null, $since = null, $until = null)`, `previewBulkJob($emails = [], $csv = null, $webhookUrl = null)`, `startBulkJob($draftId, $mapping = null, $idempotencyKey = null)`, `createBulkJob($emails, $webhookUrl = null)`, `getBulkJob($id)`, `cancelBulkJob($id)`, `resumeBulkJob($id)`, `redeliverBulkWebhook($id)`, `downloadBulkResults($id, $variant = null, $format = null)` | the bulk endpoints, up to 100,000 addresses; the results CSV comes back as a string, and a run that stopped on an empty balance is resumed from the top of its stored list |
| `listSuppressions($page = 1, $value = null, $source = null, $reason = null, $since = null, $until = null, $perPage = null)`, `addSuppressions($emails)`, `removeSuppression($id)`, `removeSuppressions($values)` | the suppression list, by id or by the address itself (up to 1,000 per call) |
| `reportFeedback($items)`, `feedbackSummary()` | delivery outcomes and measured accuracy |
| `account()`, `ledger($filters)`, `requestLog($requestId)`, `dataStatus()` | the live credit balance, the credit ledger (`since`, `until`, `reason`, `via`, `api_token_id`, paging), and one request read back by the id it answered with |
| `sendTestWebhook($url, $event)` | `POST /webhooks/test`, a real signed delivery to a URL you name, free, so a signature check can be written before a paid run depends on it |
| `Spaw\Webhook::verify($rawBody, $header, $secret)` | checks a delivery you received: the digest AND the timestamp in `X-Spaw-Signature-V2`, compared with `hash_equals()`. A static method, not an API call |
| `validatePhone($phone, $country = null, $hlr = null, $clientIp = null, $callWindow = null)`, `validatePhones($phones, $country = null, $hlr = null, $callWindow = null)` | `POST /phone` and `POST /phone/batch`, up to 1,000 numbers |
| `phoneBlockReport($country, $prefix)` | `GET /phone/block/{country}/{prefix}`, the regulator's own allocation table for one range |
| `listPhoneBulkJobs($page = 1, …)`, `previewPhoneBulkJob($phones = [], $csv = null, $country = null, $hlr = null)`, `startPhoneBulkJob($draftId, $mapping = null)`, `createPhoneBulkJob($phones, $webhookUrl = null, $idempotencyKey = null, $country = null)`, `getPhoneBulkJob($id)`, `cancelPhoneBulkJob($id)`, `resumePhoneBulkJob($id)`, `redeliverPhoneBulkWebhook($id)`, `downloadPhoneBulkResults($id, $variant = null, $format = null)` | the bulk phone endpoints, up to 100,000 numbers; the results CSV comes back as a string |
| `listPhoneSuppressions($page = 1, $value = null, $country = null, …)`, `addPhoneSuppressions($items, $country = null)`, `removePhoneSuppression($id)`, `removePhoneSuppressions($values, $country = null)` | the phone suppression list; a value is read to E.164 before it is matched |
| `getPhonePolicy()`, `updatePhonePolicy($changes)`, `resetPhonePolicy()` | the account's SMS policy behind `ok_to_send` and `blocked_by` on every phone answer (`GET`, `PUT`, `DELETE /phone/policy`); a fraud and deliverability decision, not consent and not a do-not-call scrub |
| `lookupIp($ip, privacy: null, abuseContact: null, networkDetails: null, hostname: null, browserTimezone: null, fields: null)`, `lookupMyIp()`, `lookupIps($ips)` | `POST /ip`, `GET /ip/me`, `POST /ip/batch`, up to 1,000 addresses; `browserTimezone` fills `timezone_matches`, `fields` keeps only the named keys |
| `lookupAsn($asn)`, `lookupIpRange($prefix)` | `GET /ip/asn/{asn}` (every prefix of one autonomous system) and `GET /ip/range?prefix=…` (what the feeds say about a whole prefix) |
| `listIpLists()`, `createIpList($name, $kind, $entries)`, `getIpList($id)`, `updateIpList($id, $changes)`, `deleteIpList($id)` | your own allow and deny lists, account-wide, applied to every lookup: an allow-list hit zeroes the score, a deny-list hit tops it |
| `listIpBulkJobs($page = 1, …)`, `previewIpBulkJob($ips = [], $csv = null, $privacy = null)`, `startIpBulkJob($draftId, $mapping = null)`, `createIpBulkJob($ips, $webhookUrl = null, $idempotencyKey = null, $privacy = null)`, `getIpBulkJob($id)`, `cancelIpBulkJob($id)`, `resumeIpBulkJob($id)`, `redeliverIpBulkWebhook($id)`, `downloadIpBulkResults($id, $variant = null, $format = null)` | the bulk IP endpoints, up to 100,000 addresses |
| `reportIpFeedback($items)`, `ipFeedbackSummary()` | IP outcomes (abuse, fraud, bot, chargeback), free |
| `getEmailPolicy()`, `updateEmailPolicy($changes)`, `resetEmailPolicy()` | the account's sending policy behind `ok_to_send` and `blocked_by` on every email answer (`GET`, `PUT`, `DELETE /email/policy`) |
| `listDomainMonitors($page = 1)`, `createDomainMonitor($name, $domains, $cadence, $webhookUrl = null, $webhookEvents = null)`, `getDomainMonitor($id)`, `updateDomainMonitor($id, $changes)`, `runDomainMonitor($id)`, `deleteDomainMonitor($id)` | monitored domain lists: up to 500 domains re-checked daily, weekly or monthly for mail-setup drift (`/email/domain-monitors`) |
| `listIpMonitors()`, `createIpMonitor($name, $ips, $cadence, $webhookUrl = null, $webhookEvents = null)`, `getIpMonitor($id)`, `updateIpMonitor($id, $changes)`, `runIpMonitor($id)`, `deleteIpMonitor($id)` | monitored IP lists with blocklist alerts, changeable in place and postable to a webhook |
| `verifyAddress($address, $country = null, $deliverability = null, $fields = null)`, `verifyAddresses($addresses, $country = null)` | `POST /address` and `POST /address/batch`, up to 100 addresses; the separated fields include `building`, `$fields` keeps only the named keys, and every answer carries the 61 fields incl. `unit_check`, `looks_placeholder`, `seen_by_you`, `ok_to_ship` and `blocked_by` |
| `listAddressBulkJobs($page = 1, …)`, `previewAddressBulkJob($addresses = [], $csv = null, $country = null, $deliverability = null)`, `startAddressBulkJob($draftId, $mapping = null)`, `createAddressBulkJob($addresses, $webhookUrl = null, $idempotencyKey = null, $country = null, $deliverability = null)`, `getAddressBulkJob($id)`, `cancelAddressBulkJob($id)`, `resumeAddressBulkJob($id)`, `redeliverAddressBulkWebhook($id)`, `downloadAddressBulkResults($id, $variant = null, $format = null)` | the bulk address endpoints, up to 100,000 addresses |
| `addressStreets($country, $postalCode)` | `GET /address/streets`, every street a national register lists inside a postcode, 1 credit |
| `addressesAtPostcode($postcode, $country = null)` | `GET /address/postcode/{postcode}`: the same streets from an open register, or a licensed partner's delivery points where one is configured |
| `getAddressPolicy()`, `updateAddressPolicy($changes)`, `resetAddressPolicy()` | the account's shipping policy behind `ok_to_ship` and `blocked_by` on every address answer (`GET`, `PUT`, `DELETE /address/policy`) |
| `listAddressMonitors()`, `createAddressMonitor($name, $addresses, $cadence, $country = null, $deliverability = null, $webhookUrl = null, $webhookEvents = null)`, `getAddressMonitor($id)`, `updateAddressMonitor($id, $changes)`, `runAddressMonitor($id)`, `deleteAddressMonitor($id)` | monitored address lists, alerting on newly flagged addresses |
| `reportAddressFeedback($items, $country = null)`, `addressFeedbackSummary()` | postal outcomes (delivered, returned, moved, refused), free and never shared between accounts |
| `listAddressSuppressions($page = 1, $value = null, $country = null, …)`, `addAddressSuppressions($items, $country = null)`, `removeAddressSuppression($id)`, `removeAddressSuppressions($values, $country = null)` | the address suppression list, up to 10,000 per import; a value is folded against its country before it is matched |
| `verifyEntity($identifier, $type, $country = null)`, `verifyEntities($identifiers)` | `POST /entity` and `POST /entity/batch`, up to 500 identifiers read against locally synced registers |
| `listEntityMonitors()`, `createEntityMonitor($name, $identifiers, $cadence, $webhookUrl = null, $webhookEvents = null)`, `getEntityMonitor($id)`, `updateEntityMonitor($id, $changes)`, `runEntityMonitor($id)`, `deleteEntityMonitor($id)` | monitored identifier lists, alerting when an entity is dissolved or its registration lapses |
| `listMonitors()`, `createMonitor($name, $emails, $cadence, $webhookUrl = null, $webhookEvents = null)`, `getMonitor($id)`, `updateMonitor($id, $changes)`, `runMonitor($id)`, `deleteMonitor($id)`, and the phone equivalents | monitored email and phone lists; an update sends only the keys in `$changes`, and `'webhook_url' => null` removes the webhook |
| `checkConsistency($email = null, $phone = null, $country = null, $ip = null, $address = null)` | `POST /consistency`, any subset of the four signals compared against each other |
| `listConsistencyBulkJobs()`, `createConsistencyBulkJob($items, $webhookUrl, $idempotencyKey, $country)`, `previewConsistencyBulkJob($items, $csv, $country)`, `startConsistencyBulkJob($draftId, $mapping)`, `getConsistencyBulkJob($id)`, `cancelConsistencyBulkJob($id)`, `resumeConsistencyBulkJob($id)`, `redeliverConsistencyBulkWebhook($id)`, `downloadConsistencyBulkResults($id, $variant)` | the same check over a whole list, queued: a row costs the sum of the legs it ran and the comparison is free |
A batch keeps the API's shape: `data['results']` holds one `{index, input, data, meta}` per address in input order — `index` is its place in the list you sent and `input` the value as you sent it, because the answer carries the normalized form, and the top-level `meta['stopped_reason']` is set instead of an exception when the balance runs out part-way, so paid results are never lost.
A bulk job or a monitor returns its `webhook_secret` once; keep it, because it signs the deliveries. `Spaw\Webhook::verify($rawBody, $header, $secret)` is the other half: pass the raw body and the `X-Spaw-Signature-V2` header and it answers whether the delivery is ours, unmodified and recent. It checks the timestamp as well as the digest, which is the point of the header — a signature over the body alone never goes stale, so a captured delivery could be posted back at you a year later and still verify. The original `X-Spaw-Signature`, with no timestamp in it, is sent beside it until 1 March 2027 and is deliberately not accepted by this call.
A delivery that failed is not lost: ``redeliverBulkWebhook($jobId)`` (and its phone, IP, address and cross-signal twins) posts that run's completion webhook to your URL again, for the day your endpoint was down for the half-minute the three attempts cover. The body is byte-for-byte the one the first delivery carried, rebuilt from the run's own frozen row, so the two reconcile — but the signature is stamped afresh, and has to be, because `Spaw\Webhook::verify()` refuses anything whose timestamp is more than five minutes old. Verify a redelivery with the same call you verify everything else with. Nothing deduplicates it for you: a receiver that gets both sees the event twice, and `job.id` is the key to deduplicate on. A run created without a `webhook_url` raises a `ConflictException` with code `WEBHOOK_NOT_CONFIGURED` and one that has not settled `JOB_NOT_FINISHED`; a run whose last delivery succeeded is not refused, because a receiver can lose a message it acknowledged. The delivery is queued rather than made while you wait — one attempt takes half a minute against an endpoint that is down — so the call answers `202` and the run it hands back reads `webhook_status: "pending"` until a worker has an answer; read the outcome with `getBulkJob($jobId)`. It is free and capped at ten a minute per signed-in user, neither per key nor per account.
## Addresses
`verifyAddress()` takes the address as one written line or as an array of the separated fields (`address_line1`, `locality`, `postal_code` and the rest), always with a country: that is what decides which country's format it is read under, and it is never guessed. The answer splits the address into components, formats it the way that country writes addresses, and says what open data can establish — the postcode's status and centroid, whether the town matches it, what kind of delivery point it is, how close a national address register comes to it, and how many companies are registered there.
```php
$address = $spaw->verifyAddress('10 Downing Street, London SW1A 2AA', 'GB');
$address->data['formatted']; // the address as that country writes it
$address->data['postcode_status']; // "live", "terminated", or null where no directory covers the country
$address->data['address_type']; // street, po_box, private_mailbox, general_delivery, …
$address->data['risk_signals']; // the weights behind risk_score
$spaw->verifyAddresses($addresses, 'GB'); // up to 100 per call; an item's own country wins
```
For a whole list, queue a bulk run instead of looping: up to 100,000 rows in the background, billed per row on exactly the single-lookup rules, with in-list repeats free and counted in `duplicate_count`.
Read the list before you start it: the `previewBulkJob()` call (and its phone, IP and address twins) answers what we made of it — which column filled which field and whether a heading named it or we guessed, the first rows as parsed, the repeats, and `credits_upper_bound` — and queues nothing. That figure is a CEILING, never a quote: a row can be free for reasons no preview can know in advance, so a run usually costs less and never more. Start it with the `startBulkJob()` call, correcting a column we read wrongly with `mapping`.
```php
$job = $spaw->createAddressBulkJob($addresses, null, 'addresses-2026-09-05', 'GB')->data['job'];
$spaw->getAddressBulkJob($job['id']); // status, counts, credits_used
$spaw->downloadAddressBulkResults($job['id']); // or 'invalid' / 'high'
```
A field answers null when its dataset does not cover the country, which means "not evaluated" and never "no". `exists` is true only on a premise match and false only under a register whose coverage is complete: a register whose coverage is partial, because it covers part of the country or cannot vouch for every address, cannot make an address's absence mean anything, so it answers null. `is_mail_drop` is a heuristic built from mailbox operators' own published branch lists and from how many companies are registered at an address, never a postal service's own agency flag; `mail_drop_source` says which of the two fired.
One answer is licensed and not enabled. `deliverability: true` would ask a partner whether post actually arrives and whether the delivery point is a mail-receiving agency, vacant or residential; until one is configured `deliverability_checked` comes back false, the four fields stay null and the premium credits are charged only where a partner answered. The streets inside a postcode are an open answer wherever a national register with a street listing is installed: `addressStreets($country, $postalCode)` lists them for 1 credit, `addressesAtPostcode()` answers the same streets as one entry each (a licensed partner's delivery points, 2 credits, where one is configured), and a country neither covers (Italy, for one), and a postcode the listing cannot speak for — a Northern Irish BT, forces BFPO or non-geographic BX code, or a German code outside Berlin — throws `NotEnabledException` (`ADDRESS_LIST_UNAVAILABLE`), free; an ordinary British postcode answers its district's streets from Ordnance Survey's Open Names, never its buildings. A postcode the register lists no street at throws `NotFoundException` (`ADDRESS_STREETS_NOT_FOUND`), free.
A saved list of up to 500 addresses is re-read on a `weekly` or `monthly` cadence — there is no daily one, because the registers and postcode directories an address is measured against are republished weekly at best. Every run answers each address `ok`, one of the flagged `invalid`, `not_in_register`, `terminated_postcode`, `mail_drop` and `high_risk`, or `unverifiable`, which means the address stood as written but no dataset installed on this server covers it well enough to check — a limit of the data, never a property of the address, and never an alert. An email goes out only for an address that was already known and unflagged and became flagged; a first run and a newly added address only set the baseline, and one that stays flagged never alerts twice. Runs bill per address exactly like a single lookup.
```php
$monitor = $spaw->createAddressMonitor('Supplier sites', $addresses, 'monthly', 'GB')->data['monitor'];
$spaw->getAddressMonitor($monitor['id'])->data['monitor']['results']; // address => ['status' => …, 'flags' => […], 'changed' => …]
$spaw->runAddressMonitor($monitor['id']); // re-check now
```
Tell the service what happened to the post you sent and it keeps your own suppression list. Outcomes are `delivered`, `returned`, `moved` and `refused`. A `moved` or `refused` report suppresses the address at once; a second `returned` not followed by a later `delivered` suppresses it; a `delivered` report clears an entry the feedback put there, and never touches one you imported. Batch, bulk and monitor runs answer a suppressed address free with `meta['suppressed']` true, a single lookup never consults the list, and drops an entry your reports added once they no longer stand behind it (a delivery since, or reports aged out); an import leaves only when you remove it; and an entry your reports added is re-verified rather than served once it is older than 90 days, while an import never goes stale.
```php
$spaw->reportAddressFeedback([ // free, and never logged as a lookup
['address' => '10 Downing Street, London SW1A 2AA', 'outcome' => 'returned'],
['address_line1' => 'Postfach 10 01 01', 'country' => 'DE', 'outcome' => 'moved'],
], 'GB');
$spaw->addressFeedbackSummary()->data['outcomes']; // your own counts, by outcome
$spaw->addAddressSuppressions($deadAddresses, 'GB'); // up to 10,000, free
$spaw->listAddressSuppressions()->data['suppressions'];
$spaw->removeAddressSuppression($suppressionId); // read fresh next time
```
Unlike the IP and phone products, address feedback is never shared between accounts. An IP is a network endpoint and a phone number is a handset, but a postal address is where somebody lives, and a shared "post fails here" database would be a database about residences. Your reports raise the risk of your own later lookups of an address and nobody else's, through `reported_returned` (+50, dataset `address-feedback`), which is scored while your most recent report for the address is `returned`, `moved` or `refused` rather than `delivered`.
## Businesses
Business verification reads one identifier — a Legal Entity Identifier or a UK company number — against the registers this server holds, which is the check you want before signing with a counterparty or when a supplier list needs going over again. Both registers are bulk files synced locally, so no third party sits in the request path and nothing about a lookup leaves the server; the LEI reference data is published under a CC0 public-domain dedication, and Spaw is not affiliated with its publisher. A company the national register records as dissolved is the strongest signal the product carries: a dissolved company no longer exists as a legal person and cannot enter a contract. A monitor that stops being run is deleted and the identifiers it stored go with it — `prunes_after_days` on every monitor payload says how long that takes — because an identifier can belong to a sole trader as easily as to a company.
## Cross-signal consistency
`checkConsistency()` runs any subset of the four signals in one call and reports where their answers agree and where they do not: it is built for signup, onboarding and marketplace vetting, the moment before there is an order or a card and the contact details are all you have. Each signal bills exactly as its own single lookup, and the analysis on top is free, because every comparison is arithmetic over fields the four pipelines already computed. There is no score and no verdict — you get named flags and both sides of every comparison quoted, because a difference between two fields is a fact about the fields and not a judgement about a person.
`checkConsistency()` is one record at a time. `previewConsistencyBulkJob()` and `startConsistencyBulkJob()` run the same check over a list of up to 100,000 rows, so scoring a signups export is one upload rather than four and a spreadsheet join afterwards. One header row is read as an email address, a phone number, an IP address and the address fields at once, and a row that resolves only two of the four is a perfectly good consistency check of those two.
The preview prices the run against the SIGNALS the columns feed rather than one credit a row — a file reaching only an email address and a phone number answers `credits_per_row` 2, not 4 — and it is still a ceiling and never a quote.
```php
$draft = $spaw->previewConsistencyBulkJob(csv: file_get_contents('signups.csv'), country: 'GB');
$draft->data['draft']['mapping']; // ['1' => 'email', '2' => 'phone', '3' => 'ip', …]
$draft->data['draft']['credits_upper_bound']; // the most it can cost, never what it will cost
$job = $spaw->startConsistencyBulkJob($draft->data['draft']['id']);
$rows = $spaw->downloadConsistencyBulkResults($job->data['job']['id'], 'flagged');
```
A result row is the comparison, not four lookups pasted together: `signals_requested`, `signals_answered`, `country_agreement`, `flags`, one cell per comparison, `distance_km`, and the few per-product fields those comparisons read. A comparison cell reads `agree`, `differ`, `not_compared`, `missing_value`, `low_confidence`, or nothing at all — an empty cell is a comparison never attempted, because the row did not carry both of its signals.
```php
$report = $spaw->checkConsistency(
'ada@example.com',
'+442079460018',
'GB', // the region hint for the phone number only
'81.2.69.142',
['address' => '10 Downing Street, London SW1A 2AA', 'country' => 'GB'],
);
$report->data['consistency']['flags']; // e.g. ["ip_country_differs_from_address_country"]
$report->data['consistency']['country_agreement']; // "agree", "differ", or null when none could be made
$report->data['signals']['ip']['data']; // each product's own result, unchanged
$report->creditsUsed(); // the sum of the per-signal costs
```
## Handle errors
Every failure throws a `Spaw\Exceptions\SpawException` or a subclass carrying `errorCode()`, `httpStatus()`, `requestId()` and, for validation failures, `errors()` keyed by field. No credits are spent on a failed request.
```php
use Spaw\Exceptions\InsufficientCreditsException;
use Spaw\Exceptions\RateLimitedException;
use Spaw\Exceptions\SpawException;
try {
$result = $spaw->verifyEmail($email);
} catch (InsufficientCreditsException $e) {
// nothing ran and nothing was billed
} catch (RateLimitedException $e) {
sleep($e->retryAfter() ?? 1);
} catch (SpawException $e) {
log($e->errorCode(), $e->getMessage(), $e->requestId());
}
```
`AuthenticationException` maps `UNAUTHENTICATED`, `ValidationException` maps `VALIDATION_FAILED`, `NotFoundException` covers a job or suppression entry that is not on the account, and `Spaw\Exceptions\ConnectionException` means no HTTP answer arrived at all. Pass `maxRetries` to the constructor to retry `429` and `5xx` answers automatically, waiting for `Retry-After` when the server sends one.
## What to do with the verdict
Branch on `deliverable`. Keep `deliverable` addresses, drop `undeliverable` ones, and route `risky` ones by `reason`: refuse `disposable` and `likely_typo`, decide `role` per use case, and treat `catch_all` as an unconfirmed rather than a bad address, using `mailbox_confidence` as the threshold. When `did_you_mean` is set, show it to the user.
## Cost
One credit per fresh deliverable or risky verdict, and one per five IP lookups. Unverified answers, invalid input, repeats within seven days and the `spaw.test` test addresses cost nothing, undeliverable verdicts cost nothing within a fair-use allowance, and every account gets 100 free credits a month. Use `deliverable@spaw.test` and its siblings in your tests, or implement `Spaw\Http\Transport` to fake the API entirely.
---
# Webhooks from Spaw: signed callbacks, and how to test one
The HTTPS callbacks Spaw sends, how to verify the timestamped X-Spaw-Signature-V2 HMAC in Node or PHP, how to test a receiver, and how retries work.
Updated: 2026-10-08
Spaw calls you back in three situations: when a queued bulk run finishes, when a monitored list has been re-checked, and when a single lookup that first answered unverified settles later. All of them are plain HTTPS POSTs with a JSON body and a signature header, so they work with a few lines of code on your server or with the incoming-webhook trigger of any automation tool. You can send yourself any of them on demand, before a single credit is spent — see [testing a receiver](#testing-a-receiver-before-anything-depends-on-it) below.
## The bulk completion webhook
1. Create the run with `POST https://spaw.co/api/v1/email/bulk`, passing `emails` (up to 100,000) and a `webhook_url` that speaks HTTPS.
2. Store the `webhook_secret` from the response. It is returned at creation, and again only if you replay the same request with the same `Idempotency-Key` header; otherwise losing it means creating a new run to get a new one.
3. When the job reaches `completed`, `failed` or `cancelled`, Spaw POSTs to your URL:
```json
{
"event": "bulk_email_job.finished",
"job": {
"id": 512,
"status": "completed",
"total": 2,
"processed": 2,
"deliverable": 1,
"risky": 0,
"undeliverable": 1,
"credits_used": 1,
"stopped_reason": null,
"finished_at": "2026-09-03T10:14:02+00:00"
}
}
```
4. Answer with any 2xx status quickly and do the real work afterwards. A connection error or a 5xx answer is retried twice with a short backoff — three attempts in all — and the job's `webhook_status` field then reads `delivered` or `failed`.
5. Fetch the CSV from the [results endpoint](/docs/api/download-bulk-results), optionally filtered with `?variant=deliverable`, `risky` or `undeliverable`. Results are kept for 30 days.
### Asking for a completion webhook again
A delivery gets three attempts over about half a minute, and then it is gone. If your endpoint was down for those thirty seconds, the run is finished and billed and your pipeline never hears about it — and the only way left to find out is to poll, which is the thing this webhook exists to avoid. So you can ask for it again: press **Send webhook again** on the run in the dashboard, or call `POST https://spaw.co/api/v1/email/bulk/{jobId}/webhook/redeliver` ([reference](/docs/api/redeliver-bulk-webhook)). Every bulk product has the same call — phone, IP, address and cross-signal runs each answer it under their own path.
Four things are worth knowing before you rely on it.
**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 the payload carries is frozen once a run has settled, so the bytes are the ones the first delivery carried. If you kept the original, the two reconcile exactly. (A run you resume later settles again and posts a completion webhook of its own; a redelivery always repeats the most recent one.)
**The signature is not the same signature, 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. That is required rather than incidental: your receiver refuses anything whose `t` is more than five minutes from its own clock — see [why the timestamp](#why-the-timestamp-and-what-to-do-about-clocks) — so a signature copied from the original would be refused on arrival. Verify a redelivery exactly as you verify any other delivery, with the same code. The older `X-Spaw-Signature` is the HMAC of the body alone, so it *is* byte-identical to the first attempt's, until it stops being sent on 1 March 2027.
**Nothing deduplicates it for you.** Each call is one real POST, so if the original did arrive after all, your receiver sees the event twice. Both bodies carry `job.id`: treat `bulk_email_job.finished` for a job id you have already handled as a repeat and drop it. That is the same defence you want against your own retries, so it is worth having either way.
**The delivery is queued, and the answer is `202`.** One attempt is a ten-second timeout retried twice — about half a minute against a receiver that is down, which is the receiver you are asking about — so the call does not wait for it. It answers `202` with the run at once, and `webhook_status` on that run reads `pending`: asked for, not answered yet. Poll the job until it reads `delivered` or `failed`; `webhook_detail` then carries the HTTP status your endpoint gave, or the transport failure when it gave none. The dashboard shows the same three states beside the run and refreshes them on its own. A run sitting at `pending` has a delivery in flight — asking again queues a second one, so wait for the first before deciding 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`. 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. It costs nothing, and it is capped at ten 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 spends the same budget its owner's keys do.
## The monitor run webhook
A monitored list is re-checked on a schedule, so its webhook says *when* it posts as well as what: `webhook_events` is `changes` (the default — only a run that found decay, a status change, a new flag, or that stopped early) or `every_run`, a heartbeat that also tells you the monitor is still running. Set `webhook_url` when you create the monitor, or with a `PATCH` afterwards; the `webhook_secret` comes back once from whichever call set the URL. The body is `{ "event": "email_monitor.run", "monitor": { "id", "name", "cadence", "last_run_at", "next_run_at" }, "summary": …, "changes": [ … ] }`, where `summary` is the same block the monitor reports as `last_summary` and `changes` names the entries the alert email names. A phone monitor's body adds `flags`, mapping each newly flagged number to the change flags it raised (`{}` when none did). Each product posts under its own event name, and the monitor itself carries `webhook_status` and `webhook_detail` afterwards, so a delivery that failed can be read back without waiting for the next run.
## The settled-verdict callback
Some mail servers will not say whether a mailbox exists on the first try (greylisting, timeouts). Pass `callback_url` and a `callback_secret` of 16 to 128 characters on `POST /api/v1/email`, and an unverified answer is re-checked after 5 and 20 minutes. The response to your original request then carries `meta.settling: true`, and the callback body is:
```json
{
"event": "email.settled",
"request_id": "req_01m1kgdm4xngzmbmff68g94w0c",
"settled": true,
"attempt": 1,
"data": { "email": "mia@acme.com", "deliverable": "deliverable", "smtp_checked": true },
"meta": { "credits_used": 1, "credits_remaining": 41, "cache_hit": false, "fair_use_billed": false }
}
```
`data` is the full 46-field result. `settled` is false when the last re-check still could not say. An unverified answer is free and leaves nothing to repeat, so each re-check is billed like a fresh lookup: 1 credit when it confirms the mailbox, nothing while it still cannot say. A re-check your balance cannot pay for is not run: the last callback then has `settled` false, `data` null, `meta.credits_used` 0 and `meta.stopped_reason` `insufficient_credits`.
## Verifying the signature
Every delivery carries `X-Spaw-Signature-V2`, two comma-separated elements:
```
X-Spaw-Signature-V2: t=1757502000,v2=8d4f…c1
```
`t` is when we signed the payload, in whole seconds since the Unix epoch. `v2` is the lowercase hex HMAC-SHA256, keyed with the secret you were given, of this exact string:
```
v2:{t}:{the raw request body}
```
So the signature covers the timestamp as well as the bytes, and neither can be changed without invalidating it. Compute your side over the bytes as received, before any JSON parsing — a body re-encoded from parsed JSON is a different string and will not match — and compare in constant time.
```js
import { createHmac, timingSafeEqual } from 'node:crypto';
const TOLERANCE_SECONDS = 300;
export function isFromSpaw(rawBody, header, secret) {
const elements = new Map();
for (const part of String(header ?? '').split(',')) {
const at = part.indexOf('=');
if (at > 0) {
elements.set(part.slice(0, at).trim(), part.slice(at + 1).trim());
}
}
const timestamp = elements.get('t') ?? '';
const signature = elements.get('v2') ?? '';
if (!/^\d+$/.test(timestamp) || signature === '') {
return false;
}
if (Math.abs(Math.floor(Date.now() / 1000) - Number(timestamp)) > TOLERANCE_SECONDS) {
return false;
}
const expected = createHmac('sha256', secret).update(`v2:${timestamp}:`).update(rawBody).digest('hex');
return signature.length === expected.length && timingSafeEqual(Buffer.from(signature), Buffer.from(expected));
}
```
```php
$elements = [];
foreach (explode(',', (string) $request->header('X-Spaw-Signature-V2')) as $element) {
$pair = explode('=', trim($element), 2);
if (count($pair) === 2) {
$elements[$pair[0]] = $pair[1];
}
}
$timestamp = $elements['t'] ?? '';
abort_unless(ctype_digit($timestamp) && abs(time() - (int) $timestamp) <= 300, 401);
$expected = hash_hmac('sha256', "v2:{$timestamp}:".$request->getContent(), $secret);
abort_unless(hash_equals($expected, $elements['v2'] ?? ''), 401);
```
All three clients ship this as one call, so you do not have to write it at all: `spaw.verify_webhook(body, header, secret)` in Python, `verifyWebhook(body, header, secret)` in Node, `Spaw\Webhook::verify($body, $header, $secret)` in PHP. Each returns a plain true or false and does the constant-time comparison for you. The tolerance is the one thing they do not spell the same way, so take it from your own client rather than from a sentence about all three: Python's is the keyword `tolerance=`, PHP's the fourth argument `int $tolerance`, and Node's a key on an options object — `verifyWebhook(body, header, secret, { toleranceSeconds: 600 })`. A bare number in Node's fourth position throws, rather than being read as an options object with no `toleranceSeconds` in it and leaving the window quietly at five minutes.
### Why the timestamp, and what to do about clocks
A signature over the body alone proves the bytes came from us and were not modified. It says nothing about when, so it never goes stale: anyone who gets hold of one delivery — from a log aggregator, a proxy, an error report pasted into a ticket — can post those exact bytes at your endpoint next year and your check will pass, because it is the same body and the same secret. Signing the timestamp is what fixes that, and it only works if you actually enforce it: **reject anything whose `t` is more than five minutes from your own clock**, in either direction. Too old is the replay. Too far in the future is a signature that would stay valid for as long as your clock is wrong, so a machine whose time has run away should fail loudly rather than accept everything.
Five minutes is the window we design for. We stamp the payload when we sign it, and the delivery, including both retries, is finished about half a minute later, so the rest of the window is slack for your clock. Keep it synced with NTP; if you genuinely cannot, widen the window rather than dropping the check, and never make it unbounded.
Five minutes narrows a replay rather than closing it. To close it, remember what you have already accepted for the length of the window — the signature itself is a fine key, or `job.id` plus `event`, or `request_id` — and ignore a second delivery carrying one you have seen. That is also the answer to a genuine retry arriving twice because your 2xx was lost on the way back to us.
### The older header, and until when
Deliveries also carry the original `X-Spaw-Signature`: the same HMAC-SHA256, keyed with the same secret, but over the raw body alone with no timestamp. It is unchanged, it still verifies, and receivers written against it keep working. It is also the header with the replay problem, so it is going away: both headers are sent **until 1 March 2027**, and after that only `X-Spaw-Signature-V2`. Nothing about your secret, your URL or the payloads changes when it goes — move your check over to the timestamped header any time before then, and remember to enforce the window when you do, because that is the whole point of moving.
## Testing a receiver before anything depends on it
The code above used to be written blind: the first signed request you ever saw arrived when a bulk run you had queued and paid for happened to finish. [`POST /api/v1/webhooks/test`](/docs/api/send-test-webhook) sends you a real signed delivery of any payload on demand instead. It is free, and it takes two fields:
```bash
curl https://spaw.co/api/v1/webhooks/test \
-H "Authorization: Bearer sk_live_…" \
-H "Content-Type: application/json" \
-d '{"url":"https://hooks.example.com/spaw","event":"bulk_email_job.finished"}'
```
The answer is everything about the delivery that just went out:
```json
{
"success": true,
"data": {
"delivered": true,
"status_code": 200,
"detail": "HTTP 200",
"secret": "3nQ8Zk2xVb7Lp1sTfY6wR4mJcH0aD5gU9eKqNvXz",
"signature": "45f6705121de705be218247b0f31a49d895c46d2e9f9d38d150bec2f08b629b9",
"timestamped_signature": "t=1757502000,v2=6a0e5f0f6bd0ba9a4bd6c9bbd0c0b5b6b1a1d0f04a2b9a02de3b1a3d0c4b6f21",
"timestamp": 1757502000,
"body": "{\"test\":true,\"event\":\"bulk_email_job.finished\",\"job\":{\"id\":512,…}}"
}
}
```
`body` is the exact bytes that were signed and posted, and the two signatures are the two headers that went with them, so a check that fails can be debugged with no receiver at all. `timestamped_signature` is the whole `X-Spaw-Signature-V2` value the delivery carried: hash `v2:{timestamp}:{body}` with `secret` and you should arrive at everything after `v2=`. `timestamp` is that same `t`, repeated so you need not take the header apart to get at it — in a real receiver you read it out of the header you were sent, because that is the value a replayed delivery would carry. `signature` is the older `X-Spaw-Signature` over `body` alone, which is what a check written against that header has to arrive at; it stops being sent on 1 March 2027, so a check written today belongs on the timestamped pair. The secret is minted for that one call and stored nowhere — a real job's or monitor's signing secret is shown once at creation and never handed out again, and this endpoint will not hand you one.
`event` is any of the names Spaw posts:
| Event | Sent when |
| --- | --- |
| `bulk_email_job.finished` | A bulk email run reaches `completed`, `failed` or `cancelled` |
| `bulk_phone_job.finished` | The same, for a bulk phone run |
| `bulk_ip_job.finished` | The same, for a bulk IP run |
| `bulk_address_job.finished` | The same, for a bulk address run |
| `bulk_consistency_job.finished` | The same, for a bulk cross-signal consistency run |
| `email.settled` | A lookup that first answered unverified has settled |
| `email_monitor.run` | A monitored email list has been re-checked |
| `phone_monitor.run` | A monitored phone list has been re-checked |
| `ip_monitor.run` | A monitored IP list has been re-checked |
| `address_monitor.run` | A monitored address list has been re-checked |
| `entity_monitor.run` | A monitored identifier list has been re-checked |
One field differs from a real delivery: a test body carries `"test": true`, first. It is there because most low-code webhook triggers cannot compute an HMAC over a raw body, and a receiver that cannot check a signature still has to be able to tell a test from the real thing. Everything else — both signature headers, the timeout, the retries, the shape of the body — is the same code that posts a real one.
The limits are worth knowing, because the endpoint makes an outbound request on your say-so. The URL must be HTTPS. A host that resolves onto a private or reserved address is refused with nothing sent at all: `delivered` false and `detail` `Refused: private address`, which is also what a real delivery does and why a webhook pointed at your own network never arrives. A redirect is not followed, so a `3xx` answer is reported as a failure rather than chased. The body is ours and fixed — nothing you send is copied into it. Whatever your endpoint writes in its own response body is never read or returned; only its status code comes back. And the endpoint is capped at ten requests a minute for your key, which is enough to debug a receiver and not enough to make Spaw a source of traffic against somebody.
## Receiving webhooks in an automation tool
Zapier's catch-hook trigger, Make's custom webhook and n8n's Webhook node all accept these POSTs and expose the body's fields to the following steps. Most low-code tools cannot compute an HMAC on the raw body, so keep the receiving URL private and check `event` plus the job id against the run you created. They cannot enforce the timestamp either, which is worth knowing for what it means: a delivery captured from such a flow stays useful to whoever captured it, so treat the URL as the credential it has become. For a typical flow, the webhook trigger is followed by an HTTP step that downloads the results CSV and a step that writes rows into a sheet or CRM; the [n8n page](/integrations/n8n) shows the HTTP step.
## What to do with the verdict
The completion payload is a summary. Decisions belong to the per-row verdicts in the CSV: `deliverable` rows go to your mailing tool, `risky` rows are worth a look at their `reason`, and `undeliverable` rows should be removed, or corrected when a `did_you_mean` column is filled. Undeliverable rows are also added to your account's suppression list automatically, so future batch and bulk runs answer them for free.
## Cost
Webhooks and callbacks themselves are free. The bulk run bills each row like a single lookup as it is processed: 1 credit for a fresh deliverable or risky verdict, nothing for undeliverable rows, duplicates, seven-day repeats or suppressed addresses.
---
# Email verification APIs compared
Spaw against Abstract API, Bouncer, Emailable, Hunter, Kickbox, Mailboxlayer, NeverBounce, Verifalia and ZeroBounce on pricing, billing rules, verification depth and limits, taken from each vendor's own pages on the date it was read.
Whether mail to an address will arrive, and how much to trust the answer: syntax, live MX, SPF and DMARC, the open disposable and role lists, typo-squats, and a mailbox handshake that never sends a message.
Below are the 9 services Spaw is measured against for email verification, each with the figures its own public pricing and documentation pages state. Every row carries the date that vendor's pages were read, which is its own and need not match the others'; a row those pages carry no value for reads "not on the pages read" rather than a guess. Corrections: support@spaw.co.
| Service | Pricing model | Free tier | Price per 1,000 at about 10k | SMTP mailbox check | Checked |
| --- | --- | --- | --- | --- | --- |
| Spaw | Pay-as-you-go credit packs whose credits never expire, or optional plans, billed monthly or yearly, that deposit the same credits every month for less and roll unused plan credits over up to three months' allowance; both through Stripe Checkout | 100 free credits every month, no card required | $7.80 (Growth pack: 5,000 credits for $39), or $5.80 on the Growth monthly plan (5,000 credits for $29 a month) | Yes | own pricing page, https://spaw.co/pricing |
| [Spaw vs Abstract API](https://spaw.co/compare/abstract.md) | Monthly subscription tiers with a request slider from 5,000 to 150,000 requests a month; Starter from $17, Standard from $37, Professional from $39 a month | 100 requests on the Free plan | not on the pages read | Yes | 2026-09-13 |
| [Spaw vs Bouncer](https://spaw.co/compare/bouncer.md) | Pay-as-you-go credits that never expire (from 1,000 credits for $8 up to 1,000,000 for $2,000), auto-refill, plus a Deliverability Kit subscription at $25 to $250 a month | 100 free credits, no card required | $6.00 (10,000 credits for $60) | not on the pages read | 2026-09-13 |
| [Spaw vs Emailable](https://spaw.co/compare/emailable.md) | Pay-as-you-go credits that never expire, or a monthly subscription at a 15% discount; minimum purchase 5,000 credits | 250 free credits at signup | $7.60 (the pay-as-you-go rate printed for the 5,000-credit package, $0.0076 a credit; larger packages are priced in a calculator, and the 5,000-credit subscription is $32.30 a month after the 15% discount) | not on the pages read | 2026-09-13 |
| [Spaw vs Hunter](https://spaw.co/compare/hunter.md) | Monthly or yearly subscriptions with unified credits shared across search, verification and enrichment; credit packs on paid plans, usable for up to 12 months from purchase according to the pricing page | Free plan with 50 credits a month; one verification costs 0.5 credit, so 100 verifications | $7.45 (Growth: $149 a month for 10,000 credits, 20,000 verifications at 0.5 credit each; $5.20 billed yearly) | not on the pages read | 2026-09-13 |
| [Spaw vs Kickbox](https://spaw.co/compare/kickbox.md) | not on the pages read | not on the pages read | not on the pages read | not on the pages read | 2026-09-03 |
| [Spaw vs Mailboxlayer](https://spaw.co/compare/mailboxlayer.md) | Monthly or yearly subscription (10 to 15% off yearly) with a request quota per plan and per-request overage | 100 requests a month | $3.00 for the Basic quota (5,000 requests for $14.99 a month; overage $0.012 a request, so 10,000 requests on Basic cost about $74.95, $7.50 per 1,000); Professional Plus: 50,000 for $74.99, $1.50 per 1,000 | Yes | 2026-09-13 |
| [Spaw vs NeverBounce](https://spaw.co/compare/neverbounce.md) | not on the pages read | not on the pages read | not on the pages read | not on the pages read | 2026-09-03 |
| [Spaw vs Verifalia](https://spaw.co/compare/verifalia.md) | Pay-as-you-go credit packs that never expire, or monthly, quarterly and yearly subscriptions; verification depth is priced as quality levels; pack and plan prices are shown in a calculator rather than listed | 25 free credits a day on the Free plan; unused daily credits expire each day | not on the pages read | not on the pages read | 2026-09-13 |
| [Spaw vs ZeroBounce](https://spaw.co/compare/zerobounce.md) | Pay-as-you-go credits from $39 for 2,000 addresses, 15% off with autopay, or the ZeroBounce ONE subscription at $99 a month with unused credits rolling over; volume pricing shown through a calculator rather than fixed packs | 100 validation credits a month on the free account | not on the pages read | not on the pages read | 2026-09-13 |
## Spaw vs Abstract API
Abstract API's email validation endpoint is one of a family of utility APIs sold as monthly request quotas with a free 100-request plan.
Where it fits better: Developers who want one vendor for several small utility APIs under a monthly quota.
- Full comparison: https://spaw.co/compare/abstract.md
- Abstract API alternatives: https://spaw.co/alternatives/abstract.md
- Abstract API's own site: https://www.abstractapi.com
- Checked on 2026-09-13
## Spaw vs Bouncer
Bouncer is an email verification service with pay-as-you-go credits that never expire, high-throughput batch APIs, a real-time form product (Bouncer Shield) and a Deliverability Kit subscription.
Where it fits better: Teams that want the lowest published per-check price with high batch throughput and a real-time form product.
- Full comparison: https://spaw.co/compare/bouncer.md
- Bouncer alternatives: https://spaw.co/alternatives/bouncer.md
- Bouncer's own site: https://www.usebouncer.com
- Checked on 2026-09-13
## Spaw vs Emailable
Emailable is an email verification service with pay-as-you-go credits, a bulk verifier, a widget and an API, with Node.js, Ruby and Python libraries.
Where it fits better: Marketers who want a simple per-credit price, a fast bulk cleaner and a stated 30-day deletion policy.
- Full comparison: https://spaw.co/compare/emailable.md
- Emailable alternatives: https://spaw.co/alternatives/emailable.md
- Emailable's own site: https://emailable.com
- Checked on 2026-09-13
## Spaw vs Hunter
Hunter is an outreach platform whose Email Verifier shares unified plan credits with its email finder, enrichment and campaign tools.
Where it fits better: Sales teams that prospect with Hunter's finder and campaigns and verify addresses as part of that workflow.
- Full comparison: https://spaw.co/compare/hunter.md
- Hunter alternatives: https://spaw.co/alternatives/hunter.md
- Hunter's own site: https://hunter.io
- Checked on 2026-09-13
## Spaw vs Kickbox
Kickbox is an email verification API that returns deliverable, undeliverable, risky or unknown plus a 0 to 1 Sendex quality score, with official libraries for PHP, Node.js, Ruby and Python.
Where it fits better: Teams that want a single quality score next to the verdict and already build on one of Kickbox's four official libraries.
- Full comparison: https://spaw.co/compare/kickbox.md
- Kickbox alternatives: https://spaw.co/alternatives/kickbox.md
- Kickbox's own site: https://kickbox.com
- Checked on 2026-09-03
## Spaw vs Mailboxlayer
Mailboxlayer is apilayer's email validation API, sold as monthly request quotas from a free 100-request plan up to 250,000 requests a month.
Where it fits better: Developers who prefer a flat monthly quota and already use other apilayer APIs.
- Full comparison: https://spaw.co/compare/mailboxlayer.md
- Mailboxlayer alternatives: https://spaw.co/alternatives/mailboxlayer.md
- Mailboxlayer's own site: https://mailboxlayer.com
- Checked on 2026-09-13
## Spaw vs NeverBounce
NeverBounce is a ZoomInfo-owned email verification service with a bulk list-cleaning workflow, a real-time API and official client libraries in six languages.
Where it fits better: Teams that clean large lists through jobs and want official wrappers in six languages plus an n8n node.
- Full comparison: https://spaw.co/compare/neverbounce.md
- NeverBounce alternatives: https://spaw.co/alternatives/neverbounce.md
- NeverBounce's own site: https://www.neverbounce.com
- Checked on 2026-09-03
## Spaw vs Verifalia
Verifalia is an email verification service with selectable quality levels, credit packs that never expire, subscriptions with daily free credits, and official SDKs in six languages.
Where it fits better: Developers who want to tune verification depth per request, official SDKs in six languages, and configurable retention.
- Full comparison: https://spaw.co/compare/verifalia.md
- Verifalia alternatives: https://spaw.co/alternatives/verifalia.md
- Verifalia's own site: https://verifalia.com
- Checked on 2026-09-13
## Spaw vs ZeroBounce
ZeroBounce is an email validation service with a pay-as-you-go credit model and a subscription (ZeroBounce ONE) that bundles deliverability tools such as activity data, inbox placement tests and blacklist monitoring.
Where it fits better: Marketing teams that want a deliverability suite around verification and prefer a subscription with bundled tools.
- Full comparison: https://spaw.co/compare/zerobounce.md
- ZeroBounce alternatives: https://spaw.co/alternatives/zerobounce.md
- ZeroBounce's own site: https://www.zerobounce.net
- Checked on 2026-09-13
## What Spaw answers
- **SMTP mailbox check:** Yes
- **Catch-all handling:** A catch-all answer is remembered per domain for 7 days, and every unconfirmed mailbox carries a published 0 to 100 mailbox_confidence estimate
`POST /v1/email` — 1 credit for a deliverable or risky verdict, or for an undeliverable one past fair use. Free: unverified answers, invalid syntax, a suppressed address, undeliverable verdicts within fair use. Every rule is on the pricing page: https://spaw.co/pricing
- Documentation: https://spaw.co/docs/email-verification
- Product page: https://spaw.co/products/email-verification
- Free tool: https://spaw.co/tools/email-checker
- Free tool: https://spaw.co/tools/mx-lookup
- Free tool: https://spaw.co/tools/spf-dmarc-checker
- Free tool: https://spaw.co/tools/disposable-email-checker
- Free tool: https://spaw.co/tools/dkim-checker
- Free tool: https://spaw.co/tools/mta-sts-checker
## The other signals
- [Phone intelligence](https://spaw.co/compare/phone.md): 9 services compared
- [IP intelligence](https://spaw.co/compare/ip.md): 13 services compared
- [Address verification](https://spaw.co/compare/address.md): 10 services compared
- [Business verification](https://spaw.co/compare/entity.md): 2 services compared
- [Cross-signal consistency](https://spaw.co/compare/consistency.md): no comparable vendor publishes figures for it
- [Every comparison on one page](https://spaw.co/compare)
## Check the table against your own data
Every Spaw account gets free credits every month with no card, and one key answers every signal. Upload a list, keep your columns, and download only the rows you want. The pricing page has every billing rule in full: https://spaw.co/pricing. Get your API key: https://spaw.co/register
Product names are trademarks of their owners. Spaw is not affiliated with any of them.
Reference: https://spaw.co/compare/email
---
# Phone intelligence APIs compared
Spaw against Abstract Phone Validation, ClearoutPhone, IPQualityScore Phone Validation, Loqate Phone Validation, numlookupapi, numverify, Telnyx Number Lookup, Twilio Lookup and Veriphone on pricing, billing rules, where the line type comes from and limits, taken from each vendor's own pages on the date it was read.
Any number normalised to E.164 with its line type, carrier and the regulator's block status, plus the fictional, disposable and virtual flags.
Below are the 9 services Spaw is measured against for phone intelligence, each with the figures its own public pricing and documentation pages state. Every row carries the date that vendor's pages were read, which is its own and need not match the others'; a row those pages carry no value for reads "not on the pages read" rather than a guess. Corrections: support@spaw.co.
| Service | Pricing model | Free tier | Price per 1,000 at about 10k | Line type source | Checked |
| --- | --- | --- | --- | --- | --- |
| Spaw | Pay-as-you-go credit packs whose credits never expire, or optional plans, billed monthly or yearly, that deposit the same credits every month for less and roll unused plan credits over up to three months' allowance; both through Stripe Checkout | 100 free credits every month, no card required | $7.80 (Growth pack: 5,000 credits for $39), or $5.80 on the Growth monthly plan (5,000 credits for $29 a month) | The numbering plan metadata everywhere except the North American plan, where mobiles and landlines share blocks and the plan can only say fixed_line_or_mobile; there the regulator's block holder settles it — a wireless carrier's block is mobile, a virtual-number wholesaler's is voip. line_type_source names which of the two answered. | own pricing page, https://spaw.co/pricing |
| [Spaw vs Abstract Phone Validation](https://spaw.co/compare/abstract-phone.md) | Monthly request quotas on three plans that differ by the data returned: Starter (validation, location, carrier) from $19 a month for 5,000 requests to $199 for 150,000; Standard (adds messaging) from $19 for 3,000 to $199 for 100,000; Professional (adds registration and risk) from $19 for 2,000 to $199 for 50,000. Annual billing saves 10%; the pricing page quotes $3.40 per 1,000 requests at Starter's entry volume, billed annually | 100 requests a month with no card, on the full response including registration and risk | not on the pages read | Not stated. The product page says Abstract uses "several databases of numbers" that it ingests and cross-references, with updates daily, weekly and monthly, without naming them; its changelog for 24 July 2026 mentions "recently published numbering plans from several African regulators". | 2026-09-27 |
| [Spaw vs ClearoutPhone](https://spaw.co/compare/clearoutphone.md) | Credits that never expire, bought one time or through monthly or annual subscriptions that add them each period and carry them forward. One Time, each shown as a starting price: 5,000 credits from $35, 10,000 from $58, 100,000 from $400 and 1,000,000 from $1,200. Standard validation is 1 credit; Smart validation is 1 credit for most numbers and up to 5 when it runs a deeper real-time check | 100 free credits on sign-up, one validation each | $5.80 per 1,000 one time (10,000 credits for $58), $5.20 on the monthly plan (10,000 credits for $52 a month) or $3.20 on the annual plan as its tab rendered ("Buy 120,000 credits for $384 billed yearly", 10,000 a month), although the page labels annual billing "Save 20%" against one-time prices; a Smart validation that runs the deeper check costs up to 5 credits | Validation "is carried out using multiple methods which include validation from International Telecom Authority, Multiple Carrier lines, and third party validation". Standard validation "Uses basic validation based on original number data"; Smart validation adds real-time telecom queries for ported and VoIP numbers. | 2026-09-27 |
| [Spaw vs IPQualityScore Phone Validation](https://spaw.co/compare/ipqualityscore-phone.md) | Monthly plans shared with IPQualityScore's other checks: Free $0 (1,000 lookups a month), Startup "As low as: $99 / Month" (5,000), SMB Basic "As low as: $499 / Month" (10,000), SMB+ "As low as: $999 / Month" (75,000), Enterprise custom; "Each plan requires a minimum monthly commitment". The phone validation page says "Score phone numbers on average from $0.0005 per user with no monthly contracts", on "tiered flat rate pricing across all countries that reduces as your monthly volume increases", without a tier table. Add-ons are enabled through the account manager or Support at no stated price. | 1,000 lookups a month on the Free plan, capped at 35 a day; the plan also covers IP, email and URL checks | not on the pages read | Its own carrier data: "IPQS maintains current data directly from carriers on a global level", and its system "collects phone validation and verification data from various telecom carriers". line_type answers Toll-Free, Wireless, Landline, Satellite, VOIP, Premium Rate, Pager or N/A, beside separate VOIP and prepaid booleans. | 2026-09-27 |
| [Spaw vs Loqate Phone Validation](https://spaw.co/compare/loqate-phone.md) | Pay-as-you-go credit purchases of £50, £100, £250, £500 and £1,000 (or 75€, 150€ and 300€), each buying a published per-lookup rate for every service, plus bespoke plans; "Credit is valid for 12 months after the purchase date". Phone Validation on the "New Pricing" table is 7.1p, 6.7p, 5.6p, 4.7p and 4.1p a lookup across the pound tiers and 8.6c, 7.8c and 6.6c across the euro ones. The page also shows an "Old Pricing" table without saying which accounts each applies to; the figures here are from "New Pricing". Its usage-example selector also lists a £2,500 plan that neither table prices, and all prices exclude VAT. | A trial rather than a standing tier: "Start your 45-day free trial"; the FAQ adds "You get free credits to test the service" | £47 per 1,000 (4.7p a lookup) on the £500 credit, which buys about 10,600 lookups; the smaller credits run from 7.1p (£50) to 5.6p (£250) | Loqate's "own custom libraries alongside the Home Location Register (HLR) database": the first step checks "length, format, digit count, country of origin, and whether it is a landline, mobile or VoIP number", the second the carrier and the third an HLR lookup. NumberType answers MOBILE, LANDLINE or VOIP, and the product detail page says "Full validation for Mobile numbers. Global formatting and carrier identification only for non-UK Landline and VoIP numbers." | 2026-09-27 |
| [Spaw vs numlookupapi](https://spaw.co/compare/numlookupapi.md) | Monthly subscription tiers by request quota: Free $0 for 100 requests, Basic $9.99 for 5,000, Pro $49.99 for 50,000, Scale $109.99 for 250,000, and Custom by contact; the page offers yearly billing at 20% off. Scale and Custom list "Overages" with no overage price shown, and the quota documentation says a request over the monthly quota answers 429 until the plan is upgraded or the month ends. | 100 requests a month on one API key, at 10 requests a minute | not on the pages read | Not stated beyond the pricing FAQ: "All number data delivered are sourced from multiple data providers and international carriers." The documented line_type values are landline, mobile, satellite, paging, special_services, premium_rate, toll_free and N/A; none of them is VoIP. | 2026-09-27 |
| [Spaw vs numverify](https://spaw.co/compare/numverify.md) | Monthly subscription tiers by request quota: Free 100 requests, Basic $14.99 for 5,000, Professional $59.99 for 50,000, Enterprise $129.99 for 250,000, each with a published overage price per call ($0.011992, $0.0047992 and $0.00207984 respectively); annual billing saves up to 15%, and custom pricing is offered above the Enterprise quota | 100 API requests a month, for non-commercial use | not on the pages read | Not named. The pricing FAQ says validation draws on "international numbering plans containing complex syntax rules, carrier data, line type data and location information", without saying whose plans or which carrier data. | 2026-09-27 |
| [Spaw vs Telnyx Number Lookup](https://spaw.co/compare/telnyx-lookup.md) | Per query, priced per lookup type, with "$0 platform fee"; pay as you go gets "Automatic tier discounts as usage crosses thresholds", committed plans start at $500 a month, and inbound CNAM is $0.40 per number per month | An outbound CNAM listing at no charge; the pages read list no free query allowance | $1.50 per 1,000 for a local routing number query ($0.0015 a query), $2.50 for MCC/MNC ($0.0025) and $3.00 for CNAM ($0.003) | Not named. The product page lists line type ("Mobile, landline, or VoIP") among the fields a lookup returns from "Live telecom databases, including HLR lookup"; the pricing page says portability and carrier come from "the network's own data" and CNAM from the registry. | 2026-09-27 |
| [Spaw vs Twilio Lookup](https://spaw.co/compare/twilio-lookup.md) | Per request, priced per data package on a Twilio account ("Pricing current as of August 2026"); line status and reassigned number risk are tiered by query volume, and SIM swap and call forwarding are contact sales | Formatting and validation are listed as Free, and so is the SMS Pumping Risk Score for North American numbers; no free allowance is published for the other packages | $8.00 per 1,000 for line type intelligence ($0.008 per request), $10.00 for inbound caller identification ($0.01, US only), $100.00 for identity match ($0.1, which the page says varies by country) | Not stated beyond Canada, where the docs require approved access to Canadian Number Portability Administration Center (NPAC) data. The line type intelligence documentation lists twelve types (landline, mobile, fixedVoip, nonFixedVoip, personal, tollFree, premium, sharedCost, uan, voicemail, pager, unknown) and returns carrier_name with mobile_country_code and mobile_network_code, but does not say which data settles them elsewhere. | 2026-09-27 |
| [Spaw vs Veriphone](https://spaw.co/compare/veriphone.md) | One-time credit packs that never expire, from 10,000 credits for $24 to 5,000,000 for $999, or monthly plans whose credits reset each month: Starter $12.99 for 10,000, Professional $39 for 50,000, Business $99 for 250,000 and Enterprise $249 for 1,000,000. A standard validation is 1 credit and a Current Carrier Lookup 10 | 1,000 free credits a month with no card, not carried over; on the free plan a Current Carrier Lookup draws on purchased credits only | $2.40 per 1,000 on the 10,000-credit pack ($24, credits never expire), or $1.30 on the Starter plan (10,000 credits a month for $12.99, reset each month). A Current Carrier Lookup takes 10 credits, so ten times those figures | The numbering plan: the default Static mode "reports its original carrier and line type from national numbering-plan data", checked "against the latest numbering plans from the international telecom regulator and national regulators". Current mode adds the carrier and line type serving the number today, from national portability registries. | 2026-09-27 |
## Spaw vs Abstract Phone Validation
Abstract Phone Validation is a phone API from Abstract sold as monthly request quotas on three data tiers, returning carrier, line type and location, with messaging from Standard up and registration and risk on Professional and the Free plan; the Professional example response also carries breach history.
Where it fits better: Teams already on Abstract's other APIs that want phone validation, and on the Professional plan registration, risk and breach fields, under a monthly quota.
- Full comparison: https://spaw.co/compare/abstract-phone.md
- Abstract Phone Validation alternatives: https://spaw.co/alternatives/abstract-phone.md
- Abstract Phone Validation's own site: https://www.abstractapi.com/api/phone-validation-api
- Checked on 2026-09-27
## Spaw vs ClearoutPhone
ClearoutPhone is a phone validation service from Clearout sold as credits that never expire, with a 1-credit Standard mode and a Smart mode that runs a real-time telecom query, at up to 5 credits, where static data is uncertain.
Where it fits better: Sales and marketing teams cleaning CSV or XLSX contact lists before calling or texting, with a real-time check they can switch on for numbers static data cannot settle.
- Full comparison: https://spaw.co/compare/clearoutphone.md
- ClearoutPhone alternatives: https://spaw.co/alternatives/clearoutphone.md
- ClearoutPhone's own site: https://clearoutphone.io
- Checked on 2026-09-27
## Spaw vs IPQualityScore Phone Validation
IPQualityScore Phone Validation is the phone side of a fraud-scoring service: one API returns validity, carrier, line type and a 0 to 100 fraud score, with live-line, identity, do-not-call and SMS-pumping data as add-ons, on the same monthly plans as its IP, email and URL checks.
Where it fits better: Fraud teams that want a phone fraud score beside IPQualityScore's IP, email and URL checks on one plan, with live-line, identity and SMS-pumping data available as add-ons.
- Full comparison: https://spaw.co/compare/ipqualityscore-phone.md
- IPQualityScore Phone Validation alternatives: https://spaw.co/alternatives/ipqualityscore-phone.md
- IPQualityScore Phone Validation's own site: https://www.ipqualityscore.com/solutions/phone-validation
- Checked on 2026-09-27
## Spaw vs Loqate Phone Validation
Loqate Phone Validation checks a number's format, carrier and, for mobile numbers, its live network status through an HLR lookup, priced per lookup from the same pay-as-you-go credit balance as Loqate's address, email and bank validation.
Where it fits better: Teams already using Loqate for address capture that want a live network check on mobile numbers at the form, billed from the same credit balance.
- Full comparison: https://spaw.co/compare/loqate-phone.md
- Loqate Phone Validation alternatives: https://spaw.co/alternatives/loqate-phone.md
- Loqate Phone Validation's own site: https://www.loqate.com/en-gb/phone-validation/
- Checked on 2026-09-27
## Spaw vs numlookupapi
numlookupapi is a phone number validation API sold as a monthly request quota: one validate endpoint returns validity, formats, country, location, carrier and line type.
Where it fits better: A form or a script that needs validity, formats, country, carrier and line type at a small monthly volume on a fixed bill, or an AI agent calling it over MCP.
- Full comparison: https://spaw.co/compare/numlookupapi.md
- numlookupapi alternatives: https://spaw.co/alternatives/numlookupapi.md
- numlookupapi's own site: https://numlookupapi.com
- Checked on 2026-09-27
## Spaw vs numverify
numverify is a phone number validation API sold as a monthly request quota, returning validity, country and location, carrier and line type.
Where it fits better: A form or a script that needs validity, country, carrier and line type at a small monthly volume, on a monthly plan with per-call overage.
- Full comparison: https://spaw.co/compare/numverify.md
- numverify alternatives: https://spaw.co/alternatives/numverify.md
- numverify's own site: https://numverify.com
- Checked on 2026-09-27
## Spaw vs Telnyx Number Lookup
Telnyx Number Lookup is a per-query number API sold beside Telnyx's carrier services, priced separately for local routing number (portability and carrier), MCC/MNC, and CNAM caller name.
Where it fits better: Teams that need to know who serves a number today, on numbers that have been ported, priced per dip from $0.0015 a query.
- Full comparison: https://spaw.co/compare/telnyx-lookup.md
- Telnyx Number Lookup alternatives: https://spaw.co/alternatives/telnyx-lookup.md
- Telnyx Number Lookup's own site: https://telnyx.com/products/number-lookup
- Checked on 2026-09-27
## Spaw vs Twilio Lookup
Twilio Lookup is a phone number API sold as separately priced data packages on top of a Twilio account: formatting and validation are free, and line type, caller identity, line status and the risk packages are each priced on their own, per request (the SMS Pumping Risk Score is free for North American numbers) or through sales for SIM swap and call forwarding.
Where it fits better: Teams already sending messages through Twilio that want line type, caller identity and SMS-pumping risk billed on the same account as their traffic.
- Full comparison: https://spaw.co/compare/twilio-lookup.md
- Twilio Lookup alternatives: https://spaw.co/alternatives/twilio-lookup.md
- Twilio Lookup's own site: https://www.twilio.com/en-us/user-authentication-identity/lookup
- Checked on 2026-09-27
## Spaw vs Veriphone
Veriphone is a phone validation API sold as credit packs or monthly plans, returning validity, carrier, line type and region from national numbering plans, with a 10-credit Current Carrier Lookup read from national portability registries.
Where it fits better: Teams that want numbering-plan validation with carrier and line type at a low per-number price, and the carrier serving a number today from portability registries when they pay for it.
- Full comparison: https://spaw.co/compare/veriphone.md
- Veriphone alternatives: https://spaw.co/alternatives/veriphone.md
- Veriphone's own site: https://veriphone.io
- Checked on 2026-09-27
## What Spaw answers
- **Line type source:** The numbering plan metadata everywhere except the North American plan, where mobiles and landlines share blocks and the plan can only say fixed_line_or_mobile; there the regulator's block holder settles it — a wireless carrier's block is mobile, a virtual-number wholesaler's is voip. line_type_source names which of the two answered.
- **Numbering plan coverage:** libphonenumber metadata for every region it carries, plus the regulators' own allocation tables, each named in the answer with the date of the copy it read. Complete tables, where a geographic number in a block the table leaves out answers unassigned_block: NANPA for the United States (with Puerto Rico, the U.S. Virgin Islands, Guam, American Samoa and the Northern Mariana Islands), the CNAC for Canada, ARCEP for France (with Guadeloupe, St. Barthélemy, St. Martin, Martinique, French Guiana, Réunion, Mayotte and St. Pierre & Miquelon), ACM for the Netherlands and the CRT (formerly the IFT) for Mexico. Partial tables, answered only from the ranges they list (a range listed as free, spare or held in reserve answers unassigned_block), so block_status answers null for a number outside them: Ofcom for the United Kingdom (with Guernsey, Jersey and the Isle of Man), ACMA for Australia, the NAD for New Zealand, UKE for Poland and MIMIT for Italy. Re-synced weekly, NANPA daily; /status lists each table's age. Outside those regions, block_status and is_virtual answer null rather than a guess.
- **Live network check:** Reserved behind hlr: true: 4 credits on top of the base credit, only where the network answers, reported in its own reachable, ported, current_carrier and roaming fields. The live carrier-network check (hlr: true) is not enabled on the service: hlr_checked answers false and nothing extra is charged.
- **Ported numbers:** None from the tables, and the docs say so: carrier and line_type describe the block the number was allocated in, never a later port. Only a live carrier-network check can say who serves a number today.
- **SMS pumping controls:** Flags in the risk score, and an SMS policy that turns them into ok_to_send and blocked_by: a fraud and deliverability decision, not consent and not a do-not-call scrub. allowed_countries limits sending to the regions you serve; it is empty (any country) until you set it. range_burst (+40) flags a number once your account has looked up 5 distinct numbers in its 1,000-number range within 60 minutes, and numbers_per_client (+30) once one client address (the client_ip you send, or a browser visitor's own) has tried 5 within 60 minutes, unless Spaw's own IP data places it on a mobile carrier or a corporate proxy, or it is private or reserved. Both count only your own single and browser lookups, so a new account has no history. The range is a heuristic, and in North America a burst more likely means someone testing numbers against a form; the client count does not stop an attack rotated through residential proxies. disposable (+70) flags a number SMS-receiving websites publish and disposable_stale (+40) an old listing; disposable_range (+30) flags an unlisted number in a 1,000-number range holding 10 or more fresh listings. The lists are public and synced daily. The default policy blocks the fictional, disposable, reported_abuse, reported_abuse_widely and premium_rate signals and a risk_score above 60, so neither traffic signal blocks on its own.
`POST /v1/phone` — 1 credit for a valid number. Free: invalid numbers and unassigned blocks. Every rule is on the pricing page: https://spaw.co/pricing
- Documentation: https://spaw.co/docs/phone-intelligence
- Product page: https://spaw.co/products/phone-validation
- Free tool: https://spaw.co/tools/phone-number-lookup
- Free tool: https://spaw.co/tools/phone-number-formatter
## The other signals
- [Email verification](https://spaw.co/compare/email.md): 9 services compared
- [IP intelligence](https://spaw.co/compare/ip.md): 13 services compared
- [Address verification](https://spaw.co/compare/address.md): 10 services compared
- [Business verification](https://spaw.co/compare/entity.md): 2 services compared
- [Cross-signal consistency](https://spaw.co/compare/consistency.md): no comparable vendor publishes figures for it
- [Every comparison on one page](https://spaw.co/compare)
## Check the table against your own data
Every Spaw account gets free credits every month with no card, and one key answers every signal. Upload a list, keep your columns, and download only the rows you want. The pricing page has every billing rule in full: https://spaw.co/pricing. Get your API key: https://spaw.co/register
Product names are trademarks of their owners. Spaw is not affiliated with any of them.
Reference: https://spaw.co/compare/phone
---
# IP intelligence APIs compared
Spaw against AbuseIPDB, ip-api, IP2Location, ipapi, ipdata, ipgeolocation.io, IPinfo, IPQualityScore, Ipregistry, ipstack, MaxMind, proxycheck.io and Spur on pricing, billing rules, the feeds behind each flag and limits, taken from each vendor's own pages on the date it was read.
Where an address is, who runs it, and whether it is a visitor at all: datacenter ranges, Tor exits, named VPN and relay exits, verified crawlers and public blocklists.
Below are the 13 services Spaw is measured against for IP intelligence, each with the figures its own public pricing and documentation pages state. Every row carries the date that vendor's pages were read, which is its own and need not match the others'; a row those pages carry no value for reads "not on the pages read" rather than a guess. Corrections: support@spaw.co.
| Service | Pricing model | Free tier | Price per 1,000 at about 10k | Feeds behind the flags | Checked |
| --- | --- | --- | --- | --- | --- |
| Spaw | Pay-as-you-go credit packs whose credits never expire, or optional plans, billed monthly or yearly, that deposit the same credits every month for less and roll unused plan credits over up to three months' allowance; both through Stripe Checkout. One credit buys five answered IP lookups | 100 free credits every month, no card required | $1.80 per 1,000 IP lookups at 5 to the credit (Starter pack: 1,000 credits for $9), or $1.40 on the Starter monthly plan (1,000 credits for $7 a month) | Public feeds, each named in the answer with the date it was synced: DB-IP Lite and RFC 8805 geofeeds for location, the five registries' delegated statistics for allocation, Spamhaus DROP and ASN-DROP and abuse.ch Feodo Tracker for blocklists, the Tor Project's exit list and Onionoo, the Mullvad, NordVPN and Private Internet Access server lists plus the open X4BNet lists, Apple's iCloud Private Relay egress list, the cloud providers' own published ranges, and the verification files Google, Microsoft, OpenAI, Perplexity, DuckDuckGo and Ahrefs publish for their crawlers. | own pricing page, https://spaw.co/pricing |
| [Spaw vs AbuseIPDB](https://spaw.co/compare/abuseipdb.md) | Plans with daily request allowances: Individual free "Forever!", Basic $25/mo or $228/year, Premium $99/mo or $1,068/year, Enterprise custom | Individual: free forever with no credit card, 1,000 check and report requests, 100 block checks and 5 bulk reports a day | not on the pages read | One feed, its own: the abuse confidence score is computed from reports submitted by AbuseIPDB users, and totalReports is "a sum of the reports within maxAgeInDays". A check also returns countryCode, usageType, isp, domain, isTor and isWhitelisted, which "reflects whether the IP is spotted in any of our whitelists". | 2026-09-10 |
| [Spaw vs ip-api](https://spaw.co/compare/ip-api.md) | Two tiers. The free endpoint: "Our endpoints are limited to 45 HTTP requests per minute from an IP address" and "We do not allow commercial use of the free endpoint". The Pro service: "€13.3 per month, when paying for 12 months", with "Unlimited requests", "SSL (HTTPS)", "Commercial use allowed" and a "99.9% SLA" ("If the general API uptime falls below 99.9% in a month, we will automatically extend your subscription for 10 days"). | The free endpoint, for non-commercial use only: 45 requests per minute per address over HTTP ("256-bit SSL encryption is not available for this free API") | not on the pages read | Not published as feeds. The answer carries three flags, each described in one line: mobile ("Mobile (cellular) connection"), proxy ("Proxy, VPN or Tor exit address") and hosting ("Hosting, colocated or data center"); the pages do not say where they come from. | 2026-09-12 |
| [Spaw vs IP2Location](https://spaw.co/compare/ip2location.md) | IP2Location.io API, monthly query plans: Free $0 ("50K IP Geolocation API queries/month", "11 IP Attributes", "Attribution Required"), Starter "$49/month" ("150K IP Geolocation API queries/month", "20 IP Attributes"), Plus "$249/month" ("300K IP Geolocation API queries/month", "59 IP Attributes"), Security "$499/month" ("600K IP Geolocation API queries/month", "79 IP Attributes"); overage per increment Starter $10, Plus $50, Security $100. Databases, standard licence per year: DB1 "$99", DB2 "$399", DB8 "$1,299", DB18 "$2,799", with a site licence at ten times those figures. | "50K IP Geolocation API queries/month" with 11 attributes, basic geolocation and open-proxy detection only, "Attribution Required"; without a key "up to 1,000 queries per day" | not on the pages read | Its own IP2Proxy data: is_proxy with a proxy object of "last_seen, proxy_type, threat, provider", and on the higher plans is_vpn, is_tor, is_data_center, is_public_proxy, is_web_proxy, is_web_crawler, is_ai_crawler, is_residential_proxy, is_spammer, is_scanner, is_botnet, is_bogon and a "fraud_score" from 0 to 99. The Free and Starter plans detect open proxies only. | 2026-09-12 |
| [Spaw vs ipapi](https://spaw.co/compare/ipapi.md) | Monthly plans by lookup quota: Starter "$15 billed monthly" for 60,000 lookups, Professional "$25 billed monthly" for 150,000, Business "$49 billed monthly" for 500,000, Enterprise "$99 billed monthly" for 2,000,000, Premium "$199 billed monthly" for 6,000,000, Ultimate "$399 billed monthly" for 15,000,000; annual billing is 20% less. | A free trial of "up-to 1000 IP lookups in a day" (about 30,000 a month), "for testing & development purposes" and "Not for production use" | not on the pages read | None: the documented answer carries no proxy, VPN, Tor, hosting or threat field. The pricing page names the data behind the location as "IP geolocation by DB-IP" and the "IP2Location LITE database". | 2026-09-12 |
| [Spaw vs ipdata](https://spaw.co/compare/ipdata.md) | Monthly plans by daily request quota: Personal "$10 per month" with "2,500 requests daily", Lite "$30 per month" with "10,000 requests daily", Startup "$50 per month" with "50,000 requests daily", Business "$120 per month" with "100,000 requests daily"; "20% off all annual plans" at the time of reading. | "1500 free requests daily", "restricted to non-commercial use"; on the free tier, over the quota "all further requests are blocked and you receive an email notification" | not on the pages read | not on the pages read | 2026-09-12 |
| [Spaw vs ipgeolocation.io](https://spaw.co/compare/ipgeolocation.md) | Monthly credit plans, "A standard unified IP lookup uses 1 credit": Starter "$19/month" for "150000 API Credits Per month", Plus "$49/month" for 500,000, Pro "$79/month" for 1,000,000, Business "$129/month" for 2,000,000, Premium "$249/month" for 5,000,000, Enterprise "Contact Sales"; annual billing gives "2 months free". "Paid monthly plans reset every month. Paid annual plans reset every year". | "1000 API Credits Per day for life" with no credit card, "a hard limit of 1000 requests per day" and "No overages on Free"; the free plan returns "core location without the radius and confidence fields" | not on the pages read | Its own collection: the security page describes "telemetry from active honeypots, proprietary blocklists, VPN and proxy enumeration, and real-time behavioral analysis", answering "VPN, proxy, residential proxy, and Tor flags, plus provider names, confidence scores, and last-seen dates", flags for "known attackers, bots, spam, and cloud IPs", an is_anonymous field and a "threat score from 0 to 100". Security fields are on paid plans. | 2026-09-12 |
| [Spaw vs IPinfo](https://spaw.co/compare/ipinfo.md) | Monthly plans by API request quota: Lite $0/mo, Core $26/mo for 150,000 requests, Max $163/mo for 250,000, Enterprise custom for "1M+". Database downloads are listed separately, "Starting at $1,000/month". | IPinfo Lite at "$0/mo", free forever with no credit card, giving unlimited API requests at country level only | not on the pages read | Its own measurement rather than published lists: the pages describe ProbeNet, "our proprietary internet measurement platform", reaching "44% of the world's ASNs within 1 millisecond" from "more than 1,000 points of presence". Privacy detection (VPN, Tor, proxy, relay) starts on the Core plan, with the service names added on Max; residential proxy detection is Max and above. | 2026-09-10 |
| [Spaw vs IPQualityScore](https://spaw.co/compare/ipqualityscore.md) | Monthly plans by lookup quota: Free $0 (1,000 lookups a month), Startup "As low as: $99 / Month" (5,000), SMB Basic "$499" (10,000), SMB+ "$999" (75,000), Enterprise custom. "Any valid request sent to our Proxy Detection, Email Verification, or Device Fingerprinting services including lookups performed through a batch CSV file check will consume a lookup credit." | 1,000 lookups a month, capped at 35 a day | not on the pages read | Its own collection rather than published lists: "IPQS gathers data directly from our proprietary honeypots, traps, crawlers, and thousands of live sites opted into our threat intelligence network." The proxy detection answer carries proxy, vpn, tor, active_vpn, active_tor, fraud_score, ISP, organization, ASN, connection_type, recent_abuse, bot_status, is_crawler and the location fields. | 2026-09-10 |
| [Spaw vs Ipregistry](https://spaw.co/compare/ipregistry.md) | Credits, "1 lookup = 1 request = 1 credit" (batch endpoints use "one credit per lookup in the batch request, plus 1"). Subscriptions: Starter "100,000 credits / mo" at "$29 $24 / month", Professional "300,000 credits / mo" at "$79 $66 / month", Business "1,150,000 credits / mo" at "$199 $166 / month", Scale "2,450,000 credits / mo" at "$399 $333 / month" (the lower figure is the annual rate). Pay-as-you-go packs: 15,000 credits for $10, 100,000 for $50, 250,000 for $100, 1,750,000 for $500. | "20,000 free credits on sign-up" with "No credit card, no time limit" | $0.67 (the $10 pack of 15,000 credits, $0.000667 a lookup) | Its own datasets, derived "from multiple sources using unique AI algorithms" including "BGP tables, Regional Internet Registry (RIR) databases, ISP data, data-sharing agreements, geofeeds, latency-based tracking". The security object carries is_abuser, is_attacker, is_bogon, is_cloud_provider, is_proxy, is_relay, is_tor, is_tor_exit, is_vpn, is_anonymous and is_threat. | 2026-09-12 |
| [Spaw vs ipstack](https://spaw.co/compare/ipstack.md) | Monthly plans by request quota: Free "100 Request/mo." at $0, Basic "50,000 Request/mo." at "$12.99/mo.", Professional "500,000 Request/mo." at "$59.99/mo.", Professional + "2,000,000 Request/mo." at "$99.99/mo.", Custom on request; yearly billing "Save up to 15%". Overage fees apply past the quota, with notifications at 75%, 90% and 100%. | "100 Request/mo." at $0, without SSL encryption, support or the paid modules (SSL, commercial use and the Location, Time Zone and Connection modules are listed for paid plans) | not on the pages read | A "Security Module" is listed for the Professional + plan only; its fields were not readable on the documentation pages we fetched, so they are not reproduced here | 2026-09-12 |
| [Spaw vs MaxMind](https://spaw.co/compare/maxmind.md) | Per query, by service, with no minimum published: "If you estimate your volume of queries to be over $2,500 a month, contact us to discuss rates." | A no-cost trial is offered ("Experience the functionality and benefits of our GeoIP web services at no cost"); no free monthly allowance is stated on the page checked | $0.10 per 1,000 for Country ($0.0001 a query), $0.30 for City Plus ($0.0003) and $2.00 for Insights ($0.002) | Not described on the pages checked. Insights is documented as "Everything in Country and City Plus, + Proxy Detection, and confidence factors, user count and type, static IP score, demographic data, and anonymizers", so anonymiser detection is the top tier only. | 2026-09-10 |
| [Spaw vs proxycheck.io](https://spaw.co/compare/proxycheck.md) | not on the pages read | not on the pages read | not on the pages read | not on the pages read | 2026-09-12 |
| [Spaw vs Spur](https://spaw.co/compare/spur.md) | not on the pages read | not on the pages read | not on the pages read | Its own collection, sold as feeds and as an API. Feeds: Anonymous ("all IPs relating to anonymous VPNs, datacenter proxies, ISP proxies, virtual desktops, and other datacenter-centric anonymizing infrastructure"), Anonymous Residential ("The Anonymous feed, plus all IPs relating to residential proxies, residential VPNs, malware proxies, blockchain proxies, and other callback technologies"), Service Metrics and Datacenter/Hosting ("all netblocks belonging to datacenter and hosting providers"). The Context API answers infrastructure, organization, the autonomous system, client behaviours, proxies, device types, risks such as CALLBACK_PROXY, TUNNEL and GEO_MISMATCH, services such as OPENVPN, and tunnels with operator (for example PROTON_VPN), type and an anonymous flag. | 2026-09-12 |
## Spaw vs AbuseIPDB
AbuseIPDB is a reputation service built on abuse reports its own users submit: a check returns an abuse confidence score with the report count, country, usage type, ISP and a Tor flag, and plans are sold as daily request allowances rather than credits.
Where it fits better: Teams that want to know whether other operators have complained about an address recently, and to contribute their own reports back.
- Full comparison: https://spaw.co/compare/abuseipdb.md
- AbuseIPDB alternatives: https://spaw.co/alternatives/abuseipdb.md
- AbuseIPDB's own site: https://www.abuseipdb.com
- Checked on 2026-09-10
## Spaw vs ip-api
ip-api is a geolocation JSON API with a free, non-commercial, HTTP-only endpoint limited by request rate, and a Pro subscription that adds HTTPS, unlimited queries and commercial use; its answer carries one proxy flag, one hosting flag and one mobile flag beside the location and network fields.
Where it fits better: Non-commercial projects that fit under 45 requests a minute, and small commercial ones that want a flat, unlimited subscription over per-lookup pricing.
- Full comparison: https://spaw.co/compare/ip-api.md
- ip-api alternatives: https://spaw.co/alternatives/ip-api.md
- ip-api's own site: https://ip-api.com
- Checked on 2026-09-12
## Spaw vs IP2Location
IP2Location sells geolocation as downloadable databases (DB1 to DB26, licensed yearly) and, through IP2Location.io, as an API on monthly query plans whose higher tiers add proxy, VPN, Tor, datacenter, crawler and residential-proxy detection and a fraud score; the classic web service no longer takes new registrations.
Where it fits better: Teams that want a downloadable database under a yearly licence, or an API whose top plan carries a fraud score and a wide set of proxy and crawler flags.
- Full comparison: https://spaw.co/compare/ip2location.md
- IP2Location alternatives: https://spaw.co/alternatives/ip2location.md
- IP2Location's own site: https://www.ip2location.com
- Checked on 2026-09-12
## Spaw vs ipapi
ipapi.co is a geolocation API sold as monthly lookup quotas, whose pricing page credits its data to DB-IP and the IP2Location LITE database; the answer carries location, country and network fields and no proxy, VPN or Tor flag.
Where it fits better: Teams that need country, city, currency and network facts on a predictable monthly quota and do not need any anonymity or threat signal.
- Full comparison: https://spaw.co/compare/ipapi.md
- ipapi alternatives: https://spaw.co/alternatives/ipapi.md
- ipapi's own site: https://ipapi.co
- Checked on 2026-09-12
## Spaw vs ipdata
ipdata is a geolocation and threat API sold as daily request quotas, from a free non-commercial tier to a Business plan, whose pricing page says it does not keep records of the addresses looked up beyond a day; its documentation site refused automated reading, so the fields are not reproduced here.
Where it fits better: Teams that want a daily quota with a no-records promise and are happy to read the field list from the vendor's own documentation.
- Full comparison: https://spaw.co/compare/ipdata.md
- ipdata alternatives: https://spaw.co/alternatives/ipdata.md
- ipdata's own site: https://ipdata.co
- Checked on 2026-09-12
## Spaw vs ipgeolocation.io
ipgeolocation.io sells geolocation and a separate security API on monthly credit plans, with location built from its own measurements and a security answer that flags VPN, proxy, residential proxy and Tor with provider names, confidence scores and a 0 to 100 threat score.
Where it fits better: Teams that want an accuracy radius with every city answer and a single threat score from a vendor that runs its own honeypots.
- Full comparison: https://spaw.co/compare/ipgeolocation.md
- ipgeolocation.io alternatives: https://spaw.co/alternatives/ipgeolocation.md
- ipgeolocation.io's own site: https://ipgeolocation.io
- Checked on 2026-09-12
## Spaw vs IPinfo
IPinfo is an IP data company that sells geolocation, ASN, privacy detection and company data as monthly API plans and as database downloads, collected through its own measurement network rather than assembled from third-party feeds.
Where it fits better: Teams that want city-level geolocation from a vendor that measures the internet itself, and are willing to buy a plan tier per field they need.
- Full comparison: https://spaw.co/compare/ipinfo.md
- IPinfo alternatives: https://spaw.co/alternatives/ipinfo.md
- IPinfo's own site: https://ipinfo.io
- Checked on 2026-09-10
## Spaw vs IPQualityScore
IPQualityScore sells fraud detection across several signals on one lookup credit: proxy and VPN detection, email verification and device fingerprinting all draw from the same monthly quota, and the IP answer carries a fraud score alongside the connection facts.
Where it fits better: Teams that want one vendor, one credit pool and a single fraud score across IP, email and device signals.
- Full comparison: https://spaw.co/compare/ipqualityscore.md
- IPQualityScore alternatives: https://spaw.co/alternatives/ipqualityscore.md
- IPQualityScore's own site: https://www.ipqualityscore.com
- Checked on 2026-09-10
## Spaw vs Ipregistry
Ipregistry is a geolocation and threat API sold as credits, either monthly subscriptions or pay-as-you-go packs, whose security object answers abuser, attacker, bogon, cloud provider, proxy, relay, Tor, VPN, anonymous and threat flags and whose data is described as built from BGP tables, registry databases, geofeeds and latency measurements.
Where it fits better: Teams that want a credit model with pay-as-you-go packs and a security object with a dozen named flags, from a vendor that names its data sources in general terms.
- Full comparison: https://spaw.co/compare/ipregistry.md
- Ipregistry alternatives: https://spaw.co/alternatives/ipregistry.md
- Ipregistry's own site: https://ipregistry.co
- Checked on 2026-09-12
## Spaw vs ipstack
ipstack is a geolocation API on monthly request quotas from a 100-request free tier to a two-million-request plan, with a bulk endpoint and a security module reserved for its top plan; its documentation pages rendered only a header to our fetcher, so the field list is not reproduced here.
Where it fits better: Teams already on the apilayer marketplace that want a high-volume monthly quota and are willing to buy the top plan for bulk and security answers.
- Full comparison: https://spaw.co/compare/ipstack.md
- ipstack alternatives: https://spaw.co/alternatives/ipstack.md
- ipstack's own site: https://ipstack.com
- Checked on 2026-09-12
## Spaw vs MaxMind
MaxMind sells GeoIP as three per-query web services — Country, City Plus and Insights — and as downloadable databases, with proxy and anonymiser detection reserved for the Insights tier.
Where it fits better: Teams that want geolocation at the lowest published per-query price and are content to buy Insights, at twenty times the City Plus rate, for anonymiser detection.
- Full comparison: https://spaw.co/compare/maxmind.md
- MaxMind alternatives: https://spaw.co/alternatives/maxmind.md
- MaxMind's own site: https://www.maxmind.com
- Checked on 2026-09-10
## Spaw vs proxycheck.io
proxycheck.io sells an API for checking whether an address is a proxy; every page we tried on 2026-09-12 answered 403 Forbidden to automated reading, so nothing about its pricing, limits, fields or data is reproduced here.
Where it fits better: Not assessable from the vendor's pages on the checked date.
- Full comparison: https://spaw.co/compare/proxycheck.md
- proxycheck.io alternatives: https://spaw.co/alternatives/proxycheck.md
- proxycheck.io's own site: https://proxycheck.io
- Checked on 2026-09-12
## Spaw vs Spur
Spur is an anonymity-infrastructure vendor: its Context API answers whether an address is a VPN, proxy or other tunnel with the operator named, and its feeds list anonymous, residential-proxy and datacenter address space; its pricing page rate-limited our fetcher, so prices are not reproduced here.
Where it fits better: Security teams that want anonymity infrastructure, residential proxies included, as a feed to run inside their own systems, with the operator behind each tunnel named.
- Full comparison: https://spaw.co/compare/spur.md
- Spur alternatives: https://spaw.co/alternatives/spur.md
- Spur's own site: https://spur.us
- Checked on 2026-09-12
## What Spaw answers
- **Feeds behind the flags:** Public feeds, each named in the answer with the date it was synced: DB-IP Lite and RFC 8805 geofeeds for location, the five registries' delegated statistics for allocation, Spamhaus DROP and ASN-DROP and abuse.ch Feodo Tracker for blocklists, the Tor Project's exit list and Onionoo, the Mullvad, NordVPN and Private Internet Access server lists plus the open X4BNet lists, Apple's iCloud Private Relay egress list, the cloud providers' own published ranges, and the verification files Google, Microsoft, OpenAI, Perplexity, DuckDuckGo and Ahrefs publish for their crawlers.
- **Refresh cadence:** Every feed re-syncs daily, the botnet command-and-control list hourly, and the geofeed crawl weekly. Each response names every dataset it consulted with the date it was last synced, and /status lists them all.
- **Geolocation source:** The DB-IP Lite database, overridden wherever the operator publishes the location itself — an RFC 8805 geofeed, Apple's relay list, a VPN provider's server list. location_source says which answered, and location_confidence is graded from a weekly accuracy report rather than asserted.
`POST /v1/ip` — 1 credit per 5 answered lookups. Free: reserved, private and unallocated ranges. Every rule is on the pricing page: https://spaw.co/pricing
- Documentation: https://spaw.co/docs/ip-intelligence
- Product page: https://spaw.co/products/ip-intelligence
- Free tool: https://spaw.co/tools/ip-lookup
## The other signals
- [Email verification](https://spaw.co/compare/email.md): 9 services compared
- [Phone intelligence](https://spaw.co/compare/phone.md): 9 services compared
- [Address verification](https://spaw.co/compare/address.md): 10 services compared
- [Business verification](https://spaw.co/compare/entity.md): 2 services compared
- [Cross-signal consistency](https://spaw.co/compare/consistency.md): no comparable vendor publishes figures for it
- [Every comparison on one page](https://spaw.co/compare)
## Check the table against your own data
Every Spaw account gets free credits every month with no card, and one key answers every signal. Upload a list, keep your columns, and download only the rows you want. The pricing page has every billing rule in full: https://spaw.co/pricing. Get your API key: https://spaw.co/register
Product names are trademarks of their owners. Spaw is not affiliated with any of them.
Reference: https://spaw.co/compare/ip
---
# Address verification APIs compared
Spaw against Geoapify, Google Address Validation API, Ideal Postcodes, Lob, Loqate, Melissa, Postcode.eu, Postcoder, PostGrid and Smarty on pricing, billing rules, the registers behind each answer and limits, taken from each vendor's own pages on the date it was read.
A postal address read against its own country's format: the kind of delivery point it names, whether its postcode is live, whether the building is in an open address register, and how many companies are registered there.
Below are the 10 services Spaw is measured against for address verification, each with the figures its own public pricing and documentation pages state. Every row carries the date that vendor's pages were read, which is its own and need not match the others'; a row those pages carry no value for reads "not on the pages read" rather than a guess. Corrections: support@spaw.co.
| Service | Pricing model | Free tier | Price per 1,000 at about 10k | Registers held | Checked |
| --- | --- | --- | --- | --- | --- |
| Spaw | Pay-as-you-go credit packs whose credits never expire, or optional plans, billed monthly or yearly, that deposit the same credits every month for less and roll unused plan credits over up to three months' allowance; both through Stripe Checkout | 100 free credits every month, no card required | $7.80 (Growth pack: 5,000 credits for $39), or $5.80 on the Growth monthly plan (5,000 credits for $29 a month) | Open address registers for 23 countries and territories, each named in the answer with its declared coverage, complete or partial. Complete, building by building: Australia, Austria, Belgium, Czechia, Latvia, Luxembourg, Netherlands, Norway and Switzerland. Partial, building by building: Brazil, Denmark, France (with French Guiana, Guadeloupe, Martinique, Mayotte, Réunion, St. Barthélemy, St. Martin and St. Pierre & Miquelon), Germany and United States. Partial, streets only: Great Britain. House-number ranges as well: United States. Absence from a partial register is never read as evidence that an address does not exist. | own pricing page, https://spaw.co/pricing |
| [Spaw vs Geoapify](https://spaw.co/compare/geoapify.md) | Daily credit quotas on monthly subscriptions: Free (3,000 credits a day), API 10 (10,000 a day, $59 billed monthly), API 25 (25,000, $109), API 50 (50,000, $179), API 100 (100,000, $299), API 250 (250,000, $609) and Custom ("Unmetered credits / day", from $860). "1 Geocoding API request costs 1 credit"; in a batch each address counts 0.5 credits, plus 1 credit to create the job and 1 for each result request. A yearly term is offered, but its prices are not in the page text read. | A standing free plan: "3,000 credits / day", "No credit card required", up to 5 requests a second, usable for "commercial websites and apps" with the required Geoapify and data-source attribution | not on the pages read | No national register is named. The geocoder draws on "OpenStreetMap, OpenAddresses, Who's on First, Geonames, Wikipedia, and more", and stored validation results must keep "the attribution to OpenStreetMap, OpenAddresses, Geonames". | 2026-09-22 |
| [Spaw vs Google Address Validation API](https://spaw.co/compare/google-address-validation.md) | "Pay-as-you-go", "charging per request according to the pricing table", under two SKUs named on the billing page, Address Validation Pro and Address Validation Enterprise; the per-request rates render inside the pricing table and are not stated in the page text as fetched, so no figure is quoted here. | not on the pages read | not on the pages read | Google's own data plus, for the "US" and "PR" regions when enableUspsCass is set, the USPS Coding Accuracy Support System; the overview says USPS "evaluates requests for artificially created addresses" and that Google must "stop validation and report relevant information to USPS in such cases". | 2026-09-16 |
| [Spaw vs Ideal Postcodes](https://spaw.co/compare/ideal-postcodes.md) | Pay-as-you-go credit packs, "No Subscriptions. No Monthly Fees.": 200 credits for £9 ("£0.045 per lookup"), 1,100 for £42 (£0.038), 4,300 for £155 (£0.036), 12,800 for £420 (£0.033) and 32,100 for £900 (£0.028). "Credits are valid for 12 months"; the lookup guide adds that the timer "only starts when you begin using that batch of credits". Larger volumes and UK registered charities are priced on request. | A trial rather than a standing tier: "1-Month Trial", "50 free credits, full support and no credit card needed"; the trial searches worldwide except Ireland, "which requires separate licensing" | £33 per 1,000 lookups on the 12,800-credit pack (£420, "£0.033 per lookup"), the smallest pack that covers 10,000. | Named per country on the coverage table: Royal Mail PAF and OS AddressBase Core for the United Kingdom, Guernsey, Jersey and the Isle of Man; the Eircode Address File (ECAF) and Eircode Address Database (ECAD) for Ireland; USPS for the United States, its territories and the freely associated states; Geoscape G-NAF for Australia, Christmas Island, the Cocos (Keeling) Islands and Norfolk Island; Kadaster for the Netherlands; and Kartverket for Norway and for Svalbard and Jan Mayen. The other countries' rows name no dataset. The pricing page lists Multiple Residence, Not Yet Built, Northern Ireland Pointer, Eircode, AddressBase Core and AddressBase Premium as additional datasets. | 2026-09-22 |
| [Spaw vs Lob](https://spaw.co/compare/lob.md) | Verifications are priced per call on top of a print-and-mail plan: on the Developer plan ($0 a month) "US Address Verifications $0.05/additional" and international $0.07; on Startup ($260 a month) $0.025 with "1,000 included for $25/mo" and international $0.06 with 500 included for $30 a month; on Growth ($550 a month) $0.009 with "50,000 included for $450/mo" and international $0.055 with 10,000 included for $550 a month; Enterprise is "Contact us for details". | The Developer plan is "Starting at $0 / month" with verifications billed per call; the site offers "Get started for free". | $50 per 1,000 US verifications on the Developer plan ($0.05 each) or $25 per 1,000 on Startup ($0.025 each beyond the 1,000 included for $25 a month); international $70 or $60. | Postal: "USPS CASS certification", plus a deliverability likelihood "based on Lob's mail delivery data to over half of US households". | 2026-09-16 |
| [Spaw vs Loqate](https://spaw.co/compare/loqate.md) | Pay-as-you-go credit purchases of £50, £100, £250, £500 and £1,000 (or 75€, 150€ and 300€), each buying a published per-lookup rate for every service, plus bespoke plans; "Credit is valid for 12 months after the purchase date" and there are no setup costs. The pricing page shows two rate tables headed "New Pricing" and "Old Pricing" without saying which accounts each applies to; the figures here are from "New Pricing". Its usage-example selector also lists a £2,500 plan that neither table prices. | A trial rather than a standing tier: "Start your 45-day free trial"; the FAQ adds "You get free credits to test the service" | £82 to £86 per 1,000 Address Verify lookups on the "New Pricing" table: 8.2p a lookup on the £100, £250 and £500 tiers and 8.6p on the £50 and £1,000 tiers. Address Capture is 8.6p to 4.9p in the UK and 7.1p to 4.1p for the rest of the world. | Not published as a register list. The Address Verify documentation describes "postal authority datasets and proprietary enhanced data covering countries worldwide", and the pricing page offers enhanced UK datasets ("Eircodes, Multi-Residence, Not Yet Built data and Property Attributes data") on request. | 2026-09-22 |
| [Spaw vs Melissa](https://spaw.co/compare/melissa.md) | US Address Verification pay-as-you-go "Starting at $40" for 10,000 credits at "10 Credits Per Address" (the same page's FAQ says a US address equals 3 credits and a global address 10); an API subscription "Starting at $5,145" for 1,000,000 records a year; an unlimited-records plan "Starting at $16,000". Global Address Verification subscriptions from $40 a month for 1,000 records and $95 a month for 2,500. | Global Address Verification "GAV Free" at $0 for 250 records a month; 1,000 lookup credits a month after a one-time $4.97 KYC/AML verification fee, described on the pricing page. | $40 per 1,000 US addresses on the smallest pay-as-you-go pack, taking the pack's own "10 Credits Per Address" ($40 buys 10,000 credits). | Postal, not cadastral: "a postal certified address engine using official government data including CASS (USPS), SERP (Canada Post), PAF (Royal Mail) and more"; no national address register is named. | 2026-09-16 |
| [Spaw vs Postcode.eu](https://spaw.co/compare/postcode-eu.md) | Annual tiers that include validations: Tier 1 €50, Tier 2 €100, Tier 3 €250, Tier 4 €500 and Tier 5 €1,000 a year, each with a per-validation rate by country group. "Up to the Tier amount you can perform a mix of validations", subscriptions "are automatically upgraded when the maximum of the Tier is reached", and past the highest Tier usage "will be invoiced in blocks of 10% and the rates of this Tier apply". A validation is "a bundle of related requests that were used to validate an address". An SLA (€1,000 + 2% a year) and a DORA addendum (€2,500 a year) are extra. | A trial rather than a standing tier: "After registering an account it is possible to try the APIs for free", and "Testing the API is without obligations" | €17.64 per 1,000 validations on Tier 3 (€250 a year, "€0.01764 each") for Austria, Belgium, Denmark, Finland, France, Luxembourg, Norway, Spain, Sweden, Switzerland and the Netherlands. Germany, Italy and the United Kingdom cost more a validation, so 10,000 of them fall in Tier 4 (€500 a year): €34.17 per 1,000 for Germany (€0.03417 each) and €43.30 for Italy and the United Kingdom (€0.0433 each). "Tier prices are including validations" and "Prices and quantities are per year", so the tier amount is paid in full: 10,000 validations cost €250 a year for the eleven countries (€25 per 1,000) and €500 for Germany, Italy or the United Kingdom (€50 per 1,000). | Named per country, "only official sources": the Dutch government's Basisregistraties Adressen en Gebouwen (BAG); for Belgium the Flemish CRAB, the Brussels UrbIS and the Walloon ICAR; Luxembourg's Administration du cadastre et de la topographie; Austria's federal Adressregister; the Swiss Gebäude- und Wohnungsregister; France's Base Adresse Nationale; Royal Mail's Postcode Address File for the United Kingdom; Spain's Catastro in CartoCiudad; Denmark's Adresseregister (DAR); GeoNorge for Norway; DVV and Posti for Finland; Lantmäteriet for Sweden; Poste Italiane for Italy; and Deutsche Post, with OpenStreetMap for locations, for Germany. | 2026-09-22 |
| [Spaw vs Postcoder](https://spaw.co/compare/postcoder.md) | Credit packs (500 to 200,000 credits, "valid for 12 months from purchase") or monthly plans (1,000 to 100,000 credits, with unused credits not carried over). "Your credits can be used for any Postcoder request." The pack prices themselves render as placeholders ("£--") in the pack list; the credit calculator's recommendation box shows "Credit pack £25 500 Credits" and "Monthly plan £25 1,000 Credits". Larger packs and plans with volume discounts are quoted. | A free trial is offered; its duration and allowance are not stated on the pricing page | £100 per 1,000 UK address lookups pay-as-you-go (10.00p a request, 2.0 credits) or £45 per 1,000 on a monthly plan (4.50p). This is the address comparison, so only the address rate is quoted: the same account buys email and mobile validation at their own per-request rates, which are read off the same page. | Not published as a register list. The pricing page separates UK, Ireland and "Rest of world" address lookups and mentions a "PAF Public Sector Licence" for public sector discounts. | 2026-09-22 |
| [Spaw vs PostGrid](https://spaw.co/compare/postgrid.md) | Quoted: the pricing page asks for annual mail-piece and address-verification volumes in bands from "Less than 50,000" to "More than 1,000,000" and publishes no rate. | "Try for Free. Sign Up for Address Verification API" on the product page; no standing free volume is stated. | not on the pages read | Postal: "CASS & SERP Certified", matching "customer addresses with official postal records from USPS and Canada Post"; also "CASS/NCOA Certified". | 2026-09-16 |
| [Spaw vs Smarty](https://spaw.co/compare/smarty.md) | Annual lookup bundles on the Professional plan from $552 a year, sized 12K, 60K, 120K, 300K, 600K, 1M or 2M lookups; a monthly option and bespoke Custom pricing are offered. Only the $552 entry price is shown on the page. A subscription "expires when the address lookups or the time runs out, whichever comes first"; there are no overage costs, and an exhausted plan answers 402 "payment required" unless auto-renewal buys a new block. | A free trial rather than a standing tier: "42 days" and "1,000 address lookups", "No credit card required" | not on the pages read | Postal rather than cadastral: the page claims "210M addresses (incl 20M non-USPS)" and "over 20 million non-postal addresses", with "Up to 55 points of metadata" per lookup. It names no national address register. | 2026-09-22 |
## Spaw vs Geoapify
Geoapify is a location platform (maps, routing, places and geocoding) whose Geocoding API is also sold for address validation: an address is matched against open datasets such as OpenStreetMap and OpenAddresses and returned with coordinates and confidence scores for the city, the street and the building.
Where it fits better: Teams already using Geoapify for maps or routing that want an open-data address check with coordinates on a free daily allowance.
- Full comparison: https://spaw.co/compare/geoapify.md
- Geoapify alternatives: https://spaw.co/alternatives/geoapify.md
- Geoapify's own site: https://www.geoapify.com
- Checked on 2026-09-22
## Spaw vs Google Address Validation API
Google's Address Validation API, part of Google Maps Platform, validates, standardises and geocodes an address, reports a confirmation level per component with inferred and replaced parts, and can run US and Puerto Rico addresses through USPS CASS on request.
Where it fits better: Checkout and delivery forms that want Google's component-level correction and geocoding in the countries its coverage table lists, inside a Google Maps Platform account.
- Full comparison: https://spaw.co/compare/google-address-validation.md
- Google Address Validation API alternatives: https://spaw.co/alternatives/google-address-validation.md
- Google Address Validation API's own site: https://mapsplatform.google.com
- Checked on 2026-09-16
## Spaw vs Ideal Postcodes
Ideal Postcodes is a UK-first address lookup, autocomplete and cleansing API answered from Royal Mail's Postcode Address File, with Ordnance Survey AddressBase Core and Premium offered as additional datasets, sold as pay-as-you-go credit packs, with international address data for more than 240 countries and territories; its site also lists phone and email validation among its products.
Where it fits better: UK checkouts and CRMs that want a postcode-to-address picker and address cleansing from Royal Mail's address file, with UPRNs and coordinates included, paid for by the lookup rather than by subscription.
- Full comparison: https://spaw.co/compare/ideal-postcodes.md
- Ideal Postcodes alternatives: https://spaw.co/alternatives/ideal-postcodes.md
- Ideal Postcodes' own site: https://ideal-postcodes.co.uk
- Checked on 2026-09-22
## Spaw vs Lob
Lob is a print-and-mail platform whose Address Verification API validates, standardises and enriches US and international addresses, CASS certified, with USPS delivery-point data and a deliverability likelihood drawn from Lob's own mail delivery records.
Where it fits better: Teams that already print and mail through Lob and want the same account to clean the addresses first, with the postal service's delivery-point answers for the United States.
- Full comparison: https://spaw.co/compare/lob.md
- Lob alternatives: https://spaw.co/alternatives/lob.md
- Lob's own site: https://www.lob.com
- Checked on 2026-09-16
## Spaw vs Loqate
Loqate sells address capture and address verification alongside email, phone and bank validation, priced per lookup from a pay-as-you-go credit balance, with international data for over 245 countries and territories.
Where it fits better: Teams that want address capture in a checkout across many countries, with email, phone and bank validation billed from the same balance.
- Full comparison: https://spaw.co/compare/loqate.md
- Loqate alternatives: https://spaw.co/alternatives/loqate.md
- Loqate's own site: https://www.loqate.com
- Checked on 2026-09-22
## Spaw vs Melissa
Melissa sells address verification for the United States and "250+ Countries & Territories" against postal data, CASS and DPV certified, with a credit model for pay-as-you-go use and annual subscriptions sized by records, alongside name, phone, email and identity verification.
Where it fits better: Mailers and CRM teams that want the postal services' own delivery-point answers for the United States, Canada and the United Kingdom, from one vendor that also sells change-of-address and identity checks.
- Full comparison: https://spaw.co/compare/melissa.md
- Melissa alternatives: https://spaw.co/alternatives/melissa.md
- Melissa's own site: https://www.melissa.com
- Checked on 2026-09-16
## Spaw vs Postcode.eu
Postcode.eu, from Postcode.nl B.V., sells address autocomplete and validation APIs for fourteen European countries, each answered from the official sources its pages name (government address registers for most, the postal operator's file for others), priced per validation inside an annual tier.
Where it fits better: European checkouts and CRMs that want autocomplete and validation answered from each country's official address source under one annual subscription.
- Full comparison: https://spaw.co/compare/postcode-eu.md
- Postcode.eu alternatives: https://spaw.co/alternatives/postcode-eu.md
- Postcode.eu's own site: https://www.postcode.eu
- Checked on 2026-09-22
## Spaw vs Postcoder
Postcoder is a UK-first address lookup API sold as credits that any of its services can spend: address lookup, bank validation, email validation, mobile validation and OTP verification, each costing a published number of credits per request.
Where it fits better: UK checkouts that want a postcode-to-address picker from a licensed UK dataset, with email and mobile validation on the same balance.
- Full comparison: https://spaw.co/compare/postcoder.md
- Postcoder alternatives: https://spaw.co/alternatives/postcoder.md
- Postcoder's own site: https://postcoder.com
- Checked on 2026-09-22
## Spaw vs PostGrid
PostGrid sells an address verification API for the United States and Canada, CASS and SERP certified against USPS and Canada Post data, with an international service for "245+ Countries and Territories", autocomplete, parsing, batch verification and a Salesforce integration, priced by quotation.
Where it fits better: Businesses mailing in the United States and Canada that want postal-certified verification and autocomplete under one contract, priced on their annual volume.
- Full comparison: https://spaw.co/compare/postgrid.md
- PostGrid alternatives: https://spaw.co/alternatives/postgrid.md
- PostGrid's own site: https://www.postgrid.com
- Checked on 2026-09-16
## Spaw vs Smarty
Smarty sells US address verification against postal data: a lookup validates to the secondary level and returns delivery-point metadata that open registers cannot answer, including a DPV match code, the residential indicator and vacancy status.
Where it fits better: US mailers who need the postal service's own delivery-point answers — is it a real delivery point, is it occupied, is it residential — which open data cannot give.
- Full comparison: https://spaw.co/compare/smarty.md
- Smarty alternatives: https://spaw.co/alternatives/smarty.md
- Smarty's own site: https://www.smarty.com
- Checked on 2026-09-22
## What Spaw answers
- **Registers held:** Open address registers for 23 countries and territories, each named in the answer with its declared coverage, complete or partial. Complete, building by building: Australia, Austria, Belgium, Czechia, Latvia, Luxembourg, Netherlands, Norway and Switzerland. Partial, building by building: Brazil, Denmark, France (with French Guiana, Guadeloupe, Martinique, Mayotte, Réunion, St. Barthélemy, St. Martin and St. Pierre & Miquelon), Germany and United States. Partial, streets only: Great Britain. House-number ranges as well: United States. Absence from a partial register is never read as evidence that an address does not exist.
- **Country coverage:** Postal formats for 207 countries and territories: the line layout for 199 of them, and the required fields and postcode pattern wherever the table records them; 52 postal operators' rules for naming a delivery point that is not a street; open address registers for 23 countries and territories; postcode directories for Great Britain from the ONS Postcode Directory, with Northern Ireland excluded because reusing BT postcodes needs a separate licence, and for 108 more countries and territories from GeoNames' postal codes.
- **Claims post is delivered:** No, and every answer says so: is_deliverable, is_cmra, is_vacant and is_residential answer null because that check is licensed and not enabled here. exists is a match in an open address register, never a statement that post arrives.
`POST /v1/address` — 1 credit for a valid address. Free: invalid addresses, and requests without a country. Every rule is on the pricing page: https://spaw.co/pricing
- Documentation: https://spaw.co/docs/address-verification
- Product page: https://spaw.co/products/address-verification
- Free tool: https://spaw.co/tools/address-checker
## The other signals
- [Email verification](https://spaw.co/compare/email.md): 9 services compared
- [Phone intelligence](https://spaw.co/compare/phone.md): 9 services compared
- [IP intelligence](https://spaw.co/compare/ip.md): 13 services compared
- [Business verification](https://spaw.co/compare/entity.md): 2 services compared
- [Cross-signal consistency](https://spaw.co/compare/consistency.md): no comparable vendor publishes figures for it
- [Every comparison on one page](https://spaw.co/compare)
## Check the table against your own data
Every Spaw account gets free credits every month with no card, and one key answers every signal. Upload a list, keep your columns, and download only the rows you want. The pricing page has every billing rule in full: https://spaw.co/pricing. Get your API key: https://spaw.co/register
Product names are trademarks of their owners. Spaw is not affiliated with any of them.
Reference: https://spaw.co/compare/address
---
# Business verification APIs compared
Spaw against Companies House API and OpenCorporates on pricing, billing rules, the registers behind each answer and limits, taken from each vendor's own pages on the date it was read.
A Legal Entity Identifier or a UK company number read against open registers synced onto this server: legal name, whether the entity is still live, jurisdiction and incorporation date.
Below are the 2 services Spaw is measured against for business verification, each with the figures its own public pricing and documentation pages state. Every row carries the date that vendor's pages were read, which is its own and need not match the others'; a row those pages carry no value for reads "not on the pages read" rather than a guess. Corrections: support@spaw.co.
| Service | Pricing model | Free tier | Price per 1,000 at about 10k | Registers held | Checked |
| --- | --- | --- | --- | --- | --- |
| Spaw | Pay-as-you-go credit packs whose credits never expire, or optional plans, billed monthly or yearly, that deposit the same credits every month for less and roll unused plan credits over up to three months' allowance; both through Stripe Checkout | 100 free credits every month, no card required | $7.80 (Growth pack: 5,000 credits for $39), or $5.80 on the Growth monthly plan (5,000 credits for $29 a month) | Two, synced as bulk files onto the server so no third party sits in the request path: the GLEIF Level 1 golden copy of Legal Entity Identifiers, refreshed daily by its publisher, and the Companies House free company data product for the United Kingdom, republished monthly. Every answer carries the date of the copy it read. | own pricing page, https://spaw.co/pricing |
| [Spaw vs Companies House API](https://spaw.co/compare/companies-house-api.md) | No price is published on the developer pages checked; access is through a registered application and its key. | not on the pages read | not on the pages read | One, and the authoritative one: the UK register itself, answered live rather than from a copy. | 2026-09-10 |
| [Spaw vs OpenCorporates](https://spaw.co/compare/opencorporates.md) | Annual or monthly API plans capped by call count rather than sold by volume: Essentials "£ 2,250/year" or "£225 per month" ("Up to 500 API calls/mth", "Up to 200 API calls/day"), Starter "£ 6,600/year" (2,500 a month, 500 a day), Basic "£ 12,000/year" (5,000 a month, 1,000 a day), Enterprise by consultation with bulk-data delivery | Free at-scale access for investigative journalists, NGOs, universities and anti-crime-and-corruption research groups rather than a free plan, in the site's own words | not on the pages read | Company registers, aggregated. The pricing page does not list which ones; choosing jurisdictions is an Enterprise plan feature. | 2026-09-10 |
## Spaw vs Companies House API
The Companies House Public Data API is the UK registrar's own service: "The Companies House API lets you retrieve information about limited companies (and other companies that fall within the Companies Act 2006). The data returned is live and real-time, and is simple to use and understand."
Where it fits better: Anyone who needs the UK register as it stands right now, and only the UK register. It is the registrar's own service and will always be more current than a copy of its monthly file.
- Full comparison: https://spaw.co/compare/companies-house-api.md
- Companies House API alternatives: https://spaw.co/alternatives/companies-house-api.md
- Companies House API's own site: https://developer.company-information.service.gov.uk
- Checked on 2026-09-10
## Spaw vs OpenCorporates
OpenCorporates is a company-data business built on open company registers, sold as annual API plans capped by monthly and daily call counts, with bulk-data delivery and a choice of jurisdictions on the enterprise plan.
Where it fits better: Investigators and data teams that need company records across many jurisdictions, and can work within a plan capped at a few thousand calls a month.
- Full comparison: https://spaw.co/compare/opencorporates.md
- OpenCorporates alternatives: https://spaw.co/alternatives/opencorporates.md
- OpenCorporates' own site: https://opencorporates.com
- Checked on 2026-09-10
## What Spaw answers
- **Registers held:** Two, synced as bulk files onto the server so no third party sits in the request path: the GLEIF Level 1 golden copy of Legal Entity Identifiers, refreshed daily by its publisher, and the Companies House free company data product for the United Kingdom, republished monthly. Every answer carries the date of the copy it read.
- **Jurisdiction coverage:** A LEI is global by construction; company numbers are GB only today, because a company number is unique only inside its own register. Anything else answers unsupported_register, free.
- **Cross-register comparison:** Yes: where a LEI record names a Companies House number and both files are installed, the legal name and the status are compared and both sides are quoted in agreements, with flags for a disagreement or for a company the national register records as dissolved.
- **Registered address returned:** No, deliberately: a registered address is very often somebody's home. The response carries country and jurisdiction, which are enough to answer whether the entity is real and live.
`POST /v1/entity` — 1 credit when a register answers. Free: malformed or unknown identifiers, and registers we do not carry yet. Every rule is on the pricing page: https://spaw.co/pricing
- Documentation: https://spaw.co/docs/business-verification
- Product page: https://spaw.co/products/business-verification
- Free tool: https://spaw.co/tools/company-lookup
## The other signals
- [Email verification](https://spaw.co/compare/email.md): 9 services compared
- [Phone intelligence](https://spaw.co/compare/phone.md): 9 services compared
- [IP intelligence](https://spaw.co/compare/ip.md): 13 services compared
- [Address verification](https://spaw.co/compare/address.md): 10 services compared
- [Cross-signal consistency](https://spaw.co/compare/consistency.md): no comparable vendor publishes figures for it
- [Every comparison on one page](https://spaw.co/compare)
## Check the table against your own data
Every Spaw account gets free credits every month with no card, and one key answers every signal. Check up to 500 identifiers in one call, or save a list that re-checks itself and tells you when a company leaves the register. The pricing page has every billing rule in full: https://spaw.co/pricing. Get your API key: https://spaw.co/register
Product names are trademarks of their owners. Spaw is not affiliated with any of them.
Reference: https://spaw.co/compare/entity
---
# Cross-signal consistency APIs compared
Cross-signal consistency has no comparable vendor to put in a table: what Spaw answers, what it costs, and where the comparisons for the other signals are.
Any subset of the email, phone, IP and address signals in one call, with the agreements and disagreements between them named and both sides quoted. No score, no verdict, no recommendation.
No other vendor publishes a comparable product, so there is no table on this page. What follows is what Spaw answers, what it costs, and where to find the comparisons for the signals that do have them.
## What Spaw answers
`POST /v1/consistency` — the sum of the signals you asked for. Free: the comparison itself, always. Every rule is on the pricing page: https://spaw.co/pricing
- Documentation: https://spaw.co/docs/cross-signal-consistency
- Product page: https://spaw.co/products/cross-signal-consistency
## The other signals
- [Email verification](https://spaw.co/compare/email.md): 9 services compared
- [Phone intelligence](https://spaw.co/compare/phone.md): 9 services compared
- [IP intelligence](https://spaw.co/compare/ip.md): 13 services compared
- [Address verification](https://spaw.co/compare/address.md): 10 services compared
- [Business verification](https://spaw.co/compare/entity.md): 2 services compared
- [Every comparison on one page](https://spaw.co/compare)
## Try it on your own data
Every Spaw account gets free credits every month with no card, and one key answers every signal. Upload a list, keep your columns, and download only the rows you want. The pricing page has every billing rule in full: https://spaw.co/pricing. Get your API key: https://spaw.co/register
Product names are trademarks of their owners. Spaw is not affiliated with any of them.
Reference: https://spaw.co/compare/consistency
---
# Spaw vs Abstract API: email verification API comparison
How Spaw and Abstract API compare on pricing, billing rules, verification depth and limits, from Abstract API's public pages checked on 2026-09-13. Corrections: support@spaw.co.
| | Spaw | Abstract API |
| --- | --- | --- |
| Pricing model | Pay-as-you-go credit packs whose credits never expire, or optional plans, billed monthly or yearly, that deposit the same credits every month for less and roll unused plan credits over up to three months' allowance; both through Stripe Checkout | Monthly subscription tiers with a request slider from 5,000 to 150,000 requests a month; Starter from $17, Standard from $37, Professional from $39 a month |
| Free tier | 100 free credits every month, no card required | 100 requests on the Free plan |
| Price per 1,000 at about 10k | $7.80 (Growth pack: 5,000 credits for $39), or $5.80 on the Growth monthly plan (5,000 credits for $29 a month) | not on the pages read |
| Price per 1,000 at about 100k | $4.49 (Volume pack: 100,000 credits for $449), or $3.49 on the Volume monthly plan (100,000 credits for $349 a month) | not on the pages read |
| Unknown or catch-all results billed | No: an unconfirmed mailbox (reason unverified) and an answer Spaw could not obtain because its handshake partner was unavailable are free and leave no 7-day repeat marker, so a later lookup is a fresh one; a catch-all answer costs 1 credit like any risky answer, and an automatic re-check bills 1 credit only when it confirms the mailbox | Yes |
| Undeliverable results billed | No, within a fair use: undeliverable answers that needed a mailbox probe are free up to max(500, your billed email answers) in a rolling 30 days, then 1 credit each; the rest are always free | Yes |
| SMTP mailbox check | Yes | Yes |
| Catch-all handling | A catch-all answer is remembered per domain for 7 days, and every unconfirmed mailbox carries a published 0 to 100 mailbox_confidence estimate | Catch-all detection on all plans; every request counts against the quota |
| Bulk limit | 100,000 addresses per bulk job, with an HMAC-signed completion webhook | Bulk CSV upload from the Starter plan up |
| Batch API | 50 addresses per synchronous request, probes sent concurrently | not on the pages read |
| Rate limit | 5 requests per second per key on the email and consistency endpoints | 3 requests a second on paid tiers |
| Official SDKs | Python, Node.js, PHP | not on the pages read |
| Data retention | Only email verification keeps a history row, for 30 days, then pruned; test addresses are never logged | not on the pages read |
## When Abstract API fits better
Developers who want one vendor for several small utility APIs under a monthly quota.
- Quality score, risk assessment and breach detection are listed on the Free and Professional plans and not on Starter or Standard; role detection is not on the plan table
- SOC 2 Type II and GDPR, CCPA statements
- A commercial license is listed under the Enterprise plan
## When Spaw fits better
Developers who want one flat price per answered lookup across every signal a signup form produces, a transparent risk score, and an API that documents every field.
- Abstract's quality score, risk assessment and breach detection sit on its Professional plan; Spaw returns all 46 fields, including risk_score, role and ok_to_send, on every lookup
- Abstract counts every request against the quota; Spaw bills per answered lookup
- Abstract's stated rate limit is 3 requests a second; Spaw's is 5
- Spaw's bulk jobs take 100,000 addresses through the API with a signed webhook
## Questions
**Does Spaw charge for catch-all or unknown results?** Spaw bills per answer, not per probe outcome. A catch-all domain comes back risky with reason catch_all and costs 1 credit like any risky answer. A mailbox the handshake could not confirm (greylisting, a timeout, no definite answer) comes back risky with reason unverified and is free, and so is an answer Spaw could not obtain at all because its handshake partner was unavailable; neither leaves a 7-day repeat marker, so a later lookup is a fresh one (an uncertain result is reused for up to 15 minutes, then the mailbox is asked again) and bills 1 credit only once the mailbox is confirmed, and the automatic re-checks behind callback_url bill 1 credit only when they confirm it. Undeliverable verdicts are free within a fair use of max(500, your billed email answers) in a rolling 30 days, and invalid input and repeats within 7 days are free too. Abstract API counts them according to its public pages.
**Can I move a list from Abstract API to Spaw?** Yes. Upload the CSV to the dashboard or POST it to the bulk endpoint; Spaw keeps your columns, adds the verdict columns, and lets you download only the deliverable, risky or undeliverable rows. Addresses that come back undeliverable join your suppression list so later runs skip them for free.
**Is there a free tier?** Every Spaw account receives 100 free credits at the start of each month with no card on file, and unused free credits accumulate. Abstract API offers: 100 requests on the Free plan.
## Sources (checked 2026-09-13)
- [Abstract API email validation page](https://www.abstractapi.com/api/email-verification-validation-api)
Abstract API is a trademark of its owner. Spaw is not affiliated with it.
Reference: https://spaw.co/compare/abstract
---
# Abstract API alternatives for email verification
Email verification APIs to consider instead of Abstract API, with the pricing model, free tier and price per 1,000 lookups each vendor publishes. Figures come from public pricing pages checked on 2026-09-13.
| Service | Pricing model | Free tier | Price per 1,000 at about 10k |
| --- | --- | --- | --- |
| [Spaw](https://spaw.co) | Pay-as-you-go credit packs whose credits never expire, or optional plans, billed monthly or yearly, that deposit the same credits every month for less and roll unused plan credits over up to three months' allowance; both through Stripe Checkout | 100 free credits every month, no card required | $7.80 (Growth pack: 5,000 credits for $39), or $5.80 on the Growth monthly plan (5,000 credits for $29 a month) |
| [Bouncer](https://www.usebouncer.com) | Pay-as-you-go credits that never expire (from 1,000 credits for $8 up to 1,000,000 for $2,000), auto-refill, plus a Deliverability Kit subscription at $25 to $250 a month | 100 free credits, no card required | $6.00 (10,000 credits for $60) |
| [Emailable](https://emailable.com) | Pay-as-you-go credits that never expire, or a monthly subscription at a 15% discount; minimum purchase 5,000 credits | 250 free credits at signup | $7.60 (the pay-as-you-go rate printed for the 5,000-credit package, $0.0076 a credit; larger packages are priced in a calculator, and the 5,000-credit subscription is $32.30 a month after the 15% discount) |
| [Hunter](https://hunter.io) | Monthly or yearly subscriptions with unified credits shared across search, verification and enrichment; credit packs on paid plans, usable for up to 12 months from purchase according to the pricing page | Free plan with 50 credits a month; one verification costs 0.5 credit, so 100 verifications | $7.45 (Growth: $149 a month for 10,000 credits, 20,000 verifications at 0.5 credit each; $5.20 billed yearly) |
| [Kickbox](https://kickbox.com) | not on the pages read | not on the pages read | not on the pages read |
| [Mailboxlayer](https://mailboxlayer.com) | Monthly or yearly subscription (10 to 15% off yearly) with a request quota per plan and per-request overage | 100 requests a month | $3.00 for the Basic quota (5,000 requests for $14.99 a month; overage $0.012 a request, so 10,000 requests on Basic cost about $74.95, $7.50 per 1,000); Professional Plus: 50,000 for $74.99, $1.50 per 1,000 |
| [NeverBounce](https://www.neverbounce.com) | not on the pages read | not on the pages read | not on the pages read |
| [Verifalia](https://verifalia.com) | Pay-as-you-go credit packs that never expire, or monthly, quarterly and yearly subscriptions; verification depth is priced as quality levels; pack and plan prices are shown in a calculator rather than listed | 25 free credits a day on the Free plan; unused daily credits expire each day | not on the pages read |
| [ZeroBounce](https://www.zerobounce.net) | Pay-as-you-go credits from $39 for 2,000 addresses, 15% off with autopay, or the ZeroBounce ONE subscription at $99 a month with unused credits rolling over; volume pricing shown through a calculator rather than fixed packs | 100 validation credits a month on the free account | not on the pages read |
## Spaw
Verification API that bills per answered lookup across six signals on one key and one balance: Email verification, Phone intelligence, IP intelligence, Address verification, Business verification against open registers, and Cross-signal consistency, which runs any subset of the first four in one call and names where they agree and where they do not. The email side checks syntax, live MX, SPF and DMARC, disposable and role lists, and runs an SMTP mailbox handshake that never sends a message.
## Bouncer
Bouncer is an email verification service with pay-as-you-go credits that never expire, high-throughput batch APIs, a real-time form product (Bouncer Shield) and a Deliverability Kit subscription.
Comparison: https://spaw.co/compare/bouncer
## Emailable
Emailable is an email verification service with pay-as-you-go credits, a bulk verifier, a widget and an API, with Node.js, Ruby and Python libraries.
Comparison: https://spaw.co/compare/emailable
## Hunter
Hunter is an outreach platform whose Email Verifier shares unified plan credits with its email finder, enrichment and campaign tools.
Comparison: https://spaw.co/compare/hunter
## Kickbox
Kickbox is an email verification API that returns deliverable, undeliverable, risky or unknown plus a 0 to 1 Sendex quality score, with official libraries for PHP, Node.js, Ruby and Python.
Comparison: https://spaw.co/compare/kickbox
## Mailboxlayer
Mailboxlayer is apilayer's email validation API, sold as monthly request quotas from a free 100-request plan up to 250,000 requests a month.
Comparison: https://spaw.co/compare/mailboxlayer
## NeverBounce
NeverBounce is a ZoomInfo-owned email verification service with a bulk list-cleaning workflow, a real-time API and official client libraries in six languages.
Comparison: https://spaw.co/compare/neverbounce
## Verifalia
Verifalia is an email verification service with selectable quality levels, credit packs that never expire, subscriptions with daily free credits, and official SDKs in six languages.
Comparison: https://spaw.co/compare/verifalia
## ZeroBounce
ZeroBounce is an email validation service with a pay-as-you-go credit model and a subscription (ZeroBounce ONE) that bundles deliverability tools such as activity data, inbox placement tests and blacklist monitoring.
Comparison: https://spaw.co/compare/zerobounce
Abstract API and the other product names are trademarks of their owners. Spaw is not affiliated with them.
Reference: https://spaw.co/alternatives/abstract
---
# Spaw vs Abstract Phone Validation: phone intelligence API comparison
How Spaw and Abstract Phone Validation compare on pricing, billing rules, where the line type comes from and limits, from Abstract Phone Validation's public pages checked on 2026-09-27. Corrections: support@spaw.co.
| | Spaw | Abstract Phone Validation |
| --- | --- | --- |
| Pricing model | Pay-as-you-go credit packs whose credits never expire, or optional plans, billed monthly or yearly, that deposit the same credits every month for less and roll unused plan credits over up to three months' allowance; both through Stripe Checkout | Monthly request quotas on three plans that differ by the data returned: Starter (validation, location, carrier) from $19 a month for 5,000 requests to $199 for 150,000; Standard (adds messaging) from $19 for 3,000 to $199 for 100,000; Professional (adds registration and risk) from $19 for 2,000 to $199 for 50,000. Annual billing saves 10%; the pricing page quotes $3.40 per 1,000 requests at Starter's entry volume, billed annually |
| Free tier | 100 free credits every month, no card required | 100 requests a month with no card, on the full response including registration and risk |
| Price per 1,000 at about 10k | $7.80 (Growth pack: 5,000 credits for $39), or $5.80 on the Growth monthly plan (5,000 credits for $29 a month) | not on the pages read |
| Price per 1,000 at about 100k | $4.49 (Volume pack: 100,000 credits for $449), or $3.49 on the Volume monthly plan (100,000 credits for $349 a month) | not on the pages read |
| Numbers the vendor could not settle billed | A valid number costs 1 credit whatever its flags say. The live carrier-network check (hlr: true) is not enabled on the service: hlr_checked answers false and nothing extra is charged. | Each individual phone number you submit counts as a credit used. Credits are also counted per request, not per successful response |
| Invalid numbers billed | No: invalid numbers are free, including a number in a block the regulator lists as unassigned, and so are repeats within 7 days | Yes: the documentation says a request for an invalid phone number "still counts as 1 credit" |
| Line type source | The numbering plan metadata everywhere except the North American plan, where mobiles and landlines share blocks and the plan can only say fixed_line_or_mobile; there the regulator's block holder settles it — a wireless carrier's block is mobile, a virtual-number wholesaler's is voip. line_type_source names which of the two answered. | Not stated. The product page says Abstract uses "several databases of numbers" that it ingests and cross-references, with updates daily, weekly and monthly, without naming them; its changelog for 24 July 2026 mentions "recently published numbering plans from several African regulators". |
| Numbering plan coverage | libphonenumber metadata for every region it carries, plus the regulators' own allocation tables, each named in the answer with the date of the copy it read. Complete tables, where a geographic number in a block the table leaves out answers unassigned_block: NANPA for the United States (with Puerto Rico, the U.S. Virgin Islands, Guam, American Samoa and the Northern Mariana Islands), the CNAC for Canada, ARCEP for France (with Guadeloupe, St. Barthélemy, St. Martin, Martinique, French Guiana, Réunion, Mayotte and St. Pierre & Miquelon), ACM for the Netherlands and the CRT (formerly the IFT) for Mexico. Partial tables, answered only from the ranges they list (a range listed as free, spare or held in reserve answers unassigned_block), so block_status answers null for a number outside them: Ofcom for the United Kingdom (with Guernsey, Jersey and the Isle of Man), ACMA for Australia, the NAD for New Zealand, UKE for Poland and MIMIT for Italy. Re-synced weekly, NANPA daily; /status lists each table's age. Outside those regions, block_status and is_virtual answer null rather than a guess. | "190+ countries" on the product page and "195+" in its FAQ, which lists them by name; no numbering plan or regulator table is named |
| Live network check | Reserved behind hlr: true: 4 credits on top of the base credit, only where the network answers, reported in its own reachable, ported, current_carrier and roaming fields. The live carrier-network check (hlr: true) is not enabled on the service: hlr_checked answers false and nothing extra is charged. | The Phone Intelligence response carries phone_validation.line_status ("e.g. active, inactive") on every plan. The HLR Lookup page says status comes from "carrier data sources, including HLR and MNP (Mobile Number Portability) records" and applies "primarily to mobile numbers". No separate price is published for it. |
| Ported numbers | None from the tables, and the docs say so: carrier and line_type describe the block the number was allocated in, never a later port. Only a live carrier-network check can say who serves a number today. | The HLR Lookup page says a lookup "returns the current carrier, not the original one"; the API reference describes phone_carrier.name as "The carrier that the number is registered with". |
| SMS pumping controls | Flags in the risk score, and an SMS policy that turns them into ok_to_send and blocked_by: a fraud and deliverability decision, not consent and not a do-not-call scrub. allowed_countries limits sending to the regions you serve; it is empty (any country) until you set it. range_burst (+40) flags a number once your account has looked up 5 distinct numbers in its 1,000-number range within 60 minutes, and numbers_per_client (+30) once one client address (the client_ip you send, or a browser visitor's own) has tried 5 within 60 minutes, unless Spaw's own IP data places it on a mobile carrier or a corporate proxy, or it is private or reserved. Both count only your own single and browser lookups, so a new account has no history. The range is a heuristic, and in North America a burst more likely means someone testing numbers against a form; the client count does not stop an attack rotated through residential proxies. disposable (+70) flags a number SMS-receiving websites publish and disposable_stale (+40) an old listing; disposable_range (+30) flags an unlisted number in a 1,000-number range holding 10 or more fresh listings. The lists are public and synced daily. The default policy blocks the fictional, disposable, reported_abuse, reported_abuse_widely and premium_rate signals and a risk_score above 60, so neither traffic signal blocks on its own. | An SMS pumping page recommends checking each number before the code is sent: reject VOIP and premium-rate lines and destinations you do not serve, and use the phone_risk flags (risk_level, is_disposable, is_abuse_detected, on the Free and Professional plans) with Abstract's IP data to catch bursts from data-center IPs and bots. No separate price is listed for it. |
| Bulk limit | 100,000 numbers per bulk job, with an HMAC-signed completion webhook | Bulk CSV upload on the paid plans; the documentation caps a file at 50,000 rows |
| Batch API | 1,000 numbers per synchronous request, answered in input order | not on the pages read |
| Rate limit | 50 requests per second per key on the phone endpoints | 3 requests a second at each monthly plan's entry volume, rising with the chosen volume to 50 at the largest monthly option (100 at the largest annual Standard and Professional options); the Free plan card also shows 3 a second, while the documentation's error table says free plans are "limited to 1 per second" |
| Official SDKs | Python, Node.js, PHP | not on the pages read |
| Data retention | No history row and no log line carrying the number. What a lookup leaves is keyed and never holds the number: the 7-day marker that makes a repeat free (a second one when the account paid for a live check); for single and browser lookups, a truncated keyed hash of the number filed under keyed hashes of its range and client address for 60 minutes, and the account's lookups per country and hour as counts for 30 days; and a live check's answer, held for a day under a hash of the number. The number itself is kept only in the account's own outcome reports, phone suppression list and monitors. Bulk input lists and result files are deleted with the job after 30 days. | The SMS pumping page says the check "Runs in real time on every OTP request and stores nothing after" |
## When Abstract Phone Validation fits better
Teams already on Abstract's other APIs that want phone validation, and on the Professional plan registration, risk and breach fields, under a monthly quota.
- Plans differ by the fields each response carries: messaging from Standard up, and registration and risk on Professional and the Free plan; the Professional example response also carries breach history
- The Professional response includes phone_registration.name, the name registered to the number, and phone_breaches, the domains where the number appeared in a breach
- Every request counts against the quota, including one for an invalid number
- SOC 2 Type II and GDPR statements; a commercial license is listed under the Enterprise plan
## When Spaw fits better
Developers who want the numbering plan and the regulators' own allocation tables read for them, the dataset behind every field named in the answer, and an SMS policy that turns the evidence into ok_to_send and blocked_by — a fraud and deliverability decision, not consent and not a do-not-call scrub. Every answer is made on the server: the opt-in live carrier-network check, the one call that would send a number to a partner, is not enabled on the service.
- Abstract's Professional and Free plans return the name registered to a number (phone_registration.name), and its Professional example response carries breach history; Spaw returns no owner, subscriber or caller name by design and does no reverse lookup
- Abstract counts every request against the quota, an invalid number included; Spaw bills 1 credit per valid number and answers an invalid number, and a repeat within 7 days, for free
- Abstract's disposable and abuse flags come with its Free and Professional plans; Spaw returns all 49 fields on every lookup, including is_disposable with, where the list gives one, the date the number was last seen on an SMS-receiving list, the risk score with its signals, and ok_to_send from your SMS policy, a fraud and deliverability decision rather than consent or a do-not-call scrub
- Abstract's pages do not name the databases behind its line type; on every valid answer Spaw's line_type_source says whether the numbering plan or, for a North American number, the block holder in the regulator's table settled it, and every answer's sources block names each dataset it consulted with its version or sync date
## Questions
**Does Spaw charge for numbers it cannot settle?** Spaw bills per answer. A number the numbering plan and, where one is installed, the regulator's own table agree is assignable costs 1 credit whatever its flags say. An invalid number is free — including one in a block the regulator lists as unassigned — and so is a repeat of the same number within seven days. The live carrier-network check (hlr: true) is not enabled on the service: hlr_checked answers false and nothing extra is charged. Abstract Phone Validation's pages say: Each individual phone number you submit counts as a credit used. Credits are also counted per request, not per successful response.
**Can I move a list of numbers from Abstract Phone Validation to Spaw?** Yes. Upload the CSV in the dashboard or POST it to the bulk endpoint, up to 100,000 numbers a job; Spaw keeps your columns, adds the answer columns, and lets you download the whole file or only the valid, invalid, low, medium or high-risk rows. A country column gives each row its own region, so a mixed international list needs no splitting.
**Is there a free tier?** Every Spaw account receives 100 free credits at the start of each month with no card on file, and unused free credits accumulate. Abstract Phone Validation offers: 100 requests a month with no card, on the full response including registration and risk.
## Sources (checked 2026-09-27)
- [Abstract Phone Validation API page and plan table](https://www.abstractapi.com/api/phone-validation-api)
- [Abstract API pricing](https://www.abstractapi.com/pricing)
- [Abstract Phone Intelligence API documentation](https://docs.abstractapi.com/api/phone-intelligence)
- [Abstract Phone Validation API documentation](https://docs.abstractapi.com/api/phone-validation)
- [Abstract SMS pumping page](https://www.abstractapi.com/solutions/sms-pumping)
- [Abstract HLR Lookup page](https://www.abstractapi.com/tools/hlr-lookup)
Abstract Phone Validation is a trademark of its owner. Spaw is not affiliated with it.
Reference: https://spaw.co/compare/abstract-phone
---
# Abstract Phone Validation alternatives for phone intelligence
Phone intelligence APIs to consider instead of Abstract Phone Validation, with the pricing model, free tier and price per 1,000 lookups each vendor publishes. Figures come from public pricing pages checked on 2026-09-27.
| Service | Pricing model | Free tier | Price per 1,000 at about 10k |
| --- | --- | --- | --- |
| [Spaw](https://spaw.co) | Pay-as-you-go credit packs whose credits never expire, or optional plans, billed monthly or yearly, that deposit the same credits every month for less and roll unused plan credits over up to three months' allowance; both through Stripe Checkout | 100 free credits every month, no card required | $7.80 (Growth pack: 5,000 credits for $39), or $5.80 on the Growth monthly plan (5,000 credits for $29 a month) |
| [ClearoutPhone](https://clearoutphone.io) | Credits that never expire, bought one time or through monthly or annual subscriptions that add them each period and carry them forward. One Time, each shown as a starting price: 5,000 credits from $35, 10,000 from $58, 100,000 from $400 and 1,000,000 from $1,200. Standard validation is 1 credit; Smart validation is 1 credit for most numbers and up to 5 when it runs a deeper real-time check | 100 free credits on sign-up, one validation each | $5.80 per 1,000 one time (10,000 credits for $58), $5.20 on the monthly plan (10,000 credits for $52 a month) or $3.20 on the annual plan as its tab rendered ("Buy 120,000 credits for $384 billed yearly", 10,000 a month), although the page labels annual billing "Save 20%" against one-time prices; a Smart validation that runs the deeper check costs up to 5 credits |
| [IPQualityScore Phone Validation](https://www.ipqualityscore.com/solutions/phone-validation) | Monthly plans shared with IPQualityScore's other checks: Free $0 (1,000 lookups a month), Startup "As low as: $99 / Month" (5,000), SMB Basic "As low as: $499 / Month" (10,000), SMB+ "As low as: $999 / Month" (75,000), Enterprise custom; "Each plan requires a minimum monthly commitment". The phone validation page says "Score phone numbers on average from $0.0005 per user with no monthly contracts", on "tiered flat rate pricing across all countries that reduces as your monthly volume increases", without a tier table. Add-ons are enabled through the account manager or Support at no stated price. | 1,000 lookups a month on the Free plan, capped at 35 a day; the plan also covers IP, email and URL checks | not on the pages read |
| [Loqate Phone Validation](https://www.loqate.com/en-gb/phone-validation/) | Pay-as-you-go credit purchases of £50, £100, £250, £500 and £1,000 (or 75€, 150€ and 300€), each buying a published per-lookup rate for every service, plus bespoke plans; "Credit is valid for 12 months after the purchase date". Phone Validation on the "New Pricing" table is 7.1p, 6.7p, 5.6p, 4.7p and 4.1p a lookup across the pound tiers and 8.6c, 7.8c and 6.6c across the euro ones. The page also shows an "Old Pricing" table without saying which accounts each applies to; the figures here are from "New Pricing". Its usage-example selector also lists a £2,500 plan that neither table prices, and all prices exclude VAT. | A trial rather than a standing tier: "Start your 45-day free trial"; the FAQ adds "You get free credits to test the service" | £47 per 1,000 (4.7p a lookup) on the £500 credit, which buys about 10,600 lookups; the smaller credits run from 7.1p (£50) to 5.6p (£250) |
| [numlookupapi](https://numlookupapi.com) | Monthly subscription tiers by request quota: Free $0 for 100 requests, Basic $9.99 for 5,000, Pro $49.99 for 50,000, Scale $109.99 for 250,000, and Custom by contact; the page offers yearly billing at 20% off. Scale and Custom list "Overages" with no overage price shown, and the quota documentation says a request over the monthly quota answers 429 until the plan is upgraded or the month ends. | 100 requests a month on one API key, at 10 requests a minute | not on the pages read |
| [numverify](https://numverify.com) | Monthly subscription tiers by request quota: Free 100 requests, Basic $14.99 for 5,000, Professional $59.99 for 50,000, Enterprise $129.99 for 250,000, each with a published overage price per call ($0.011992, $0.0047992 and $0.00207984 respectively); annual billing saves up to 15%, and custom pricing is offered above the Enterprise quota | 100 API requests a month, for non-commercial use | not on the pages read |
| [Telnyx Number Lookup](https://telnyx.com/products/number-lookup) | Per query, priced per lookup type, with "$0 platform fee"; pay as you go gets "Automatic tier discounts as usage crosses thresholds", committed plans start at $500 a month, and inbound CNAM is $0.40 per number per month | An outbound CNAM listing at no charge; the pages read list no free query allowance | $1.50 per 1,000 for a local routing number query ($0.0015 a query), $2.50 for MCC/MNC ($0.0025) and $3.00 for CNAM ($0.003) |
| [Twilio Lookup](https://www.twilio.com/en-us/user-authentication-identity/lookup) | Per request, priced per data package on a Twilio account ("Pricing current as of August 2026"); line status and reassigned number risk are tiered by query volume, and SIM swap and call forwarding are contact sales | Formatting and validation are listed as Free, and so is the SMS Pumping Risk Score for North American numbers; no free allowance is published for the other packages | $8.00 per 1,000 for line type intelligence ($0.008 per request), $10.00 for inbound caller identification ($0.01, US only), $100.00 for identity match ($0.1, which the page says varies by country) |
| [Veriphone](https://veriphone.io) | One-time credit packs that never expire, from 10,000 credits for $24 to 5,000,000 for $999, or monthly plans whose credits reset each month: Starter $12.99 for 10,000, Professional $39 for 50,000, Business $99 for 250,000 and Enterprise $249 for 1,000,000. A standard validation is 1 credit and a Current Carrier Lookup 10 | 1,000 free credits a month with no card, not carried over; on the free plan a Current Carrier Lookup draws on purchased credits only | $2.40 per 1,000 on the 10,000-credit pack ($24, credits never expire), or $1.30 on the Starter plan (10,000 credits a month for $12.99, reset each month). A Current Carrier Lookup takes 10 credits, so ten times those figures |
## Spaw
Verification API that bills per answered lookup across six signals on one key and one balance: Email verification, Phone intelligence, IP intelligence, Address verification, Business verification against open registers, and Cross-signal consistency, which runs any subset of the first four in one call and names where they agree and where they do not. The phone side reads the numbering plan and the regulators' own allocation tables to name the line type, the block holder and the blocks nobody holds, and flags the numbers that are not a person's handset.
## ClearoutPhone
ClearoutPhone is a phone validation service from Clearout sold as credits that never expire, with a 1-credit Standard mode and a Smart mode that runs a real-time telecom query, at up to 5 credits, where static data is uncertain.
Comparison: https://spaw.co/compare/clearoutphone
## IPQualityScore Phone Validation
IPQualityScore Phone Validation is the phone side of a fraud-scoring service: one API returns validity, carrier, line type and a 0 to 100 fraud score, with live-line, identity, do-not-call and SMS-pumping data as add-ons, on the same monthly plans as its IP, email and URL checks.
Comparison: https://spaw.co/compare/ipqualityscore-phone
## Loqate Phone Validation
Loqate Phone Validation checks a number's format, carrier and, for mobile numbers, its live network status through an HLR lookup, priced per lookup from the same pay-as-you-go credit balance as Loqate's address, email and bank validation.
Comparison: https://spaw.co/compare/loqate-phone
## numlookupapi
numlookupapi is a phone number validation API sold as a monthly request quota: one validate endpoint returns validity, formats, country, location, carrier and line type.
Comparison: https://spaw.co/compare/numlookupapi
## numverify
numverify is a phone number validation API sold as a monthly request quota, returning validity, country and location, carrier and line type.
Comparison: https://spaw.co/compare/numverify
## Telnyx Number Lookup
Telnyx Number Lookup is a per-query number API sold beside Telnyx's carrier services, priced separately for local routing number (portability and carrier), MCC/MNC, and CNAM caller name.
Comparison: https://spaw.co/compare/telnyx-lookup
## Twilio Lookup
Twilio Lookup is a phone number API sold as separately priced data packages on top of a Twilio account: formatting and validation are free, and line type, caller identity, line status and the risk packages are each priced on their own, per request (the SMS Pumping Risk Score is free for North American numbers) or through sales for SIM swap and call forwarding.
Comparison: https://spaw.co/compare/twilio-lookup
## Veriphone
Veriphone is a phone validation API sold as credit packs or monthly plans, returning validity, carrier, line type and region from national numbering plans, with a 10-credit Current Carrier Lookup read from national portability registries.
Comparison: https://spaw.co/compare/veriphone
Abstract Phone Validation and the other product names are trademarks of their owners. Spaw is not affiliated with them.
Reference: https://spaw.co/alternatives/abstract-phone
---
# Spaw vs AbuseIPDB: IP intelligence API comparison
How Spaw and AbuseIPDB compare on pricing, billing rules, the feeds behind each flag and limits, from AbuseIPDB's public pages checked on 2026-09-10. Corrections: support@spaw.co.
| | Spaw | AbuseIPDB |
| --- | --- | --- |
| Pricing model | Pay-as-you-go credit packs whose credits never expire, or optional plans, billed monthly or yearly, that deposit the same credits every month for less and roll unused plan credits over up to three months' allowance; both through Stripe Checkout. One credit buys five answered IP lookups | Plans with daily request allowances: Individual free "Forever!", Basic $25/mo or $228/year, Premium $99/mo or $1,068/year, Enterprise custom |
| Free tier | 100 free credits every month, no card required | Individual: free forever with no credit card, 1,000 check and report requests, 100 block checks and 5 bulk reports a day |
| Price per 1,000 at about 10k | $1.80 per 1,000 IP lookups at 5 to the credit (Starter pack: 1,000 credits for $9), or $1.40 on the Starter monthly plan (1,000 credits for $7 a month) | not on the pages read |
| Price per 1,000 at about 100k | $1.56 per 1,000 IP lookups at 5 to the credit (Growth pack: 5,000 credits for $39), or $1.16 on the Growth monthly plan (5,000 credits for $29 a month) | not on the pages read |
| Addresses no feed covered billed | One credit buys five answered addresses whatever their flags say; a flag no feed could evaluate answers null rather than false, and the answer costs the same | not on the pages read |
| Reserved and private ranges billed | No: reserved, private, loopback, CGNAT, documentation and multicast ranges answer reason reserved_range and are free, unallocated space is free, and so are repeats within 7 days | not on the pages read |
| Feeds behind the flags | Public feeds, each named in the answer with the date it was synced: DB-IP Lite and RFC 8805 geofeeds for location, the five registries' delegated statistics for allocation, Spamhaus DROP and ASN-DROP and abuse.ch Feodo Tracker for blocklists, the Tor Project's exit list and Onionoo, the Mullvad, NordVPN and Private Internet Access server lists plus the open X4BNet lists, Apple's iCloud Private Relay egress list, the cloud providers' own published ranges, and the verification files Google, Microsoft, OpenAI, Perplexity, DuckDuckGo and Ahrefs publish for their crawlers. | One feed, its own: the abuse confidence score is computed from reports submitted by AbuseIPDB users, and totalReports is "a sum of the reports within maxAgeInDays". A check also returns countryCode, usageType, isp, domain, isTor and isWhitelisted, which "reflects whether the IP is spotted in any of our whitelists". |
| Refresh cadence | Every feed re-syncs daily, the botnet command-and-control list hourly, and the geofeed crawl weekly. Each response names every dataset it consulted with the date it was last synced, and /status lists them all. | Not published as a schedule; reports arrive continuously and the caller chooses the window with maxAgeInDays, which "determines how far back in time we go to fetch reports" (1 to 365 days, default 30). |
| Geolocation source | The DB-IP Lite database, overridden wherever the operator publishes the location itself — an RFC 8805 geofeed, Apple's relay list, a VPN provider's server list. location_source says which answered, and location_confidence is graded from a weekly accuracy report rather than asserted. | Country only: a check returns countryCode alongside the ISP and usage type. No city, coordinates or ASN answer is documented. |
| Bulk limit | 100,000 addresses per bulk job, with an HMAC-signed completion webhook | not on the pages read |
| Batch API | 1,000 addresses per synchronous request; repeats inside the list are looked up once | Bulk reporting rather than bulk checking: 5 bulk reports a day on the free plan, 100 on Basic and 500 on Premium |
| Rate limit | 50 requests per second per key on the IP endpoints | Daily rather than per second: the check endpoint documents 1,000 requests a day (Standard), 3,000 (Webmaster), 5,000 (Supporter), 10,000 (Basic) and 50,000 (Premium) |
| Official SDKs | Python, Node.js, PHP | not on the pages read |
| Data retention | No history row and no log line carrying the address. What a lookup leaves is keyed and never holds the address: the 7-day marker that makes a repeat free and, under the same keyed hash, the count behind seen_by_you; privacy: true skips both. The account's own outcome reports are kept as salted hashes for 90 days. Bulk input lists and result files are deleted with the job after 30 days. | not on the pages read |
## When AbuseIPDB fits better
Teams that want to know whether other operators have complained about an address recently, and to contribute their own reports back.
- The score is a community judgement: it counts what other operators reported, over a window you choose
- maxAgeInDays lets a caller decide how stale a report may be, from 1 to 365 days
- A free plan generous enough to run a small production service on
## When Spaw fits better
Developers who want every public IP feed compiled, dated and named for them, with a risk score whose weights are published so it can be recomputed or ignored.
- AbuseIPDB scores an address from user reports; Spaw's is_blocklisted comes from published netblock lists (Spamhaus DROP and ASN-DROP, abuse.ch Feodo Tracker) and names which list matched
- Spaw answers datacenter, Tor, VPN, relay, crawler, anycast and unallocated flags with geolocation in the same lookup; an AbuseIPDB check answers reputation, country, ISP and Tor
- AbuseIPDB counts requests per day; Spaw bills per answered address and charges nothing for reserved, private and documentation ranges
- Spaw takes your own abuse reports too, and they feed the reported_abuse weights in its risk score for your account
## Questions
**Does Spaw charge for addresses its feeds do not cover?** Spaw bills per answered address, five to the credit: the answered lookup that opens a block costs 1 credit and the next four cost nothing. Reserved, private, loopback, CGNAT, documentation and multicast ranges answer reason reserved_range and are free, unallocated space is free, and so is a repeat of the same address within seven days. A flag no feed could evaluate answers null rather than false: the address is still billed, and the null tells you the signal was not checked instead of pretending it was. AbuseIPDB does not publish how it bills them.
**Can I run a log file or an export through Spaw?** Yes. A bulk run takes up to 100,000 addresses from the dashboard or the API and returns one row per input address, in input order, with every flag, the risk score and the signals that made it. Five answered addresses cost a credit, repeats inside the list are looked up once, reserved ranges cost nothing, and the finish is announced on an HMAC-signed webhook.
**Is there a free tier?** Every Spaw account receives 100 free credits at the start of each month with no card on file, and unused free credits accumulate. AbuseIPDB offers: Individual: free forever with no credit card, 1,000 check and report requests, 100 block checks and 5 bulk reports a day.
## Sources (checked 2026-09-10)
- [AbuseIPDB pricing](https://www.abuseipdb.com/pricing)
- [AbuseIPDB API documentation](https://docs.abuseipdb.com/)
AbuseIPDB is a trademark of its owner. Spaw is not affiliated with it.
Reference: https://spaw.co/compare/abuseipdb
---
# AbuseIPDB alternatives for IP intelligence
IP intelligence APIs to consider instead of AbuseIPDB, with the pricing model, free tier and price per 1,000 lookups each vendor publishes. Figures come from public pricing pages checked on 2026-09-10.
| Service | Pricing model | Free tier | Price per 1,000 at about 10k |
| --- | --- | --- | --- |
| [Spaw](https://spaw.co) | Pay-as-you-go credit packs whose credits never expire, or optional plans, billed monthly or yearly, that deposit the same credits every month for less and roll unused plan credits over up to three months' allowance; both through Stripe Checkout. One credit buys five answered IP lookups | 100 free credits every month, no card required | $1.80 per 1,000 IP lookups at 5 to the credit (Starter pack: 1,000 credits for $9), or $1.40 on the Starter monthly plan (1,000 credits for $7 a month) |
| [ip-api](https://ip-api.com) | Two tiers. The free endpoint: "Our endpoints are limited to 45 HTTP requests per minute from an IP address" and "We do not allow commercial use of the free endpoint". The Pro service: "€13.3 per month, when paying for 12 months", with "Unlimited requests", "SSL (HTTPS)", "Commercial use allowed" and a "99.9% SLA" ("If the general API uptime falls below 99.9% in a month, we will automatically extend your subscription for 10 days"). | The free endpoint, for non-commercial use only: 45 requests per minute per address over HTTP ("256-bit SSL encryption is not available for this free API") | not on the pages read |
| [IP2Location](https://www.ip2location.com) | IP2Location.io API, monthly query plans: Free $0 ("50K IP Geolocation API queries/month", "11 IP Attributes", "Attribution Required"), Starter "$49/month" ("150K IP Geolocation API queries/month", "20 IP Attributes"), Plus "$249/month" ("300K IP Geolocation API queries/month", "59 IP Attributes"), Security "$499/month" ("600K IP Geolocation API queries/month", "79 IP Attributes"); overage per increment Starter $10, Plus $50, Security $100. Databases, standard licence per year: DB1 "$99", DB2 "$399", DB8 "$1,299", DB18 "$2,799", with a site licence at ten times those figures. | "50K IP Geolocation API queries/month" with 11 attributes, basic geolocation and open-proxy detection only, "Attribution Required"; without a key "up to 1,000 queries per day" | not on the pages read |
| [ipapi](https://ipapi.co) | Monthly plans by lookup quota: Starter "$15 billed monthly" for 60,000 lookups, Professional "$25 billed monthly" for 150,000, Business "$49 billed monthly" for 500,000, Enterprise "$99 billed monthly" for 2,000,000, Premium "$199 billed monthly" for 6,000,000, Ultimate "$399 billed monthly" for 15,000,000; annual billing is 20% less. | A free trial of "up-to 1000 IP lookups in a day" (about 30,000 a month), "for testing & development purposes" and "Not for production use" | not on the pages read |
| [ipdata](https://ipdata.co) | Monthly plans by daily request quota: Personal "$10 per month" with "2,500 requests daily", Lite "$30 per month" with "10,000 requests daily", Startup "$50 per month" with "50,000 requests daily", Business "$120 per month" with "100,000 requests daily"; "20% off all annual plans" at the time of reading. | "1500 free requests daily", "restricted to non-commercial use"; on the free tier, over the quota "all further requests are blocked and you receive an email notification" | not on the pages read |
| [ipgeolocation.io](https://ipgeolocation.io) | Monthly credit plans, "A standard unified IP lookup uses 1 credit": Starter "$19/month" for "150000 API Credits Per month", Plus "$49/month" for 500,000, Pro "$79/month" for 1,000,000, Business "$129/month" for 2,000,000, Premium "$249/month" for 5,000,000, Enterprise "Contact Sales"; annual billing gives "2 months free". "Paid monthly plans reset every month. Paid annual plans reset every year". | "1000 API Credits Per day for life" with no credit card, "a hard limit of 1000 requests per day" and "No overages on Free"; the free plan returns "core location without the radius and confidence fields" | not on the pages read |
| [IPinfo](https://ipinfo.io) | Monthly plans by API request quota: Lite $0/mo, Core $26/mo for 150,000 requests, Max $163/mo for 250,000, Enterprise custom for "1M+". Database downloads are listed separately, "Starting at $1,000/month". | IPinfo Lite at "$0/mo", free forever with no credit card, giving unlimited API requests at country level only | not on the pages read |
| [IPQualityScore](https://www.ipqualityscore.com) | Monthly plans by lookup quota: Free $0 (1,000 lookups a month), Startup "As low as: $99 / Month" (5,000), SMB Basic "$499" (10,000), SMB+ "$999" (75,000), Enterprise custom. "Any valid request sent to our Proxy Detection, Email Verification, or Device Fingerprinting services including lookups performed through a batch CSV file check will consume a lookup credit." | 1,000 lookups a month, capped at 35 a day | not on the pages read |
| [Ipregistry](https://ipregistry.co) | Credits, "1 lookup = 1 request = 1 credit" (batch endpoints use "one credit per lookup in the batch request, plus 1"). Subscriptions: Starter "100,000 credits / mo" at "$29 $24 / month", Professional "300,000 credits / mo" at "$79 $66 / month", Business "1,150,000 credits / mo" at "$199 $166 / month", Scale "2,450,000 credits / mo" at "$399 $333 / month" (the lower figure is the annual rate). Pay-as-you-go packs: 15,000 credits for $10, 100,000 for $50, 250,000 for $100, 1,750,000 for $500. | "20,000 free credits on sign-up" with "No credit card, no time limit" | $0.67 (the $10 pack of 15,000 credits, $0.000667 a lookup) |
| [ipstack](https://ipstack.com) | Monthly plans by request quota: Free "100 Request/mo." at $0, Basic "50,000 Request/mo." at "$12.99/mo.", Professional "500,000 Request/mo." at "$59.99/mo.", Professional + "2,000,000 Request/mo." at "$99.99/mo.", Custom on request; yearly billing "Save up to 15%". Overage fees apply past the quota, with notifications at 75%, 90% and 100%. | "100 Request/mo." at $0, without SSL encryption, support or the paid modules (SSL, commercial use and the Location, Time Zone and Connection modules are listed for paid plans) | not on the pages read |
| [MaxMind](https://www.maxmind.com) | Per query, by service, with no minimum published: "If you estimate your volume of queries to be over $2,500 a month, contact us to discuss rates." | A no-cost trial is offered ("Experience the functionality and benefits of our GeoIP web services at no cost"); no free monthly allowance is stated on the page checked | $0.10 per 1,000 for Country ($0.0001 a query), $0.30 for City Plus ($0.0003) and $2.00 for Insights ($0.002) |
| [proxycheck.io](https://proxycheck.io) | not on the pages read | not on the pages read | not on the pages read |
| [Spur](https://spur.us) | not on the pages read | not on the pages read | not on the pages read |
## Spaw
Verification API that bills per answered lookup across six signals on one key and one balance: Email verification, Phone intelligence, IP intelligence, Address verification, Business verification against open registers, and Cross-signal consistency, which runs any subset of the first four in one call and names where they agree and where they do not. The IP side answers from local databases and compiled public feeds: where an address is, who runs it, and whether it is a visitor at all.
## ip-api
ip-api is a geolocation JSON API with a free, non-commercial, HTTP-only endpoint limited by request rate, and a Pro subscription that adds HTTPS, unlimited queries and commercial use; its answer carries one proxy flag, one hosting flag and one mobile flag beside the location and network fields.
Comparison: https://spaw.co/compare/ip-api
## IP2Location
IP2Location sells geolocation as downloadable databases (DB1 to DB26, licensed yearly) and, through IP2Location.io, as an API on monthly query plans whose higher tiers add proxy, VPN, Tor, datacenter, crawler and residential-proxy detection and a fraud score; the classic web service no longer takes new registrations.
Comparison: https://spaw.co/compare/ip2location
## ipapi
ipapi.co is a geolocation API sold as monthly lookup quotas, whose pricing page credits its data to DB-IP and the IP2Location LITE database; the answer carries location, country and network fields and no proxy, VPN or Tor flag.
Comparison: https://spaw.co/compare/ipapi
## ipdata
ipdata is a geolocation and threat API sold as daily request quotas, from a free non-commercial tier to a Business plan, whose pricing page says it does not keep records of the addresses looked up beyond a day; its documentation site refused automated reading, so the fields are not reproduced here.
Comparison: https://spaw.co/compare/ipdata
## ipgeolocation.io
ipgeolocation.io sells geolocation and a separate security API on monthly credit plans, with location built from its own measurements and a security answer that flags VPN, proxy, residential proxy and Tor with provider names, confidence scores and a 0 to 100 threat score.
Comparison: https://spaw.co/compare/ipgeolocation
## IPinfo
IPinfo is an IP data company that sells geolocation, ASN, privacy detection and company data as monthly API plans and as database downloads, collected through its own measurement network rather than assembled from third-party feeds.
Comparison: https://spaw.co/compare/ipinfo
## IPQualityScore
IPQualityScore sells fraud detection across several signals on one lookup credit: proxy and VPN detection, email verification and device fingerprinting all draw from the same monthly quota, and the IP answer carries a fraud score alongside the connection facts.
Comparison: https://spaw.co/compare/ipqualityscore
## Ipregistry
Ipregistry is a geolocation and threat API sold as credits, either monthly subscriptions or pay-as-you-go packs, whose security object answers abuser, attacker, bogon, cloud provider, proxy, relay, Tor, VPN, anonymous and threat flags and whose data is described as built from BGP tables, registry databases, geofeeds and latency measurements.
Comparison: https://spaw.co/compare/ipregistry
## ipstack
ipstack is a geolocation API on monthly request quotas from a 100-request free tier to a two-million-request plan, with a bulk endpoint and a security module reserved for its top plan; its documentation pages rendered only a header to our fetcher, so the field list is not reproduced here.
Comparison: https://spaw.co/compare/ipstack
## MaxMind
MaxMind sells GeoIP as three per-query web services — Country, City Plus and Insights — and as downloadable databases, with proxy and anonymiser detection reserved for the Insights tier.
Comparison: https://spaw.co/compare/maxmind
## proxycheck.io
proxycheck.io sells an API for checking whether an address is a proxy; every page we tried on 2026-09-12 answered 403 Forbidden to automated reading, so nothing about its pricing, limits, fields or data is reproduced here.
Comparison: https://spaw.co/compare/proxycheck
## Spur
Spur is an anonymity-infrastructure vendor: its Context API answers whether an address is a VPN, proxy or other tunnel with the operator named, and its feeds list anonymous, residential-proxy and datacenter address space; its pricing page rate-limited our fetcher, so prices are not reproduced here.
Comparison: https://spaw.co/compare/spur
AbuseIPDB and the other product names are trademarks of their owners. Spaw is not affiliated with them.
Reference: https://spaw.co/alternatives/abuseipdb
---
# Spaw vs Bouncer: email verification API comparison
How Spaw and Bouncer compare on pricing, billing rules, verification depth and limits, from Bouncer's public pages checked on 2026-09-13. Corrections: support@spaw.co.
| | Spaw | Bouncer |
| --- | --- | --- |
| Pricing model | Pay-as-you-go credit packs whose credits never expire, or optional plans, billed monthly or yearly, that deposit the same credits every month for less and roll unused plan credits over up to three months' allowance; both through Stripe Checkout | Pay-as-you-go credits that never expire (from 1,000 credits for $8 up to 1,000,000 for $2,000), auto-refill, plus a Deliverability Kit subscription at $25 to $250 a month |
| Free tier | 100 free credits every month, no card required | 100 free credits, no card required |
| Price per 1,000 at about 10k | $7.80 (Growth pack: 5,000 credits for $39), or $5.80 on the Growth monthly plan (5,000 credits for $29 a month) | $6.00 (10,000 credits for $60) |
| Price per 1,000 at about 100k | $4.49 (Volume pack: 100,000 credits for $449), or $3.49 on the Volume monthly plan (100,000 credits for $349 a month) | $4.00 (100,000 credits for $400) |
| Unknown or catch-all results billed | No: an unconfirmed mailbox (reason unverified) and an answer Spaw could not obtain because its handshake partner was unavailable are free and leave no 7-day repeat marker, so a later lookup is a fresh one; a catch-all answer costs 1 credit like any risky answer, and an automatic re-check bills 1 credit only when it confirms the mailbox | No |
| Undeliverable results billed | No, within a fair use: undeliverable answers that needed a mailbox probe are free up to max(500, your billed email answers) in a rolling 30 days, then 1 credit each; the rest are always free | Yes |
| SMTP mailbox check | Yes | not on the pages read |
| Catch-all handling | A catch-all answer is remembered per domain for 7 days, and every unconfirmed mailbox carries a published 0 to 100 mailbox_confidence estimate | Returns an acceptAll domain property; unknown results and duplicates within a list are never charged |
| Bulk limit | 100,000 addresses per bulk job, with an HMAC-signed completion webhook | Up to 100,000 emails per batch (1,000 to 10,000 recommended); up to 60 batches a minute and 200 requests a minute to the other batch endpoints |
| Batch API | 50 addresses per synchronous request, probes sent concurrently | Synchronous batch of up to 10,000 emails at 100 requests a minute; asynchronous batches as above |
| Rate limit | 5 requests per second per key on the email and consistency endpoints | Real-time API: 1,000 requests a minute by default |
| Official SDKs | Python, Node.js, PHP | Java |
| Data retention | Only email verification keeps a history row, for 30 days, then pruned; test addresses are never logged | Results are kept for 24 hours so an address is not verified twice in that time; after that it is verified and charged again. A broader retention policy is not stated on the pages checked |
## When Bouncer fits better
Teams that want the lowest published per-check price with high batch throughput and a real-time form product.
- Bouncer Shield protects forms in real time
- Toxicity list checks
- Auto-refill and volume discounts
## When Spaw fits better
Developers who want one flat price per answered lookup across every signal a signup form produces, a transparent risk score, and an API that documents every field.
- Bouncer's published rates are $6.00 per 1,000 at 10,000 credits and $4.00 at 100,000; Spaw's packs are $7.80 and $4.49, and its monthly plans $5.80 and $3.49
- Both leave unknown results unbilled; Spaw also leaves undeliverable verdicts free within a fair-use allowance
- Spaw's rate limit is 5 requests a second, 300 a minute; Bouncer's real-time API allows 1,000 a minute
- Spaw ships Python, Node.js and PHP clients plus an MCP server; Bouncer ships a Java client
## Questions
**Does Spaw charge for catch-all or unknown results?** Spaw bills per answer, not per probe outcome. A catch-all domain comes back risky with reason catch_all and costs 1 credit like any risky answer. A mailbox the handshake could not confirm (greylisting, a timeout, no definite answer) comes back risky with reason unverified and is free, and so is an answer Spaw could not obtain at all because its handshake partner was unavailable; neither leaves a 7-day repeat marker, so a later lookup is a fresh one (an uncertain result is reused for up to 15 minutes, then the mailbox is asked again) and bills 1 credit only once the mailbox is confirmed, and the automatic re-checks behind callback_url bill 1 credit only when they confirm it. Undeliverable verdicts are free within a fair use of max(500, your billed email answers) in a rolling 30 days, and invalid input and repeats within 7 days are free too. Bouncer states that it does not bill unknown results.
**Can I move a list from Bouncer to Spaw?** Yes. Upload the CSV to the dashboard or POST it to the bulk endpoint; Spaw keeps your columns, adds the verdict columns, and lets you download only the deliverable, risky or undeliverable rows. Addresses that come back undeliverable join your suppression list so later runs skip them for free.
**Is there a free tier?** Every Spaw account receives 100 free credits at the start of each month with no card on file, and unused free credits accumulate. Bouncer offers: 100 free credits, no card required.
## Sources (checked 2026-09-13)
- [Bouncer pricing](https://www.usebouncer.com/pricing/)
- [Bouncer batch API](https://docs.usebouncer.com/api-reference/batch/batch-create)
- [Bouncer batch sync API](https://docs.usebouncer.com/api-reference/batch-sync/batch-sync)
- [Bouncer real-time API](https://docs.usebouncer.com/api-reference/real-time/verify-email)
- [Bouncer integration guidelines](https://docs.usebouncer.com/integration-guidelines)
Bouncer is a trademark of its owner. Spaw is not affiliated with it.
Reference: https://spaw.co/compare/bouncer
---
# Bouncer alternatives for email verification
Email verification APIs to consider instead of Bouncer, with the pricing model, free tier and price per 1,000 lookups each vendor publishes. Figures come from public pricing pages checked on 2026-09-13.
| Service | Pricing model | Free tier | Price per 1,000 at about 10k |
| --- | --- | --- | --- |
| [Spaw](https://spaw.co) | Pay-as-you-go credit packs whose credits never expire, or optional plans, billed monthly or yearly, that deposit the same credits every month for less and roll unused plan credits over up to three months' allowance; both through Stripe Checkout | 100 free credits every month, no card required | $7.80 (Growth pack: 5,000 credits for $39), or $5.80 on the Growth monthly plan (5,000 credits for $29 a month) |
| [Abstract API](https://www.abstractapi.com) | Monthly subscription tiers with a request slider from 5,000 to 150,000 requests a month; Starter from $17, Standard from $37, Professional from $39 a month | 100 requests on the Free plan | not on the pages read |
| [Emailable](https://emailable.com) | Pay-as-you-go credits that never expire, or a monthly subscription at a 15% discount; minimum purchase 5,000 credits | 250 free credits at signup | $7.60 (the pay-as-you-go rate printed for the 5,000-credit package, $0.0076 a credit; larger packages are priced in a calculator, and the 5,000-credit subscription is $32.30 a month after the 15% discount) |
| [Hunter](https://hunter.io) | Monthly or yearly subscriptions with unified credits shared across search, verification and enrichment; credit packs on paid plans, usable for up to 12 months from purchase according to the pricing page | Free plan with 50 credits a month; one verification costs 0.5 credit, so 100 verifications | $7.45 (Growth: $149 a month for 10,000 credits, 20,000 verifications at 0.5 credit each; $5.20 billed yearly) |
| [Kickbox](https://kickbox.com) | not on the pages read | not on the pages read | not on the pages read |
| [Mailboxlayer](https://mailboxlayer.com) | Monthly or yearly subscription (10 to 15% off yearly) with a request quota per plan and per-request overage | 100 requests a month | $3.00 for the Basic quota (5,000 requests for $14.99 a month; overage $0.012 a request, so 10,000 requests on Basic cost about $74.95, $7.50 per 1,000); Professional Plus: 50,000 for $74.99, $1.50 per 1,000 |
| [NeverBounce](https://www.neverbounce.com) | not on the pages read | not on the pages read | not on the pages read |
| [Verifalia](https://verifalia.com) | Pay-as-you-go credit packs that never expire, or monthly, quarterly and yearly subscriptions; verification depth is priced as quality levels; pack and plan prices are shown in a calculator rather than listed | 25 free credits a day on the Free plan; unused daily credits expire each day | not on the pages read |
| [ZeroBounce](https://www.zerobounce.net) | Pay-as-you-go credits from $39 for 2,000 addresses, 15% off with autopay, or the ZeroBounce ONE subscription at $99 a month with unused credits rolling over; volume pricing shown through a calculator rather than fixed packs | 100 validation credits a month on the free account | not on the pages read |
## Spaw
Verification API that bills per answered lookup across six signals on one key and one balance: Email verification, Phone intelligence, IP intelligence, Address verification, Business verification against open registers, and Cross-signal consistency, which runs any subset of the first four in one call and names where they agree and where they do not. The email side checks syntax, live MX, SPF and DMARC, disposable and role lists, and runs an SMTP mailbox handshake that never sends a message.
## Abstract API
Abstract API's email validation endpoint is one of a family of utility APIs sold as monthly request quotas with a free 100-request plan.
Comparison: https://spaw.co/compare/abstract
## Emailable
Emailable is an email verification service with pay-as-you-go credits, a bulk verifier, a widget and an API, with Node.js, Ruby and Python libraries.
Comparison: https://spaw.co/compare/emailable
## Hunter
Hunter is an outreach platform whose Email Verifier shares unified plan credits with its email finder, enrichment and campaign tools.
Comparison: https://spaw.co/compare/hunter
## Kickbox
Kickbox is an email verification API that returns deliverable, undeliverable, risky or unknown plus a 0 to 1 Sendex quality score, with official libraries for PHP, Node.js, Ruby and Python.
Comparison: https://spaw.co/compare/kickbox
## Mailboxlayer
Mailboxlayer is apilayer's email validation API, sold as monthly request quotas from a free 100-request plan up to 250,000 requests a month.
Comparison: https://spaw.co/compare/mailboxlayer
## NeverBounce
NeverBounce is a ZoomInfo-owned email verification service with a bulk list-cleaning workflow, a real-time API and official client libraries in six languages.
Comparison: https://spaw.co/compare/neverbounce
## Verifalia
Verifalia is an email verification service with selectable quality levels, credit packs that never expire, subscriptions with daily free credits, and official SDKs in six languages.
Comparison: https://spaw.co/compare/verifalia
## ZeroBounce
ZeroBounce is an email validation service with a pay-as-you-go credit model and a subscription (ZeroBounce ONE) that bundles deliverability tools such as activity data, inbox placement tests and blacklist monitoring.
Comparison: https://spaw.co/compare/zerobounce
Bouncer and the other product names are trademarks of their owners. Spaw is not affiliated with them.
Reference: https://spaw.co/alternatives/bouncer
---
# Spaw vs ClearoutPhone: phone intelligence API comparison
How Spaw and ClearoutPhone compare on pricing, billing rules, where the line type comes from and limits, from ClearoutPhone's public pages checked on 2026-09-27. Corrections: support@spaw.co.
| | Spaw | ClearoutPhone |
| --- | --- | --- |
| Pricing model | Pay-as-you-go credit packs whose credits never expire, or optional plans, billed monthly or yearly, that deposit the same credits every month for less and roll unused plan credits over up to three months' allowance; both through Stripe Checkout | Credits that never expire, bought one time or through monthly or annual subscriptions that add them each period and carry them forward. One Time, each shown as a starting price: 5,000 credits from $35, 10,000 from $58, 100,000 from $400 and 1,000,000 from $1,200. Standard validation is 1 credit; Smart validation is 1 credit for most numbers and up to 5 when it runs a deeper real-time check |
| Free tier | 100 free credits every month, no card required | 100 free credits on sign-up, one validation each |
| Price per 1,000 at about 10k | $7.80 (Growth pack: 5,000 credits for $39), or $5.80 on the Growth monthly plan (5,000 credits for $29 a month) | $5.80 per 1,000 one time (10,000 credits for $58), $5.20 on the monthly plan (10,000 credits for $52 a month) or $3.20 on the annual plan as its tab rendered ("Buy 120,000 credits for $384 billed yearly", 10,000 a month), although the page labels annual billing "Save 20%" against one-time prices; a Smart validation that runs the deeper check costs up to 5 credits |
| Price per 1,000 at about 100k | $4.49 (Volume pack: 100,000 credits for $449), or $3.49 on the Volume monthly plan (100,000 credits for $349 a month) | $4.00 per 1,000 one time (100,000 credits for $400) or $3.60 on the monthly plan ($360 a month). The annual tab rendered on 2026-09-27 showed "Buy 1,200,000 credits for $1,152 billed yearly" (100,000 a month, $0.96 per 1,000), while the same page labels annual billing "Save 20%" against one-time prices, its monthly tab says "Get 20% discount for the price when you pay annually" and its FAQ says "Discounts upto 40%" |
| Numbers the vendor could not settle billed | A valid number costs 1 credit whatever its flags say. The live carrier-network check (hlr: true) is not enabled on the service: hlr_checked answers false and nothing extra is charged. | No credits are charged for numbers with "unknown" status or for duplicates in a list. Smart validation charges up to 5 credits only where it runs the deeper check, and within its caching window "repeated validations of the same number won't trigger additional deep validation charges" |
| Invalid numbers billed | No: invalid numbers are free, including a number in a block the regulator lists as unassigned, and so are repeats within 7 days | Not stated separately: the pages say "1 credit charged per phone number validation" and name unknown and duplicate numbers as the ones not charged |
| Line type source | The numbering plan metadata everywhere except the North American plan, where mobiles and landlines share blocks and the plan can only say fixed_line_or_mobile; there the regulator's block holder settles it — a wireless carrier's block is mobile, a virtual-number wholesaler's is voip. line_type_source names which of the two answered. | Validation "is carried out using multiple methods which include validation from International Telecom Authority, Multiple Carrier lines, and third party validation". Standard validation "Uses basic validation based on original number data"; Smart validation adds real-time telecom queries for ported and VoIP numbers. |
| Numbering plan coverage | libphonenumber metadata for every region it carries, plus the regulators' own allocation tables, each named in the answer with the date of the copy it read. Complete tables, where a geographic number in a block the table leaves out answers unassigned_block: NANPA for the United States (with Puerto Rico, the U.S. Virgin Islands, Guam, American Samoa and the Northern Mariana Islands), the CNAC for Canada, ARCEP for France (with Guadeloupe, St. Barthélemy, St. Martin, Martinique, French Guiana, Réunion, Mayotte and St. Pierre & Miquelon), ACM for the Netherlands and the CRT (formerly the IFT) for Mexico. Partial tables, answered only from the ranges they list (a range listed as free, spare or held in reserve answers unassigned_block), so block_status answers null for a number outside them: Ofcom for the United Kingdom (with Guernsey, Jersey and the Isle of Man), ACMA for Australia, the NAD for New Zealand, UKE for Poland and MIMIT for Italy. Re-synced weekly, NANPA daily; /status lists each table's age. Outside those regions, block_status and is_virtual answer null rather than a guess. | "248 countries and territories" in the API documentation ("248+" elsewhere); no numbering plan or regulator table is named |
| Live network check | Reserved behind hlr: true: 4 credits on top of the base credit, only where the network answers, reported in its own reachable, ported, current_carrier and roaming fields. The live carrier-network check (hlr: true) is not enabled on the service: hlr_checked answers false and nothing extra is charged. | The API documentation says the API "queries live telecom network infrastructure to determine the live, active connection status of a number", and Smart validation runs "a deep, real-time telecom query to verify carrier, linetype, and live reachability" at up to 5 credits. The FAQ says it is "impossible to determine whether they are active or not until a message is sent or a call is made". |
| Ported numbers | None from the tables, and the docs say so: carrier and line_type describe the block the number was allocated in, never a later port. Only a live carrier-network check can say who serves a number today. | The FAQ says line types follow "their original carrier allocation" and a ported number "may continue to show the original type"; Smart validation "Detects recently ported lines and VoIP numbers that standard checks misclassify", at up to 5 credits. |
| SMS pumping controls | Flags in the risk score, and an SMS policy that turns them into ok_to_send and blocked_by: a fraud and deliverability decision, not consent and not a do-not-call scrub. allowed_countries limits sending to the regions you serve; it is empty (any country) until you set it. range_burst (+40) flags a number once your account has looked up 5 distinct numbers in its 1,000-number range within 60 minutes, and numbers_per_client (+30) once one client address (the client_ip you send, or a browser visitor's own) has tried 5 within 60 minutes, unless Spaw's own IP data places it on a mobile carrier or a corporate proxy, or it is private or reserved. Both count only your own single and browser lookups, so a new account has no history. The range is a heuristic, and in North America a burst more likely means someone testing numbers against a form; the client count does not stop an attack rotated through residential proxies. disposable (+70) flags a number SMS-receiving websites publish and disposable_stale (+40) an old listing; disposable_range (+30) flags an unlisted number in a 1,000-number range holding 10 or more fresh listings. The lists are public and synced daily. The default policy blocks the fictional, disposable, reported_abuse, reported_abuse_widely and premium_rate signals and a risk_score above 60, so neither traffic signal blocks on its own. | not on the pages read |
| Bulk limit | 100,000 numbers per bulk job, with an HMAC-signed completion webhook | CSV or XLSX upload through the app or POST /v1/phonenumber/bulk, with up to 3 lists queued or running at a time per user; no row limit is given on the pages read |
| Batch API | 1,000 numbers per synchronous request, answered in input order | not on the pages read |
| Rate limit | 50 requests per second per key on the phone endpoints | "No explicit rate limit" and "virtually unlimited requests" under a fair usage policy, with x-ratelimit-* headers reporting the quota in each 60-second window; the error list includes a 429 "You have reached API rate limit" and a daily verify limit (code 1017) |
| Official SDKs | Python, Node.js, PHP | not on the pages read |
| Data retention | No history row and no log line carrying the number. What a lookup leaves is keyed and never holds the number: the 7-day marker that makes a repeat free (a second one when the account paid for a live check); for single and browser lookups, a truncated keyed hash of the number filed under keyed hashes of its range and client address for 60 minutes, and the account's lookups per country and hour as counts for 30 days; and a live check's answer, held for a day under a hash of the number. The number itself is kept only in the account's own outcome reports, phone suppression list and monitors. Bulk input lists and result files are deleted with the job after 30 days. | Lists are kept 30 days by default, with custom periods on request, and the FAQ says "the data is deleted after 30days from validation" |
## When ClearoutPhone fits better
Sales and marketing teams cleaning CSV or XLSX contact lists before calling or texting, with a real-time check they can switch on for numbers static data cannot settle.
- Smart validation costs 1 credit for most numbers and up to 5 where it runs a real-time telecom query, and a caching window keeps a repeat from being charged the deeper check again
- Credits bought on any plan never expire and carry forward
- The numbers on its supported-countries page validate without spending credits, for testing
- The API is server-side only: requests straight from a browser are not supported
- An OTP article on its blog (30 June 2026) calls Smart validation "the recommended mode for OTP use cases and fraud prevention" and advises skipping the send when a number returns as landline, VoIP or invalid
## When Spaw fits better
Developers who want the numbering plan and the regulators' own allocation tables read for them, the dataset behind every field named in the answer, and an SMS policy that turns the evidence into ok_to_send and blocked_by — a fraud and deliverability decision, not consent and not a do-not-call scrub. Every answer is made on the server: the opt-in live carrier-network check, the one call that would send a number to a partner, is not enabled on the service.
- ClearoutPhone's Smart validation spends up to 5 credits on a real-time telecom query for uncertain numbers; Spaw answers from local tables for 1 credit, and its carrier and line_type describe the original allocation, never a later port
- ClearoutPhone charges a credit per validation, with unknown and duplicate numbers free; Spaw charges only for a valid number, so an invalid one is free, including one in a block the regulator lists as unassigned, and so are a repeated row in a bulk list and a repeat within 7 days
- ClearoutPhone's documented response carries status, line type, carrier, location and formats, with no risk or disposable field (an article on its blog says Smart validation "Identifies VoIP, mobile, landline, or disposable numbers"); Spaw returns 49 fields, including is_disposable with, where the list gives one, the date the number was last seen on an SMS-receiving list, a risk score with the signals behind it, and ok_to_send from your SMS policy, a fraud and deliverability decision rather than consent or a do-not-call scrub
- ClearoutPhone takes lists as CSV or XLSX files, at most 3 queued or running per user; Spaw answers up to 1,000 numbers in one synchronous request and takes 100,000 per bulk job with a signed webhook
## Questions
**Does Spaw charge for numbers it cannot settle?** Spaw bills per answer. A number the numbering plan and, where one is installed, the regulator's own table agree is assignable costs 1 credit whatever its flags say. An invalid number is free — including one in a block the regulator lists as unassigned — and so is a repeat of the same number within seven days. The live carrier-network check (hlr: true) is not enabled on the service: hlr_checked answers false and nothing extra is charged. ClearoutPhone's pages say: No credits are charged for numbers with "unknown" status or for duplicates in a list. Smart validation charges up to 5 credits only where it runs the deeper check, and within its caching window "repeated validations of the same number won't trigger additional deep validation charges".
**Can I move a list of numbers from ClearoutPhone to Spaw?** Yes. Upload the CSV in the dashboard or POST it to the bulk endpoint, up to 100,000 numbers a job; Spaw keeps your columns, adds the answer columns, and lets you download the whole file or only the valid, invalid, low, medium or high-risk rows. A country column gives each row its own region, so a mixed international list needs no splitting.
**Is there a free tier?** Every Spaw account receives 100 free credits at the start of each month with no card on file, and unused free credits accumulate. ClearoutPhone offers: 100 free credits on sign-up, one validation each.
## Sources (checked 2026-09-27)
- [ClearoutPhone pricing](https://clearoutphone.io/pricing/)
- [ClearoutPhone home page and FAQ](https://clearoutphone.io/)
- [ClearoutPhone API documentation](https://docs.clearoutphone.io/)
- [ClearoutPhone FAQs](https://clearoutphone.io/faqs/)
- [ClearoutPhone supported countries](https://clearoutphone.io/supported-countries/)
- [ClearoutPhone article on OTP delivery failures](https://clearoutphone.io/blog/prevent-otp-delivery-failures/)
ClearoutPhone is a trademark of its owner. Spaw is not affiliated with it.
Reference: https://spaw.co/compare/clearoutphone
---
# ClearoutPhone alternatives for phone intelligence
Phone intelligence APIs to consider instead of ClearoutPhone, with the pricing model, free tier and price per 1,000 lookups each vendor publishes. Figures come from public pricing pages checked on 2026-09-27.
| Service | Pricing model | Free tier | Price per 1,000 at about 10k |
| --- | --- | --- | --- |
| [Spaw](https://spaw.co) | Pay-as-you-go credit packs whose credits never expire, or optional plans, billed monthly or yearly, that deposit the same credits every month for less and roll unused plan credits over up to three months' allowance; both through Stripe Checkout | 100 free credits every month, no card required | $7.80 (Growth pack: 5,000 credits for $39), or $5.80 on the Growth monthly plan (5,000 credits for $29 a month) |
| [Abstract Phone Validation](https://www.abstractapi.com/api/phone-validation-api) | Monthly request quotas on three plans that differ by the data returned: Starter (validation, location, carrier) from $19 a month for 5,000 requests to $199 for 150,000; Standard (adds messaging) from $19 for 3,000 to $199 for 100,000; Professional (adds registration and risk) from $19 for 2,000 to $199 for 50,000. Annual billing saves 10%; the pricing page quotes $3.40 per 1,000 requests at Starter's entry volume, billed annually | 100 requests a month with no card, on the full response including registration and risk | not on the pages read |
| [IPQualityScore Phone Validation](https://www.ipqualityscore.com/solutions/phone-validation) | Monthly plans shared with IPQualityScore's other checks: Free $0 (1,000 lookups a month), Startup "As low as: $99 / Month" (5,000), SMB Basic "As low as: $499 / Month" (10,000), SMB+ "As low as: $999 / Month" (75,000), Enterprise custom; "Each plan requires a minimum monthly commitment". The phone validation page says "Score phone numbers on average from $0.0005 per user with no monthly contracts", on "tiered flat rate pricing across all countries that reduces as your monthly volume increases", without a tier table. Add-ons are enabled through the account manager or Support at no stated price. | 1,000 lookups a month on the Free plan, capped at 35 a day; the plan also covers IP, email and URL checks | not on the pages read |
| [Loqate Phone Validation](https://www.loqate.com/en-gb/phone-validation/) | Pay-as-you-go credit purchases of £50, £100, £250, £500 and £1,000 (or 75€, 150€ and 300€), each buying a published per-lookup rate for every service, plus bespoke plans; "Credit is valid for 12 months after the purchase date". Phone Validation on the "New Pricing" table is 7.1p, 6.7p, 5.6p, 4.7p and 4.1p a lookup across the pound tiers and 8.6c, 7.8c and 6.6c across the euro ones. The page also shows an "Old Pricing" table without saying which accounts each applies to; the figures here are from "New Pricing". Its usage-example selector also lists a £2,500 plan that neither table prices, and all prices exclude VAT. | A trial rather than a standing tier: "Start your 45-day free trial"; the FAQ adds "You get free credits to test the service" | £47 per 1,000 (4.7p a lookup) on the £500 credit, which buys about 10,600 lookups; the smaller credits run from 7.1p (£50) to 5.6p (£250) |
| [numlookupapi](https://numlookupapi.com) | Monthly subscription tiers by request quota: Free $0 for 100 requests, Basic $9.99 for 5,000, Pro $49.99 for 50,000, Scale $109.99 for 250,000, and Custom by contact; the page offers yearly billing at 20% off. Scale and Custom list "Overages" with no overage price shown, and the quota documentation says a request over the monthly quota answers 429 until the plan is upgraded or the month ends. | 100 requests a month on one API key, at 10 requests a minute | not on the pages read |
| [numverify](https://numverify.com) | Monthly subscription tiers by request quota: Free 100 requests, Basic $14.99 for 5,000, Professional $59.99 for 50,000, Enterprise $129.99 for 250,000, each with a published overage price per call ($0.011992, $0.0047992 and $0.00207984 respectively); annual billing saves up to 15%, and custom pricing is offered above the Enterprise quota | 100 API requests a month, for non-commercial use | not on the pages read |
| [Telnyx Number Lookup](https://telnyx.com/products/number-lookup) | Per query, priced per lookup type, with "$0 platform fee"; pay as you go gets "Automatic tier discounts as usage crosses thresholds", committed plans start at $500 a month, and inbound CNAM is $0.40 per number per month | An outbound CNAM listing at no charge; the pages read list no free query allowance | $1.50 per 1,000 for a local routing number query ($0.0015 a query), $2.50 for MCC/MNC ($0.0025) and $3.00 for CNAM ($0.003) |
| [Twilio Lookup](https://www.twilio.com/en-us/user-authentication-identity/lookup) | Per request, priced per data package on a Twilio account ("Pricing current as of August 2026"); line status and reassigned number risk are tiered by query volume, and SIM swap and call forwarding are contact sales | Formatting and validation are listed as Free, and so is the SMS Pumping Risk Score for North American numbers; no free allowance is published for the other packages | $8.00 per 1,000 for line type intelligence ($0.008 per request), $10.00 for inbound caller identification ($0.01, US only), $100.00 for identity match ($0.1, which the page says varies by country) |
| [Veriphone](https://veriphone.io) | One-time credit packs that never expire, from 10,000 credits for $24 to 5,000,000 for $999, or monthly plans whose credits reset each month: Starter $12.99 for 10,000, Professional $39 for 50,000, Business $99 for 250,000 and Enterprise $249 for 1,000,000. A standard validation is 1 credit and a Current Carrier Lookup 10 | 1,000 free credits a month with no card, not carried over; on the free plan a Current Carrier Lookup draws on purchased credits only | $2.40 per 1,000 on the 10,000-credit pack ($24, credits never expire), or $1.30 on the Starter plan (10,000 credits a month for $12.99, reset each month). A Current Carrier Lookup takes 10 credits, so ten times those figures |
## Spaw
Verification API that bills per answered lookup across six signals on one key and one balance: Email verification, Phone intelligence, IP intelligence, Address verification, Business verification against open registers, and Cross-signal consistency, which runs any subset of the first four in one call and names where they agree and where they do not. The phone side reads the numbering plan and the regulators' own allocation tables to name the line type, the block holder and the blocks nobody holds, and flags the numbers that are not a person's handset.
## Abstract Phone Validation
Abstract Phone Validation is a phone API from Abstract sold as monthly request quotas on three data tiers, returning carrier, line type and location, with messaging from Standard up and registration and risk on Professional and the Free plan; the Professional example response also carries breach history.
Comparison: https://spaw.co/compare/abstract-phone
## IPQualityScore Phone Validation
IPQualityScore Phone Validation is the phone side of a fraud-scoring service: one API returns validity, carrier, line type and a 0 to 100 fraud score, with live-line, identity, do-not-call and SMS-pumping data as add-ons, on the same monthly plans as its IP, email and URL checks.
Comparison: https://spaw.co/compare/ipqualityscore-phone
## Loqate Phone Validation
Loqate Phone Validation checks a number's format, carrier and, for mobile numbers, its live network status through an HLR lookup, priced per lookup from the same pay-as-you-go credit balance as Loqate's address, email and bank validation.
Comparison: https://spaw.co/compare/loqate-phone
## numlookupapi
numlookupapi is a phone number validation API sold as a monthly request quota: one validate endpoint returns validity, formats, country, location, carrier and line type.
Comparison: https://spaw.co/compare/numlookupapi
## numverify
numverify is a phone number validation API sold as a monthly request quota, returning validity, country and location, carrier and line type.
Comparison: https://spaw.co/compare/numverify
## Telnyx Number Lookup
Telnyx Number Lookup is a per-query number API sold beside Telnyx's carrier services, priced separately for local routing number (portability and carrier), MCC/MNC, and CNAM caller name.
Comparison: https://spaw.co/compare/telnyx-lookup
## Twilio Lookup
Twilio Lookup is a phone number API sold as separately priced data packages on top of a Twilio account: formatting and validation are free, and line type, caller identity, line status and the risk packages are each priced on their own, per request (the SMS Pumping Risk Score is free for North American numbers) or through sales for SIM swap and call forwarding.
Comparison: https://spaw.co/compare/twilio-lookup
## Veriphone
Veriphone is a phone validation API sold as credit packs or monthly plans, returning validity, carrier, line type and region from national numbering plans, with a 10-credit Current Carrier Lookup read from national portability registries.
Comparison: https://spaw.co/compare/veriphone
ClearoutPhone and the other product names are trademarks of their owners. Spaw is not affiliated with them.
Reference: https://spaw.co/alternatives/clearoutphone
---
# Spaw vs Companies House API: business verification API comparison
How Spaw and Companies House API compare on pricing, billing rules, the registers behind each answer and limits, from Companies House API's public pages checked on 2026-09-10. Corrections: support@spaw.co.
| | Spaw | Companies House API |
| --- | --- | --- |
| Pricing model | Pay-as-you-go credit packs whose credits never expire, or optional plans, billed monthly or yearly, that deposit the same credits every month for less and roll unused plan credits over up to three months' allowance; both through Stripe Checkout | No price is published on the developer pages checked; access is through a registered application and its key. |
| Free tier | 100 free credits every month, no card required | not on the pages read |
| Price per 1,000 at about 10k | $7.80 (Growth pack: 5,000 credits for $39), or $5.80 on the Growth monthly plan (5,000 credits for $29 a month) | not on the pages read |
| Price per 1,000 at about 100k | $4.49 (Volume pack: 100,000 credits for $449), or $3.49 on the Volume monthly plan (100,000 credits for $349 a month) | not on the pages read |
| Identifiers no register carried billed | No: an identifier no installed register carries answers unknown_identifier or unsupported_register and costs nothing | not on the pages read |
| Malformed identifiers billed | No: a malformed identifier and a register that has not synced are free too, as are repeats within 7 days | not on the pages read |
| Registers held | Two, synced as bulk files onto the server so no third party sits in the request path: the GLEIF Level 1 golden copy of Legal Entity Identifiers, refreshed daily by its publisher, and the Companies House free company data product for the United Kingdom, republished monthly. Every answer carries the date of the copy it read. | One, and the authoritative one: the UK register itself, answered live rather than from a copy. |
| Jurisdiction coverage | A LEI is global by construction; company numbers are GB only today, because a company number is unique only inside its own register. Anything else answers unsupported_register, free. | The United Kingdom. |
| Cross-register comparison | Yes: where a LEI record names a Companies House number and both files are installed, the legal name and the status are compared and both sides are quoted in agreements, with flags for a disagreement or for a company the national register records as dissolved. | None: it is a single register, so reconciling it with another is the caller's work. |
| Registered address returned | No, deliberately: a registered address is very often somebody's home. The response carries country and jurisdiction, which are enough to answer whether the entity is real and live. | not on the pages read |
| Bulk limit | None: there is no bulk upload for this product, because nobody types a company number into a signup form | not on the pages read |
| Batch API | 500 identifiers per synchronous request | not on the pages read |
| Rate limit | 50 requests per second per key on the business endpoints | "You can make up to 600 requests within a five-minute period"; every further request in that window answers 429 Too Many Requests. |
| Official SDKs | Python, Node.js, PHP | not on the pages read |
| Data retention | Nothing beyond the credit ledger entry that bills the lookup; a monitored list stores the identifiers you saved and each one's status from its last run, and nothing else. | not on the pages read |
## When Companies House API fits better
Anyone who needs the UK register as it stands right now, and only the UK register. It is the registrar's own service and will always be more current than a copy of its monthly file.
- The data is live: a strike-off, an incorporation or a status change is visible the moment the registrar records it
- It is the register of record, so nothing sits between you and the answer
- 600 requests per five minutes is generous for a lookup workload
## When Spaw fits better
Developers who want to know an identifier belongs to a real, live entity, and want the LEI record and the national register checked against each other rather than trusted separately.
- Companies House answers live; Spaw answers from the free company data product, republished monthly within five working days of the month end, so a Spaw answer can be up to five weeks behind, and every answer carries the date of the copy
- Spaw reads Legal Entity Identifiers as well, against the GLEIF golden copy, and compares the two records where a LEI names a UK company number
- Spaw's identifiers are one endpoint, one key and one balance with its other five signals
- Spaw returns no registered address and holds no register of where people live: it carries country and jurisdiction, which answer whether the entity is real and live
## Questions
**Does Spaw charge for identifiers it cannot find?** No. A lookup costs 1 credit only when a register answers. A malformed identifier, one no register carries, an identifier from a register this server does not hold, and a register that has not synced are all free, because none of them told you anything you could not have worked out. A repeat of the same identifier within seven days is free too. Companies House API does not publish how it bills them.
**Can I check a list of identifiers?** In batches, and on a schedule. There is deliberately no bulk upload for this product: POST /v1/entity/batch takes 500 identifiers a call under the single-lookup billing rules, and a monitored list keeps up to 500 identifiers under a weekly or monthly re-check that emails you when one leaves a register, is recorded as dissolved, or stops agreeing with the other register.
**Is there a free tier?** Every Spaw account receives 100 free credits at the start of each month with no card on file, and unused free credits accumulate. Companies House API does not publish a free tier on the pages checked.
## Sources (checked 2026-09-10)
- [Companies House developer hub](https://developer.company-information.service.gov.uk/)
- [Companies House API rate limiting](https://developer-specs.company-information.service.gov.uk/guides/rateLimiting)
Companies House API is a trademark of its owner. Spaw is not affiliated with it.
Reference: https://spaw.co/compare/companies-house-api
---
# Companies House API alternatives for business verification
Business verification APIs to consider instead of Companies House API, with the pricing model, free tier and price per 1,000 lookups each vendor publishes. Figures come from public pricing pages checked on 2026-09-10.
| Service | Pricing model | Free tier | Price per 1,000 at about 10k |
| --- | --- | --- | --- |
| [Spaw](https://spaw.co) | Pay-as-you-go credit packs whose credits never expire, or optional plans, billed monthly or yearly, that deposit the same credits every month for less and roll unused plan credits over up to three months' allowance; both through Stripe Checkout | 100 free credits every month, no card required | $7.80 (Growth pack: 5,000 credits for $39), or $5.80 on the Growth monthly plan (5,000 credits for $29 a month) |
| [OpenCorporates](https://opencorporates.com) | Annual or monthly API plans capped by call count rather than sold by volume: Essentials "£ 2,250/year" or "£225 per month" ("Up to 500 API calls/mth", "Up to 200 API calls/day"), Starter "£ 6,600/year" (2,500 a month, 500 a day), Basic "£ 12,000/year" (5,000 a month, 1,000 a day), Enterprise by consultation with bulk-data delivery | Free at-scale access for investigative journalists, NGOs, universities and anti-crime-and-corruption research groups rather than a free plan, in the site's own words | not on the pages read |
## Spaw
Verification API that bills per answered lookup across six signals on one key and one balance: Email verification, Phone intelligence, IP intelligence, Address verification, Business verification against open registers, and Cross-signal consistency, which runs any subset of the first four in one call and names where they agree and where they do not. The business side reads a Legal Entity Identifier or a UK company number against register files synced onto the server, and compares the two records where a LEI names a UK company.
## OpenCorporates
OpenCorporates is a company-data business built on open company registers, sold as annual API plans capped by monthly and daily call counts, with bulk-data delivery and a choice of jurisdictions on the enterprise plan.
Comparison: https://spaw.co/compare/opencorporates
Companies House API and the other product names are trademarks of their owners. Spaw is not affiliated with them.
Reference: https://spaw.co/alternatives/companies-house-api
---
# Spaw vs Emailable: email verification API comparison
How Spaw and Emailable compare on pricing, billing rules, verification depth and limits, from Emailable's public pages checked on 2026-09-13. Corrections: support@spaw.co.
| | Spaw | Emailable |
| --- | --- | --- |
| Pricing model | Pay-as-you-go credit packs whose credits never expire, or optional plans, billed monthly or yearly, that deposit the same credits every month for less and roll unused plan credits over up to three months' allowance; both through Stripe Checkout | Pay-as-you-go credits that never expire, or a monthly subscription at a 15% discount; minimum purchase 5,000 credits |
| Free tier | 100 free credits every month, no card required | 250 free credits at signup |
| Price per 1,000 at about 10k | $7.80 (Growth pack: 5,000 credits for $39), or $5.80 on the Growth monthly plan (5,000 credits for $29 a month) | $7.60 (the pay-as-you-go rate printed for the 5,000-credit package, $0.0076 a credit; larger packages are priced in a calculator, and the 5,000-credit subscription is $32.30 a month after the 15% discount) |
| Price per 1,000 at about 100k | $4.49 (Volume pack: 100,000 credits for $449), or $3.49 on the Volume monthly plan (100,000 credits for $349 a month) | not on the pages read |
| Unknown or catch-all results billed | No: an unconfirmed mailbox (reason unverified) and an answer Spaw could not obtain because its handshake partner was unavailable are free and leave no 7-day repeat marker, so a later lookup is a fresh one; a catch-all answer costs 1 credit like any risky answer, and an automatic re-check bills 1 credit only when it confirms the mailbox | No |
| Undeliverable results billed | No, within a fair use: undeliverable answers that needed a mailbox probe are free up to max(500, your billed email answers) in a rolling 30 days, then 1 credit each; the rest are always free | Yes |
| SMTP mailbox check | Yes | not on the pages read |
| Catch-all handling | A catch-all answer is remembered per domain for 7 days, and every unconfirmed mailbox carries a published 0 to 100 mailbox_confidence estimate | Returns an accept_all flag with the result; unknown results and duplicates are refunded |
| Bulk limit | 100,000 addresses per bulk job, with an HMAC-signed completion webhook | Lists of any size; about 10,000 addresses processed in 2 to 3 minutes |
| Batch API | 50 addresses per synchronous request, probes sent concurrently | not on the pages read |
| Rate limit | 5 requests per second per key on the email and consistency endpoints | not on the pages read |
| Official SDKs | Python, Node.js, PHP | Node.js, Ruby, Python |
| Data retention | Only email verification keeps a history row, for 30 days, then pruned; test addresses are never logged | All data is deleted after 30 days |
## When Emailable fits better
Marketers who want a simple per-credit price, a fast bulk cleaner and a stated 30-day deletion policy.
- Credits never expire
- SOC 2 Type II and GDPR statements
- 1 credit per verification across bulk, single, API and widget
- No long-term contracts; pays by card, PayPal, Apple Pay, Google Pay or bank transfer
## When Spaw fits better
Developers who want one flat price per answered lookup across every signal a signup form produces, a transparent risk score, and an API that documents every field.
- At the 5,000-credit package Emailable's pay-as-you-go rate is $7.60 per 1,000 and Spaw's Growth pack is $7.80
- Spaw bills nothing for unverified mailboxes or for undeliverable verdicts within a fair-use allowance; Emailable bills each completed verification except unknowns and duplicates
- Both delete lookup data after 30 days
- Spaw ships an OpenAPI document, Markdown docs and an MCP server; Emailable ships Node.js, Ruby and Python libraries
## Questions
**Does Spaw charge for catch-all or unknown results?** Spaw bills per answer, not per probe outcome. A catch-all domain comes back risky with reason catch_all and costs 1 credit like any risky answer. A mailbox the handshake could not confirm (greylisting, a timeout, no definite answer) comes back risky with reason unverified and is free, and so is an answer Spaw could not obtain at all because its handshake partner was unavailable; neither leaves a 7-day repeat marker, so a later lookup is a fresh one (an uncertain result is reused for up to 15 minutes, then the mailbox is asked again) and bills 1 credit only once the mailbox is confirmed, and the automatic re-checks behind callback_url bill 1 credit only when they confirm it. Undeliverable verdicts are free within a fair use of max(500, your billed email answers) in a rolling 30 days, and invalid input and repeats within 7 days are free too. Emailable states that it does not bill unknown results.
**Can I move a list from Emailable to Spaw?** Yes. Upload the CSV to the dashboard or POST it to the bulk endpoint; Spaw keeps your columns, adds the verdict columns, and lets you download only the deliverable, risky or undeliverable rows. Addresses that come back undeliverable join your suppression list so later runs skip them for free.
**Is there a free tier?** Every Spaw account receives 100 free credits at the start of each month with no card on file, and unused free credits accumulate. Emailable offers: 250 free credits at signup.
## Sources (checked 2026-09-13)
- [Emailable pricing](https://emailable.com/pricing/)
- [Emailable API page](https://emailable.com/api/)
- [Emailable API documentation](https://emailable.com/docs/api/)
Emailable is a trademark of its owner. Spaw is not affiliated with it.
Reference: https://spaw.co/compare/emailable
---
# Emailable alternatives for email verification
Email verification APIs to consider instead of Emailable, with the pricing model, free tier and price per 1,000 lookups each vendor publishes. Figures come from public pricing pages checked on 2026-09-13.
| Service | Pricing model | Free tier | Price per 1,000 at about 10k |
| --- | --- | --- | --- |
| [Spaw](https://spaw.co) | Pay-as-you-go credit packs whose credits never expire, or optional plans, billed monthly or yearly, that deposit the same credits every month for less and roll unused plan credits over up to three months' allowance; both through Stripe Checkout | 100 free credits every month, no card required | $7.80 (Growth pack: 5,000 credits for $39), or $5.80 on the Growth monthly plan (5,000 credits for $29 a month) |
| [Abstract API](https://www.abstractapi.com) | Monthly subscription tiers with a request slider from 5,000 to 150,000 requests a month; Starter from $17, Standard from $37, Professional from $39 a month | 100 requests on the Free plan | not on the pages read |
| [Bouncer](https://www.usebouncer.com) | Pay-as-you-go credits that never expire (from 1,000 credits for $8 up to 1,000,000 for $2,000), auto-refill, plus a Deliverability Kit subscription at $25 to $250 a month | 100 free credits, no card required | $6.00 (10,000 credits for $60) |
| [Hunter](https://hunter.io) | Monthly or yearly subscriptions with unified credits shared across search, verification and enrichment; credit packs on paid plans, usable for up to 12 months from purchase according to the pricing page | Free plan with 50 credits a month; one verification costs 0.5 credit, so 100 verifications | $7.45 (Growth: $149 a month for 10,000 credits, 20,000 verifications at 0.5 credit each; $5.20 billed yearly) |
| [Kickbox](https://kickbox.com) | not on the pages read | not on the pages read | not on the pages read |
| [Mailboxlayer](https://mailboxlayer.com) | Monthly or yearly subscription (10 to 15% off yearly) with a request quota per plan and per-request overage | 100 requests a month | $3.00 for the Basic quota (5,000 requests for $14.99 a month; overage $0.012 a request, so 10,000 requests on Basic cost about $74.95, $7.50 per 1,000); Professional Plus: 50,000 for $74.99, $1.50 per 1,000 |
| [NeverBounce](https://www.neverbounce.com) | not on the pages read | not on the pages read | not on the pages read |
| [Verifalia](https://verifalia.com) | Pay-as-you-go credit packs that never expire, or monthly, quarterly and yearly subscriptions; verification depth is priced as quality levels; pack and plan prices are shown in a calculator rather than listed | 25 free credits a day on the Free plan; unused daily credits expire each day | not on the pages read |
| [ZeroBounce](https://www.zerobounce.net) | Pay-as-you-go credits from $39 for 2,000 addresses, 15% off with autopay, or the ZeroBounce ONE subscription at $99 a month with unused credits rolling over; volume pricing shown through a calculator rather than fixed packs | 100 validation credits a month on the free account | not on the pages read |
## Spaw
Verification API that bills per answered lookup across six signals on one key and one balance: Email verification, Phone intelligence, IP intelligence, Address verification, Business verification against open registers, and Cross-signal consistency, which runs any subset of the first four in one call and names where they agree and where they do not. The email side checks syntax, live MX, SPF and DMARC, disposable and role lists, and runs an SMTP mailbox handshake that never sends a message.
## Abstract API
Abstract API's email validation endpoint is one of a family of utility APIs sold as monthly request quotas with a free 100-request plan.
Comparison: https://spaw.co/compare/abstract
## Bouncer
Bouncer is an email verification service with pay-as-you-go credits that never expire, high-throughput batch APIs, a real-time form product (Bouncer Shield) and a Deliverability Kit subscription.
Comparison: https://spaw.co/compare/bouncer
## Hunter
Hunter is an outreach platform whose Email Verifier shares unified plan credits with its email finder, enrichment and campaign tools.
Comparison: https://spaw.co/compare/hunter
## Kickbox
Kickbox is an email verification API that returns deliverable, undeliverable, risky or unknown plus a 0 to 1 Sendex quality score, with official libraries for PHP, Node.js, Ruby and Python.
Comparison: https://spaw.co/compare/kickbox
## Mailboxlayer
Mailboxlayer is apilayer's email validation API, sold as monthly request quotas from a free 100-request plan up to 250,000 requests a month.
Comparison: https://spaw.co/compare/mailboxlayer
## NeverBounce
NeverBounce is a ZoomInfo-owned email verification service with a bulk list-cleaning workflow, a real-time API and official client libraries in six languages.
Comparison: https://spaw.co/compare/neverbounce
## Verifalia
Verifalia is an email verification service with selectable quality levels, credit packs that never expire, subscriptions with daily free credits, and official SDKs in six languages.
Comparison: https://spaw.co/compare/verifalia
## ZeroBounce
ZeroBounce is an email validation service with a pay-as-you-go credit model and a subscription (ZeroBounce ONE) that bundles deliverability tools such as activity data, inbox placement tests and blacklist monitoring.
Comparison: https://spaw.co/compare/zerobounce
Emailable and the other product names are trademarks of their owners. Spaw is not affiliated with them.
Reference: https://spaw.co/alternatives/emailable
---
# Spaw vs Geoapify: address verification API comparison
How Spaw and Geoapify compare on pricing, billing rules, the registers behind each answer and limits, from Geoapify's public pages checked on 2026-09-22. Corrections: support@spaw.co.
| | Spaw | Geoapify |
| --- | --- | --- |
| Pricing model | Pay-as-you-go credit packs whose credits never expire, or optional plans, billed monthly or yearly, that deposit the same credits every month for less and roll unused plan credits over up to three months' allowance; both through Stripe Checkout | Daily credit quotas on monthly subscriptions: Free (3,000 credits a day), API 10 (10,000 a day, $59 billed monthly), API 25 (25,000, $109), API 50 (50,000, $179), API 100 (100,000, $299), API 250 (250,000, $609) and Custom ("Unmetered credits / day", from $860). "1 Geocoding API request costs 1 credit"; in a batch each address counts 0.5 credits, plus 1 credit to create the job and 1 for each result request. A yearly term is offered, but its prices are not in the page text read. |
| Free tier | 100 free credits every month, no card required | A standing free plan: "3,000 credits / day", "No credit card required", up to 5 requests a second, usable for "commercial websites and apps" with the required Geoapify and data-source attribution |
| Price per 1,000 at about 10k | $7.80 (Growth pack: 5,000 credits for $39), or $5.80 on the Growth monthly plan (5,000 credits for $29 a month) | not on the pages read |
| Price per 1,000 at about 100k | $4.49 (Volume pack: 100,000 credits for $449), or $3.49 on the Volume monthly plan (100,000 credits for $349 a month) | not on the pages read |
| Addresses no register covered billed | An address that stands as written costs 1 credit whether or not a register carried the building; where no register covers the country, exists answers null rather than a verdict | not on the pages read |
| Addresses that cannot stand billed | No: an address that cannot stand as written is free whatever the reason, and so are repeats within 7 days and rows repeating an address already in the same job | not on the pages read |
| Registers held | Open address registers for 23 countries and territories, each named in the answer with its declared coverage, complete or partial. Complete, building by building: Australia, Austria, Belgium, Czechia, Latvia, Luxembourg, Netherlands, Norway and Switzerland. Partial, building by building: Brazil, Denmark, France (with French Guiana, Guadeloupe, Martinique, Mayotte, Réunion, St. Barthélemy, St. Martin and St. Pierre & Miquelon), Germany and United States. Partial, streets only: Great Britain. House-number ranges as well: United States. Absence from a partial register is never read as evidence that an address does not exist. | No national register is named. The geocoder draws on "OpenStreetMap, OpenAddresses, Who's on First, Geonames, Wikipedia, and more", and stored validation results must keep "the attribution to OpenStreetMap, OpenAddresses, Geonames". |
| Country coverage | Postal formats for 207 countries and territories: the line layout for 199 of them, and the required fields and postcode pattern wherever the table records them; 52 postal operators' rules for naming a delivery point that is not a street; open address registers for 23 countries and territories; postcode directories for Great Britain from the ONS Postcode Directory, with Northern Ireland excluded because reusing BT postcodes needs a separate licence, and for 108 more countries and territories from GeoNames' postal codes. | "Our Geocoding API returns international addresses in any language"; the validation tool "works with international addresses". No country count is published on the pages read. |
| Claims post is delivered | No, and every answer says so: is_deliverable, is_cmra, is_vacant and is_residential answer null because that check is licensed and not enabled here. exists is a match in an open address register, never a statement that post arrives. | No. The validation tool grades each address by what the geocoder found: CONFIRMED ("the address exists"), PARTIALLY_CONFIRMED with a reason such as BUILDING_NOT_FOUND, or NOT_CONFIRMED ("the address is not found"). Nothing on the pages read says whether post is delivered. |
| Bulk limit | 100,000 addresses per bulk job, with an HMAC-signed completion webhook | The free Bulk Address Validation tool processes "Only the first 500 rows" of an uploaded Excel, CSV or text file; larger jobs are arranged on request. |
| Batch API | 100 addresses per synchronous request | Asynchronous batch geocoding takes "up to 1000 addresses in one request" at 0.5 credits an address; batch jobs run on spare capacity and at peak times can take "several hours". |
| Rate limit | 50 requests per second per key on the address endpoints | By plan, from "Up to 5 requests/second" on Free to "Up to 30 requests/second" on API 250; the limits are soft ("In general, we do not throttle when you go over the limits"). |
| Official SDKs | Python, Node.js, PHP | not on the pages read |
| Data retention | No history row and no log line carrying the address. What a lookup leaves is the 7-day marker that makes a repeat free and the count behind seen_by_you, both kept under a digest, never the address. The address itself is kept only on the account's own suppression list and in its monitors. Bulk input lists and result files are deleted with the job after 30 days. | not on the pages read |
## When Geoapify fits better
Teams already using Geoapify for maps or routing that want an open-data address check with coordinates on a free daily allowance.
- A standing free plan of 3,000 credits a day that may be used in production with attribution
- Batch geocoding at half the credit cost of single requests, answered asynchronously
- A free browser tool that validates up to 500 rows of a spreadsheet without code
- Results may be stored without restriction as long as the data-source attributions are kept
- The default API endpoints run in European data centres
## When Spaw fits better
Developers who want a written address parsed against its own country's rules and checked against open registers, where a field nobody could evaluate answers null instead of a guess.
- Geoapify grades an address by confidence scores from its geocoding data; Spaw's exists is a match in a country's own address register; where that register's coverage is partial, an address it does not hold answers null rather than false
- Geoapify sells a daily credit quota on a monthly subscription; Spaw sells pack credits that never expire, and an address that cannot stand as written costs nothing
- Spaw reads an address against its own country's postal format and names the kind of delivery point it describes, a PO box, a private mailbox or a street address, alongside the postcode's status in the national directory
- Every Spaw answer names the datasets and versions behind it in sources, so a result can be traced to the register that gave it
## Questions
**Does Spaw charge for addresses no register covers?** An address that stands as written costs 1 credit, whether or not a register carried the building. An address that cannot stand costs nothing, whatever the reason, and a repeat within seven days is free. Where no register covers the country, or where the register that answered is one whose coverage is partial and does not carry the address, exists answers null — not evaluated — rather than a verdict, and the not_in_register weight cannot fire. Geoapify does not publish how it bills them.
**Can I move an address list from Geoapify to Spaw?** Yes. Upload the CSV in the dashboard or POST it to the bulk endpoint, up to 100,000 addresses a job; Spaw keeps your columns and adds 64 result columns after them (the address as submitted, the answer, the datasets behind it, what the row cost and whether your suppression list answered it), and a country column gives each row its own country. Rows repeating an address already in the list are answered once, cost nothing, and are counted as duplicates.
**Is there a free tier?** Every Spaw account receives 100 free credits at the start of each month with no card on file, and unused free credits accumulate. Geoapify offers: A standing free plan: "3,000 credits / day", "No credit card required", up to 5 requests a second, usable for "commercial websites and apps" with the required Geoapify and data-source attribution.
## Sources (checked 2026-09-22)
- [Geoapify pricing](https://www.geoapify.com/pricing/)
- [Geoapify pricing details](https://www.geoapify.com/pricing-details/)
- [Geoapify Geocoding API](https://www.geoapify.com/geocoding-api/)
- [Geoapify address validation tool](https://www.geoapify.com/tools/address-validation/)
Geoapify is a trademark of its owner. Spaw is not affiliated with it.
Reference: https://spaw.co/compare/geoapify
---
# Geoapify alternatives for address verification
Address verification APIs to consider instead of Geoapify, with the pricing model, free tier and price per 1,000 lookups each vendor publishes. Figures come from public pricing pages checked on 2026-09-22.
| Service | Pricing model | Free tier | Price per 1,000 at about 10k |
| --- | --- | --- | --- |
| [Spaw](https://spaw.co) | Pay-as-you-go credit packs whose credits never expire, or optional plans, billed monthly or yearly, that deposit the same credits every month for less and roll unused plan credits over up to three months' allowance; both through Stripe Checkout | 100 free credits every month, no card required | $7.80 (Growth pack: 5,000 credits for $39), or $5.80 on the Growth monthly plan (5,000 credits for $29 a month) |
| [Google Address Validation API](https://mapsplatform.google.com) | "Pay-as-you-go", "charging per request according to the pricing table", under two SKUs named on the billing page, Address Validation Pro and Address Validation Enterprise; the per-request rates render inside the pricing table and are not stated in the page text as fetched, so no figure is quoted here. | not on the pages read | not on the pages read |
| [Ideal Postcodes](https://ideal-postcodes.co.uk) | Pay-as-you-go credit packs, "No Subscriptions. No Monthly Fees.": 200 credits for £9 ("£0.045 per lookup"), 1,100 for £42 (£0.038), 4,300 for £155 (£0.036), 12,800 for £420 (£0.033) and 32,100 for £900 (£0.028). "Credits are valid for 12 months"; the lookup guide adds that the timer "only starts when you begin using that batch of credits". Larger volumes and UK registered charities are priced on request. | A trial rather than a standing tier: "1-Month Trial", "50 free credits, full support and no credit card needed"; the trial searches worldwide except Ireland, "which requires separate licensing" | £33 per 1,000 lookups on the 12,800-credit pack (£420, "£0.033 per lookup"), the smallest pack that covers 10,000. |
| [Lob](https://www.lob.com) | Verifications are priced per call on top of a print-and-mail plan: on the Developer plan ($0 a month) "US Address Verifications $0.05/additional" and international $0.07; on Startup ($260 a month) $0.025 with "1,000 included for $25/mo" and international $0.06 with 500 included for $30 a month; on Growth ($550 a month) $0.009 with "50,000 included for $450/mo" and international $0.055 with 10,000 included for $550 a month; Enterprise is "Contact us for details". | The Developer plan is "Starting at $0 / month" with verifications billed per call; the site offers "Get started for free". | $50 per 1,000 US verifications on the Developer plan ($0.05 each) or $25 per 1,000 on Startup ($0.025 each beyond the 1,000 included for $25 a month); international $70 or $60. |
| [Loqate](https://www.loqate.com) | Pay-as-you-go credit purchases of £50, £100, £250, £500 and £1,000 (or 75€, 150€ and 300€), each buying a published per-lookup rate for every service, plus bespoke plans; "Credit is valid for 12 months after the purchase date" and there are no setup costs. The pricing page shows two rate tables headed "New Pricing" and "Old Pricing" without saying which accounts each applies to; the figures here are from "New Pricing". Its usage-example selector also lists a £2,500 plan that neither table prices. | A trial rather than a standing tier: "Start your 45-day free trial"; the FAQ adds "You get free credits to test the service" | £82 to £86 per 1,000 Address Verify lookups on the "New Pricing" table: 8.2p a lookup on the £100, £250 and £500 tiers and 8.6p on the £50 and £1,000 tiers. Address Capture is 8.6p to 4.9p in the UK and 7.1p to 4.1p for the rest of the world. |
| [Melissa](https://www.melissa.com) | US Address Verification pay-as-you-go "Starting at $40" for 10,000 credits at "10 Credits Per Address" (the same page's FAQ says a US address equals 3 credits and a global address 10); an API subscription "Starting at $5,145" for 1,000,000 records a year; an unlimited-records plan "Starting at $16,000". Global Address Verification subscriptions from $40 a month for 1,000 records and $95 a month for 2,500. | Global Address Verification "GAV Free" at $0 for 250 records a month; 1,000 lookup credits a month after a one-time $4.97 KYC/AML verification fee, described on the pricing page. | $40 per 1,000 US addresses on the smallest pay-as-you-go pack, taking the pack's own "10 Credits Per Address" ($40 buys 10,000 credits). |
| [Postcode.eu](https://www.postcode.eu) | Annual tiers that include validations: Tier 1 €50, Tier 2 €100, Tier 3 €250, Tier 4 €500 and Tier 5 €1,000 a year, each with a per-validation rate by country group. "Up to the Tier amount you can perform a mix of validations", subscriptions "are automatically upgraded when the maximum of the Tier is reached", and past the highest Tier usage "will be invoiced in blocks of 10% and the rates of this Tier apply". A validation is "a bundle of related requests that were used to validate an address". An SLA (€1,000 + 2% a year) and a DORA addendum (€2,500 a year) are extra. | A trial rather than a standing tier: "After registering an account it is possible to try the APIs for free", and "Testing the API is without obligations" | €17.64 per 1,000 validations on Tier 3 (€250 a year, "€0.01764 each") for Austria, Belgium, Denmark, Finland, France, Luxembourg, Norway, Spain, Sweden, Switzerland and the Netherlands. Germany, Italy and the United Kingdom cost more a validation, so 10,000 of them fall in Tier 4 (€500 a year): €34.17 per 1,000 for Germany (€0.03417 each) and €43.30 for Italy and the United Kingdom (€0.0433 each). "Tier prices are including validations" and "Prices and quantities are per year", so the tier amount is paid in full: 10,000 validations cost €250 a year for the eleven countries (€25 per 1,000) and €500 for Germany, Italy or the United Kingdom (€50 per 1,000). |
| [Postcoder](https://postcoder.com) | Credit packs (500 to 200,000 credits, "valid for 12 months from purchase") or monthly plans (1,000 to 100,000 credits, with unused credits not carried over). "Your credits can be used for any Postcoder request." The pack prices themselves render as placeholders ("£--") in the pack list; the credit calculator's recommendation box shows "Credit pack £25 500 Credits" and "Monthly plan £25 1,000 Credits". Larger packs and plans with volume discounts are quoted. | A free trial is offered; its duration and allowance are not stated on the pricing page | £100 per 1,000 UK address lookups pay-as-you-go (10.00p a request, 2.0 credits) or £45 per 1,000 on a monthly plan (4.50p). This is the address comparison, so only the address rate is quoted: the same account buys email and mobile validation at their own per-request rates, which are read off the same page. |
| [PostGrid](https://www.postgrid.com) | Quoted: the pricing page asks for annual mail-piece and address-verification volumes in bands from "Less than 50,000" to "More than 1,000,000" and publishes no rate. | "Try for Free. Sign Up for Address Verification API" on the product page; no standing free volume is stated. | not on the pages read |
| [Smarty](https://www.smarty.com) | Annual lookup bundles on the Professional plan from $552 a year, sized 12K, 60K, 120K, 300K, 600K, 1M or 2M lookups; a monthly option and bespoke Custom pricing are offered. Only the $552 entry price is shown on the page. A subscription "expires when the address lookups or the time runs out, whichever comes first"; there are no overage costs, and an exhausted plan answers 402 "payment required" unless auto-renewal buys a new block. | A free trial rather than a standing tier: "42 days" and "1,000 address lookups", "No credit card required" | not on the pages read |
## Spaw
Verification API that bills per answered lookup across six signals on one key and one balance: Email verification, Phone intelligence, IP intelligence, Address verification, Business verification against open registers, and Cross-signal consistency, which runs any subset of the first four in one call and names where they agree and where they do not. The address side reads a written address against its own country's format, names the kind of delivery point behind it, checks the postcode against the national directory and follows the building into that country's own address register where one is installed.
## Google Address Validation API
Google's Address Validation API, part of Google Maps Platform, validates, standardises and geocodes an address, reports a confirmation level per component with inferred and replaced parts, and can run US and Puerto Rico addresses through USPS CASS on request.
Comparison: https://spaw.co/compare/google-address-validation
## Ideal Postcodes
Ideal Postcodes is a UK-first address lookup, autocomplete and cleansing API answered from Royal Mail's Postcode Address File, with Ordnance Survey AddressBase Core and Premium offered as additional datasets, sold as pay-as-you-go credit packs, with international address data for more than 240 countries and territories; its site also lists phone and email validation among its products.
Comparison: https://spaw.co/compare/ideal-postcodes
## Lob
Lob is a print-and-mail platform whose Address Verification API validates, standardises and enriches US and international addresses, CASS certified, with USPS delivery-point data and a deliverability likelihood drawn from Lob's own mail delivery records.
Comparison: https://spaw.co/compare/lob
## Loqate
Loqate sells address capture and address verification alongside email, phone and bank validation, priced per lookup from a pay-as-you-go credit balance, with international data for over 245 countries and territories.
Comparison: https://spaw.co/compare/loqate
## Melissa
Melissa sells address verification for the United States and "250+ Countries & Territories" against postal data, CASS and DPV certified, with a credit model for pay-as-you-go use and annual subscriptions sized by records, alongside name, phone, email and identity verification.
Comparison: https://spaw.co/compare/melissa
## Postcode.eu
Postcode.eu, from Postcode.nl B.V., sells address autocomplete and validation APIs for fourteen European countries, each answered from the official sources its pages name (government address registers for most, the postal operator's file for others), priced per validation inside an annual tier.
Comparison: https://spaw.co/compare/postcode-eu
## Postcoder
Postcoder is a UK-first address lookup API sold as credits that any of its services can spend: address lookup, bank validation, email validation, mobile validation and OTP verification, each costing a published number of credits per request.
Comparison: https://spaw.co/compare/postcoder
## PostGrid
PostGrid sells an address verification API for the United States and Canada, CASS and SERP certified against USPS and Canada Post data, with an international service for "245+ Countries and Territories", autocomplete, parsing, batch verification and a Salesforce integration, priced by quotation.
Comparison: https://spaw.co/compare/postgrid
## Smarty
Smarty sells US address verification against postal data: a lookup validates to the secondary level and returns delivery-point metadata that open registers cannot answer, including a DPV match code, the residential indicator and vacancy status.
Comparison: https://spaw.co/compare/smarty
Geoapify and the other product names are trademarks of their owners. Spaw is not affiliated with them.
Reference: https://spaw.co/alternatives/geoapify
---
# Spaw vs Google Address Validation API: address verification API comparison
How Spaw and Google Address Validation API compare on pricing, billing rules, the registers behind each answer and limits, from Google Address Validation API's public pages checked on 2026-09-16. Corrections: support@spaw.co.
| | Spaw | Google Address Validation API |
| --- | --- | --- |
| Pricing model | Pay-as-you-go credit packs whose credits never expire, or optional plans, billed monthly or yearly, that deposit the same credits every month for less and roll unused plan credits over up to three months' allowance; both through Stripe Checkout | "Pay-as-you-go", "charging per request according to the pricing table", under two SKUs named on the billing page, Address Validation Pro and Address Validation Enterprise; the per-request rates render inside the pricing table and are not stated in the page text as fetched, so no figure is quoted here. |
| Free tier | 100 free credits every month, no card required | not on the pages read |
| Price per 1,000 at about 10k | $7.80 (Growth pack: 5,000 credits for $39), or $5.80 on the Growth monthly plan (5,000 credits for $29 a month) | not on the pages read |
| Price per 1,000 at about 100k | $4.49 (Volume pack: 100,000 credits for $449), or $3.49 on the Volume monthly plan (100,000 credits for $349 a month) | not on the pages read |
| Addresses no register covered billed | An address that stands as written costs 1 credit whether or not a register carried the building; where no register covers the country, exists answers null rather than a verdict | not on the pages read |
| Addresses that cannot stand billed | No: an address that cannot stand as written is free whatever the reason, and so are repeats within 7 days and rows repeating an address already in the same job | not on the pages read |
| Registers held | Open address registers for 23 countries and territories, each named in the answer with its declared coverage, complete or partial. Complete, building by building: Australia, Austria, Belgium, Czechia, Latvia, Luxembourg, Netherlands, Norway and Switzerland. Partial, building by building: Brazil, Denmark, France (with French Guiana, Guadeloupe, Martinique, Mayotte, Réunion, St. Barthélemy, St. Martin and St. Pierre & Miquelon), Germany and United States. Partial, streets only: Great Britain. House-number ranges as well: United States. Absence from a partial register is never read as evidence that an address does not exist. | Google's own data plus, for the "US" and "PR" regions when enableUspsCass is set, the USPS Coding Accuracy Support System; the overview says USPS "evaluates requests for artificially created addresses" and that Google must "stop validation and report relevant information to USPS in such cases". |
| Country coverage | Postal formats for 207 countries and territories: the line layout for 199 of them, and the required fields and postcode pattern wherever the table records them; 52 postal operators' rules for naming a delivery point that is not a street; open address registers for 23 countries and territories; postcode directories for Great Britain from the ONS Postcode Directory, with Northern Ireland excluded because reusing BT postcodes needs a separate licence, and for 108 more countries and territories from GeoNames' postal codes. | 38 countries and regions in the coverage table on the page checked, with India "in preview"; the residential and commercial address metadata is marked as populated for 37 of them. |
| Claims post is delivered | No, and every answer says so: is_deliverable, is_cmra, is_vacant and is_residential answer null because that check is licensed and not enabled here. exists is a match in an open address register, never a statement that post arrives. | Partly. The API returns a validation status per address component ("Corrects", "Completes", "Formats"), a geocode and, with CASS enabled for the United States and Puerto Rico, USPS delivery-point data; residential and commercial metadata is limited to the countries the coverage table marks. |
| Bulk limit | 100,000 addresses per bulk job, with an HMAC-signed completion webhook | not on the pages read |
| Batch API | 100 addresses per synchronous request | not on the pages read |
| Rate limit | 50 requests per second per key on the address endpoints | "6,000 queries per minute for validation and feedback methods", per the billing page's usage limits. |
| Official SDKs | Python, Node.js, PHP | not on the pages read |
| Data retention | No history row and no log line carrying the address. What a lookup leaves is the 7-day marker that makes a repeat free and the count behind seen_by_you, both kept under a digest, never the address. The address itself is kept only on the account's own suppression list and in its monitors. Bulk input lists and result files are deleted with the job after 30 days. | not on the pages read |
## When Google Address Validation API fits better
Checkout and delivery forms that want Google's component-level correction and geocoding in the countries its coverage table lists, inside a Google Maps Platform account.
- A confirmation level per address component, with components the API inferred or replaced marked as such
- CASS for the United States and Puerto Rico on request, with the USPS artificial-address reporting clause spelled out
- A geocode with every validated address
- Coverage stated country by country, with metadata availability marked per country
## When Spaw fits better
Developers who want a written address parsed against its own country's rules and checked against open registers, where a field nobody could evaluate answers null instead of a guess.
- Both answer per part: Google marks each component confirmed, inferred or replaced, and Spaw's confirmation map names the open dataset that stood behind each part, or says nothing could check it
- Google can run US addresses through USPS CASS; Spaw's US answers come from the public-domain National Address Database and the Census Bureau's address ranges, and the licensed delivery-point fields answer null until a partner is enabled
- Google publishes coverage for 38 countries; Spaw reads an address against its own country's postal format, with postcode directories for more than a hundred countries and territories and an open address register wherever one is installed
- Google prices per request under Maps Platform terms; a Spaw pack credit never expires and an address that cannot stand as written costs nothing
## Questions
**Does Spaw charge for addresses no register covers?** An address that stands as written costs 1 credit, whether or not a register carried the building. An address that cannot stand costs nothing, whatever the reason, and a repeat within seven days is free. Where no register covers the country, or where the register that answered is one whose coverage is partial and does not carry the address, exists answers null — not evaluated — rather than a verdict, and the not_in_register weight cannot fire. Google Address Validation API does not publish how it bills them.
**Can I move an address list from Google Address Validation API to Spaw?** Yes. Upload the CSV in the dashboard or POST it to the bulk endpoint, up to 100,000 addresses a job; Spaw keeps your columns and adds 64 result columns after them (the address as submitted, the answer, the datasets behind it, what the row cost and whether your suppression list answered it), and a country column gives each row its own country. Rows repeating an address already in the list are answered once, cost nothing, and are counted as duplicates.
**Is there a free tier?** Every Spaw account receives 100 free credits at the start of each month with no card on file, and unused free credits accumulate. Google Address Validation API does not publish a free tier on the pages checked.
## Sources (checked 2026-09-16)
- [Address Validation API usage and billing](https://developers.google.com/maps/documentation/address-validation/usage-and-billing)
- [Address Validation API overview](https://developers.google.com/maps/documentation/address-validation/overview)
- [Address Validation API country and region coverage](https://developers.google.com/maps/documentation/address-validation/coverage)
Google Address Validation API is a trademark of its owner. Spaw is not affiliated with it.
Reference: https://spaw.co/compare/google-address-validation
---
# Google Address Validation API alternatives for address verification
Address verification APIs to consider instead of Google Address Validation API, with the pricing model, free tier and price per 1,000 lookups each vendor publishes. Figures come from public pricing pages checked on 2026-09-16.
| Service | Pricing model | Free tier | Price per 1,000 at about 10k |
| --- | --- | --- | --- |
| [Spaw](https://spaw.co) | Pay-as-you-go credit packs whose credits never expire, or optional plans, billed monthly or yearly, that deposit the same credits every month for less and roll unused plan credits over up to three months' allowance; both through Stripe Checkout | 100 free credits every month, no card required | $7.80 (Growth pack: 5,000 credits for $39), or $5.80 on the Growth monthly plan (5,000 credits for $29 a month) |
| [Geoapify](https://www.geoapify.com) | Daily credit quotas on monthly subscriptions: Free (3,000 credits a day), API 10 (10,000 a day, $59 billed monthly), API 25 (25,000, $109), API 50 (50,000, $179), API 100 (100,000, $299), API 250 (250,000, $609) and Custom ("Unmetered credits / day", from $860). "1 Geocoding API request costs 1 credit"; in a batch each address counts 0.5 credits, plus 1 credit to create the job and 1 for each result request. A yearly term is offered, but its prices are not in the page text read. | A standing free plan: "3,000 credits / day", "No credit card required", up to 5 requests a second, usable for "commercial websites and apps" with the required Geoapify and data-source attribution | not on the pages read |
| [Ideal Postcodes](https://ideal-postcodes.co.uk) | Pay-as-you-go credit packs, "No Subscriptions. No Monthly Fees.": 200 credits for £9 ("£0.045 per lookup"), 1,100 for £42 (£0.038), 4,300 for £155 (£0.036), 12,800 for £420 (£0.033) and 32,100 for £900 (£0.028). "Credits are valid for 12 months"; the lookup guide adds that the timer "only starts when you begin using that batch of credits". Larger volumes and UK registered charities are priced on request. | A trial rather than a standing tier: "1-Month Trial", "50 free credits, full support and no credit card needed"; the trial searches worldwide except Ireland, "which requires separate licensing" | £33 per 1,000 lookups on the 12,800-credit pack (£420, "£0.033 per lookup"), the smallest pack that covers 10,000. |
| [Lob](https://www.lob.com) | Verifications are priced per call on top of a print-and-mail plan: on the Developer plan ($0 a month) "US Address Verifications $0.05/additional" and international $0.07; on Startup ($260 a month) $0.025 with "1,000 included for $25/mo" and international $0.06 with 500 included for $30 a month; on Growth ($550 a month) $0.009 with "50,000 included for $450/mo" and international $0.055 with 10,000 included for $550 a month; Enterprise is "Contact us for details". | The Developer plan is "Starting at $0 / month" with verifications billed per call; the site offers "Get started for free". | $50 per 1,000 US verifications on the Developer plan ($0.05 each) or $25 per 1,000 on Startup ($0.025 each beyond the 1,000 included for $25 a month); international $70 or $60. |
| [Loqate](https://www.loqate.com) | Pay-as-you-go credit purchases of £50, £100, £250, £500 and £1,000 (or 75€, 150€ and 300€), each buying a published per-lookup rate for every service, plus bespoke plans; "Credit is valid for 12 months after the purchase date" and there are no setup costs. The pricing page shows two rate tables headed "New Pricing" and "Old Pricing" without saying which accounts each applies to; the figures here are from "New Pricing". Its usage-example selector also lists a £2,500 plan that neither table prices. | A trial rather than a standing tier: "Start your 45-day free trial"; the FAQ adds "You get free credits to test the service" | £82 to £86 per 1,000 Address Verify lookups on the "New Pricing" table: 8.2p a lookup on the £100, £250 and £500 tiers and 8.6p on the £50 and £1,000 tiers. Address Capture is 8.6p to 4.9p in the UK and 7.1p to 4.1p for the rest of the world. |
| [Melissa](https://www.melissa.com) | US Address Verification pay-as-you-go "Starting at $40" for 10,000 credits at "10 Credits Per Address" (the same page's FAQ says a US address equals 3 credits and a global address 10); an API subscription "Starting at $5,145" for 1,000,000 records a year; an unlimited-records plan "Starting at $16,000". Global Address Verification subscriptions from $40 a month for 1,000 records and $95 a month for 2,500. | Global Address Verification "GAV Free" at $0 for 250 records a month; 1,000 lookup credits a month after a one-time $4.97 KYC/AML verification fee, described on the pricing page. | $40 per 1,000 US addresses on the smallest pay-as-you-go pack, taking the pack's own "10 Credits Per Address" ($40 buys 10,000 credits). |
| [Postcode.eu](https://www.postcode.eu) | Annual tiers that include validations: Tier 1 €50, Tier 2 €100, Tier 3 €250, Tier 4 €500 and Tier 5 €1,000 a year, each with a per-validation rate by country group. "Up to the Tier amount you can perform a mix of validations", subscriptions "are automatically upgraded when the maximum of the Tier is reached", and past the highest Tier usage "will be invoiced in blocks of 10% and the rates of this Tier apply". A validation is "a bundle of related requests that were used to validate an address". An SLA (€1,000 + 2% a year) and a DORA addendum (€2,500 a year) are extra. | A trial rather than a standing tier: "After registering an account it is possible to try the APIs for free", and "Testing the API is without obligations" | €17.64 per 1,000 validations on Tier 3 (€250 a year, "€0.01764 each") for Austria, Belgium, Denmark, Finland, France, Luxembourg, Norway, Spain, Sweden, Switzerland and the Netherlands. Germany, Italy and the United Kingdom cost more a validation, so 10,000 of them fall in Tier 4 (€500 a year): €34.17 per 1,000 for Germany (€0.03417 each) and €43.30 for Italy and the United Kingdom (€0.0433 each). "Tier prices are including validations" and "Prices and quantities are per year", so the tier amount is paid in full: 10,000 validations cost €250 a year for the eleven countries (€25 per 1,000) and €500 for Germany, Italy or the United Kingdom (€50 per 1,000). |
| [Postcoder](https://postcoder.com) | Credit packs (500 to 200,000 credits, "valid for 12 months from purchase") or monthly plans (1,000 to 100,000 credits, with unused credits not carried over). "Your credits can be used for any Postcoder request." The pack prices themselves render as placeholders ("£--") in the pack list; the credit calculator's recommendation box shows "Credit pack £25 500 Credits" and "Monthly plan £25 1,000 Credits". Larger packs and plans with volume discounts are quoted. | A free trial is offered; its duration and allowance are not stated on the pricing page | £100 per 1,000 UK address lookups pay-as-you-go (10.00p a request, 2.0 credits) or £45 per 1,000 on a monthly plan (4.50p). This is the address comparison, so only the address rate is quoted: the same account buys email and mobile validation at their own per-request rates, which are read off the same page. |
| [PostGrid](https://www.postgrid.com) | Quoted: the pricing page asks for annual mail-piece and address-verification volumes in bands from "Less than 50,000" to "More than 1,000,000" and publishes no rate. | "Try for Free. Sign Up for Address Verification API" on the product page; no standing free volume is stated. | not on the pages read |
| [Smarty](https://www.smarty.com) | Annual lookup bundles on the Professional plan from $552 a year, sized 12K, 60K, 120K, 300K, 600K, 1M or 2M lookups; a monthly option and bespoke Custom pricing are offered. Only the $552 entry price is shown on the page. A subscription "expires when the address lookups or the time runs out, whichever comes first"; there are no overage costs, and an exhausted plan answers 402 "payment required" unless auto-renewal buys a new block. | A free trial rather than a standing tier: "42 days" and "1,000 address lookups", "No credit card required" | not on the pages read |
## Spaw
Verification API that bills per answered lookup across six signals on one key and one balance: Email verification, Phone intelligence, IP intelligence, Address verification, Business verification against open registers, and Cross-signal consistency, which runs any subset of the first four in one call and names where they agree and where they do not. The address side reads a written address against its own country's format, names the kind of delivery point behind it, checks the postcode against the national directory and follows the building into that country's own address register where one is installed.
## Geoapify
Geoapify is a location platform (maps, routing, places and geocoding) whose Geocoding API is also sold for address validation: an address is matched against open datasets such as OpenStreetMap and OpenAddresses and returned with coordinates and confidence scores for the city, the street and the building.
Comparison: https://spaw.co/compare/geoapify
## Ideal Postcodes
Ideal Postcodes is a UK-first address lookup, autocomplete and cleansing API answered from Royal Mail's Postcode Address File, with Ordnance Survey AddressBase Core and Premium offered as additional datasets, sold as pay-as-you-go credit packs, with international address data for more than 240 countries and territories; its site also lists phone and email validation among its products.
Comparison: https://spaw.co/compare/ideal-postcodes
## Lob
Lob is a print-and-mail platform whose Address Verification API validates, standardises and enriches US and international addresses, CASS certified, with USPS delivery-point data and a deliverability likelihood drawn from Lob's own mail delivery records.
Comparison: https://spaw.co/compare/lob
## Loqate
Loqate sells address capture and address verification alongside email, phone and bank validation, priced per lookup from a pay-as-you-go credit balance, with international data for over 245 countries and territories.
Comparison: https://spaw.co/compare/loqate
## Melissa
Melissa sells address verification for the United States and "250+ Countries & Territories" against postal data, CASS and DPV certified, with a credit model for pay-as-you-go use and annual subscriptions sized by records, alongside name, phone, email and identity verification.
Comparison: https://spaw.co/compare/melissa
## Postcode.eu
Postcode.eu, from Postcode.nl B.V., sells address autocomplete and validation APIs for fourteen European countries, each answered from the official sources its pages name (government address registers for most, the postal operator's file for others), priced per validation inside an annual tier.
Comparison: https://spaw.co/compare/postcode-eu
## Postcoder
Postcoder is a UK-first address lookup API sold as credits that any of its services can spend: address lookup, bank validation, email validation, mobile validation and OTP verification, each costing a published number of credits per request.
Comparison: https://spaw.co/compare/postcoder
## PostGrid
PostGrid sells an address verification API for the United States and Canada, CASS and SERP certified against USPS and Canada Post data, with an international service for "245+ Countries and Territories", autocomplete, parsing, batch verification and a Salesforce integration, priced by quotation.
Comparison: https://spaw.co/compare/postgrid
## Smarty
Smarty sells US address verification against postal data: a lookup validates to the secondary level and returns delivery-point metadata that open registers cannot answer, including a DPV match code, the residential indicator and vacancy status.
Comparison: https://spaw.co/compare/smarty
Google Address Validation API and the other product names are trademarks of their owners. Spaw is not affiliated with them.
Reference: https://spaw.co/alternatives/google-address-validation
---
# Spaw vs Hunter: email verification API comparison
How Spaw and Hunter compare on pricing, billing rules, verification depth and limits, from Hunter's public pages checked on 2026-09-13. Corrections: support@spaw.co.
| | Spaw | Hunter |
| --- | --- | --- |
| Pricing model | Pay-as-you-go credit packs whose credits never expire, or optional plans, billed monthly or yearly, that deposit the same credits every month for less and roll unused plan credits over up to three months' allowance; both through Stripe Checkout | Monthly or yearly subscriptions with unified credits shared across search, verification and enrichment; credit packs on paid plans, usable for up to 12 months from purchase according to the pricing page |
| Free tier | 100 free credits every month, no card required | Free plan with 50 credits a month; one verification costs 0.5 credit, so 100 verifications |
| Price per 1,000 at about 10k | $7.80 (Growth pack: 5,000 credits for $39), or $5.80 on the Growth monthly plan (5,000 credits for $29 a month) | $7.45 (Growth: $149 a month for 10,000 credits, 20,000 verifications at 0.5 credit each; $5.20 billed yearly) |
| Price per 1,000 at about 100k | $4.49 (Volume pack: 100,000 credits for $449), or $3.49 on the Volume monthly plan (100,000 credits for $349 a month) | $5.98 (Scale: $299 a month for 25,000 credits, 50,000 verifications; larger volumes need an Enterprise quote) |
| Unknown or catch-all results billed | No: an unconfirmed mailbox (reason unverified) and an answer Spaw could not obtain because its handshake partner was unavailable are free and leave no 7-day repeat marker, so a later lookup is a fresh one; a catch-all answer costs 1 credit like any risky answer, and an automatic re-check bills 1 credit only when it confirms the mailbox | No |
| Undeliverable results billed | No, within a fair use: undeliverable answers that needed a mailbox probe are free up to max(500, your billed email answers) in a rolling 30 days, then 1 credit each; the rest are always free | Yes |
| SMTP mailbox check | Yes | not on the pages read |
| Catch-all handling | A catch-all answer is remembered per domain for 7 days, and every unconfirmed mailbox carries a published 0 to 100 mailbox_confidence estimate | accept_all is one of six statuses (valid, invalid, accept_all, webmail, disposable, unknown); a completed verification costs 0.5 credit, a failed one none |
| Bulk limit | 100,000 addresses per bulk job, with an HMAC-signed completion webhook | not on the pages read |
| Batch API | 50 addresses per synchronous request, probes sent concurrently | not on the pages read |
| Rate limit | 5 requests per second per key on the email and consistency endpoints | Email Verifier: 10 requests a second and 300 a minute per key |
| Official SDKs | Python, Node.js, PHP | not on the pages read |
| Data retention | Only email verification keeps a history row, for 30 days, then pruned; test addresses are never logged | not on the pages read |
## When Hunter fits better
Sales teams that prospect with Hunter's finder and campaigns and verify addresses as part of that workflow.
- Verification is 0.5 credit; searches and enrichment use the same credits
- Monthly plan credits expire at the end of the billing cycle; yearly plans get the year's credits upfront
- Auto-verification included on paid plans
- USD prices as read on 2026-09-03: Starter $49, Growth $149, Scale $299 a month, or $34, $104, $209 billed yearly; the pricing page shows a visitor local prices, and to a UK reader on 2026-09-13 they were £41, £125 and £251 a month, or £28, £87 and £175 billed yearly
- The help centre still says credit packs are valid for 3 months while the pricing page says 12; the newer page is quoted above
## When Spaw fits better
Developers who want one flat price per answered lookup across every signal a signup form produces, a transparent risk score, and an API that documents every field.
- Spaw sells verification alone, as pack credits that never expire or as plans whose unused credits roll over up to three months' allowance; Hunter's plan credits expire with the billing cycle
- Hunter's free plan allows 100 verifications a month against Spaw's monthly free grant, listed at spaw.co/pricing
- Both skip billing when a verification cannot be completed; Spaw also leaves undeliverable verdicts free within a fair-use allowance
- Spaw returns 46 named fields and a machine-readable reason; Hunter's verifier returns one of six statuses
## Questions
**Does Spaw charge for catch-all or unknown results?** Spaw bills per answer, not per probe outcome. A catch-all domain comes back risky with reason catch_all and costs 1 credit like any risky answer. A mailbox the handshake could not confirm (greylisting, a timeout, no definite answer) comes back risky with reason unverified and is free, and so is an answer Spaw could not obtain at all because its handshake partner was unavailable; neither leaves a 7-day repeat marker, so a later lookup is a fresh one (an uncertain result is reused for up to 15 minutes, then the mailbox is asked again) and bills 1 credit only once the mailbox is confirmed, and the automatic re-checks behind callback_url bill 1 credit only when they confirm it. Undeliverable verdicts are free within a fair use of max(500, your billed email answers) in a rolling 30 days, and invalid input and repeats within 7 days are free too. Hunter states that it does not bill unknown results.
**Can I move a list from Hunter to Spaw?** Yes. Upload the CSV to the dashboard or POST it to the bulk endpoint; Spaw keeps your columns, adds the verdict columns, and lets you download only the deliverable, risky or undeliverable rows. Addresses that come back undeliverable join your suppression list so later runs skip them for free.
**Is there a free tier?** Every Spaw account receives 100 free credits at the start of each month with no card on file, and unused free credits accumulate. Hunter offers: Free plan with 50 credits a month; one verification costs 0.5 credit, so 100 verifications.
## Sources (checked 2026-09-13)
- [Hunter pricing](https://hunter.io/pricing)
- [Hunter plans (help center)](https://help.hunter.io/en/articles/6001212-all-in-one-outreach-platform-plans)
- [Hunter pricing FAQ](https://help.hunter.io/en/articles/11131690-pricing-plans-and-feature-faqs)
- [Hunter rate limits](https://help.hunter.io/en/articles/1971004-is-there-a-request-p-second-limit)
- [Hunter API documentation](https://hunter.io/api-documentation/v2)
Hunter is a trademark of its owner. Spaw is not affiliated with it.
Reference: https://spaw.co/compare/hunter
---
# Hunter alternatives for email verification
Email verification APIs to consider instead of Hunter, with the pricing model, free tier and price per 1,000 lookups each vendor publishes. Figures come from public pricing pages checked on 2026-09-13.
| Service | Pricing model | Free tier | Price per 1,000 at about 10k |
| --- | --- | --- | --- |
| [Spaw](https://spaw.co) | Pay-as-you-go credit packs whose credits never expire, or optional plans, billed monthly or yearly, that deposit the same credits every month for less and roll unused plan credits over up to three months' allowance; both through Stripe Checkout | 100 free credits every month, no card required | $7.80 (Growth pack: 5,000 credits for $39), or $5.80 on the Growth monthly plan (5,000 credits for $29 a month) |
| [Abstract API](https://www.abstractapi.com) | Monthly subscription tiers with a request slider from 5,000 to 150,000 requests a month; Starter from $17, Standard from $37, Professional from $39 a month | 100 requests on the Free plan | not on the pages read |
| [Bouncer](https://www.usebouncer.com) | Pay-as-you-go credits that never expire (from 1,000 credits for $8 up to 1,000,000 for $2,000), auto-refill, plus a Deliverability Kit subscription at $25 to $250 a month | 100 free credits, no card required | $6.00 (10,000 credits for $60) |
| [Emailable](https://emailable.com) | Pay-as-you-go credits that never expire, or a monthly subscription at a 15% discount; minimum purchase 5,000 credits | 250 free credits at signup | $7.60 (the pay-as-you-go rate printed for the 5,000-credit package, $0.0076 a credit; larger packages are priced in a calculator, and the 5,000-credit subscription is $32.30 a month after the 15% discount) |
| [Kickbox](https://kickbox.com) | not on the pages read | not on the pages read | not on the pages read |
| [Mailboxlayer](https://mailboxlayer.com) | Monthly or yearly subscription (10 to 15% off yearly) with a request quota per plan and per-request overage | 100 requests a month | $3.00 for the Basic quota (5,000 requests for $14.99 a month; overage $0.012 a request, so 10,000 requests on Basic cost about $74.95, $7.50 per 1,000); Professional Plus: 50,000 for $74.99, $1.50 per 1,000 |
| [NeverBounce](https://www.neverbounce.com) | not on the pages read | not on the pages read | not on the pages read |
| [Verifalia](https://verifalia.com) | Pay-as-you-go credit packs that never expire, or monthly, quarterly and yearly subscriptions; verification depth is priced as quality levels; pack and plan prices are shown in a calculator rather than listed | 25 free credits a day on the Free plan; unused daily credits expire each day | not on the pages read |
| [ZeroBounce](https://www.zerobounce.net) | Pay-as-you-go credits from $39 for 2,000 addresses, 15% off with autopay, or the ZeroBounce ONE subscription at $99 a month with unused credits rolling over; volume pricing shown through a calculator rather than fixed packs | 100 validation credits a month on the free account | not on the pages read |
## Spaw
Verification API that bills per answered lookup across six signals on one key and one balance: Email verification, Phone intelligence, IP intelligence, Address verification, Business verification against open registers, and Cross-signal consistency, which runs any subset of the first four in one call and names where they agree and where they do not. The email side checks syntax, live MX, SPF and DMARC, disposable and role lists, and runs an SMTP mailbox handshake that never sends a message.
## Abstract API
Abstract API's email validation endpoint is one of a family of utility APIs sold as monthly request quotas with a free 100-request plan.
Comparison: https://spaw.co/compare/abstract
## Bouncer
Bouncer is an email verification service with pay-as-you-go credits that never expire, high-throughput batch APIs, a real-time form product (Bouncer Shield) and a Deliverability Kit subscription.
Comparison: https://spaw.co/compare/bouncer
## Emailable
Emailable is an email verification service with pay-as-you-go credits, a bulk verifier, a widget and an API, with Node.js, Ruby and Python libraries.
Comparison: https://spaw.co/compare/emailable
## Kickbox
Kickbox is an email verification API that returns deliverable, undeliverable, risky or unknown plus a 0 to 1 Sendex quality score, with official libraries for PHP, Node.js, Ruby and Python.
Comparison: https://spaw.co/compare/kickbox
## Mailboxlayer
Mailboxlayer is apilayer's email validation API, sold as monthly request quotas from a free 100-request plan up to 250,000 requests a month.
Comparison: https://spaw.co/compare/mailboxlayer
## NeverBounce
NeverBounce is a ZoomInfo-owned email verification service with a bulk list-cleaning workflow, a real-time API and official client libraries in six languages.
Comparison: https://spaw.co/compare/neverbounce
## Verifalia
Verifalia is an email verification service with selectable quality levels, credit packs that never expire, subscriptions with daily free credits, and official SDKs in six languages.
Comparison: https://spaw.co/compare/verifalia
## ZeroBounce
ZeroBounce is an email validation service with a pay-as-you-go credit model and a subscription (ZeroBounce ONE) that bundles deliverability tools such as activity data, inbox placement tests and blacklist monitoring.
Comparison: https://spaw.co/compare/zerobounce
Hunter and the other product names are trademarks of their owners. Spaw is not affiliated with them.
Reference: https://spaw.co/alternatives/hunter
---
# Spaw vs Ideal Postcodes: address verification API comparison
How Spaw and Ideal Postcodes compare on pricing, billing rules, the registers behind each answer and limits, from Ideal Postcodes' public pages checked on 2026-09-22. Corrections: support@spaw.co.
| | Spaw | Ideal Postcodes |
| --- | --- | --- |
| Pricing model | Pay-as-you-go credit packs whose credits never expire, or optional plans, billed monthly or yearly, that deposit the same credits every month for less and roll unused plan credits over up to three months' allowance; both through Stripe Checkout | Pay-as-you-go credit packs, "No Subscriptions. No Monthly Fees.": 200 credits for £9 ("£0.045 per lookup"), 1,100 for £42 (£0.038), 4,300 for £155 (£0.036), 12,800 for £420 (£0.033) and 32,100 for £900 (£0.028). "Credits are valid for 12 months"; the lookup guide adds that the timer "only starts when you begin using that batch of credits". Larger volumes and UK registered charities are priced on request. |
| Free tier | 100 free credits every month, no card required | A trial rather than a standing tier: "1-Month Trial", "50 free credits, full support and no credit card needed"; the trial searches worldwide except Ireland, "which requires separate licensing" |
| Price per 1,000 at about 10k | $7.80 (Growth pack: 5,000 credits for $39), or $5.80 on the Growth monthly plan (5,000 credits for $29 a month) | £33 per 1,000 lookups on the 12,800-credit pack (£420, "£0.033 per lookup"), the smallest pack that covers 10,000. |
| Price per 1,000 at about 100k | $4.49 (Volume pack: 100,000 credits for $449), or $3.49 on the Volume monthly plan (100,000 credits for $349 a month) | £28 per 1,000 lookups on the largest published pack (32,100 credits for £900, "£0.028 per lookup"); larger volumes are a custom package. |
| Addresses no register covered billed | An address that stands as written costs 1 credit whether or not a register carried the building; where no register covers the country, exists answers null rather than a verdict | No |
| Addresses that cannot stand billed | No: an address that cannot stand as written is free whatever the reason, and so are repeats within 7 days and rows repeating an address already in the same job | Not when nothing comes back: "Invalid postcodes (ones that return nothing) are free" and a cleanse's "no-match response is free". But "A cleanse that returns a match costs a lookup", and the cleanse returns the closest match, which its documentation says can be a poor one. Autocomplete suggestions cost nothing until a full address is returned. |
| Registers held | Open address registers for 23 countries and territories, each named in the answer with its declared coverage, complete or partial. Complete, building by building: Australia, Austria, Belgium, Czechia, Latvia, Luxembourg, Netherlands, Norway and Switzerland. Partial, building by building: Brazil, Denmark, France (with French Guiana, Guadeloupe, Martinique, Mayotte, Réunion, St. Barthélemy, St. Martin and St. Pierre & Miquelon), Germany and United States. Partial, streets only: Great Britain. House-number ranges as well: United States. Absence from a partial register is never read as evidence that an address does not exist. | Named per country on the coverage table: Royal Mail PAF and OS AddressBase Core for the United Kingdom, Guernsey, Jersey and the Isle of Man; the Eircode Address File (ECAF) and Eircode Address Database (ECAD) for Ireland; USPS for the United States, its territories and the freely associated states; Geoscape G-NAF for Australia, Christmas Island, the Cocos (Keeling) Islands and Norfolk Island; Kadaster for the Netherlands; and Kartverket for Norway and for Svalbard and Jan Mayen. The other countries' rows name no dataset. The pricing page lists Multiple Residence, Not Yet Built, Northern Ireland Pointer, Eircode, AddressBase Core and AddressBase Premium as additional datasets. |
| Country coverage | Postal formats for 207 countries and territories: the line layout for 199 of them, and the required fields and postcode pattern wherever the table records them; 52 postal operators' rules for naming a delivery point that is not a street; open address registers for 23 countries and territories; postcode directories for Great Britain from the ONS Postcode Directory, with Northern Ireland excluded because reusing BT postcodes needs a separate licence, and for 108 more countries and territories from GeoNames' postal codes. | The coverage page counts "243 Countries & territories", "8 Data providers" and "103 With postal codes", grading each country from sub-building down to region level; the pricing page says "240+". |
| Claims post is delivered | No, and every answer says so: is_deliverable, is_cmra, is_vacant and is_residential answer null because that check is licensed and not enabled here. exists is a match in an open address register, never a statement that post arrives. | Not as a deliverability flag. A UK match is a Postcode Address File record returned with its UDPRN and delivery point suffix; the pages read make no separate claim about whether post arrives. |
| Bulk limit | 100,000 addresses per bulk job, with an HMAC-signed completion webhook | ClickCleanse cleans "up to 1,100 UK addresses in your browser"; larger Bulk Cleanse jobs ("Batch-clean thousands of addresses in one go") are arranged on request. |
| Batch API | 100 addresses per synchronous request | not on the pages read |
| Rate limit | 50 requests per second per key on the address endpoints | "Each IP address is rate limited at 30 requests per second"; tripping the limit returns a 503, and the autocomplete API carries an additional limit. |
| Official SDKs | Python, Node.js, PHP | not on the pages read |
| Data retention | No history row and no log line carrying the address. What a lookup leaves is the 7-day marker that makes a repeat free and the count behind seen_by_you, both kept under a digest, never the address. The address itself is kept only on the account's own suppression list and in its monitors. Bulk input lists and result files are deleted with the job after 30 days. | Paid lookups are logged per key with the IP address, search term and URL, downloadable as CSV. Personal data in the log is redacted weekly: the key-logs page says "By default we redact PII older than 28 days" and, further down, that it is redacted "if older than 21 days"; the interval is set from the dashboard, and "Set the interval to 0 days to prevent PII collection altogether". |
## When Ideal Postcodes fits better
UK checkouts and CRMs that want a postcode-to-address picker and address cleansing from Royal Mail's address file, with UPRNs and coordinates included, paid for by the lookup rather than by subscription.
- UPRNs and rooftop geocodes with every UK lookup at no extra cost
- Autocomplete suggestions are free; a credit is spent only when a full address is returned
- A cleanse that finds no match costs nothing: "A no-match response is free."
- One-off credit packs with no subscription, valid for 12 months
- UK address data "refreshed daily against Royal Mail's PAF and Ordnance Survey datasets"
- Discounted pricing for UK registered charities
## When Spaw fits better
Developers who want a written address parsed against its own country's rules and checked against open registers, where a field nobody could evaluate answers null instead of a guess.
- Ideal Postcodes can list the addresses at a UK postcode; Spaw cannot, because every full UK list derives from the licensed Postcode Address File: for a postcode in Great Britain its postcode endpoint answers the streets Ordnance Survey's Open Names files under the postcode's district, with no house numbers
- Spaw reads a written address against its own country's format and follows the building into that country's open address register where one is installed; Great Britain has no open building register, so an address in Great Britain is checked against the postcode directory and the streets Ordnance Survey lists, and a listed street never means a building exists
- Ideal Postcodes logs each paid lookup's search term against the key until a redaction interval the account sets; Spaw keeps no history row and no log line carrying the address, only a hashed marker that makes a repeat free
- Spaw's pack credits never expire and one balance buys every signal it sells; an address that cannot stand as written costs nothing
## Questions
**Does Spaw charge for addresses no register covers?** An address that stands as written costs 1 credit, whether or not a register carried the building. An address that cannot stand costs nothing, whatever the reason, and a repeat within seven days is free. Where no register covers the country, or where the register that answered is one whose coverage is partial and does not carry the address, exists answers null — not evaluated — rather than a verdict, and the not_in_register weight cannot fire. Ideal Postcodes states that it does not bill unknown results.
**Can I move an address list from Ideal Postcodes to Spaw?** Yes. Upload the CSV in the dashboard or POST it to the bulk endpoint, up to 100,000 addresses a job; Spaw keeps your columns and adds 64 result columns after them (the address as submitted, the answer, the datasets behind it, what the row cost and whether your suppression list answered it), and a country column gives each row its own country. Rows repeating an address already in the list are answered once, cost nothing, and are counted as duplicates.
**Is there a free tier?** Every Spaw account receives 100 free credits at the start of each month with no card on file, and unused free credits accumulate. Ideal Postcodes offers: A trial rather than a standing tier: "1-Month Trial", "50 free credits, full support and no credit card needed"; the trial searches worldwide except Ireland, "which requires separate licensing".
## Sources (checked 2026-09-22)
- [Ideal Postcodes pricing](https://ideal-postcodes.co.uk/pricing)
- [Ideal Postcodes: what is a lookup and a credit](https://ideal-postcodes.co.uk/guides/what-is-a-lookup)
- [Ideal Postcodes global addressing coverage](https://ideal-postcodes.co.uk/global-addressing)
- [Ideal Postcodes address cleansing](https://ideal-postcodes.co.uk/address-cleanse)
- [Ideal Postcodes API reference](https://docs.ideal-postcodes.co.uk/docs/api/api-reference/)
- [Ideal Postcodes cleanse API](https://docs.ideal-postcodes.co.uk/docs/api/address-cleanse/)
- [Ideal Postcodes key logs](https://docs.ideal-postcodes.co.uk/docs/api/key-logs/)
Ideal Postcodes is a trademark of its owner. Spaw is not affiliated with it.
Reference: https://spaw.co/compare/ideal-postcodes
---
# Ideal Postcodes alternatives for address verification
Address verification APIs to consider instead of Ideal Postcodes, with the pricing model, free tier and price per 1,000 lookups each vendor publishes. Figures come from public pricing pages checked on 2026-09-22.
| Service | Pricing model | Free tier | Price per 1,000 at about 10k |
| --- | --- | --- | --- |
| [Spaw](https://spaw.co) | Pay-as-you-go credit packs whose credits never expire, or optional plans, billed monthly or yearly, that deposit the same credits every month for less and roll unused plan credits over up to three months' allowance; both through Stripe Checkout | 100 free credits every month, no card required | $7.80 (Growth pack: 5,000 credits for $39), or $5.80 on the Growth monthly plan (5,000 credits for $29 a month) |
| [Geoapify](https://www.geoapify.com) | Daily credit quotas on monthly subscriptions: Free (3,000 credits a day), API 10 (10,000 a day, $59 billed monthly), API 25 (25,000, $109), API 50 (50,000, $179), API 100 (100,000, $299), API 250 (250,000, $609) and Custom ("Unmetered credits / day", from $860). "1 Geocoding API request costs 1 credit"; in a batch each address counts 0.5 credits, plus 1 credit to create the job and 1 for each result request. A yearly term is offered, but its prices are not in the page text read. | A standing free plan: "3,000 credits / day", "No credit card required", up to 5 requests a second, usable for "commercial websites and apps" with the required Geoapify and data-source attribution | not on the pages read |
| [Google Address Validation API](https://mapsplatform.google.com) | "Pay-as-you-go", "charging per request according to the pricing table", under two SKUs named on the billing page, Address Validation Pro and Address Validation Enterprise; the per-request rates render inside the pricing table and are not stated in the page text as fetched, so no figure is quoted here. | not on the pages read | not on the pages read |
| [Lob](https://www.lob.com) | Verifications are priced per call on top of a print-and-mail plan: on the Developer plan ($0 a month) "US Address Verifications $0.05/additional" and international $0.07; on Startup ($260 a month) $0.025 with "1,000 included for $25/mo" and international $0.06 with 500 included for $30 a month; on Growth ($550 a month) $0.009 with "50,000 included for $450/mo" and international $0.055 with 10,000 included for $550 a month; Enterprise is "Contact us for details". | The Developer plan is "Starting at $0 / month" with verifications billed per call; the site offers "Get started for free". | $50 per 1,000 US verifications on the Developer plan ($0.05 each) or $25 per 1,000 on Startup ($0.025 each beyond the 1,000 included for $25 a month); international $70 or $60. |
| [Loqate](https://www.loqate.com) | Pay-as-you-go credit purchases of £50, £100, £250, £500 and £1,000 (or 75€, 150€ and 300€), each buying a published per-lookup rate for every service, plus bespoke plans; "Credit is valid for 12 months after the purchase date" and there are no setup costs. The pricing page shows two rate tables headed "New Pricing" and "Old Pricing" without saying which accounts each applies to; the figures here are from "New Pricing". Its usage-example selector also lists a £2,500 plan that neither table prices. | A trial rather than a standing tier: "Start your 45-day free trial"; the FAQ adds "You get free credits to test the service" | £82 to £86 per 1,000 Address Verify lookups on the "New Pricing" table: 8.2p a lookup on the £100, £250 and £500 tiers and 8.6p on the £50 and £1,000 tiers. Address Capture is 8.6p to 4.9p in the UK and 7.1p to 4.1p for the rest of the world. |
| [Melissa](https://www.melissa.com) | US Address Verification pay-as-you-go "Starting at $40" for 10,000 credits at "10 Credits Per Address" (the same page's FAQ says a US address equals 3 credits and a global address 10); an API subscription "Starting at $5,145" for 1,000,000 records a year; an unlimited-records plan "Starting at $16,000". Global Address Verification subscriptions from $40 a month for 1,000 records and $95 a month for 2,500. | Global Address Verification "GAV Free" at $0 for 250 records a month; 1,000 lookup credits a month after a one-time $4.97 KYC/AML verification fee, described on the pricing page. | $40 per 1,000 US addresses on the smallest pay-as-you-go pack, taking the pack's own "10 Credits Per Address" ($40 buys 10,000 credits). |
| [Postcode.eu](https://www.postcode.eu) | Annual tiers that include validations: Tier 1 €50, Tier 2 €100, Tier 3 €250, Tier 4 €500 and Tier 5 €1,000 a year, each with a per-validation rate by country group. "Up to the Tier amount you can perform a mix of validations", subscriptions "are automatically upgraded when the maximum of the Tier is reached", and past the highest Tier usage "will be invoiced in blocks of 10% and the rates of this Tier apply". A validation is "a bundle of related requests that were used to validate an address". An SLA (€1,000 + 2% a year) and a DORA addendum (€2,500 a year) are extra. | A trial rather than a standing tier: "After registering an account it is possible to try the APIs for free", and "Testing the API is without obligations" | €17.64 per 1,000 validations on Tier 3 (€250 a year, "€0.01764 each") for Austria, Belgium, Denmark, Finland, France, Luxembourg, Norway, Spain, Sweden, Switzerland and the Netherlands. Germany, Italy and the United Kingdom cost more a validation, so 10,000 of them fall in Tier 4 (€500 a year): €34.17 per 1,000 for Germany (€0.03417 each) and €43.30 for Italy and the United Kingdom (€0.0433 each). "Tier prices are including validations" and "Prices and quantities are per year", so the tier amount is paid in full: 10,000 validations cost €250 a year for the eleven countries (€25 per 1,000) and €500 for Germany, Italy or the United Kingdom (€50 per 1,000). |
| [Postcoder](https://postcoder.com) | Credit packs (500 to 200,000 credits, "valid for 12 months from purchase") or monthly plans (1,000 to 100,000 credits, with unused credits not carried over). "Your credits can be used for any Postcoder request." The pack prices themselves render as placeholders ("£--") in the pack list; the credit calculator's recommendation box shows "Credit pack £25 500 Credits" and "Monthly plan £25 1,000 Credits". Larger packs and plans with volume discounts are quoted. | A free trial is offered; its duration and allowance are not stated on the pricing page | £100 per 1,000 UK address lookups pay-as-you-go (10.00p a request, 2.0 credits) or £45 per 1,000 on a monthly plan (4.50p). This is the address comparison, so only the address rate is quoted: the same account buys email and mobile validation at their own per-request rates, which are read off the same page. |
| [PostGrid](https://www.postgrid.com) | Quoted: the pricing page asks for annual mail-piece and address-verification volumes in bands from "Less than 50,000" to "More than 1,000,000" and publishes no rate. | "Try for Free. Sign Up for Address Verification API" on the product page; no standing free volume is stated. | not on the pages read |
| [Smarty](https://www.smarty.com) | Annual lookup bundles on the Professional plan from $552 a year, sized 12K, 60K, 120K, 300K, 600K, 1M or 2M lookups; a monthly option and bespoke Custom pricing are offered. Only the $552 entry price is shown on the page. A subscription "expires when the address lookups or the time runs out, whichever comes first"; there are no overage costs, and an exhausted plan answers 402 "payment required" unless auto-renewal buys a new block. | A free trial rather than a standing tier: "42 days" and "1,000 address lookups", "No credit card required" | not on the pages read |
## Spaw
Verification API that bills per answered lookup across six signals on one key and one balance: Email verification, Phone intelligence, IP intelligence, Address verification, Business verification against open registers, and Cross-signal consistency, which runs any subset of the first four in one call and names where they agree and where they do not. The address side reads a written address against its own country's format, names the kind of delivery point behind it, checks the postcode against the national directory and follows the building into that country's own address register where one is installed.
## Geoapify
Geoapify is a location platform (maps, routing, places and geocoding) whose Geocoding API is also sold for address validation: an address is matched against open datasets such as OpenStreetMap and OpenAddresses and returned with coordinates and confidence scores for the city, the street and the building.
Comparison: https://spaw.co/compare/geoapify
## Google Address Validation API
Google's Address Validation API, part of Google Maps Platform, validates, standardises and geocodes an address, reports a confirmation level per component with inferred and replaced parts, and can run US and Puerto Rico addresses through USPS CASS on request.
Comparison: https://spaw.co/compare/google-address-validation
## Lob
Lob is a print-and-mail platform whose Address Verification API validates, standardises and enriches US and international addresses, CASS certified, with USPS delivery-point data and a deliverability likelihood drawn from Lob's own mail delivery records.
Comparison: https://spaw.co/compare/lob
## Loqate
Loqate sells address capture and address verification alongside email, phone and bank validation, priced per lookup from a pay-as-you-go credit balance, with international data for over 245 countries and territories.
Comparison: https://spaw.co/compare/loqate
## Melissa
Melissa sells address verification for the United States and "250+ Countries & Territories" against postal data, CASS and DPV certified, with a credit model for pay-as-you-go use and annual subscriptions sized by records, alongside name, phone, email and identity verification.
Comparison: https://spaw.co/compare/melissa
## Postcode.eu
Postcode.eu, from Postcode.nl B.V., sells address autocomplete and validation APIs for fourteen European countries, each answered from the official sources its pages name (government address registers for most, the postal operator's file for others), priced per validation inside an annual tier.
Comparison: https://spaw.co/compare/postcode-eu
## Postcoder
Postcoder is a UK-first address lookup API sold as credits that any of its services can spend: address lookup, bank validation, email validation, mobile validation and OTP verification, each costing a published number of credits per request.
Comparison: https://spaw.co/compare/postcoder
## PostGrid
PostGrid sells an address verification API for the United States and Canada, CASS and SERP certified against USPS and Canada Post data, with an international service for "245+ Countries and Territories", autocomplete, parsing, batch verification and a Salesforce integration, priced by quotation.
Comparison: https://spaw.co/compare/postgrid
## Smarty
Smarty sells US address verification against postal data: a lookup validates to the secondary level and returns delivery-point metadata that open registers cannot answer, including a DPV match code, the residential indicator and vacancy status.
Comparison: https://spaw.co/compare/smarty
Ideal Postcodes and the other product names are trademarks of their owners. Spaw is not affiliated with them.
Reference: https://spaw.co/alternatives/ideal-postcodes
---
# Spaw vs ip-api: IP intelligence API comparison
How Spaw and ip-api compare on pricing, billing rules, the feeds behind each flag and limits, from ip-api's public pages checked on 2026-09-12. Corrections: support@spaw.co.
| | Spaw | ip-api |
| --- | --- | --- |
| Pricing model | Pay-as-you-go credit packs whose credits never expire, or optional plans, billed monthly or yearly, that deposit the same credits every month for less and roll unused plan credits over up to three months' allowance; both through Stripe Checkout. One credit buys five answered IP lookups | Two tiers. The free endpoint: "Our endpoints are limited to 45 HTTP requests per minute from an IP address" and "We do not allow commercial use of the free endpoint". The Pro service: "€13.3 per month, when paying for 12 months", with "Unlimited requests", "SSL (HTTPS)", "Commercial use allowed" and a "99.9% SLA" ("If the general API uptime falls below 99.9% in a month, we will automatically extend your subscription for 10 days"). |
| Free tier | 100 free credits every month, no card required | The free endpoint, for non-commercial use only: 45 requests per minute per address over HTTP ("256-bit SSL encryption is not available for this free API") |
| Price per 1,000 at about 10k | $1.80 per 1,000 IP lookups at 5 to the credit (Starter pack: 1,000 credits for $9), or $1.40 on the Starter monthly plan (1,000 credits for $7 a month) | not on the pages read |
| Price per 1,000 at about 100k | $1.56 per 1,000 IP lookups at 5 to the credit (Growth pack: 5,000 credits for $39), or $1.16 on the Growth monthly plan (5,000 credits for $29 a month) | not on the pages read |
| Addresses no feed covered billed | One credit buys five answered addresses whatever their flags say; a flag no feed could evaluate answers null rather than false, and the answer costs the same | not on the pages read |
| Reserved and private ranges billed | No: reserved, private, loopback, CGNAT, documentation and multicast ranges answer reason reserved_range and are free, unallocated space is free, and so are repeats within 7 days | not on the pages read |
| Feeds behind the flags | Public feeds, each named in the answer with the date it was synced: DB-IP Lite and RFC 8805 geofeeds for location, the five registries' delegated statistics for allocation, Spamhaus DROP and ASN-DROP and abuse.ch Feodo Tracker for blocklists, the Tor Project's exit list and Onionoo, the Mullvad, NordVPN and Private Internet Access server lists plus the open X4BNet lists, Apple's iCloud Private Relay egress list, the cloud providers' own published ranges, and the verification files Google, Microsoft, OpenAI, Perplexity, DuckDuckGo and Ahrefs publish for their crawlers. | Not published as feeds. The answer carries three flags, each described in one line: mobile ("Mobile (cellular) connection"), proxy ("Proxy, VPN or Tor exit address") and hosting ("Hosting, colocated or data center"); the pages do not say where they come from. |
| Refresh cadence | Every feed re-syncs daily, the botnet command-and-control list hourly, and the geofeed crawl weekly. Each response names every dataset it consulted with the date it was last synced, and /status lists them all. | "We update our database as soon as we have new information about an IP block" |
| Geolocation source | The DB-IP Lite database, overridden wherever the operator publishes the location itself — an RFC 8805 geofeed, Apple's relay list, a VPN provider's server list. location_source says which answered, and location_confidence is graded from a weekly accuracy report rather than asserted. | Its own database; the pages name no source. Location fields: continent, continentCode, country, countryCode, region, regionName, city, district, zip, lat, lon, timezone, offset, currency, isp, org, as, asname and reverse. |
| Bulk limit | 100,000 addresses per bulk job, with an HMAC-signed completion webhook | not on the pages read |
| Batch API | 1,000 addresses per synchronous request; repeats inside the list are looked up once | The batch endpoint takes "up to 100 IP addresses or objects" per request and is "limited to 15 requests per minute from an IP address" |
| Rate limit | 50 requests per second per key on the IP endpoints | 45 requests per minute per address on the free endpoint, 15 per minute for batch; "Unlimited requests" on Pro |
| Official SDKs | Python, Node.js, PHP | not on the pages read |
| Data retention | No history row and no log line carrying the address. What a lookup leaves is keyed and never holds the address: the 7-day marker that makes a repeat free and, under the same keyed hash, the count behind seen_by_you; privacy: true skips both. The account's own outcome reports are kept as salted hashes for 90 days. Bulk input lists and result files are deleted with the job after 30 days. | not on the pages read |
## When ip-api fits better
Non-commercial projects that fit under 45 requests a minute, and small commercial ones that want a flat, unlimited subscription over per-lookup pricing.
- The free endpoint is HTTP only and excludes commercial use; HTTPS and commercial use are what Pro sells
- Pro is a flat subscription with unlimited queries rather than a quota
- Proxy, VPN and Tor are folded into one `proxy` flag
- Database updates are described as immediate, "as soon as we have new information about an IP block"
## When Spaw fits better
Developers who want every public IP feed compiled, dated and named for them, with a risk score whose weights are published so it can be recomputed or ignored.
- ip-api answers one `proxy` flag for proxy, VPN and Tor together; Spaw answers is_tor, is_vpn, is_relay and is_corporate_proxy separately, names the provider when its own list has the address, and dates the feed it read
- ip-api's free endpoint excludes commercial use and HTTPS; Spaw's free monthly credits go through the same HTTPS API as paid lookups. Spaw sells pack credits that never expire at five IP lookups to the credit, and reserved, private, documentation and unallocated ranges are free
- Every dataset behind a Spaw IP answer is a public feed named in the response with the date it was synced, and the risk score is a published sum of weights anyone can recompute
- Spaw applies your own allow and deny lists inside every answer (an allow-list hit scores 0, a deny-list hit 100) and counts your own lookups of an address; neither is a field the other vendor's pages describe
## Questions
**Does Spaw charge for addresses its feeds do not cover?** Spaw bills per answered address, five to the credit: the answered lookup that opens a block costs 1 credit and the next four cost nothing. Reserved, private, loopback, CGNAT, documentation and multicast ranges answer reason reserved_range and are free, unallocated space is free, and so is a repeat of the same address within seven days. A flag no feed could evaluate answers null rather than false: the address is still billed, and the null tells you the signal was not checked instead of pretending it was. ip-api does not publish how it bills them.
**Can I run a log file or an export through Spaw?** Yes. A bulk run takes up to 100,000 addresses from the dashboard or the API and returns one row per input address, in input order, with every flag, the risk score and the signals that made it. Five answered addresses cost a credit, repeats inside the list are looked up once, reserved ranges cost nothing, and the finish is announced on an HMAC-signed webhook.
**Is there a free tier?** Every Spaw account receives 100 free credits at the start of each month with no card on file, and unused free credits accumulate. ip-api offers: The free endpoint, for non-commercial use only: 45 requests per minute per address over HTTP ("256-bit SSL encryption is not available for this free API").
## Sources (checked 2026-09-12)
- [ip-api documentation](https://ip-api.com/)
- [ip-api batch endpoint](https://ip-api.com/docs/api:batch)
- [ip-api Pro](https://members.ip-api.com/)
ip-api is a trademark of its owner. Spaw is not affiliated with it.
Reference: https://spaw.co/compare/ip-api
---
# ip-api alternatives for IP intelligence
IP intelligence APIs to consider instead of ip-api, with the pricing model, free tier and price per 1,000 lookups each vendor publishes. Figures come from public pricing pages checked on 2026-09-12.
| Service | Pricing model | Free tier | Price per 1,000 at about 10k |
| --- | --- | --- | --- |
| [Spaw](https://spaw.co) | Pay-as-you-go credit packs whose credits never expire, or optional plans, billed monthly or yearly, that deposit the same credits every month for less and roll unused plan credits over up to three months' allowance; both through Stripe Checkout. One credit buys five answered IP lookups | 100 free credits every month, no card required | $1.80 per 1,000 IP lookups at 5 to the credit (Starter pack: 1,000 credits for $9), or $1.40 on the Starter monthly plan (1,000 credits for $7 a month) |
| [AbuseIPDB](https://www.abuseipdb.com) | Plans with daily request allowances: Individual free "Forever!", Basic $25/mo or $228/year, Premium $99/mo or $1,068/year, Enterprise custom | Individual: free forever with no credit card, 1,000 check and report requests, 100 block checks and 5 bulk reports a day | not on the pages read |
| [IP2Location](https://www.ip2location.com) | IP2Location.io API, monthly query plans: Free $0 ("50K IP Geolocation API queries/month", "11 IP Attributes", "Attribution Required"), Starter "$49/month" ("150K IP Geolocation API queries/month", "20 IP Attributes"), Plus "$249/month" ("300K IP Geolocation API queries/month", "59 IP Attributes"), Security "$499/month" ("600K IP Geolocation API queries/month", "79 IP Attributes"); overage per increment Starter $10, Plus $50, Security $100. Databases, standard licence per year: DB1 "$99", DB2 "$399", DB8 "$1,299", DB18 "$2,799", with a site licence at ten times those figures. | "50K IP Geolocation API queries/month" with 11 attributes, basic geolocation and open-proxy detection only, "Attribution Required"; without a key "up to 1,000 queries per day" | not on the pages read |
| [ipapi](https://ipapi.co) | Monthly plans by lookup quota: Starter "$15 billed monthly" for 60,000 lookups, Professional "$25 billed monthly" for 150,000, Business "$49 billed monthly" for 500,000, Enterprise "$99 billed monthly" for 2,000,000, Premium "$199 billed monthly" for 6,000,000, Ultimate "$399 billed monthly" for 15,000,000; annual billing is 20% less. | A free trial of "up-to 1000 IP lookups in a day" (about 30,000 a month), "for testing & development purposes" and "Not for production use" | not on the pages read |
| [ipdata](https://ipdata.co) | Monthly plans by daily request quota: Personal "$10 per month" with "2,500 requests daily", Lite "$30 per month" with "10,000 requests daily", Startup "$50 per month" with "50,000 requests daily", Business "$120 per month" with "100,000 requests daily"; "20% off all annual plans" at the time of reading. | "1500 free requests daily", "restricted to non-commercial use"; on the free tier, over the quota "all further requests are blocked and you receive an email notification" | not on the pages read |
| [ipgeolocation.io](https://ipgeolocation.io) | Monthly credit plans, "A standard unified IP lookup uses 1 credit": Starter "$19/month" for "150000 API Credits Per month", Plus "$49/month" for 500,000, Pro "$79/month" for 1,000,000, Business "$129/month" for 2,000,000, Premium "$249/month" for 5,000,000, Enterprise "Contact Sales"; annual billing gives "2 months free". "Paid monthly plans reset every month. Paid annual plans reset every year". | "1000 API Credits Per day for life" with no credit card, "a hard limit of 1000 requests per day" and "No overages on Free"; the free plan returns "core location without the radius and confidence fields" | not on the pages read |
| [IPinfo](https://ipinfo.io) | Monthly plans by API request quota: Lite $0/mo, Core $26/mo for 150,000 requests, Max $163/mo for 250,000, Enterprise custom for "1M+". Database downloads are listed separately, "Starting at $1,000/month". | IPinfo Lite at "$0/mo", free forever with no credit card, giving unlimited API requests at country level only | not on the pages read |
| [IPQualityScore](https://www.ipqualityscore.com) | Monthly plans by lookup quota: Free $0 (1,000 lookups a month), Startup "As low as: $99 / Month" (5,000), SMB Basic "$499" (10,000), SMB+ "$999" (75,000), Enterprise custom. "Any valid request sent to our Proxy Detection, Email Verification, or Device Fingerprinting services including lookups performed through a batch CSV file check will consume a lookup credit." | 1,000 lookups a month, capped at 35 a day | not on the pages read |
| [Ipregistry](https://ipregistry.co) | Credits, "1 lookup = 1 request = 1 credit" (batch endpoints use "one credit per lookup in the batch request, plus 1"). Subscriptions: Starter "100,000 credits / mo" at "$29 $24 / month", Professional "300,000 credits / mo" at "$79 $66 / month", Business "1,150,000 credits / mo" at "$199 $166 / month", Scale "2,450,000 credits / mo" at "$399 $333 / month" (the lower figure is the annual rate). Pay-as-you-go packs: 15,000 credits for $10, 100,000 for $50, 250,000 for $100, 1,750,000 for $500. | "20,000 free credits on sign-up" with "No credit card, no time limit" | $0.67 (the $10 pack of 15,000 credits, $0.000667 a lookup) |
| [ipstack](https://ipstack.com) | Monthly plans by request quota: Free "100 Request/mo." at $0, Basic "50,000 Request/mo." at "$12.99/mo.", Professional "500,000 Request/mo." at "$59.99/mo.", Professional + "2,000,000 Request/mo." at "$99.99/mo.", Custom on request; yearly billing "Save up to 15%". Overage fees apply past the quota, with notifications at 75%, 90% and 100%. | "100 Request/mo." at $0, without SSL encryption, support or the paid modules (SSL, commercial use and the Location, Time Zone and Connection modules are listed for paid plans) | not on the pages read |
| [MaxMind](https://www.maxmind.com) | Per query, by service, with no minimum published: "If you estimate your volume of queries to be over $2,500 a month, contact us to discuss rates." | A no-cost trial is offered ("Experience the functionality and benefits of our GeoIP web services at no cost"); no free monthly allowance is stated on the page checked | $0.10 per 1,000 for Country ($0.0001 a query), $0.30 for City Plus ($0.0003) and $2.00 for Insights ($0.002) |
| [proxycheck.io](https://proxycheck.io) | not on the pages read | not on the pages read | not on the pages read |
| [Spur](https://spur.us) | not on the pages read | not on the pages read | not on the pages read |
## Spaw
Verification API that bills per answered lookup across six signals on one key and one balance: Email verification, Phone intelligence, IP intelligence, Address verification, Business verification against open registers, and Cross-signal consistency, which runs any subset of the first four in one call and names where they agree and where they do not. The IP side answers from local databases and compiled public feeds: where an address is, who runs it, and whether it is a visitor at all.
## AbuseIPDB
AbuseIPDB is a reputation service built on abuse reports its own users submit: a check returns an abuse confidence score with the report count, country, usage type, ISP and a Tor flag, and plans are sold as daily request allowances rather than credits.
Comparison: https://spaw.co/compare/abuseipdb
## IP2Location
IP2Location sells geolocation as downloadable databases (DB1 to DB26, licensed yearly) and, through IP2Location.io, as an API on monthly query plans whose higher tiers add proxy, VPN, Tor, datacenter, crawler and residential-proxy detection and a fraud score; the classic web service no longer takes new registrations.
Comparison: https://spaw.co/compare/ip2location
## ipapi
ipapi.co is a geolocation API sold as monthly lookup quotas, whose pricing page credits its data to DB-IP and the IP2Location LITE database; the answer carries location, country and network fields and no proxy, VPN or Tor flag.
Comparison: https://spaw.co/compare/ipapi
## ipdata
ipdata is a geolocation and threat API sold as daily request quotas, from a free non-commercial tier to a Business plan, whose pricing page says it does not keep records of the addresses looked up beyond a day; its documentation site refused automated reading, so the fields are not reproduced here.
Comparison: https://spaw.co/compare/ipdata
## ipgeolocation.io
ipgeolocation.io sells geolocation and a separate security API on monthly credit plans, with location built from its own measurements and a security answer that flags VPN, proxy, residential proxy and Tor with provider names, confidence scores and a 0 to 100 threat score.
Comparison: https://spaw.co/compare/ipgeolocation
## IPinfo
IPinfo is an IP data company that sells geolocation, ASN, privacy detection and company data as monthly API plans and as database downloads, collected through its own measurement network rather than assembled from third-party feeds.
Comparison: https://spaw.co/compare/ipinfo
## IPQualityScore
IPQualityScore sells fraud detection across several signals on one lookup credit: proxy and VPN detection, email verification and device fingerprinting all draw from the same monthly quota, and the IP answer carries a fraud score alongside the connection facts.
Comparison: https://spaw.co/compare/ipqualityscore
## Ipregistry
Ipregistry is a geolocation and threat API sold as credits, either monthly subscriptions or pay-as-you-go packs, whose security object answers abuser, attacker, bogon, cloud provider, proxy, relay, Tor, VPN, anonymous and threat flags and whose data is described as built from BGP tables, registry databases, geofeeds and latency measurements.
Comparison: https://spaw.co/compare/ipregistry
## ipstack
ipstack is a geolocation API on monthly request quotas from a 100-request free tier to a two-million-request plan, with a bulk endpoint and a security module reserved for its top plan; its documentation pages rendered only a header to our fetcher, so the field list is not reproduced here.
Comparison: https://spaw.co/compare/ipstack
## MaxMind
MaxMind sells GeoIP as three per-query web services — Country, City Plus and Insights — and as downloadable databases, with proxy and anonymiser detection reserved for the Insights tier.
Comparison: https://spaw.co/compare/maxmind
## proxycheck.io
proxycheck.io sells an API for checking whether an address is a proxy; every page we tried on 2026-09-12 answered 403 Forbidden to automated reading, so nothing about its pricing, limits, fields or data is reproduced here.
Comparison: https://spaw.co/compare/proxycheck
## Spur
Spur is an anonymity-infrastructure vendor: its Context API answers whether an address is a VPN, proxy or other tunnel with the operator named, and its feeds list anonymous, residential-proxy and datacenter address space; its pricing page rate-limited our fetcher, so prices are not reproduced here.
Comparison: https://spaw.co/compare/spur
ip-api and the other product names are trademarks of their owners. Spaw is not affiliated with them.
Reference: https://spaw.co/alternatives/ip-api
---
# Spaw vs IP2Location: IP intelligence API comparison
How Spaw and IP2Location compare on pricing, billing rules, the feeds behind each flag and limits, from IP2Location's public pages checked on 2026-09-12. Corrections: support@spaw.co.
| | Spaw | IP2Location |
| --- | --- | --- |
| Pricing model | Pay-as-you-go credit packs whose credits never expire, or optional plans, billed monthly or yearly, that deposit the same credits every month for less and roll unused plan credits over up to three months' allowance; both through Stripe Checkout. One credit buys five answered IP lookups | IP2Location.io API, monthly query plans: Free $0 ("50K IP Geolocation API queries/month", "11 IP Attributes", "Attribution Required"), Starter "$49/month" ("150K IP Geolocation API queries/month", "20 IP Attributes"), Plus "$249/month" ("300K IP Geolocation API queries/month", "59 IP Attributes"), Security "$499/month" ("600K IP Geolocation API queries/month", "79 IP Attributes"); overage per increment Starter $10, Plus $50, Security $100. Databases, standard licence per year: DB1 "$99", DB2 "$399", DB8 "$1,299", DB18 "$2,799", with a site licence at ten times those figures. |
| Free tier | 100 free credits every month, no card required | "50K IP Geolocation API queries/month" with 11 attributes, basic geolocation and open-proxy detection only, "Attribution Required"; without a key "up to 1,000 queries per day" |
| Price per 1,000 at about 10k | $1.80 per 1,000 IP lookups at 5 to the credit (Starter pack: 1,000 credits for $9), or $1.40 on the Starter monthly plan (1,000 credits for $7 a month) | not on the pages read |
| Price per 1,000 at about 100k | $1.56 per 1,000 IP lookups at 5 to the credit (Growth pack: 5,000 credits for $39), or $1.16 on the Growth monthly plan (5,000 credits for $29 a month) | not on the pages read |
| Addresses no feed covered billed | One credit buys five answered addresses whatever their flags say; a flag no feed could evaluate answers null rather than false, and the answer costs the same | not on the pages read |
| Reserved and private ranges billed | No: reserved, private, loopback, CGNAT, documentation and multicast ranges answer reason reserved_range and are free, unallocated space is free, and so are repeats within 7 days | not on the pages read |
| Feeds behind the flags | Public feeds, each named in the answer with the date it was synced: DB-IP Lite and RFC 8805 geofeeds for location, the five registries' delegated statistics for allocation, Spamhaus DROP and ASN-DROP and abuse.ch Feodo Tracker for blocklists, the Tor Project's exit list and Onionoo, the Mullvad, NordVPN and Private Internet Access server lists plus the open X4BNet lists, Apple's iCloud Private Relay egress list, the cloud providers' own published ranges, and the verification files Google, Microsoft, OpenAI, Perplexity, DuckDuckGo and Ahrefs publish for their crawlers. | Its own IP2Proxy data: is_proxy with a proxy object of "last_seen, proxy_type, threat, provider", and on the higher plans is_vpn, is_tor, is_data_center, is_public_proxy, is_web_proxy, is_web_crawler, is_ai_crawler, is_residential_proxy, is_spammer, is_scanner, is_botnet, is_bogon and a "fraud_score" from 0 to 99. The Free and Starter plans detect open proxies only. |
| Refresh cadence | Every feed re-syncs daily, the botnet command-and-control list hourly, and the geofeed crawl weekly. Each response names every dataset it consulted with the date it was last synced, and /status lists them all. | "Database Updated Daily" on the API pricing page; the database product page states no cadence |
| Geolocation source | The DB-IP Lite database, overridden wherever the operator publishes the location itself — an RFC 8805 geofeed, Apple's relay list, a VPN provider's server list. location_source says which answered, and location_confidence is graded from a weekly accuracy report rather than asserted. | Its own databases: country, region, city, latitude, longitude, ZIP code, time zone, ISP, domain, net speed, IDD and area code, weather station, mobile carrier with MCC and MNC, elevation, usage type, address type, category, district and ASN, per the database comparison chart. No accuracy figure is published on the pages read. |
| Bulk limit | 100,000 addresses per bulk job, with an HMAC-signed completion webhook | not on the pages read |
| Batch API | 1,000 addresses per synchronous request; repeats inside the list are looked up once | not on the pages read |
| Rate limit | 50 requests per second per key on the IP endpoints | Monthly plan quotas with overage; keyless access "up to 1,000 queries per day" resetting at 00:00 UTC |
| Official SDKs | Python, Node.js, PHP | not on the pages read |
| Data retention | No history row and no log line carrying the address. What a lookup leaves is keyed and never holds the address: the 7-day marker that makes a repeat free and, under the same keyed hash, the count behind seen_by_you; privacy: true skips both. The account's own outcome reports are kept as salted hashes for 90 days. Bulk input lists and result files are deleted with the job after 30 days. | not on the pages read |
## When IP2Location fits better
Teams that want a downloadable database under a yearly licence, or an API whose top plan carries a fraud score and a wide set of proxy and crawler flags.
- Fields are gated by plan: 11 attributes on Free, 79 on the Security plan
- The Free API plan requires attribution
- Proxy detection beyond open proxies starts on the Plus plan; the fraud score, datacenter and crawler flags on Security
- Databases are sold for download, so the data can run inside your own infrastructure
## When Spaw fits better
Developers who want every public IP feed compiled, dated and named for them, with a risk score whose weights are published so it can be recomputed or ignored.
- IP2Location gates attributes by plan, from 11 to 79; Spaw answers every one of its 62 fields on every lookup at the same price, five lookups to the credit
- IP2Location's proxy and fraud data are proprietary; Every dataset behind a Spaw IP answer is a public feed named in the response with the date it was synced, and the risk score is a published sum of weights anyone can recompute
- IP2Location sells a monthly quota that resets; Spaw sells pack credits that never expire at five IP lookups to the credit, and reserved, private, documentation and unallocated ranges are free
- Spaw does not sell database downloads; it names each public feed behind an answer so a customer can read the same file
## Questions
**Does Spaw charge for addresses its feeds do not cover?** Spaw bills per answered address, five to the credit: the answered lookup that opens a block costs 1 credit and the next four cost nothing. Reserved, private, loopback, CGNAT, documentation and multicast ranges answer reason reserved_range and are free, unallocated space is free, and so is a repeat of the same address within seven days. A flag no feed could evaluate answers null rather than false: the address is still billed, and the null tells you the signal was not checked instead of pretending it was. IP2Location does not publish how it bills them.
**Can I run a log file or an export through Spaw?** Yes. A bulk run takes up to 100,000 addresses from the dashboard or the API and returns one row per input address, in input order, with every flag, the risk score and the signals that made it. Five answered addresses cost a credit, repeats inside the list are looked up once, reserved ranges cost nothing, and the finish is announced on an HMAC-signed webhook.
**Is there a free tier?** Every Spaw account receives 100 free credits at the start of each month with no card on file, and unused free credits accumulate. IP2Location offers: "50K IP Geolocation API queries/month" with 11 attributes, basic geolocation and open-proxy detection only, "Attribution Required"; without a key "up to 1,000 queries per day".
## Sources (checked 2026-09-12)
- [IP2Location.io pricing](https://www.ip2location.io/pricing)
- [IP2Location.io API documentation](https://www.ip2location.io/ip2location-documentation)
- [IP2Location database products](https://www.ip2location.com/database/ip2location)
IP2Location is a trademark of its owner. Spaw is not affiliated with it.
Reference: https://spaw.co/compare/ip2location
---
# IP2Location alternatives for IP intelligence
IP intelligence APIs to consider instead of IP2Location, with the pricing model, free tier and price per 1,000 lookups each vendor publishes. Figures come from public pricing pages checked on 2026-09-12.
| Service | Pricing model | Free tier | Price per 1,000 at about 10k |
| --- | --- | --- | --- |
| [Spaw](https://spaw.co) | Pay-as-you-go credit packs whose credits never expire, or optional plans, billed monthly or yearly, that deposit the same credits every month for less and roll unused plan credits over up to three months' allowance; both through Stripe Checkout. One credit buys five answered IP lookups | 100 free credits every month, no card required | $1.80 per 1,000 IP lookups at 5 to the credit (Starter pack: 1,000 credits for $9), or $1.40 on the Starter monthly plan (1,000 credits for $7 a month) |
| [AbuseIPDB](https://www.abuseipdb.com) | Plans with daily request allowances: Individual free "Forever!", Basic $25/mo or $228/year, Premium $99/mo or $1,068/year, Enterprise custom | Individual: free forever with no credit card, 1,000 check and report requests, 100 block checks and 5 bulk reports a day | not on the pages read |
| [ip-api](https://ip-api.com) | Two tiers. The free endpoint: "Our endpoints are limited to 45 HTTP requests per minute from an IP address" and "We do not allow commercial use of the free endpoint". The Pro service: "€13.3 per month, when paying for 12 months", with "Unlimited requests", "SSL (HTTPS)", "Commercial use allowed" and a "99.9% SLA" ("If the general API uptime falls below 99.9% in a month, we will automatically extend your subscription for 10 days"). | The free endpoint, for non-commercial use only: 45 requests per minute per address over HTTP ("256-bit SSL encryption is not available for this free API") | not on the pages read |
| [ipapi](https://ipapi.co) | Monthly plans by lookup quota: Starter "$15 billed monthly" for 60,000 lookups, Professional "$25 billed monthly" for 150,000, Business "$49 billed monthly" for 500,000, Enterprise "$99 billed monthly" for 2,000,000, Premium "$199 billed monthly" for 6,000,000, Ultimate "$399 billed monthly" for 15,000,000; annual billing is 20% less. | A free trial of "up-to 1000 IP lookups in a day" (about 30,000 a month), "for testing & development purposes" and "Not for production use" | not on the pages read |
| [ipdata](https://ipdata.co) | Monthly plans by daily request quota: Personal "$10 per month" with "2,500 requests daily", Lite "$30 per month" with "10,000 requests daily", Startup "$50 per month" with "50,000 requests daily", Business "$120 per month" with "100,000 requests daily"; "20% off all annual plans" at the time of reading. | "1500 free requests daily", "restricted to non-commercial use"; on the free tier, over the quota "all further requests are blocked and you receive an email notification" | not on the pages read |
| [ipgeolocation.io](https://ipgeolocation.io) | Monthly credit plans, "A standard unified IP lookup uses 1 credit": Starter "$19/month" for "150000 API Credits Per month", Plus "$49/month" for 500,000, Pro "$79/month" for 1,000,000, Business "$129/month" for 2,000,000, Premium "$249/month" for 5,000,000, Enterprise "Contact Sales"; annual billing gives "2 months free". "Paid monthly plans reset every month. Paid annual plans reset every year". | "1000 API Credits Per day for life" with no credit card, "a hard limit of 1000 requests per day" and "No overages on Free"; the free plan returns "core location without the radius and confidence fields" | not on the pages read |
| [IPinfo](https://ipinfo.io) | Monthly plans by API request quota: Lite $0/mo, Core $26/mo for 150,000 requests, Max $163/mo for 250,000, Enterprise custom for "1M+". Database downloads are listed separately, "Starting at $1,000/month". | IPinfo Lite at "$0/mo", free forever with no credit card, giving unlimited API requests at country level only | not on the pages read |
| [IPQualityScore](https://www.ipqualityscore.com) | Monthly plans by lookup quota: Free $0 (1,000 lookups a month), Startup "As low as: $99 / Month" (5,000), SMB Basic "$499" (10,000), SMB+ "$999" (75,000), Enterprise custom. "Any valid request sent to our Proxy Detection, Email Verification, or Device Fingerprinting services including lookups performed through a batch CSV file check will consume a lookup credit." | 1,000 lookups a month, capped at 35 a day | not on the pages read |
| [Ipregistry](https://ipregistry.co) | Credits, "1 lookup = 1 request = 1 credit" (batch endpoints use "one credit per lookup in the batch request, plus 1"). Subscriptions: Starter "100,000 credits / mo" at "$29 $24 / month", Professional "300,000 credits / mo" at "$79 $66 / month", Business "1,150,000 credits / mo" at "$199 $166 / month", Scale "2,450,000 credits / mo" at "$399 $333 / month" (the lower figure is the annual rate). Pay-as-you-go packs: 15,000 credits for $10, 100,000 for $50, 250,000 for $100, 1,750,000 for $500. | "20,000 free credits on sign-up" with "No credit card, no time limit" | $0.67 (the $10 pack of 15,000 credits, $0.000667 a lookup) |
| [ipstack](https://ipstack.com) | Monthly plans by request quota: Free "100 Request/mo." at $0, Basic "50,000 Request/mo." at "$12.99/mo.", Professional "500,000 Request/mo." at "$59.99/mo.", Professional + "2,000,000 Request/mo." at "$99.99/mo.", Custom on request; yearly billing "Save up to 15%". Overage fees apply past the quota, with notifications at 75%, 90% and 100%. | "100 Request/mo." at $0, without SSL encryption, support or the paid modules (SSL, commercial use and the Location, Time Zone and Connection modules are listed for paid plans) | not on the pages read |
| [MaxMind](https://www.maxmind.com) | Per query, by service, with no minimum published: "If you estimate your volume of queries to be over $2,500 a month, contact us to discuss rates." | A no-cost trial is offered ("Experience the functionality and benefits of our GeoIP web services at no cost"); no free monthly allowance is stated on the page checked | $0.10 per 1,000 for Country ($0.0001 a query), $0.30 for City Plus ($0.0003) and $2.00 for Insights ($0.002) |
| [proxycheck.io](https://proxycheck.io) | not on the pages read | not on the pages read | not on the pages read |
| [Spur](https://spur.us) | not on the pages read | not on the pages read | not on the pages read |
## Spaw
Verification API that bills per answered lookup across six signals on one key and one balance: Email verification, Phone intelligence, IP intelligence, Address verification, Business verification against open registers, and Cross-signal consistency, which runs any subset of the first four in one call and names where they agree and where they do not. The IP side answers from local databases and compiled public feeds: where an address is, who runs it, and whether it is a visitor at all.
## AbuseIPDB
AbuseIPDB is a reputation service built on abuse reports its own users submit: a check returns an abuse confidence score with the report count, country, usage type, ISP and a Tor flag, and plans are sold as daily request allowances rather than credits.
Comparison: https://spaw.co/compare/abuseipdb
## ip-api
ip-api is a geolocation JSON API with a free, non-commercial, HTTP-only endpoint limited by request rate, and a Pro subscription that adds HTTPS, unlimited queries and commercial use; its answer carries one proxy flag, one hosting flag and one mobile flag beside the location and network fields.
Comparison: https://spaw.co/compare/ip-api
## ipapi
ipapi.co is a geolocation API sold as monthly lookup quotas, whose pricing page credits its data to DB-IP and the IP2Location LITE database; the answer carries location, country and network fields and no proxy, VPN or Tor flag.
Comparison: https://spaw.co/compare/ipapi
## ipdata
ipdata is a geolocation and threat API sold as daily request quotas, from a free non-commercial tier to a Business plan, whose pricing page says it does not keep records of the addresses looked up beyond a day; its documentation site refused automated reading, so the fields are not reproduced here.
Comparison: https://spaw.co/compare/ipdata
## ipgeolocation.io
ipgeolocation.io sells geolocation and a separate security API on monthly credit plans, with location built from its own measurements and a security answer that flags VPN, proxy, residential proxy and Tor with provider names, confidence scores and a 0 to 100 threat score.
Comparison: https://spaw.co/compare/ipgeolocation
## IPinfo
IPinfo is an IP data company that sells geolocation, ASN, privacy detection and company data as monthly API plans and as database downloads, collected through its own measurement network rather than assembled from third-party feeds.
Comparison: https://spaw.co/compare/ipinfo
## IPQualityScore
IPQualityScore sells fraud detection across several signals on one lookup credit: proxy and VPN detection, email verification and device fingerprinting all draw from the same monthly quota, and the IP answer carries a fraud score alongside the connection facts.
Comparison: https://spaw.co/compare/ipqualityscore
## Ipregistry
Ipregistry is a geolocation and threat API sold as credits, either monthly subscriptions or pay-as-you-go packs, whose security object answers abuser, attacker, bogon, cloud provider, proxy, relay, Tor, VPN, anonymous and threat flags and whose data is described as built from BGP tables, registry databases, geofeeds and latency measurements.
Comparison: https://spaw.co/compare/ipregistry
## ipstack
ipstack is a geolocation API on monthly request quotas from a 100-request free tier to a two-million-request plan, with a bulk endpoint and a security module reserved for its top plan; its documentation pages rendered only a header to our fetcher, so the field list is not reproduced here.
Comparison: https://spaw.co/compare/ipstack
## MaxMind
MaxMind sells GeoIP as three per-query web services — Country, City Plus and Insights — and as downloadable databases, with proxy and anonymiser detection reserved for the Insights tier.
Comparison: https://spaw.co/compare/maxmind
## proxycheck.io
proxycheck.io sells an API for checking whether an address is a proxy; every page we tried on 2026-09-12 answered 403 Forbidden to automated reading, so nothing about its pricing, limits, fields or data is reproduced here.
Comparison: https://spaw.co/compare/proxycheck
## Spur
Spur is an anonymity-infrastructure vendor: its Context API answers whether an address is a VPN, proxy or other tunnel with the operator named, and its feeds list anonymous, residential-proxy and datacenter address space; its pricing page rate-limited our fetcher, so prices are not reproduced here.
Comparison: https://spaw.co/compare/spur
IP2Location and the other product names are trademarks of their owners. Spaw is not affiliated with them.
Reference: https://spaw.co/alternatives/ip2location
---
# Spaw vs ipapi: IP intelligence API comparison
How Spaw and ipapi compare on pricing, billing rules, the feeds behind each flag and limits, from ipapi's public pages checked on 2026-09-12. Corrections: support@spaw.co.
| | Spaw | ipapi |
| --- | --- | --- |
| Pricing model | Pay-as-you-go credit packs whose credits never expire, or optional plans, billed monthly or yearly, that deposit the same credits every month for less and roll unused plan credits over up to three months' allowance; both through Stripe Checkout. One credit buys five answered IP lookups | Monthly plans by lookup quota: Starter "$15 billed monthly" for 60,000 lookups, Professional "$25 billed monthly" for 150,000, Business "$49 billed monthly" for 500,000, Enterprise "$99 billed monthly" for 2,000,000, Premium "$199 billed monthly" for 6,000,000, Ultimate "$399 billed monthly" for 15,000,000; annual billing is 20% less. |
| Free tier | 100 free credits every month, no card required | A free trial of "up-to 1000 IP lookups in a day" (about 30,000 a month), "for testing & development purposes" and "Not for production use" |
| Price per 1,000 at about 10k | $1.80 per 1,000 IP lookups at 5 to the credit (Starter pack: 1,000 credits for $9), or $1.40 on the Starter monthly plan (1,000 credits for $7 a month) | not on the pages read |
| Price per 1,000 at about 100k | $1.56 per 1,000 IP lookups at 5 to the credit (Growth pack: 5,000 credits for $39), or $1.16 on the Growth monthly plan (5,000 credits for $29 a month) | not on the pages read |
| Addresses no feed covered billed | One credit buys five answered addresses whatever their flags say; a flag no feed could evaluate answers null rather than false, and the answer costs the same | not on the pages read |
| Reserved and private ranges billed | No: reserved, private, loopback, CGNAT, documentation and multicast ranges answer reason reserved_range and are free, unallocated space is free, and so are repeats within 7 days | not on the pages read |
| Feeds behind the flags | Public feeds, each named in the answer with the date it was synced: DB-IP Lite and RFC 8805 geofeeds for location, the five registries' delegated statistics for allocation, Spamhaus DROP and ASN-DROP and abuse.ch Feodo Tracker for blocklists, the Tor Project's exit list and Onionoo, the Mullvad, NordVPN and Private Internet Access server lists plus the open X4BNet lists, Apple's iCloud Private Relay egress list, the cloud providers' own published ranges, and the verification files Google, Microsoft, OpenAI, Perplexity, DuckDuckGo and Ahrefs publish for their crawlers. | None: the documented answer carries no proxy, VPN, Tor, hosting or threat field. The pricing page names the data behind the location as "IP geolocation by DB-IP" and the "IP2Location LITE database". |
| Refresh cadence | Every feed re-syncs daily, the botnet command-and-control list hourly, and the geofeed crawl weekly. Each response names every dataset it consulted with the date it was last synced, and /status lists them all. | not on the pages read |
| Geolocation source | The DB-IP Lite database, overridden wherever the operator publishes the location itself — an RFC 8805 geofeed, Apple's relay list, a VPN provider's server list. location_source says which answered, and location_confidence is graded from a weekly accuracy report rather than asserted. | DB-IP and IP2Location LITE, per the pricing page's data credits. Fields: ip, version, city, region, region_code, country_code, country_code_iso3, country_name, country_capital, country_tld, continent_code, in_eu, postal, latitude, longitude, latlong, timezone, utc_offset, country_calling_code, currency, currency_name, languages, country_area, country_population, asn, org, and a hostname add-on in beta. |
| Bulk limit | 100,000 addresses per bulk job, with an HMAC-signed completion webhook | "Bulk IP lookup" is listed as a feature of the paid plans; no size is published |
| Batch API | 1,000 addresses per synchronous request; repeats inside the list are looked up once | not on the pages read |
| Rate limit | 50 requests per second per key on the IP endpoints | Not published as a number; a call over quota answers a 429 with reason "RateLimited" |
| Official SDKs | Python, Node.js, PHP | not on the pages read |
| Data retention | No history row and no log line carrying the address. What a lookup leaves is keyed and never holds the address: the 7-day marker that makes a repeat free and, under the same keyed hash, the count behind seen_by_you; privacy: true skips both. The account's own outcome reports are kept as salted hashes for 90 days. Bulk input lists and result files are deleted with the job after 30 days. | "IP queries stored in logs" under the standard option; the pricing page offers a no-logging arrangement on request |
## When ipapi fits better
Teams that need country, city, currency and network facts on a predictable monthly quota and do not need any anonymity or threat signal.
- The pricing page credits DB-IP and the IP2Location LITE database as its sources
- The free tier is a trial: "Not for production use"
- No proxy, VPN, Tor or datacenter field is documented
- Queries are stored in logs unless a no-logging arrangement is made
## When Spaw fits better
Developers who want every public IP feed compiled, dated and named for them, with a risk score whose weights are published so it can be recomputed or ignored.
- ipapi.co and Spaw both read the DB-IP database for location; Spaw adds forty-odd public feeds on top of it (clouds, VPN exits, Tor, relay, blocklists, crawlers, registries) and answers each as a tri-state flag with the feed dated
- ipapi.co stores queries in logs under its standard option; Spaw stores nothing about an IP lookup beyond a keyed seven-day repeat marker, and privacy mode skips even that
- ipapi.co sells a monthly quota that resets; Spaw sells pack credits that never expire at five IP lookups to the credit, and reserved, private, documentation and unallocated ranges are free
- Every dataset behind a Spaw IP answer is a public feed named in the response with the date it was synced, and the risk score is a published sum of weights anyone can recompute
## Questions
**Does Spaw charge for addresses its feeds do not cover?** Spaw bills per answered address, five to the credit: the answered lookup that opens a block costs 1 credit and the next four cost nothing. Reserved, private, loopback, CGNAT, documentation and multicast ranges answer reason reserved_range and are free, unallocated space is free, and so is a repeat of the same address within seven days. A flag no feed could evaluate answers null rather than false: the address is still billed, and the null tells you the signal was not checked instead of pretending it was. ipapi does not publish how it bills them.
**Can I run a log file or an export through Spaw?** Yes. A bulk run takes up to 100,000 addresses from the dashboard or the API and returns one row per input address, in input order, with every flag, the risk score and the signals that made it. Five answered addresses cost a credit, repeats inside the list are looked up once, reserved ranges cost nothing, and the finish is announced on an HMAC-signed webhook.
**Is there a free tier?** Every Spaw account receives 100 free credits at the start of each month with no card on file, and unused free credits accumulate. ipapi offers: A free trial of "up-to 1000 IP lookups in a day" (about 30,000 a month), "for testing & development purposes" and "Not for production use".
## Sources (checked 2026-09-12)
- [ipapi.co pricing](https://ipapi.co/pricing/)
- [ipapi.co API documentation](https://ipapi.co/api/)
ipapi is a trademark of its owner. Spaw is not affiliated with it.
Reference: https://spaw.co/compare/ipapi
---
# ipapi alternatives for IP intelligence
IP intelligence APIs to consider instead of ipapi, with the pricing model, free tier and price per 1,000 lookups each vendor publishes. Figures come from public pricing pages checked on 2026-09-12.
| Service | Pricing model | Free tier | Price per 1,000 at about 10k |
| --- | --- | --- | --- |
| [Spaw](https://spaw.co) | Pay-as-you-go credit packs whose credits never expire, or optional plans, billed monthly or yearly, that deposit the same credits every month for less and roll unused plan credits over up to three months' allowance; both through Stripe Checkout. One credit buys five answered IP lookups | 100 free credits every month, no card required | $1.80 per 1,000 IP lookups at 5 to the credit (Starter pack: 1,000 credits for $9), or $1.40 on the Starter monthly plan (1,000 credits for $7 a month) |
| [AbuseIPDB](https://www.abuseipdb.com) | Plans with daily request allowances: Individual free "Forever!", Basic $25/mo or $228/year, Premium $99/mo or $1,068/year, Enterprise custom | Individual: free forever with no credit card, 1,000 check and report requests, 100 block checks and 5 bulk reports a day | not on the pages read |
| [ip-api](https://ip-api.com) | Two tiers. The free endpoint: "Our endpoints are limited to 45 HTTP requests per minute from an IP address" and "We do not allow commercial use of the free endpoint". The Pro service: "€13.3 per month, when paying for 12 months", with "Unlimited requests", "SSL (HTTPS)", "Commercial use allowed" and a "99.9% SLA" ("If the general API uptime falls below 99.9% in a month, we will automatically extend your subscription for 10 days"). | The free endpoint, for non-commercial use only: 45 requests per minute per address over HTTP ("256-bit SSL encryption is not available for this free API") | not on the pages read |
| [IP2Location](https://www.ip2location.com) | IP2Location.io API, monthly query plans: Free $0 ("50K IP Geolocation API queries/month", "11 IP Attributes", "Attribution Required"), Starter "$49/month" ("150K IP Geolocation API queries/month", "20 IP Attributes"), Plus "$249/month" ("300K IP Geolocation API queries/month", "59 IP Attributes"), Security "$499/month" ("600K IP Geolocation API queries/month", "79 IP Attributes"); overage per increment Starter $10, Plus $50, Security $100. Databases, standard licence per year: DB1 "$99", DB2 "$399", DB8 "$1,299", DB18 "$2,799", with a site licence at ten times those figures. | "50K IP Geolocation API queries/month" with 11 attributes, basic geolocation and open-proxy detection only, "Attribution Required"; without a key "up to 1,000 queries per day" | not on the pages read |
| [ipdata](https://ipdata.co) | Monthly plans by daily request quota: Personal "$10 per month" with "2,500 requests daily", Lite "$30 per month" with "10,000 requests daily", Startup "$50 per month" with "50,000 requests daily", Business "$120 per month" with "100,000 requests daily"; "20% off all annual plans" at the time of reading. | "1500 free requests daily", "restricted to non-commercial use"; on the free tier, over the quota "all further requests are blocked and you receive an email notification" | not on the pages read |
| [ipgeolocation.io](https://ipgeolocation.io) | Monthly credit plans, "A standard unified IP lookup uses 1 credit": Starter "$19/month" for "150000 API Credits Per month", Plus "$49/month" for 500,000, Pro "$79/month" for 1,000,000, Business "$129/month" for 2,000,000, Premium "$249/month" for 5,000,000, Enterprise "Contact Sales"; annual billing gives "2 months free". "Paid monthly plans reset every month. Paid annual plans reset every year". | "1000 API Credits Per day for life" with no credit card, "a hard limit of 1000 requests per day" and "No overages on Free"; the free plan returns "core location without the radius and confidence fields" | not on the pages read |
| [IPinfo](https://ipinfo.io) | Monthly plans by API request quota: Lite $0/mo, Core $26/mo for 150,000 requests, Max $163/mo for 250,000, Enterprise custom for "1M+". Database downloads are listed separately, "Starting at $1,000/month". | IPinfo Lite at "$0/mo", free forever with no credit card, giving unlimited API requests at country level only | not on the pages read |
| [IPQualityScore](https://www.ipqualityscore.com) | Monthly plans by lookup quota: Free $0 (1,000 lookups a month), Startup "As low as: $99 / Month" (5,000), SMB Basic "$499" (10,000), SMB+ "$999" (75,000), Enterprise custom. "Any valid request sent to our Proxy Detection, Email Verification, or Device Fingerprinting services including lookups performed through a batch CSV file check will consume a lookup credit." | 1,000 lookups a month, capped at 35 a day | not on the pages read |
| [Ipregistry](https://ipregistry.co) | Credits, "1 lookup = 1 request = 1 credit" (batch endpoints use "one credit per lookup in the batch request, plus 1"). Subscriptions: Starter "100,000 credits / mo" at "$29 $24 / month", Professional "300,000 credits / mo" at "$79 $66 / month", Business "1,150,000 credits / mo" at "$199 $166 / month", Scale "2,450,000 credits / mo" at "$399 $333 / month" (the lower figure is the annual rate). Pay-as-you-go packs: 15,000 credits for $10, 100,000 for $50, 250,000 for $100, 1,750,000 for $500. | "20,000 free credits on sign-up" with "No credit card, no time limit" | $0.67 (the $10 pack of 15,000 credits, $0.000667 a lookup) |
| [ipstack](https://ipstack.com) | Monthly plans by request quota: Free "100 Request/mo." at $0, Basic "50,000 Request/mo." at "$12.99/mo.", Professional "500,000 Request/mo." at "$59.99/mo.", Professional + "2,000,000 Request/mo." at "$99.99/mo.", Custom on request; yearly billing "Save up to 15%". Overage fees apply past the quota, with notifications at 75%, 90% and 100%. | "100 Request/mo." at $0, without SSL encryption, support or the paid modules (SSL, commercial use and the Location, Time Zone and Connection modules are listed for paid plans) | not on the pages read |
| [MaxMind](https://www.maxmind.com) | Per query, by service, with no minimum published: "If you estimate your volume of queries to be over $2,500 a month, contact us to discuss rates." | A no-cost trial is offered ("Experience the functionality and benefits of our GeoIP web services at no cost"); no free monthly allowance is stated on the page checked | $0.10 per 1,000 for Country ($0.0001 a query), $0.30 for City Plus ($0.0003) and $2.00 for Insights ($0.002) |
| [proxycheck.io](https://proxycheck.io) | not on the pages read | not on the pages read | not on the pages read |
| [Spur](https://spur.us) | not on the pages read | not on the pages read | not on the pages read |
## Spaw
Verification API that bills per answered lookup across six signals on one key and one balance: Email verification, Phone intelligence, IP intelligence, Address verification, Business verification against open registers, and Cross-signal consistency, which runs any subset of the first four in one call and names where they agree and where they do not. The IP side answers from local databases and compiled public feeds: where an address is, who runs it, and whether it is a visitor at all.
## AbuseIPDB
AbuseIPDB is a reputation service built on abuse reports its own users submit: a check returns an abuse confidence score with the report count, country, usage type, ISP and a Tor flag, and plans are sold as daily request allowances rather than credits.
Comparison: https://spaw.co/compare/abuseipdb
## ip-api
ip-api is a geolocation JSON API with a free, non-commercial, HTTP-only endpoint limited by request rate, and a Pro subscription that adds HTTPS, unlimited queries and commercial use; its answer carries one proxy flag, one hosting flag and one mobile flag beside the location and network fields.
Comparison: https://spaw.co/compare/ip-api
## IP2Location
IP2Location sells geolocation as downloadable databases (DB1 to DB26, licensed yearly) and, through IP2Location.io, as an API on monthly query plans whose higher tiers add proxy, VPN, Tor, datacenter, crawler and residential-proxy detection and a fraud score; the classic web service no longer takes new registrations.
Comparison: https://spaw.co/compare/ip2location
## ipdata
ipdata is a geolocation and threat API sold as daily request quotas, from a free non-commercial tier to a Business plan, whose pricing page says it does not keep records of the addresses looked up beyond a day; its documentation site refused automated reading, so the fields are not reproduced here.
Comparison: https://spaw.co/compare/ipdata
## ipgeolocation.io
ipgeolocation.io sells geolocation and a separate security API on monthly credit plans, with location built from its own measurements and a security answer that flags VPN, proxy, residential proxy and Tor with provider names, confidence scores and a 0 to 100 threat score.
Comparison: https://spaw.co/compare/ipgeolocation
## IPinfo
IPinfo is an IP data company that sells geolocation, ASN, privacy detection and company data as monthly API plans and as database downloads, collected through its own measurement network rather than assembled from third-party feeds.
Comparison: https://spaw.co/compare/ipinfo
## IPQualityScore
IPQualityScore sells fraud detection across several signals on one lookup credit: proxy and VPN detection, email verification and device fingerprinting all draw from the same monthly quota, and the IP answer carries a fraud score alongside the connection facts.
Comparison: https://spaw.co/compare/ipqualityscore
## Ipregistry
Ipregistry is a geolocation and threat API sold as credits, either monthly subscriptions or pay-as-you-go packs, whose security object answers abuser, attacker, bogon, cloud provider, proxy, relay, Tor, VPN, anonymous and threat flags and whose data is described as built from BGP tables, registry databases, geofeeds and latency measurements.
Comparison: https://spaw.co/compare/ipregistry
## ipstack
ipstack is a geolocation API on monthly request quotas from a 100-request free tier to a two-million-request plan, with a bulk endpoint and a security module reserved for its top plan; its documentation pages rendered only a header to our fetcher, so the field list is not reproduced here.
Comparison: https://spaw.co/compare/ipstack
## MaxMind
MaxMind sells GeoIP as three per-query web services — Country, City Plus and Insights — and as downloadable databases, with proxy and anonymiser detection reserved for the Insights tier.
Comparison: https://spaw.co/compare/maxmind
## proxycheck.io
proxycheck.io sells an API for checking whether an address is a proxy; every page we tried on 2026-09-12 answered 403 Forbidden to automated reading, so nothing about its pricing, limits, fields or data is reproduced here.
Comparison: https://spaw.co/compare/proxycheck
## Spur
Spur is an anonymity-infrastructure vendor: its Context API answers whether an address is a VPN, proxy or other tunnel with the operator named, and its feeds list anonymous, residential-proxy and datacenter address space; its pricing page rate-limited our fetcher, so prices are not reproduced here.
Comparison: https://spaw.co/compare/spur
ipapi and the other product names are trademarks of their owners. Spaw is not affiliated with them.
Reference: https://spaw.co/alternatives/ipapi
---
# Spaw vs ipdata: IP intelligence API comparison
How Spaw and ipdata compare on pricing, billing rules, the feeds behind each flag and limits, from ipdata's public pages checked on 2026-09-12. Corrections: support@spaw.co.
| | Spaw | ipdata |
| --- | --- | --- |
| Pricing model | Pay-as-you-go credit packs whose credits never expire, or optional plans, billed monthly or yearly, that deposit the same credits every month for less and roll unused plan credits over up to three months' allowance; both through Stripe Checkout. One credit buys five answered IP lookups | Monthly plans by daily request quota: Personal "$10 per month" with "2,500 requests daily", Lite "$30 per month" with "10,000 requests daily", Startup "$50 per month" with "50,000 requests daily", Business "$120 per month" with "100,000 requests daily"; "20% off all annual plans" at the time of reading. |
| Free tier | 100 free credits every month, no card required | "1500 free requests daily", "restricted to non-commercial use"; on the free tier, over the quota "all further requests are blocked and you receive an email notification" |
| Price per 1,000 at about 10k | $1.80 per 1,000 IP lookups at 5 to the credit (Starter pack: 1,000 credits for $9), or $1.40 on the Starter monthly plan (1,000 credits for $7 a month) | not on the pages read |
| Price per 1,000 at about 100k | $1.56 per 1,000 IP lookups at 5 to the credit (Growth pack: 5,000 credits for $39), or $1.16 on the Growth monthly plan (5,000 credits for $29 a month) | not on the pages read |
| Addresses no feed covered billed | One credit buys five answered addresses whatever their flags say; a flag no feed could evaluate answers null rather than false, and the answer costs the same | not on the pages read |
| Reserved and private ranges billed | No: reserved, private, loopback, CGNAT, documentation and multicast ranges answer reason reserved_range and are free, unallocated space is free, and so are repeats within 7 days | not on the pages read |
| Feeds behind the flags | Public feeds, each named in the answer with the date it was synced: DB-IP Lite and RFC 8805 geofeeds for location, the five registries' delegated statistics for allocation, Spamhaus DROP and ASN-DROP and abuse.ch Feodo Tracker for blocklists, the Tor Project's exit list and Onionoo, the Mullvad, NordVPN and Private Internet Access server lists plus the open X4BNet lists, Apple's iCloud Private Relay egress list, the cloud providers' own published ranges, and the verification files Google, Microsoft, OpenAI, Perplexity, DuckDuckGo and Ahrefs publish for their crawlers. | not on the pages read |
| Refresh cadence | Every feed re-syncs daily, the botnet command-and-control list hourly, and the geofeed crawl weekly. Each response names every dataset it consulted with the date it was last synced, and /status lists them all. | not on the pages read |
| Geolocation source | The DB-IP Lite database, overridden wherever the operator publishes the location itself — an RFC 8805 geofeed, Apple's relay list, a VPN provider's server list. location_source says which answered, and location_confidence is graded from a weekly accuracy report rather than asserted. | not on the pages read |
| Bulk limit | 100,000 addresses per bulk job, with an HMAC-signed completion webhook | not on the pages read |
| Batch API | 1,000 addresses per synchronous request; repeats inside the list are looked up once | not on the pages read |
| Rate limit | 50 requests per second per key on the IP endpoints | Daily quotas per plan; "you receive a limit warning when you have used 90% of your quota and we pad your quota by 10% to cover small spikes" |
| Official SDKs | Python, Node.js, PHP | not on the pages read |
| Data retention | No history row and no log line carrying the address. What a lookup leaves is keyed and never holds the address: the 7-day marker that makes a repeat free and, under the same keyed hash, the count behind seen_by_you; privacy: true skips both. The account's own outcome reports are kept as salted hashes for 90 days. Bulk input lists and result files are deleted with the job after 30 days. | The pricing page says the service does "not keep records of the IP Addresses you look up", apart from "24hrs" for rate limiting |
## When ipdata fits better
Teams that want a daily quota with a no-records promise and are happy to read the field list from the vendor's own documentation.
- docs.ipdata.co answered 403 Forbidden to automated reading on 2026-09-12, so the response fields and the threat object are not reproduced here
- Quotas are daily rather than monthly, padded by 10% with a warning at 90%
- The free tier is for non-commercial use only
- The pricing page states that looked-up addresses are not kept beyond a day
## When Spaw fits better
Developers who want every public IP feed compiled, dated and named for them, with a risk score whose weights are published so it can be recomputed or ignored.
- ipdata's documentation could not be read by our fetcher, so its fields and feeds are not compared here; Spaw's 62 fields and the feed behind each one are on its docs page
- ipdata sells daily quotas; Spaw sells pack credits that never expire at five IP lookups to the credit, and reserved, private, documentation and unallocated ranges are free
- Every dataset behind a Spaw IP answer is a public feed named in the response with the date it was synced, and the risk score is a published sum of weights anyone can recompute
- Both keep no record of the addresses looked up; Spaw's keyed seven-day repeat marker is the one trace, and privacy mode skips it
## Questions
**Does Spaw charge for addresses its feeds do not cover?** Spaw bills per answered address, five to the credit: the answered lookup that opens a block costs 1 credit and the next four cost nothing. Reserved, private, loopback, CGNAT, documentation and multicast ranges answer reason reserved_range and are free, unallocated space is free, and so is a repeat of the same address within seven days. A flag no feed could evaluate answers null rather than false: the address is still billed, and the null tells you the signal was not checked instead of pretending it was. ipdata does not publish how it bills them.
**Can I run a log file or an export through Spaw?** Yes. A bulk run takes up to 100,000 addresses from the dashboard or the API and returns one row per input address, in input order, with every flag, the risk score and the signals that made it. Five answered addresses cost a credit, repeats inside the list are looked up once, reserved ranges cost nothing, and the finish is announced on an HMAC-signed webhook.
**Is there a free tier?** Every Spaw account receives 100 free credits at the start of each month with no card on file, and unused free credits accumulate. ipdata offers: "1500 free requests daily", "restricted to non-commercial use"; on the free tier, over the quota "all further requests are blocked and you receive an email notification".
## Sources (checked 2026-09-12)
- [ipdata pricing](https://ipdata.co/pricing.html)
- [ipdata documentation (answered 403 on 2026-09-12)](https://docs.ipdata.co/)
ipdata is a trademark of its owner. Spaw is not affiliated with it.
Reference: https://spaw.co/compare/ipdata
---
# ipdata alternatives for IP intelligence
IP intelligence APIs to consider instead of ipdata, with the pricing model, free tier and price per 1,000 lookups each vendor publishes. Figures come from public pricing pages checked on 2026-09-12.
| Service | Pricing model | Free tier | Price per 1,000 at about 10k |
| --- | --- | --- | --- |
| [Spaw](https://spaw.co) | Pay-as-you-go credit packs whose credits never expire, or optional plans, billed monthly or yearly, that deposit the same credits every month for less and roll unused plan credits over up to three months' allowance; both through Stripe Checkout. One credit buys five answered IP lookups | 100 free credits every month, no card required | $1.80 per 1,000 IP lookups at 5 to the credit (Starter pack: 1,000 credits for $9), or $1.40 on the Starter monthly plan (1,000 credits for $7 a month) |
| [AbuseIPDB](https://www.abuseipdb.com) | Plans with daily request allowances: Individual free "Forever!", Basic $25/mo or $228/year, Premium $99/mo or $1,068/year, Enterprise custom | Individual: free forever with no credit card, 1,000 check and report requests, 100 block checks and 5 bulk reports a day | not on the pages read |
| [ip-api](https://ip-api.com) | Two tiers. The free endpoint: "Our endpoints are limited to 45 HTTP requests per minute from an IP address" and "We do not allow commercial use of the free endpoint". The Pro service: "€13.3 per month, when paying for 12 months", with "Unlimited requests", "SSL (HTTPS)", "Commercial use allowed" and a "99.9% SLA" ("If the general API uptime falls below 99.9% in a month, we will automatically extend your subscription for 10 days"). | The free endpoint, for non-commercial use only: 45 requests per minute per address over HTTP ("256-bit SSL encryption is not available for this free API") | not on the pages read |
| [IP2Location](https://www.ip2location.com) | IP2Location.io API, monthly query plans: Free $0 ("50K IP Geolocation API queries/month", "11 IP Attributes", "Attribution Required"), Starter "$49/month" ("150K IP Geolocation API queries/month", "20 IP Attributes"), Plus "$249/month" ("300K IP Geolocation API queries/month", "59 IP Attributes"), Security "$499/month" ("600K IP Geolocation API queries/month", "79 IP Attributes"); overage per increment Starter $10, Plus $50, Security $100. Databases, standard licence per year: DB1 "$99", DB2 "$399", DB8 "$1,299", DB18 "$2,799", with a site licence at ten times those figures. | "50K IP Geolocation API queries/month" with 11 attributes, basic geolocation and open-proxy detection only, "Attribution Required"; without a key "up to 1,000 queries per day" | not on the pages read |
| [ipapi](https://ipapi.co) | Monthly plans by lookup quota: Starter "$15 billed monthly" for 60,000 lookups, Professional "$25 billed monthly" for 150,000, Business "$49 billed monthly" for 500,000, Enterprise "$99 billed monthly" for 2,000,000, Premium "$199 billed monthly" for 6,000,000, Ultimate "$399 billed monthly" for 15,000,000; annual billing is 20% less. | A free trial of "up-to 1000 IP lookups in a day" (about 30,000 a month), "for testing & development purposes" and "Not for production use" | not on the pages read |
| [ipgeolocation.io](https://ipgeolocation.io) | Monthly credit plans, "A standard unified IP lookup uses 1 credit": Starter "$19/month" for "150000 API Credits Per month", Plus "$49/month" for 500,000, Pro "$79/month" for 1,000,000, Business "$129/month" for 2,000,000, Premium "$249/month" for 5,000,000, Enterprise "Contact Sales"; annual billing gives "2 months free". "Paid monthly plans reset every month. Paid annual plans reset every year". | "1000 API Credits Per day for life" with no credit card, "a hard limit of 1000 requests per day" and "No overages on Free"; the free plan returns "core location without the radius and confidence fields" | not on the pages read |
| [IPinfo](https://ipinfo.io) | Monthly plans by API request quota: Lite $0/mo, Core $26/mo for 150,000 requests, Max $163/mo for 250,000, Enterprise custom for "1M+". Database downloads are listed separately, "Starting at $1,000/month". | IPinfo Lite at "$0/mo", free forever with no credit card, giving unlimited API requests at country level only | not on the pages read |
| [IPQualityScore](https://www.ipqualityscore.com) | Monthly plans by lookup quota: Free $0 (1,000 lookups a month), Startup "As low as: $99 / Month" (5,000), SMB Basic "$499" (10,000), SMB+ "$999" (75,000), Enterprise custom. "Any valid request sent to our Proxy Detection, Email Verification, or Device Fingerprinting services including lookups performed through a batch CSV file check will consume a lookup credit." | 1,000 lookups a month, capped at 35 a day | not on the pages read |
| [Ipregistry](https://ipregistry.co) | Credits, "1 lookup = 1 request = 1 credit" (batch endpoints use "one credit per lookup in the batch request, plus 1"). Subscriptions: Starter "100,000 credits / mo" at "$29 $24 / month", Professional "300,000 credits / mo" at "$79 $66 / month", Business "1,150,000 credits / mo" at "$199 $166 / month", Scale "2,450,000 credits / mo" at "$399 $333 / month" (the lower figure is the annual rate). Pay-as-you-go packs: 15,000 credits for $10, 100,000 for $50, 250,000 for $100, 1,750,000 for $500. | "20,000 free credits on sign-up" with "No credit card, no time limit" | $0.67 (the $10 pack of 15,000 credits, $0.000667 a lookup) |
| [ipstack](https://ipstack.com) | Monthly plans by request quota: Free "100 Request/mo." at $0, Basic "50,000 Request/mo." at "$12.99/mo.", Professional "500,000 Request/mo." at "$59.99/mo.", Professional + "2,000,000 Request/mo." at "$99.99/mo.", Custom on request; yearly billing "Save up to 15%". Overage fees apply past the quota, with notifications at 75%, 90% and 100%. | "100 Request/mo." at $0, without SSL encryption, support or the paid modules (SSL, commercial use and the Location, Time Zone and Connection modules are listed for paid plans) | not on the pages read |
| [MaxMind](https://www.maxmind.com) | Per query, by service, with no minimum published: "If you estimate your volume of queries to be over $2,500 a month, contact us to discuss rates." | A no-cost trial is offered ("Experience the functionality and benefits of our GeoIP web services at no cost"); no free monthly allowance is stated on the page checked | $0.10 per 1,000 for Country ($0.0001 a query), $0.30 for City Plus ($0.0003) and $2.00 for Insights ($0.002) |
| [proxycheck.io](https://proxycheck.io) | not on the pages read | not on the pages read | not on the pages read |
| [Spur](https://spur.us) | not on the pages read | not on the pages read | not on the pages read |
## Spaw
Verification API that bills per answered lookup across six signals on one key and one balance: Email verification, Phone intelligence, IP intelligence, Address verification, Business verification against open registers, and Cross-signal consistency, which runs any subset of the first four in one call and names where they agree and where they do not. The IP side answers from local databases and compiled public feeds: where an address is, who runs it, and whether it is a visitor at all.
## AbuseIPDB
AbuseIPDB is a reputation service built on abuse reports its own users submit: a check returns an abuse confidence score with the report count, country, usage type, ISP and a Tor flag, and plans are sold as daily request allowances rather than credits.
Comparison: https://spaw.co/compare/abuseipdb
## ip-api
ip-api is a geolocation JSON API with a free, non-commercial, HTTP-only endpoint limited by request rate, and a Pro subscription that adds HTTPS, unlimited queries and commercial use; its answer carries one proxy flag, one hosting flag and one mobile flag beside the location and network fields.
Comparison: https://spaw.co/compare/ip-api
## IP2Location
IP2Location sells geolocation as downloadable databases (DB1 to DB26, licensed yearly) and, through IP2Location.io, as an API on monthly query plans whose higher tiers add proxy, VPN, Tor, datacenter, crawler and residential-proxy detection and a fraud score; the classic web service no longer takes new registrations.
Comparison: https://spaw.co/compare/ip2location
## ipapi
ipapi.co is a geolocation API sold as monthly lookup quotas, whose pricing page credits its data to DB-IP and the IP2Location LITE database; the answer carries location, country and network fields and no proxy, VPN or Tor flag.
Comparison: https://spaw.co/compare/ipapi
## ipgeolocation.io
ipgeolocation.io sells geolocation and a separate security API on monthly credit plans, with location built from its own measurements and a security answer that flags VPN, proxy, residential proxy and Tor with provider names, confidence scores and a 0 to 100 threat score.
Comparison: https://spaw.co/compare/ipgeolocation
## IPinfo
IPinfo is an IP data company that sells geolocation, ASN, privacy detection and company data as monthly API plans and as database downloads, collected through its own measurement network rather than assembled from third-party feeds.
Comparison: https://spaw.co/compare/ipinfo
## IPQualityScore
IPQualityScore sells fraud detection across several signals on one lookup credit: proxy and VPN detection, email verification and device fingerprinting all draw from the same monthly quota, and the IP answer carries a fraud score alongside the connection facts.
Comparison: https://spaw.co/compare/ipqualityscore
## Ipregistry
Ipregistry is a geolocation and threat API sold as credits, either monthly subscriptions or pay-as-you-go packs, whose security object answers abuser, attacker, bogon, cloud provider, proxy, relay, Tor, VPN, anonymous and threat flags and whose data is described as built from BGP tables, registry databases, geofeeds and latency measurements.
Comparison: https://spaw.co/compare/ipregistry
## ipstack
ipstack is a geolocation API on monthly request quotas from a 100-request free tier to a two-million-request plan, with a bulk endpoint and a security module reserved for its top plan; its documentation pages rendered only a header to our fetcher, so the field list is not reproduced here.
Comparison: https://spaw.co/compare/ipstack
## MaxMind
MaxMind sells GeoIP as three per-query web services — Country, City Plus and Insights — and as downloadable databases, with proxy and anonymiser detection reserved for the Insights tier.
Comparison: https://spaw.co/compare/maxmind
## proxycheck.io
proxycheck.io sells an API for checking whether an address is a proxy; every page we tried on 2026-09-12 answered 403 Forbidden to automated reading, so nothing about its pricing, limits, fields or data is reproduced here.
Comparison: https://spaw.co/compare/proxycheck
## Spur
Spur is an anonymity-infrastructure vendor: its Context API answers whether an address is a VPN, proxy or other tunnel with the operator named, and its feeds list anonymous, residential-proxy and datacenter address space; its pricing page rate-limited our fetcher, so prices are not reproduced here.
Comparison: https://spaw.co/compare/spur
ipdata and the other product names are trademarks of their owners. Spaw is not affiliated with them.
Reference: https://spaw.co/alternatives/ipdata
---
# Spaw vs ipgeolocation.io: IP intelligence API comparison
How Spaw and ipgeolocation.io compare on pricing, billing rules, the feeds behind each flag and limits, from ipgeolocation.io's public pages checked on 2026-09-12. Corrections: support@spaw.co.
| | Spaw | ipgeolocation.io |
| --- | --- | --- |
| Pricing model | Pay-as-you-go credit packs whose credits never expire, or optional plans, billed monthly or yearly, that deposit the same credits every month for less and roll unused plan credits over up to three months' allowance; both through Stripe Checkout. One credit buys five answered IP lookups | Monthly credit plans, "A standard unified IP lookup uses 1 credit": Starter "$19/month" for "150000 API Credits Per month", Plus "$49/month" for 500,000, Pro "$79/month" for 1,000,000, Business "$129/month" for 2,000,000, Premium "$249/month" for 5,000,000, Enterprise "Contact Sales"; annual billing gives "2 months free". "Paid monthly plans reset every month. Paid annual plans reset every year". |
| Free tier | 100 free credits every month, no card required | "1000 API Credits Per day for life" with no credit card, "a hard limit of 1000 requests per day" and "No overages on Free"; the free plan returns "core location without the radius and confidence fields" |
| Price per 1,000 at about 10k | $1.80 per 1,000 IP lookups at 5 to the credit (Starter pack: 1,000 credits for $9), or $1.40 on the Starter monthly plan (1,000 credits for $7 a month) | not on the pages read |
| Price per 1,000 at about 100k | $1.56 per 1,000 IP lookups at 5 to the credit (Growth pack: 5,000 credits for $39), or $1.16 on the Growth monthly plan (5,000 credits for $29 a month) | not on the pages read |
| Addresses no feed covered billed | One credit buys five answered addresses whatever their flags say; a flag no feed could evaluate answers null rather than false, and the answer costs the same | not on the pages read |
| Reserved and private ranges billed | No: reserved, private, loopback, CGNAT, documentation and multicast ranges answer reason reserved_range and are free, unallocated space is free, and so are repeats within 7 days | not on the pages read |
| Feeds behind the flags | Public feeds, each named in the answer with the date it was synced: DB-IP Lite and RFC 8805 geofeeds for location, the five registries' delegated statistics for allocation, Spamhaus DROP and ASN-DROP and abuse.ch Feodo Tracker for blocklists, the Tor Project's exit list and Onionoo, the Mullvad, NordVPN and Private Internet Access server lists plus the open X4BNet lists, Apple's iCloud Private Relay egress list, the cloud providers' own published ranges, and the verification files Google, Microsoft, OpenAI, Perplexity, DuckDuckGo and Ahrefs publish for their crawlers. | Its own collection: the security page describes "telemetry from active honeypots, proprietary blocklists, VPN and proxy enumeration, and real-time behavioral analysis", answering "VPN, proxy, residential proxy, and Tor flags, plus provider names, confidence scores, and last-seen dates", flags for "known attackers, bots, spam, and cloud IPs", an is_anonymous field and a "threat score from 0 to 100". Security fields are on paid plans. |
| Refresh cadence | Every feed re-syncs daily, the botnet command-and-control list hourly, and the geofeed crawl weekly. Each response names every dataset it consulted with the date it was last synced, and /status lists them all. | Detection data "updated continuously"; locations "refreshed continuously"; the pricing page says "Proprietary crawlers run 24/7 across the globe" |
| Geolocation source | The DB-IP Lite database, overridden wherever the operator publishes the location itself — an RFC 8805 geofeed, Apple's relay list, a VPN provider's server list. location_source says which answered, and location_confidence is graded from a weekly accuracy report rather than asserted. | Its own: locations "are built from multiple independent signals, including active measurements, that are cross-checked and refreshed continuously", resolving to "city and locality level"; paid plans return "an accuracy radius and a confidence value". No numeric accuracy claim is published. |
| Bulk limit | 100,000 addresses per bulk job, with an HMAC-signed completion webhook | not on the pages read |
| Batch API | 1,000 addresses per synchronous request; repeats inside the list are looked up once | "The bulk endpoint accepts up to 50,000 IPv4 or IPv6 addresses in a single request" |
| Rate limit | 50 requests per second per key on the IP endpoints | "There are no rate limits on any plan"; the free plan has a hard limit of 1,000 requests a day |
| Official SDKs | Python, Node.js, PHP | not on the pages read |
| Data retention | No history row and no log line carrying the address. What a lookup leaves is keyed and never holds the address: the 7-day marker that makes a repeat free and, under the same keyed hash, the count behind seen_by_you; privacy: true skips both. The account's own outcome reports are kept as salted hashes for 90 days. Bulk input lists and result files are deleted with the job after 30 days. | not on the pages read |
## When ipgeolocation.io fits better
Teams that want an accuracy radius with every city answer and a single threat score from a vendor that runs its own honeypots.
- Paid plans return an accuracy radius and a confidence value per lookup; the free plan does not
- The security answer names the VPN or proxy provider and a last-seen date
- One bulk request takes up to 50,000 addresses
- Detection data comes from the vendor's own honeypots, enumeration and behavioural analysis
## When Spaw fits better
Developers who want every public IP feed compiled, dated and named for them, with a risk score whose weights are published so it can be recomputed or ignored.
- ipgeolocation.io's location and threat data are proprietary; Every dataset behind a Spaw IP answer is a public feed named in the response with the date it was synced, and the risk score is a published sum of weights anyone can recompute
- ipgeolocation.io answers a proprietary 0 to 100 threat score; Spaw's risk_score is arithmetic over named signals with published weights, and risk_signals shows every term
- Both name the VPN provider; Spaw also says when an answer is inferred rather than listed (Cloudflare WARP) and answers residential proxies as not detectable from open data rather than claiming them
- ipgeolocation.io sells monthly credits that reset; Spaw sells pack credits that never expire at five IP lookups to the credit, and reserved, private, documentation and unallocated ranges are free
## Questions
**Does Spaw charge for addresses its feeds do not cover?** Spaw bills per answered address, five to the credit: the answered lookup that opens a block costs 1 credit and the next four cost nothing. Reserved, private, loopback, CGNAT, documentation and multicast ranges answer reason reserved_range and are free, unallocated space is free, and so is a repeat of the same address within seven days. A flag no feed could evaluate answers null rather than false: the address is still billed, and the null tells you the signal was not checked instead of pretending it was. ipgeolocation.io does not publish how it bills them.
**Can I run a log file or an export through Spaw?** Yes. A bulk run takes up to 100,000 addresses from the dashboard or the API and returns one row per input address, in input order, with every flag, the risk score and the signals that made it. Five answered addresses cost a credit, repeats inside the list are looked up once, reserved ranges cost nothing, and the finish is announced on an HMAC-signed webhook.
**Is there a free tier?** Every Spaw account receives 100 free credits at the start of each month with no card on file, and unused free credits accumulate. ipgeolocation.io offers: "1000 API Credits Per day for life" with no credit card, "a hard limit of 1000 requests per day" and "No overages on Free"; the free plan returns "core location without the radius and confidence fields".
## Sources (checked 2026-09-12)
- [ipgeolocation.io pricing](https://ipgeolocation.io/pricing.html)
- [ipgeolocation.io IP location API](https://ipgeolocation.io/ip-location-api.html)
- [ipgeolocation.io IP security API](https://ipgeolocation.io/ip-security-api.html)
ipgeolocation.io is a trademark of its owner. Spaw is not affiliated with it.
Reference: https://spaw.co/compare/ipgeolocation
---
# ipgeolocation.io alternatives for IP intelligence
IP intelligence APIs to consider instead of ipgeolocation.io, with the pricing model, free tier and price per 1,000 lookups each vendor publishes. Figures come from public pricing pages checked on 2026-09-12.
| Service | Pricing model | Free tier | Price per 1,000 at about 10k |
| --- | --- | --- | --- |
| [Spaw](https://spaw.co) | Pay-as-you-go credit packs whose credits never expire, or optional plans, billed monthly or yearly, that deposit the same credits every month for less and roll unused plan credits over up to three months' allowance; both through Stripe Checkout. One credit buys five answered IP lookups | 100 free credits every month, no card required | $1.80 per 1,000 IP lookups at 5 to the credit (Starter pack: 1,000 credits for $9), or $1.40 on the Starter monthly plan (1,000 credits for $7 a month) |
| [AbuseIPDB](https://www.abuseipdb.com) | Plans with daily request allowances: Individual free "Forever!", Basic $25/mo or $228/year, Premium $99/mo or $1,068/year, Enterprise custom | Individual: free forever with no credit card, 1,000 check and report requests, 100 block checks and 5 bulk reports a day | not on the pages read |
| [ip-api](https://ip-api.com) | Two tiers. The free endpoint: "Our endpoints are limited to 45 HTTP requests per minute from an IP address" and "We do not allow commercial use of the free endpoint". The Pro service: "€13.3 per month, when paying for 12 months", with "Unlimited requests", "SSL (HTTPS)", "Commercial use allowed" and a "99.9% SLA" ("If the general API uptime falls below 99.9% in a month, we will automatically extend your subscription for 10 days"). | The free endpoint, for non-commercial use only: 45 requests per minute per address over HTTP ("256-bit SSL encryption is not available for this free API") | not on the pages read |
| [IP2Location](https://www.ip2location.com) | IP2Location.io API, monthly query plans: Free $0 ("50K IP Geolocation API queries/month", "11 IP Attributes", "Attribution Required"), Starter "$49/month" ("150K IP Geolocation API queries/month", "20 IP Attributes"), Plus "$249/month" ("300K IP Geolocation API queries/month", "59 IP Attributes"), Security "$499/month" ("600K IP Geolocation API queries/month", "79 IP Attributes"); overage per increment Starter $10, Plus $50, Security $100. Databases, standard licence per year: DB1 "$99", DB2 "$399", DB8 "$1,299", DB18 "$2,799", with a site licence at ten times those figures. | "50K IP Geolocation API queries/month" with 11 attributes, basic geolocation and open-proxy detection only, "Attribution Required"; without a key "up to 1,000 queries per day" | not on the pages read |
| [ipapi](https://ipapi.co) | Monthly plans by lookup quota: Starter "$15 billed monthly" for 60,000 lookups, Professional "$25 billed monthly" for 150,000, Business "$49 billed monthly" for 500,000, Enterprise "$99 billed monthly" for 2,000,000, Premium "$199 billed monthly" for 6,000,000, Ultimate "$399 billed monthly" for 15,000,000; annual billing is 20% less. | A free trial of "up-to 1000 IP lookups in a day" (about 30,000 a month), "for testing & development purposes" and "Not for production use" | not on the pages read |
| [ipdata](https://ipdata.co) | Monthly plans by daily request quota: Personal "$10 per month" with "2,500 requests daily", Lite "$30 per month" with "10,000 requests daily", Startup "$50 per month" with "50,000 requests daily", Business "$120 per month" with "100,000 requests daily"; "20% off all annual plans" at the time of reading. | "1500 free requests daily", "restricted to non-commercial use"; on the free tier, over the quota "all further requests are blocked and you receive an email notification" | not on the pages read |
| [IPinfo](https://ipinfo.io) | Monthly plans by API request quota: Lite $0/mo, Core $26/mo for 150,000 requests, Max $163/mo for 250,000, Enterprise custom for "1M+". Database downloads are listed separately, "Starting at $1,000/month". | IPinfo Lite at "$0/mo", free forever with no credit card, giving unlimited API requests at country level only | not on the pages read |
| [IPQualityScore](https://www.ipqualityscore.com) | Monthly plans by lookup quota: Free $0 (1,000 lookups a month), Startup "As low as: $99 / Month" (5,000), SMB Basic "$499" (10,000), SMB+ "$999" (75,000), Enterprise custom. "Any valid request sent to our Proxy Detection, Email Verification, or Device Fingerprinting services including lookups performed through a batch CSV file check will consume a lookup credit." | 1,000 lookups a month, capped at 35 a day | not on the pages read |
| [Ipregistry](https://ipregistry.co) | Credits, "1 lookup = 1 request = 1 credit" (batch endpoints use "one credit per lookup in the batch request, plus 1"). Subscriptions: Starter "100,000 credits / mo" at "$29 $24 / month", Professional "300,000 credits / mo" at "$79 $66 / month", Business "1,150,000 credits / mo" at "$199 $166 / month", Scale "2,450,000 credits / mo" at "$399 $333 / month" (the lower figure is the annual rate). Pay-as-you-go packs: 15,000 credits for $10, 100,000 for $50, 250,000 for $100, 1,750,000 for $500. | "20,000 free credits on sign-up" with "No credit card, no time limit" | $0.67 (the $10 pack of 15,000 credits, $0.000667 a lookup) |
| [ipstack](https://ipstack.com) | Monthly plans by request quota: Free "100 Request/mo." at $0, Basic "50,000 Request/mo." at "$12.99/mo.", Professional "500,000 Request/mo." at "$59.99/mo.", Professional + "2,000,000 Request/mo." at "$99.99/mo.", Custom on request; yearly billing "Save up to 15%". Overage fees apply past the quota, with notifications at 75%, 90% and 100%. | "100 Request/mo." at $0, without SSL encryption, support or the paid modules (SSL, commercial use and the Location, Time Zone and Connection modules are listed for paid plans) | not on the pages read |
| [MaxMind](https://www.maxmind.com) | Per query, by service, with no minimum published: "If you estimate your volume of queries to be over $2,500 a month, contact us to discuss rates." | A no-cost trial is offered ("Experience the functionality and benefits of our GeoIP web services at no cost"); no free monthly allowance is stated on the page checked | $0.10 per 1,000 for Country ($0.0001 a query), $0.30 for City Plus ($0.0003) and $2.00 for Insights ($0.002) |
| [proxycheck.io](https://proxycheck.io) | not on the pages read | not on the pages read | not on the pages read |
| [Spur](https://spur.us) | not on the pages read | not on the pages read | not on the pages read |
## Spaw
Verification API that bills per answered lookup across six signals on one key and one balance: Email verification, Phone intelligence, IP intelligence, Address verification, Business verification against open registers, and Cross-signal consistency, which runs any subset of the first four in one call and names where they agree and where they do not. The IP side answers from local databases and compiled public feeds: where an address is, who runs it, and whether it is a visitor at all.
## AbuseIPDB
AbuseIPDB is a reputation service built on abuse reports its own users submit: a check returns an abuse confidence score with the report count, country, usage type, ISP and a Tor flag, and plans are sold as daily request allowances rather than credits.
Comparison: https://spaw.co/compare/abuseipdb
## ip-api
ip-api is a geolocation JSON API with a free, non-commercial, HTTP-only endpoint limited by request rate, and a Pro subscription that adds HTTPS, unlimited queries and commercial use; its answer carries one proxy flag, one hosting flag and one mobile flag beside the location and network fields.
Comparison: https://spaw.co/compare/ip-api
## IP2Location
IP2Location sells geolocation as downloadable databases (DB1 to DB26, licensed yearly) and, through IP2Location.io, as an API on monthly query plans whose higher tiers add proxy, VPN, Tor, datacenter, crawler and residential-proxy detection and a fraud score; the classic web service no longer takes new registrations.
Comparison: https://spaw.co/compare/ip2location
## ipapi
ipapi.co is a geolocation API sold as monthly lookup quotas, whose pricing page credits its data to DB-IP and the IP2Location LITE database; the answer carries location, country and network fields and no proxy, VPN or Tor flag.
Comparison: https://spaw.co/compare/ipapi
## ipdata
ipdata is a geolocation and threat API sold as daily request quotas, from a free non-commercial tier to a Business plan, whose pricing page says it does not keep records of the addresses looked up beyond a day; its documentation site refused automated reading, so the fields are not reproduced here.
Comparison: https://spaw.co/compare/ipdata
## IPinfo
IPinfo is an IP data company that sells geolocation, ASN, privacy detection and company data as monthly API plans and as database downloads, collected through its own measurement network rather than assembled from third-party feeds.
Comparison: https://spaw.co/compare/ipinfo
## IPQualityScore
IPQualityScore sells fraud detection across several signals on one lookup credit: proxy and VPN detection, email verification and device fingerprinting all draw from the same monthly quota, and the IP answer carries a fraud score alongside the connection facts.
Comparison: https://spaw.co/compare/ipqualityscore
## Ipregistry
Ipregistry is a geolocation and threat API sold as credits, either monthly subscriptions or pay-as-you-go packs, whose security object answers abuser, attacker, bogon, cloud provider, proxy, relay, Tor, VPN, anonymous and threat flags and whose data is described as built from BGP tables, registry databases, geofeeds and latency measurements.
Comparison: https://spaw.co/compare/ipregistry
## ipstack
ipstack is a geolocation API on monthly request quotas from a 100-request free tier to a two-million-request plan, with a bulk endpoint and a security module reserved for its top plan; its documentation pages rendered only a header to our fetcher, so the field list is not reproduced here.
Comparison: https://spaw.co/compare/ipstack
## MaxMind
MaxMind sells GeoIP as three per-query web services — Country, City Plus and Insights — and as downloadable databases, with proxy and anonymiser detection reserved for the Insights tier.
Comparison: https://spaw.co/compare/maxmind
## proxycheck.io
proxycheck.io sells an API for checking whether an address is a proxy; every page we tried on 2026-09-12 answered 403 Forbidden to automated reading, so nothing about its pricing, limits, fields or data is reproduced here.
Comparison: https://spaw.co/compare/proxycheck
## Spur
Spur is an anonymity-infrastructure vendor: its Context API answers whether an address is a VPN, proxy or other tunnel with the operator named, and its feeds list anonymous, residential-proxy and datacenter address space; its pricing page rate-limited our fetcher, so prices are not reproduced here.
Comparison: https://spaw.co/compare/spur
ipgeolocation.io and the other product names are trademarks of their owners. Spaw is not affiliated with them.
Reference: https://spaw.co/alternatives/ipgeolocation
---
# Spaw vs IPinfo: IP intelligence API comparison
How Spaw and IPinfo compare on pricing, billing rules, the feeds behind each flag and limits, from IPinfo's public pages checked on 2026-09-10. Corrections: support@spaw.co.
| | Spaw | IPinfo |
| --- | --- | --- |
| Pricing model | Pay-as-you-go credit packs whose credits never expire, or optional plans, billed monthly or yearly, that deposit the same credits every month for less and roll unused plan credits over up to three months' allowance; both through Stripe Checkout. One credit buys five answered IP lookups | Monthly plans by API request quota: Lite $0/mo, Core $26/mo for 150,000 requests, Max $163/mo for 250,000, Enterprise custom for "1M+". Database downloads are listed separately, "Starting at $1,000/month". |
| Free tier | 100 free credits every month, no card required | IPinfo Lite at "$0/mo", free forever with no credit card, giving unlimited API requests at country level only |
| Price per 1,000 at about 10k | $1.80 per 1,000 IP lookups at 5 to the credit (Starter pack: 1,000 credits for $9), or $1.40 on the Starter monthly plan (1,000 credits for $7 a month) | not on the pages read |
| Price per 1,000 at about 100k | $1.56 per 1,000 IP lookups at 5 to the credit (Growth pack: 5,000 credits for $39), or $1.16 on the Growth monthly plan (5,000 credits for $29 a month) | not on the pages read |
| Addresses no feed covered billed | One credit buys five answered addresses whatever their flags say; a flag no feed could evaluate answers null rather than false, and the answer costs the same | not on the pages read |
| Reserved and private ranges billed | No: reserved, private, loopback, CGNAT, documentation and multicast ranges answer reason reserved_range and are free, unallocated space is free, and so are repeats within 7 days | not on the pages read |
| Feeds behind the flags | Public feeds, each named in the answer with the date it was synced: DB-IP Lite and RFC 8805 geofeeds for location, the five registries' delegated statistics for allocation, Spamhaus DROP and ASN-DROP and abuse.ch Feodo Tracker for blocklists, the Tor Project's exit list and Onionoo, the Mullvad, NordVPN and Private Internet Access server lists plus the open X4BNet lists, Apple's iCloud Private Relay egress list, the cloud providers' own published ranges, and the verification files Google, Microsoft, OpenAI, Perplexity, DuckDuckGo and Ahrefs publish for their crawlers. | Its own measurement rather than published lists: the pages describe ProbeNet, "our proprietary internet measurement platform", reaching "44% of the world's ASNs within 1 millisecond" from "more than 1,000 points of presence". Privacy detection (VPN, Tor, proxy, relay) starts on the Core plan, with the service names added on Max; residential proxy detection is Max and above. |
| Refresh cadence | Every feed re-syncs daily, the botnet command-and-control list hourly, and the geofeed crawl weekly. Each response names every dataset it consulted with the date it was last synced, and /status lists them all. | "Data updates every 24 hours" |
| Geolocation source | The DB-IP Lite database, overridden wherever the operator publishes the location itself — an RFC 8805 geofeed, Apple's relay list, a VPN provider's server list. location_source says which answered, and location_confidence is graded from a weekly accuracy report rather than asserted. | Its own database: country level on Lite, city level from Core up, with a geolocation accuracy radius from Max up. The pages make no numeric accuracy claim and note that "rural accuracy is still inherently lower than urban areas". |
| Bulk limit | 100,000 addresses per bulk job, with an HMAC-signed completion webhook | not on the pages read |
| Batch API | 1,000 addresses per synchronous request; repeats inside the list are looked up once | not on the pages read |
| Rate limit | 50 requests per second per key on the IP endpoints | not on the pages read |
| Official SDKs | Python, Node.js, PHP | not on the pages read |
| Data retention | No history row and no log line carrying the address. What a lookup leaves is keyed and never holds the address: the 7-day marker that makes a repeat free and, under the same keyed hash, the count behind seen_by_you; privacy: true skips both. The account's own outcome reports are kept as salted hashes for 90 days. Bulk input lists and result files are deleted with the job after 30 days. | not on the pages read |
## When IPinfo fits better
Teams that want city-level geolocation from a vendor that measures the internet itself, and are willing to buy a plan tier per field they need.
- The data is measured rather than aggregated: ProbeNet is IPinfo's own network of probes
- The free Lite plan puts no cap on API requests, but answers country only
- Abuse contact and company data are Enterprise-only
- Database downloads are sold, so the data can run inside your own infrastructure
## When Spaw fits better
Developers who want every public IP feed compiled, dated and named for them, with a risk score whose weights are published so it can be recomputed or ignored.
- IPinfo gates fields by plan — privacy detection from Core, carrier and residential proxy from Max, abuse contact and company from Enterprise; Spaw answers every field it has on every lookup at the same price, five lookups to the credit
- IPinfo sells a monthly request quota; Spaw sells pack credits that never expire at five IP lookups to the credit, and reserved, private, documentation and unallocated ranges are free
- Spaw names each feed and its sync date in the response and grades location_confidence from a weekly accuracy report; IPinfo publishes no per-answer provenance
- IPinfo's data is proprietary; every dataset behind a Spaw IP answer is a public feed a customer can read for themselves
## Questions
**Does Spaw charge for addresses its feeds do not cover?** Spaw bills per answered address, five to the credit: the answered lookup that opens a block costs 1 credit and the next four cost nothing. Reserved, private, loopback, CGNAT, documentation and multicast ranges answer reason reserved_range and are free, unallocated space is free, and so is a repeat of the same address within seven days. A flag no feed could evaluate answers null rather than false: the address is still billed, and the null tells you the signal was not checked instead of pretending it was. IPinfo does not publish how it bills them.
**Can I run a log file or an export through Spaw?** Yes. A bulk run takes up to 100,000 addresses from the dashboard or the API and returns one row per input address, in input order, with every flag, the risk score and the signals that made it. Five answered addresses cost a credit, repeats inside the list are looked up once, reserved ranges cost nothing, and the finish is announced on an HMAC-signed webhook.
**Is there a free tier?** Every Spaw account receives 100 free credits at the start of each month with no card on file, and unused free credits accumulate. IPinfo offers: IPinfo Lite at "$0/mo", free forever with no credit card, giving unlimited API requests at country level only.
## Sources (checked 2026-09-10)
- [IPinfo pricing](https://ipinfo.io/pricing)
- [IPinfo IP geolocation database](https://ipinfo.io/products/ip-geolocation-database)
IPinfo is a trademark of its owner. Spaw is not affiliated with it.
Reference: https://spaw.co/compare/ipinfo
---
# IPinfo alternatives for IP intelligence
IP intelligence APIs to consider instead of IPinfo, with the pricing model, free tier and price per 1,000 lookups each vendor publishes. Figures come from public pricing pages checked on 2026-09-10.
| Service | Pricing model | Free tier | Price per 1,000 at about 10k |
| --- | --- | --- | --- |
| [Spaw](https://spaw.co) | Pay-as-you-go credit packs whose credits never expire, or optional plans, billed monthly or yearly, that deposit the same credits every month for less and roll unused plan credits over up to three months' allowance; both through Stripe Checkout. One credit buys five answered IP lookups | 100 free credits every month, no card required | $1.80 per 1,000 IP lookups at 5 to the credit (Starter pack: 1,000 credits for $9), or $1.40 on the Starter monthly plan (1,000 credits for $7 a month) |
| [AbuseIPDB](https://www.abuseipdb.com) | Plans with daily request allowances: Individual free "Forever!", Basic $25/mo or $228/year, Premium $99/mo or $1,068/year, Enterprise custom | Individual: free forever with no credit card, 1,000 check and report requests, 100 block checks and 5 bulk reports a day | not on the pages read |
| [ip-api](https://ip-api.com) | Two tiers. The free endpoint: "Our endpoints are limited to 45 HTTP requests per minute from an IP address" and "We do not allow commercial use of the free endpoint". The Pro service: "€13.3 per month, when paying for 12 months", with "Unlimited requests", "SSL (HTTPS)", "Commercial use allowed" and a "99.9% SLA" ("If the general API uptime falls below 99.9% in a month, we will automatically extend your subscription for 10 days"). | The free endpoint, for non-commercial use only: 45 requests per minute per address over HTTP ("256-bit SSL encryption is not available for this free API") | not on the pages read |
| [IP2Location](https://www.ip2location.com) | IP2Location.io API, monthly query plans: Free $0 ("50K IP Geolocation API queries/month", "11 IP Attributes", "Attribution Required"), Starter "$49/month" ("150K IP Geolocation API queries/month", "20 IP Attributes"), Plus "$249/month" ("300K IP Geolocation API queries/month", "59 IP Attributes"), Security "$499/month" ("600K IP Geolocation API queries/month", "79 IP Attributes"); overage per increment Starter $10, Plus $50, Security $100. Databases, standard licence per year: DB1 "$99", DB2 "$399", DB8 "$1,299", DB18 "$2,799", with a site licence at ten times those figures. | "50K IP Geolocation API queries/month" with 11 attributes, basic geolocation and open-proxy detection only, "Attribution Required"; without a key "up to 1,000 queries per day" | not on the pages read |
| [ipapi](https://ipapi.co) | Monthly plans by lookup quota: Starter "$15 billed monthly" for 60,000 lookups, Professional "$25 billed monthly" for 150,000, Business "$49 billed monthly" for 500,000, Enterprise "$99 billed monthly" for 2,000,000, Premium "$199 billed monthly" for 6,000,000, Ultimate "$399 billed monthly" for 15,000,000; annual billing is 20% less. | A free trial of "up-to 1000 IP lookups in a day" (about 30,000 a month), "for testing & development purposes" and "Not for production use" | not on the pages read |
| [ipdata](https://ipdata.co) | Monthly plans by daily request quota: Personal "$10 per month" with "2,500 requests daily", Lite "$30 per month" with "10,000 requests daily", Startup "$50 per month" with "50,000 requests daily", Business "$120 per month" with "100,000 requests daily"; "20% off all annual plans" at the time of reading. | "1500 free requests daily", "restricted to non-commercial use"; on the free tier, over the quota "all further requests are blocked and you receive an email notification" | not on the pages read |
| [ipgeolocation.io](https://ipgeolocation.io) | Monthly credit plans, "A standard unified IP lookup uses 1 credit": Starter "$19/month" for "150000 API Credits Per month", Plus "$49/month" for 500,000, Pro "$79/month" for 1,000,000, Business "$129/month" for 2,000,000, Premium "$249/month" for 5,000,000, Enterprise "Contact Sales"; annual billing gives "2 months free". "Paid monthly plans reset every month. Paid annual plans reset every year". | "1000 API Credits Per day for life" with no credit card, "a hard limit of 1000 requests per day" and "No overages on Free"; the free plan returns "core location without the radius and confidence fields" | not on the pages read |
| [IPQualityScore](https://www.ipqualityscore.com) | Monthly plans by lookup quota: Free $0 (1,000 lookups a month), Startup "As low as: $99 / Month" (5,000), SMB Basic "$499" (10,000), SMB+ "$999" (75,000), Enterprise custom. "Any valid request sent to our Proxy Detection, Email Verification, or Device Fingerprinting services including lookups performed through a batch CSV file check will consume a lookup credit." | 1,000 lookups a month, capped at 35 a day | not on the pages read |
| [Ipregistry](https://ipregistry.co) | Credits, "1 lookup = 1 request = 1 credit" (batch endpoints use "one credit per lookup in the batch request, plus 1"). Subscriptions: Starter "100,000 credits / mo" at "$29 $24 / month", Professional "300,000 credits / mo" at "$79 $66 / month", Business "1,150,000 credits / mo" at "$199 $166 / month", Scale "2,450,000 credits / mo" at "$399 $333 / month" (the lower figure is the annual rate). Pay-as-you-go packs: 15,000 credits for $10, 100,000 for $50, 250,000 for $100, 1,750,000 for $500. | "20,000 free credits on sign-up" with "No credit card, no time limit" | $0.67 (the $10 pack of 15,000 credits, $0.000667 a lookup) |
| [ipstack](https://ipstack.com) | Monthly plans by request quota: Free "100 Request/mo." at $0, Basic "50,000 Request/mo." at "$12.99/mo.", Professional "500,000 Request/mo." at "$59.99/mo.", Professional + "2,000,000 Request/mo." at "$99.99/mo.", Custom on request; yearly billing "Save up to 15%". Overage fees apply past the quota, with notifications at 75%, 90% and 100%. | "100 Request/mo." at $0, without SSL encryption, support or the paid modules (SSL, commercial use and the Location, Time Zone and Connection modules are listed for paid plans) | not on the pages read |
| [MaxMind](https://www.maxmind.com) | Per query, by service, with no minimum published: "If you estimate your volume of queries to be over $2,500 a month, contact us to discuss rates." | A no-cost trial is offered ("Experience the functionality and benefits of our GeoIP web services at no cost"); no free monthly allowance is stated on the page checked | $0.10 per 1,000 for Country ($0.0001 a query), $0.30 for City Plus ($0.0003) and $2.00 for Insights ($0.002) |
| [proxycheck.io](https://proxycheck.io) | not on the pages read | not on the pages read | not on the pages read |
| [Spur](https://spur.us) | not on the pages read | not on the pages read | not on the pages read |
## Spaw
Verification API that bills per answered lookup across six signals on one key and one balance: Email verification, Phone intelligence, IP intelligence, Address verification, Business verification against open registers, and Cross-signal consistency, which runs any subset of the first four in one call and names where they agree and where they do not. The IP side answers from local databases and compiled public feeds: where an address is, who runs it, and whether it is a visitor at all.
## AbuseIPDB
AbuseIPDB is a reputation service built on abuse reports its own users submit: a check returns an abuse confidence score with the report count, country, usage type, ISP and a Tor flag, and plans are sold as daily request allowances rather than credits.
Comparison: https://spaw.co/compare/abuseipdb
## ip-api
ip-api is a geolocation JSON API with a free, non-commercial, HTTP-only endpoint limited by request rate, and a Pro subscription that adds HTTPS, unlimited queries and commercial use; its answer carries one proxy flag, one hosting flag and one mobile flag beside the location and network fields.
Comparison: https://spaw.co/compare/ip-api
## IP2Location
IP2Location sells geolocation as downloadable databases (DB1 to DB26, licensed yearly) and, through IP2Location.io, as an API on monthly query plans whose higher tiers add proxy, VPN, Tor, datacenter, crawler and residential-proxy detection and a fraud score; the classic web service no longer takes new registrations.
Comparison: https://spaw.co/compare/ip2location
## ipapi
ipapi.co is a geolocation API sold as monthly lookup quotas, whose pricing page credits its data to DB-IP and the IP2Location LITE database; the answer carries location, country and network fields and no proxy, VPN or Tor flag.
Comparison: https://spaw.co/compare/ipapi
## ipdata
ipdata is a geolocation and threat API sold as daily request quotas, from a free non-commercial tier to a Business plan, whose pricing page says it does not keep records of the addresses looked up beyond a day; its documentation site refused automated reading, so the fields are not reproduced here.
Comparison: https://spaw.co/compare/ipdata
## ipgeolocation.io
ipgeolocation.io sells geolocation and a separate security API on monthly credit plans, with location built from its own measurements and a security answer that flags VPN, proxy, residential proxy and Tor with provider names, confidence scores and a 0 to 100 threat score.
Comparison: https://spaw.co/compare/ipgeolocation
## IPQualityScore
IPQualityScore sells fraud detection across several signals on one lookup credit: proxy and VPN detection, email verification and device fingerprinting all draw from the same monthly quota, and the IP answer carries a fraud score alongside the connection facts.
Comparison: https://spaw.co/compare/ipqualityscore
## Ipregistry
Ipregistry is a geolocation and threat API sold as credits, either monthly subscriptions or pay-as-you-go packs, whose security object answers abuser, attacker, bogon, cloud provider, proxy, relay, Tor, VPN, anonymous and threat flags and whose data is described as built from BGP tables, registry databases, geofeeds and latency measurements.
Comparison: https://spaw.co/compare/ipregistry
## ipstack
ipstack is a geolocation API on monthly request quotas from a 100-request free tier to a two-million-request plan, with a bulk endpoint and a security module reserved for its top plan; its documentation pages rendered only a header to our fetcher, so the field list is not reproduced here.
Comparison: https://spaw.co/compare/ipstack
## MaxMind
MaxMind sells GeoIP as three per-query web services — Country, City Plus and Insights — and as downloadable databases, with proxy and anonymiser detection reserved for the Insights tier.
Comparison: https://spaw.co/compare/maxmind
## proxycheck.io
proxycheck.io sells an API for checking whether an address is a proxy; every page we tried on 2026-09-12 answered 403 Forbidden to automated reading, so nothing about its pricing, limits, fields or data is reproduced here.
Comparison: https://spaw.co/compare/proxycheck
## Spur
Spur is an anonymity-infrastructure vendor: its Context API answers whether an address is a VPN, proxy or other tunnel with the operator named, and its feeds list anonymous, residential-proxy and datacenter address space; its pricing page rate-limited our fetcher, so prices are not reproduced here.
Comparison: https://spaw.co/compare/spur
IPinfo and the other product names are trademarks of their owners. Spaw is not affiliated with them.
Reference: https://spaw.co/alternatives/ipinfo
---
# Spaw vs IPQualityScore: IP intelligence API comparison
How Spaw and IPQualityScore compare on pricing, billing rules, the feeds behind each flag and limits, from IPQualityScore's public pages checked on 2026-09-10. Corrections: support@spaw.co.
| | Spaw | IPQualityScore |
| --- | --- | --- |
| Pricing model | Pay-as-you-go credit packs whose credits never expire, or optional plans, billed monthly or yearly, that deposit the same credits every month for less and roll unused plan credits over up to three months' allowance; both through Stripe Checkout. One credit buys five answered IP lookups | Monthly plans by lookup quota: Free $0 (1,000 lookups a month), Startup "As low as: $99 / Month" (5,000), SMB Basic "$499" (10,000), SMB+ "$999" (75,000), Enterprise custom. "Any valid request sent to our Proxy Detection, Email Verification, or Device Fingerprinting services including lookups performed through a batch CSV file check will consume a lookup credit." |
| Free tier | 100 free credits every month, no card required | 1,000 lookups a month, capped at 35 a day |
| Price per 1,000 at about 10k | $1.80 per 1,000 IP lookups at 5 to the credit (Starter pack: 1,000 credits for $9), or $1.40 on the Starter monthly plan (1,000 credits for $7 a month) | not on the pages read |
| Price per 1,000 at about 100k | $1.56 per 1,000 IP lookups at 5 to the credit (Growth pack: 5,000 credits for $39), or $1.16 on the Growth monthly plan (5,000 credits for $29 a month) | not on the pages read |
| Addresses no feed covered billed | One credit buys five answered addresses whatever their flags say; a flag no feed could evaluate answers null rather than false, and the answer costs the same | not on the pages read |
| Reserved and private ranges billed | No: reserved, private, loopback, CGNAT, documentation and multicast ranges answer reason reserved_range and are free, unallocated space is free, and so are repeats within 7 days | not on the pages read |
| Feeds behind the flags | Public feeds, each named in the answer with the date it was synced: DB-IP Lite and RFC 8805 geofeeds for location, the five registries' delegated statistics for allocation, Spamhaus DROP and ASN-DROP and abuse.ch Feodo Tracker for blocklists, the Tor Project's exit list and Onionoo, the Mullvad, NordVPN and Private Internet Access server lists plus the open X4BNet lists, Apple's iCloud Private Relay egress list, the cloud providers' own published ranges, and the verification files Google, Microsoft, OpenAI, Perplexity, DuckDuckGo and Ahrefs publish for their crawlers. | Its own collection rather than published lists: "IPQS gathers data directly from our proprietary honeypots, traps, crawlers, and thousands of live sites opted into our threat intelligence network." The proxy detection answer carries proxy, vpn, tor, active_vpn, active_tor, fraud_score, ISP, organization, ASN, connection_type, recent_abuse, bot_status, is_crawler and the location fields. |
| Refresh cadence | Every feed re-syncs daily, the botnet command-and-control list hourly, and the geofeed crawl weekly. Each response names every dataset it consulted with the date it was last synced, and /status lists them all. | Not published as a schedule: the documentation says the data is collected continuously, "scanning billions of IP addresses every day", without naming a refresh cycle. |
| Geolocation source | The DB-IP Lite database, overridden wherever the operator publishes the location itself — an RFC 8805 geofeed, Apple's relay list, a VPN provider's server list. location_source says which answered, and location_confidence is graded from a weekly accuracy report rather than asserted. | Its own data: country_code, city, region, latitude, longitude, timezone and host come back with the fraud fields. |
| Bulk limit | 100,000 addresses per bulk job, with an HMAC-signed completion webhook | Batch CSV file checks are offered, and each row consumes a lookup credit; no row limit is published on the pages checked |
| Batch API | 1,000 addresses per synchronous request; repeats inside the list are looked up once | not on the pages read |
| Rate limit | 50 requests per second per key on the IP endpoints | 20 lookups a second on the SMB Basic plan; the other plans' limits are not stated on the page checked |
| Official SDKs | Python, Node.js, PHP | not on the pages read |
| Data retention | No history row and no log line carrying the address. What a lookup leaves is keyed and never holds the address: the 7-day marker that makes a repeat free and, under the same keyed hash, the count behind seen_by_you; privacy: true skips both. The account's own outcome reports are kept as salted hashes for 90 days. Bulk input lists and result files are deleted with the job after 30 days. | not on the pages read |
## When IPQualityScore fits better
Teams that want one vendor, one credit pool and a single fraud score across IP, email and device signals.
- One credit pool across proxy detection, email verification and device fingerprinting
- active_vpn and active_tor are separate from vpn and tor, so a listed range and a live connection are different answers
- The free plan allows 1,000 lookups a month, capped at 35 a day
## When Spaw fits better
Developers who want every public IP feed compiled, dated and named for them, with a risk score whose weights are published so it can be recomputed or ignored.
- Both sell several signals on one balance; IPQualityScore's are IP, email and device, Spaw's are email, phone, IP, address, business identity and the consistency check across them
- IPQualityScore's data is proprietary; every dataset behind a Spaw IP answer is a public feed named in the response with the date it was synced
- IPQualityScore's plans are a monthly quota that resets; Spaw's pack credits never expire, five IP lookups to the credit, and reserved and unallocated ranges cost nothing
- Spaw publishes the weight behind every point of its risk score, so the number can be recomputed or ignored
## Questions
**Does Spaw charge for addresses its feeds do not cover?** Spaw bills per answered address, five to the credit: the answered lookup that opens a block costs 1 credit and the next four cost nothing. Reserved, private, loopback, CGNAT, documentation and multicast ranges answer reason reserved_range and are free, unallocated space is free, and so is a repeat of the same address within seven days. A flag no feed could evaluate answers null rather than false: the address is still billed, and the null tells you the signal was not checked instead of pretending it was. IPQualityScore does not publish how it bills them.
**Can I run a log file or an export through Spaw?** Yes. A bulk run takes up to 100,000 addresses from the dashboard or the API and returns one row per input address, in input order, with every flag, the risk score and the signals that made it. Five answered addresses cost a credit, repeats inside the list are looked up once, reserved ranges cost nothing, and the finish is announced on an HMAC-signed webhook.
**Is there a free tier?** Every Spaw account receives 100 free credits at the start of each month with no card on file, and unused free credits accumulate. IPQualityScore offers: 1,000 lookups a month, capped at 35 a day.
## Sources (checked 2026-09-10)
- [IPQualityScore plans](https://www.ipqualityscore.com/plans)
- [IPQualityScore proxy detection documentation](https://www.ipqualityscore.com/documentation/proxy-detection-api/overview)
IPQualityScore is a trademark of its owner. Spaw is not affiliated with it.
Reference: https://spaw.co/compare/ipqualityscore
---
# IPQualityScore alternatives for IP intelligence
IP intelligence APIs to consider instead of IPQualityScore, with the pricing model, free tier and price per 1,000 lookups each vendor publishes. Figures come from public pricing pages checked on 2026-09-10.
| Service | Pricing model | Free tier | Price per 1,000 at about 10k |
| --- | --- | --- | --- |
| [Spaw](https://spaw.co) | Pay-as-you-go credit packs whose credits never expire, or optional plans, billed monthly or yearly, that deposit the same credits every month for less and roll unused plan credits over up to three months' allowance; both through Stripe Checkout. One credit buys five answered IP lookups | 100 free credits every month, no card required | $1.80 per 1,000 IP lookups at 5 to the credit (Starter pack: 1,000 credits for $9), or $1.40 on the Starter monthly plan (1,000 credits for $7 a month) |
| [AbuseIPDB](https://www.abuseipdb.com) | Plans with daily request allowances: Individual free "Forever!", Basic $25/mo or $228/year, Premium $99/mo or $1,068/year, Enterprise custom | Individual: free forever with no credit card, 1,000 check and report requests, 100 block checks and 5 bulk reports a day | not on the pages read |
| [ip-api](https://ip-api.com) | Two tiers. The free endpoint: "Our endpoints are limited to 45 HTTP requests per minute from an IP address" and "We do not allow commercial use of the free endpoint". The Pro service: "€13.3 per month, when paying for 12 months", with "Unlimited requests", "SSL (HTTPS)", "Commercial use allowed" and a "99.9% SLA" ("If the general API uptime falls below 99.9% in a month, we will automatically extend your subscription for 10 days"). | The free endpoint, for non-commercial use only: 45 requests per minute per address over HTTP ("256-bit SSL encryption is not available for this free API") | not on the pages read |
| [IP2Location](https://www.ip2location.com) | IP2Location.io API, monthly query plans: Free $0 ("50K IP Geolocation API queries/month", "11 IP Attributes", "Attribution Required"), Starter "$49/month" ("150K IP Geolocation API queries/month", "20 IP Attributes"), Plus "$249/month" ("300K IP Geolocation API queries/month", "59 IP Attributes"), Security "$499/month" ("600K IP Geolocation API queries/month", "79 IP Attributes"); overage per increment Starter $10, Plus $50, Security $100. Databases, standard licence per year: DB1 "$99", DB2 "$399", DB8 "$1,299", DB18 "$2,799", with a site licence at ten times those figures. | "50K IP Geolocation API queries/month" with 11 attributes, basic geolocation and open-proxy detection only, "Attribution Required"; without a key "up to 1,000 queries per day" | not on the pages read |
| [ipapi](https://ipapi.co) | Monthly plans by lookup quota: Starter "$15 billed monthly" for 60,000 lookups, Professional "$25 billed monthly" for 150,000, Business "$49 billed monthly" for 500,000, Enterprise "$99 billed monthly" for 2,000,000, Premium "$199 billed monthly" for 6,000,000, Ultimate "$399 billed monthly" for 15,000,000; annual billing is 20% less. | A free trial of "up-to 1000 IP lookups in a day" (about 30,000 a month), "for testing & development purposes" and "Not for production use" | not on the pages read |
| [ipdata](https://ipdata.co) | Monthly plans by daily request quota: Personal "$10 per month" with "2,500 requests daily", Lite "$30 per month" with "10,000 requests daily", Startup "$50 per month" with "50,000 requests daily", Business "$120 per month" with "100,000 requests daily"; "20% off all annual plans" at the time of reading. | "1500 free requests daily", "restricted to non-commercial use"; on the free tier, over the quota "all further requests are blocked and you receive an email notification" | not on the pages read |
| [ipgeolocation.io](https://ipgeolocation.io) | Monthly credit plans, "A standard unified IP lookup uses 1 credit": Starter "$19/month" for "150000 API Credits Per month", Plus "$49/month" for 500,000, Pro "$79/month" for 1,000,000, Business "$129/month" for 2,000,000, Premium "$249/month" for 5,000,000, Enterprise "Contact Sales"; annual billing gives "2 months free". "Paid monthly plans reset every month. Paid annual plans reset every year". | "1000 API Credits Per day for life" with no credit card, "a hard limit of 1000 requests per day" and "No overages on Free"; the free plan returns "core location without the radius and confidence fields" | not on the pages read |
| [IPinfo](https://ipinfo.io) | Monthly plans by API request quota: Lite $0/mo, Core $26/mo for 150,000 requests, Max $163/mo for 250,000, Enterprise custom for "1M+". Database downloads are listed separately, "Starting at $1,000/month". | IPinfo Lite at "$0/mo", free forever with no credit card, giving unlimited API requests at country level only | not on the pages read |
| [Ipregistry](https://ipregistry.co) | Credits, "1 lookup = 1 request = 1 credit" (batch endpoints use "one credit per lookup in the batch request, plus 1"). Subscriptions: Starter "100,000 credits / mo" at "$29 $24 / month", Professional "300,000 credits / mo" at "$79 $66 / month", Business "1,150,000 credits / mo" at "$199 $166 / month", Scale "2,450,000 credits / mo" at "$399 $333 / month" (the lower figure is the annual rate). Pay-as-you-go packs: 15,000 credits for $10, 100,000 for $50, 250,000 for $100, 1,750,000 for $500. | "20,000 free credits on sign-up" with "No credit card, no time limit" | $0.67 (the $10 pack of 15,000 credits, $0.000667 a lookup) |
| [ipstack](https://ipstack.com) | Monthly plans by request quota: Free "100 Request/mo." at $0, Basic "50,000 Request/mo." at "$12.99/mo.", Professional "500,000 Request/mo." at "$59.99/mo.", Professional + "2,000,000 Request/mo." at "$99.99/mo.", Custom on request; yearly billing "Save up to 15%". Overage fees apply past the quota, with notifications at 75%, 90% and 100%. | "100 Request/mo." at $0, without SSL encryption, support or the paid modules (SSL, commercial use and the Location, Time Zone and Connection modules are listed for paid plans) | not on the pages read |
| [MaxMind](https://www.maxmind.com) | Per query, by service, with no minimum published: "If you estimate your volume of queries to be over $2,500 a month, contact us to discuss rates." | A no-cost trial is offered ("Experience the functionality and benefits of our GeoIP web services at no cost"); no free monthly allowance is stated on the page checked | $0.10 per 1,000 for Country ($0.0001 a query), $0.30 for City Plus ($0.0003) and $2.00 for Insights ($0.002) |
| [proxycheck.io](https://proxycheck.io) | not on the pages read | not on the pages read | not on the pages read |
| [Spur](https://spur.us) | not on the pages read | not on the pages read | not on the pages read |
## Spaw
Verification API that bills per answered lookup across six signals on one key and one balance: Email verification, Phone intelligence, IP intelligence, Address verification, Business verification against open registers, and Cross-signal consistency, which runs any subset of the first four in one call and names where they agree and where they do not. The IP side answers from local databases and compiled public feeds: where an address is, who runs it, and whether it is a visitor at all.
## AbuseIPDB
AbuseIPDB is a reputation service built on abuse reports its own users submit: a check returns an abuse confidence score with the report count, country, usage type, ISP and a Tor flag, and plans are sold as daily request allowances rather than credits.
Comparison: https://spaw.co/compare/abuseipdb
## ip-api
ip-api is a geolocation JSON API with a free, non-commercial, HTTP-only endpoint limited by request rate, and a Pro subscription that adds HTTPS, unlimited queries and commercial use; its answer carries one proxy flag, one hosting flag and one mobile flag beside the location and network fields.
Comparison: https://spaw.co/compare/ip-api
## IP2Location
IP2Location sells geolocation as downloadable databases (DB1 to DB26, licensed yearly) and, through IP2Location.io, as an API on monthly query plans whose higher tiers add proxy, VPN, Tor, datacenter, crawler and residential-proxy detection and a fraud score; the classic web service no longer takes new registrations.
Comparison: https://spaw.co/compare/ip2location
## ipapi
ipapi.co is a geolocation API sold as monthly lookup quotas, whose pricing page credits its data to DB-IP and the IP2Location LITE database; the answer carries location, country and network fields and no proxy, VPN or Tor flag.
Comparison: https://spaw.co/compare/ipapi
## ipdata
ipdata is a geolocation and threat API sold as daily request quotas, from a free non-commercial tier to a Business plan, whose pricing page says it does not keep records of the addresses looked up beyond a day; its documentation site refused automated reading, so the fields are not reproduced here.
Comparison: https://spaw.co/compare/ipdata
## ipgeolocation.io
ipgeolocation.io sells geolocation and a separate security API on monthly credit plans, with location built from its own measurements and a security answer that flags VPN, proxy, residential proxy and Tor with provider names, confidence scores and a 0 to 100 threat score.
Comparison: https://spaw.co/compare/ipgeolocation
## IPinfo
IPinfo is an IP data company that sells geolocation, ASN, privacy detection and company data as monthly API plans and as database downloads, collected through its own measurement network rather than assembled from third-party feeds.
Comparison: https://spaw.co/compare/ipinfo
## Ipregistry
Ipregistry is a geolocation and threat API sold as credits, either monthly subscriptions or pay-as-you-go packs, whose security object answers abuser, attacker, bogon, cloud provider, proxy, relay, Tor, VPN, anonymous and threat flags and whose data is described as built from BGP tables, registry databases, geofeeds and latency measurements.
Comparison: https://spaw.co/compare/ipregistry
## ipstack
ipstack is a geolocation API on monthly request quotas from a 100-request free tier to a two-million-request plan, with a bulk endpoint and a security module reserved for its top plan; its documentation pages rendered only a header to our fetcher, so the field list is not reproduced here.
Comparison: https://spaw.co/compare/ipstack
## MaxMind
MaxMind sells GeoIP as three per-query web services — Country, City Plus and Insights — and as downloadable databases, with proxy and anonymiser detection reserved for the Insights tier.
Comparison: https://spaw.co/compare/maxmind
## proxycheck.io
proxycheck.io sells an API for checking whether an address is a proxy; every page we tried on 2026-09-12 answered 403 Forbidden to automated reading, so nothing about its pricing, limits, fields or data is reproduced here.
Comparison: https://spaw.co/compare/proxycheck
## Spur
Spur is an anonymity-infrastructure vendor: its Context API answers whether an address is a VPN, proxy or other tunnel with the operator named, and its feeds list anonymous, residential-proxy and datacenter address space; its pricing page rate-limited our fetcher, so prices are not reproduced here.
Comparison: https://spaw.co/compare/spur
IPQualityScore and the other product names are trademarks of their owners. Spaw is not affiliated with them.
Reference: https://spaw.co/alternatives/ipqualityscore
---
# Spaw vs IPQualityScore Phone Validation: phone intelligence API comparison
How Spaw and IPQualityScore Phone Validation compare on pricing, billing rules, where the line type comes from and limits, from IPQualityScore Phone Validation's public pages checked on 2026-09-27. Corrections: support@spaw.co.
| | Spaw | IPQualityScore Phone Validation |
| --- | --- | --- |
| Pricing model | Pay-as-you-go credit packs whose credits never expire, or optional plans, billed monthly or yearly, that deposit the same credits every month for less and roll unused plan credits over up to three months' allowance; both through Stripe Checkout | Monthly plans shared with IPQualityScore's other checks: Free $0 (1,000 lookups a month), Startup "As low as: $99 / Month" (5,000), SMB Basic "As low as: $499 / Month" (10,000), SMB+ "As low as: $999 / Month" (75,000), Enterprise custom; "Each plan requires a minimum monthly commitment". The phone validation page says "Score phone numbers on average from $0.0005 per user with no monthly contracts", on "tiered flat rate pricing across all countries that reduces as your monthly volume increases", without a tier table. Add-ons are enabled through the account manager or Support at no stated price. |
| Free tier | 100 free credits every month, no card required | 1,000 lookups a month on the Free plan, capped at 35 a day; the plan also covers IP, email and URL checks |
| Price per 1,000 at about 10k | $7.80 (Growth pack: 5,000 credits for $39), or $5.80 on the Growth monthly plan (5,000 credits for $29 a month) | not on the pages read |
| Price per 1,000 at about 100k | $4.49 (Volume pack: 100,000 credits for $449), or $3.49 on the Volume monthly plan (100,000 credits for $349 a month) | not on the pages read |
| Numbers the vendor could not settle billed | A valid number costs 1 credit whatever its flags say. The live carrier-network check (hlr: true) is not enabled on the service: hlr_checked answers false and nothing extra is charged. | not on the pages read |
| Invalid numbers billed | No: invalid numbers are free, including a number in a block the regulator lists as unassigned, and so are repeats within 7 days | An invalid number answers "success": false with "Invalid/nonexistent phone number or no country specified."; the plans page's "What Counts as a Lookup?" answer names the proxy, email and device services and does not mention phone validation |
| Line type source | The numbering plan metadata everywhere except the North American plan, where mobiles and landlines share blocks and the plan can only say fixed_line_or_mobile; there the regulator's block holder settles it — a wireless carrier's block is mobile, a virtual-number wholesaler's is voip. line_type_source names which of the two answered. | Its own carrier data: "IPQS maintains current data directly from carriers on a global level", and its system "collects phone validation and verification data from various telecom carriers". line_type answers Toll-Free, Wireless, Landline, Satellite, VOIP, Premium Rate, Pager or N/A, beside separate VOIP and prepaid booleans. |
| Numbering plan coverage | libphonenumber metadata for every region it carries, plus the regulators' own allocation tables, each named in the answer with the date of the copy it read. Complete tables, where a geographic number in a block the table leaves out answers unassigned_block: NANPA for the United States (with Puerto Rico, the U.S. Virgin Islands, Guam, American Samoa and the Northern Mariana Islands), the CNAC for Canada, ARCEP for France (with Guadeloupe, St. Barthélemy, St. Martin, Martinique, French Guiana, Réunion, Mayotte and St. Pierre & Miquelon), ACM for the Netherlands and the CRT (formerly the IFT) for Mexico. Partial tables, answered only from the ranges they list (a range listed as free, spare or held in reserve answers unassigned_block), so block_status answers null for a number outside them: Ofcom for the United Kingdom (with Guernsey, Jersey and the Isle of Man), ACMA for Australia, the NAD for New Zealand, UKE for Poland and MIMIT for Italy. Re-synced weekly, NANPA daily; /status lists each table's age. Outside those regions, block_status and is_virtual answer null rather than a guess. | "Over 150 countries" per the API documentation; the product page says "All countries and carriers are supported" and the SMS pumping page "All 195 countries and territories". valid is defined against "assigned phone numbers available to carriers in that country"; no numbering plan or regulator table is named. |
| Live network check | Reserved behind hlr: true: 4 credits on top of the base credit, only where the network answers, reported in its own reachable, ported, current_carrier and roaming fields. The live carrier-network check (hlr: true) is not enabled on the service: hlr_checked answers false and nothing extra is charged. | An add-on: active and active_status come from "our HLR lookup service and carrier signals", enabled by contacting the account manager or Support; active answers null until it is, and active_status carries its detail only once enhanced active line checks are enabled. No price is stated on the pages read. |
| Ported numbers | None from the tables, and the docs say so: carrier and line_type describe the block the number was allocated in, never a later port. Only a live carrier-network check can say who serves a number today. | The HLR add-on is described as returning "the latest carrier details including number porting and roaming data", and a separate number_recycling add-on reports whether a number "has recently changed owners or moved between providers". |
| SMS pumping controls | Flags in the risk score, and an SMS policy that turns them into ok_to_send and blocked_by: a fraud and deliverability decision, not consent and not a do-not-call scrub. allowed_countries limits sending to the regions you serve; it is empty (any country) until you set it. range_burst (+40) flags a number once your account has looked up 5 distinct numbers in its 1,000-number range within 60 minutes, and numbers_per_client (+30) once one client address (the client_ip you send, or a browser visitor's own) has tried 5 within 60 minutes, unless Spaw's own IP data places it on a mobile carrier or a corporate proxy, or it is private or reserved. Both count only your own single and browser lookups, so a new account has no history. The range is a heuristic, and in North America a burst more likely means someone testing numbers against a form; the client count does not stop an attack rotated through residential proxies. disposable (+70) flags a number SMS-receiving websites publish and disposable_stale (+40) an old listing; disposable_range (+30) flags an unlisted number in a 1,000-number range holding 10 or more fresh listings. The lists are public and synced daily. The default policy blocks the fictional, disposable, reported_abuse, reported_abuse_widely and premium_rate signals and a risk_score above 60, so neither traffic signal blocks on its own. | An sms_pumping object (risk_score, message, velocity) is an add-on enabled through the account manager or Support; it "requires at least 5 days of recent data and at least 50 phone validation API calls per hour". The SMS Pumping Detection page describes velocity, carrier, device and IP signals from IPQualityScore's own network. No price is stated on the pages read. |
| Bulk limit | 100,000 numbers per bulk job, with an HMAC-signed completion webhook | CSV uploads through the dashboard or the Bulk Validation CSV API: 3 files a day on Free and unlimited on Enterprise, up to the monthly lookup limit; no row limit is stated on the pages read |
| Batch API | 1,000 numbers per synchronous request, answered in input order | not on the pages read |
| Rate limit | 50 requests per second per key on the phone endpoints | 35 lookups a day on Free and 250 on Startup, 20 lookups a second on SMB Basic; Enterprise lists "Enterprise Rate Limits" without a figure |
| Official SDKs | Python, Node.js, PHP | not on the pages read |
| Data retention | No history row and no log line carrying the number. What a lookup leaves is keyed and never holds the number: the 7-day marker that makes a repeat free (a second one when the account paid for a live check); for single and browser lookups, a truncated keyed hash of the number filed under keyed hashes of its range and client address for 60 minutes, and the account's lookups per country and hour as counts for 30 days; and a live check's answer, held for a day under a hash of the number. The number itself is kept only in the account's own outcome reports, phone suppression list and monitors. Bulk input lists and result files are deleted with the job after 30 days. | The privacy policy says personal data submitted to the fraud-detection services is "generally retained for a shorter period (approximately one year), after which it is deleted or de-identified" |
## When IPQualityScore Phone Validation fits better
Fraud teams that want a phone fraud score beside IPQualityScore's IP, email and URL checks on one plan, with live-line, identity and SMS-pumping data available as add-ons.
- A 0 to 100 fraud_score with recent_abuse and risky flags from IPQualityScore's own threat network, which its clients' fraud reports feed, and a leaked flag for numbers exposed in an online database breach
- Identity data: a name field carrying "the name of the owner associated with the phone number", and an Enterprise identity_data add-on with names, addresses, ages and associated email addresses
- do_not_call (United States and Canada only) and tcpa_blacklist are add-ons that answer null until enabled
- One plan covers phone, IP, email and URL checks, and lists go through CSV upload
## When Spaw fits better
Developers who want the numbering plan and the regulators' own allocation tables read for them, the dataset behind every field named in the answer, and an SMS policy that turns the evidence into ok_to_send and blocked_by — a fraud and deliverability decision, not consent and not a do-not-call scrub. Every answer is made on the server: the opt-in live carrier-network check, the one call that would send a number to a partner, is not enabled on the service.
- IPQualityScore returns the name associated with a number and, as an add-on, identity records; Spaw does no reverse lookup and returns no owner, subscriber or caller name by design
- IPQualityScore's score comes from its own network and carrier data; every Spaw answer lists the datasets it consulted in its sources block, each with its version or sync date, and the weight behind every point of the risk score is published
- IPQualityScore's sms_pumping score is an add-on that needs at least 50 phone validation calls an hour and draws on its own network as well; Spaw counts only your own account's single and browser lookups (range_burst, and numbers_per_client when you send client_ip or the lookup comes from the browser) and applies your SMS policy, including a country allow-list, in ok_to_send, a fraud and deliverability decision rather than consent or a do-not-call scrub
- IPQualityScore sells do-not-call and TCPA-litigator flags as add-ons; Spaw checks neither, and its docs say ok_to_send is not a do-not-call scrub
- IPQualityScore's plans are a monthly quota with a minimum monthly commitment; Spaw's pack credits never expire and an invalid number costs nothing
## Questions
**Does Spaw charge for numbers it cannot settle?** Spaw bills per answer. A number the numbering plan and, where one is installed, the regulator's own table agree is assignable costs 1 credit whatever its flags say. An invalid number is free — including one in a block the regulator lists as unassigned — and so is a repeat of the same number within seven days. The live carrier-network check (hlr: true) is not enabled on the service: hlr_checked answers false and nothing extra is charged. IPQualityScore Phone Validation does not publish how it bills them.
**Can I move a list of numbers from IPQualityScore Phone Validation to Spaw?** Yes. Upload the CSV in the dashboard or POST it to the bulk endpoint, up to 100,000 numbers a job; Spaw keeps your columns, adds the answer columns, and lets you download the whole file or only the valid, invalid, low, medium or high-risk rows. A country column gives each row its own region, so a mixed international list needs no splitting.
**Is there a free tier?** Every Spaw account receives 100 free credits at the start of each month with no card on file, and unused free credits accumulate. IPQualityScore Phone Validation offers: 1,000 lookups a month on the Free plan, capped at 35 a day; the plan also covers IP, email and URL checks.
## Sources (checked 2026-09-27)
- [IPQualityScore phone validation](https://www.ipqualityscore.com/solutions/phone-validation)
- [IPQualityScore plans](https://www.ipqualityscore.com/plans)
- [IPQualityScore Phone Number Validation API overview](https://www.ipqualityscore.com/documentation/phone-number-validation-api/overview)
- [IPQualityScore Phone Number Validation API response parameters](https://www.ipqualityscore.com/documentation/phone-number-validation-api/response-parameters)
- [IPQualityScore SMS pumping detection](https://www.ipqualityscore.com/toll-fraud-prevention/sms-pumping-detection)
- [IPQualityScore Bulk Validation CSV API](https://www.ipqualityscore.com/documentation/csv-api/overview)
- [IPQualityScore privacy policy](https://www.ipqualityscore.com/privacy-policy)
IPQualityScore Phone Validation is a trademark of its owner. Spaw is not affiliated with it.
Reference: https://spaw.co/compare/ipqualityscore-phone
---
# IPQualityScore Phone Validation alternatives for phone intelligence
Phone intelligence APIs to consider instead of IPQualityScore Phone Validation, with the pricing model, free tier and price per 1,000 lookups each vendor publishes. Figures come from public pricing pages checked on 2026-09-27.
| Service | Pricing model | Free tier | Price per 1,000 at about 10k |
| --- | --- | --- | --- |
| [Spaw](https://spaw.co) | Pay-as-you-go credit packs whose credits never expire, or optional plans, billed monthly or yearly, that deposit the same credits every month for less and roll unused plan credits over up to three months' allowance; both through Stripe Checkout | 100 free credits every month, no card required | $7.80 (Growth pack: 5,000 credits for $39), or $5.80 on the Growth monthly plan (5,000 credits for $29 a month) |
| [Abstract Phone Validation](https://www.abstractapi.com/api/phone-validation-api) | Monthly request quotas on three plans that differ by the data returned: Starter (validation, location, carrier) from $19 a month for 5,000 requests to $199 for 150,000; Standard (adds messaging) from $19 for 3,000 to $199 for 100,000; Professional (adds registration and risk) from $19 for 2,000 to $199 for 50,000. Annual billing saves 10%; the pricing page quotes $3.40 per 1,000 requests at Starter's entry volume, billed annually | 100 requests a month with no card, on the full response including registration and risk | not on the pages read |
| [ClearoutPhone](https://clearoutphone.io) | Credits that never expire, bought one time or through monthly or annual subscriptions that add them each period and carry them forward. One Time, each shown as a starting price: 5,000 credits from $35, 10,000 from $58, 100,000 from $400 and 1,000,000 from $1,200. Standard validation is 1 credit; Smart validation is 1 credit for most numbers and up to 5 when it runs a deeper real-time check | 100 free credits on sign-up, one validation each | $5.80 per 1,000 one time (10,000 credits for $58), $5.20 on the monthly plan (10,000 credits for $52 a month) or $3.20 on the annual plan as its tab rendered ("Buy 120,000 credits for $384 billed yearly", 10,000 a month), although the page labels annual billing "Save 20%" against one-time prices; a Smart validation that runs the deeper check costs up to 5 credits |
| [Loqate Phone Validation](https://www.loqate.com/en-gb/phone-validation/) | Pay-as-you-go credit purchases of £50, £100, £250, £500 and £1,000 (or 75€, 150€ and 300€), each buying a published per-lookup rate for every service, plus bespoke plans; "Credit is valid for 12 months after the purchase date". Phone Validation on the "New Pricing" table is 7.1p, 6.7p, 5.6p, 4.7p and 4.1p a lookup across the pound tiers and 8.6c, 7.8c and 6.6c across the euro ones. The page also shows an "Old Pricing" table without saying which accounts each applies to; the figures here are from "New Pricing". Its usage-example selector also lists a £2,500 plan that neither table prices, and all prices exclude VAT. | A trial rather than a standing tier: "Start your 45-day free trial"; the FAQ adds "You get free credits to test the service" | £47 per 1,000 (4.7p a lookup) on the £500 credit, which buys about 10,600 lookups; the smaller credits run from 7.1p (£50) to 5.6p (£250) |
| [numlookupapi](https://numlookupapi.com) | Monthly subscription tiers by request quota: Free $0 for 100 requests, Basic $9.99 for 5,000, Pro $49.99 for 50,000, Scale $109.99 for 250,000, and Custom by contact; the page offers yearly billing at 20% off. Scale and Custom list "Overages" with no overage price shown, and the quota documentation says a request over the monthly quota answers 429 until the plan is upgraded or the month ends. | 100 requests a month on one API key, at 10 requests a minute | not on the pages read |
| [numverify](https://numverify.com) | Monthly subscription tiers by request quota: Free 100 requests, Basic $14.99 for 5,000, Professional $59.99 for 50,000, Enterprise $129.99 for 250,000, each with a published overage price per call ($0.011992, $0.0047992 and $0.00207984 respectively); annual billing saves up to 15%, and custom pricing is offered above the Enterprise quota | 100 API requests a month, for non-commercial use | not on the pages read |
| [Telnyx Number Lookup](https://telnyx.com/products/number-lookup) | Per query, priced per lookup type, with "$0 platform fee"; pay as you go gets "Automatic tier discounts as usage crosses thresholds", committed plans start at $500 a month, and inbound CNAM is $0.40 per number per month | An outbound CNAM listing at no charge; the pages read list no free query allowance | $1.50 per 1,000 for a local routing number query ($0.0015 a query), $2.50 for MCC/MNC ($0.0025) and $3.00 for CNAM ($0.003) |
| [Twilio Lookup](https://www.twilio.com/en-us/user-authentication-identity/lookup) | Per request, priced per data package on a Twilio account ("Pricing current as of August 2026"); line status and reassigned number risk are tiered by query volume, and SIM swap and call forwarding are contact sales | Formatting and validation are listed as Free, and so is the SMS Pumping Risk Score for North American numbers; no free allowance is published for the other packages | $8.00 per 1,000 for line type intelligence ($0.008 per request), $10.00 for inbound caller identification ($0.01, US only), $100.00 for identity match ($0.1, which the page says varies by country) |
| [Veriphone](https://veriphone.io) | One-time credit packs that never expire, from 10,000 credits for $24 to 5,000,000 for $999, or monthly plans whose credits reset each month: Starter $12.99 for 10,000, Professional $39 for 50,000, Business $99 for 250,000 and Enterprise $249 for 1,000,000. A standard validation is 1 credit and a Current Carrier Lookup 10 | 1,000 free credits a month with no card, not carried over; on the free plan a Current Carrier Lookup draws on purchased credits only | $2.40 per 1,000 on the 10,000-credit pack ($24, credits never expire), or $1.30 on the Starter plan (10,000 credits a month for $12.99, reset each month). A Current Carrier Lookup takes 10 credits, so ten times those figures |
## Spaw
Verification API that bills per answered lookup across six signals on one key and one balance: Email verification, Phone intelligence, IP intelligence, Address verification, Business verification against open registers, and Cross-signal consistency, which runs any subset of the first four in one call and names where they agree and where they do not. The phone side reads the numbering plan and the regulators' own allocation tables to name the line type, the block holder and the blocks nobody holds, and flags the numbers that are not a person's handset.
## Abstract Phone Validation
Abstract Phone Validation is a phone API from Abstract sold as monthly request quotas on three data tiers, returning carrier, line type and location, with messaging from Standard up and registration and risk on Professional and the Free plan; the Professional example response also carries breach history.
Comparison: https://spaw.co/compare/abstract-phone
## ClearoutPhone
ClearoutPhone is a phone validation service from Clearout sold as credits that never expire, with a 1-credit Standard mode and a Smart mode that runs a real-time telecom query, at up to 5 credits, where static data is uncertain.
Comparison: https://spaw.co/compare/clearoutphone
## Loqate Phone Validation
Loqate Phone Validation checks a number's format, carrier and, for mobile numbers, its live network status through an HLR lookup, priced per lookup from the same pay-as-you-go credit balance as Loqate's address, email and bank validation.
Comparison: https://spaw.co/compare/loqate-phone
## numlookupapi
numlookupapi is a phone number validation API sold as a monthly request quota: one validate endpoint returns validity, formats, country, location, carrier and line type.
Comparison: https://spaw.co/compare/numlookupapi
## numverify
numverify is a phone number validation API sold as a monthly request quota, returning validity, country and location, carrier and line type.
Comparison: https://spaw.co/compare/numverify
## Telnyx Number Lookup
Telnyx Number Lookup is a per-query number API sold beside Telnyx's carrier services, priced separately for local routing number (portability and carrier), MCC/MNC, and CNAM caller name.
Comparison: https://spaw.co/compare/telnyx-lookup
## Twilio Lookup
Twilio Lookup is a phone number API sold as separately priced data packages on top of a Twilio account: formatting and validation are free, and line type, caller identity, line status and the risk packages are each priced on their own, per request (the SMS Pumping Risk Score is free for North American numbers) or through sales for SIM swap and call forwarding.
Comparison: https://spaw.co/compare/twilio-lookup
## Veriphone
Veriphone is a phone validation API sold as credit packs or monthly plans, returning validity, carrier, line type and region from national numbering plans, with a 10-credit Current Carrier Lookup read from national portability registries.
Comparison: https://spaw.co/compare/veriphone
IPQualityScore Phone Validation and the other product names are trademarks of their owners. Spaw is not affiliated with them.
Reference: https://spaw.co/alternatives/ipqualityscore-phone
---
# Spaw vs Ipregistry: IP intelligence API comparison
How Spaw and Ipregistry compare on pricing, billing rules, the feeds behind each flag and limits, from Ipregistry's public pages checked on 2026-09-12. Corrections: support@spaw.co.
| | Spaw | Ipregistry |
| --- | --- | --- |
| Pricing model | Pay-as-you-go credit packs whose credits never expire, or optional plans, billed monthly or yearly, that deposit the same credits every month for less and roll unused plan credits over up to three months' allowance; both through Stripe Checkout. One credit buys five answered IP lookups | Credits, "1 lookup = 1 request = 1 credit" (batch endpoints use "one credit per lookup in the batch request, plus 1"). Subscriptions: Starter "100,000 credits / mo" at "$29 $24 / month", Professional "300,000 credits / mo" at "$79 $66 / month", Business "1,150,000 credits / mo" at "$199 $166 / month", Scale "2,450,000 credits / mo" at "$399 $333 / month" (the lower figure is the annual rate). Pay-as-you-go packs: 15,000 credits for $10, 100,000 for $50, 250,000 for $100, 1,750,000 for $500. |
| Free tier | 100 free credits every month, no card required | "20,000 free credits on sign-up" with "No credit card, no time limit" |
| Price per 1,000 at about 10k | $1.80 per 1,000 IP lookups at 5 to the credit (Starter pack: 1,000 credits for $9), or $1.40 on the Starter monthly plan (1,000 credits for $7 a month) | $0.67 (the $10 pack of 15,000 credits, $0.000667 a lookup) |
| Price per 1,000 at about 100k | $1.56 per 1,000 IP lookups at 5 to the credit (Growth pack: 5,000 credits for $39), or $1.16 on the Growth monthly plan (5,000 credits for $29 a month) | $0.50 (the $50 pack of 100,000 credits, $0.0005 a lookup) |
| Addresses no feed covered billed | One credit buys five answered addresses whatever their flags say; a flag no feed could evaluate answers null rather than false, and the answer costs the same | not on the pages read |
| Reserved and private ranges billed | No: reserved, private, loopback, CGNAT, documentation and multicast ranges answer reason reserved_range and are free, unallocated space is free, and so are repeats within 7 days | not on the pages read |
| Feeds behind the flags | Public feeds, each named in the answer with the date it was synced: DB-IP Lite and RFC 8805 geofeeds for location, the five registries' delegated statistics for allocation, Spamhaus DROP and ASN-DROP and abuse.ch Feodo Tracker for blocklists, the Tor Project's exit list and Onionoo, the Mullvad, NordVPN and Private Internet Access server lists plus the open X4BNet lists, Apple's iCloud Private Relay egress list, the cloud providers' own published ranges, and the verification files Google, Microsoft, OpenAI, Perplexity, DuckDuckGo and Ahrefs publish for their crawlers. | Its own datasets, derived "from multiple sources using unique AI algorithms" including "BGP tables, Regional Internet Registry (RIR) databases, ISP data, data-sharing agreements, geofeeds, latency-based tracking". The security object carries is_abuser, is_attacker, is_bogon, is_cloud_provider, is_proxy, is_relay, is_tor, is_tor_exit, is_vpn, is_anonymous and is_threat. |
| Refresh cadence | Every feed re-syncs daily, the botnet command-and-control list hourly, and the geofeed crawl weekly. Each response names every dataset it consulted with the date it was last synced, and /status lists them all. | "All IP geolocation data is refreshed at least once per day, and threat/security data is updated on average every 15 minutes" |
| Geolocation source | The DB-IP Lite database, overridden wherever the operator publishes the location itself — an RFC 8805 geofeed, Apple's relay list, a VPN provider's server list. location_source says which answered, and location_confidence is graded from a weekly accuracy report rather than asserted. | Its own, from the sources above; no numeric accuracy claim is published on the pages read |
| Bulk limit | 100,000 addresses per bulk job, with an HMAC-signed completion webhook | not on the pages read |
| Batch API | 1,000 addresses per synchronous request; repeats inside the list are looked up once | Batch endpoints exist and cost "one credit per lookup in the batch request, plus 1"; the maximum per request is on a documentation page that answered 404 on the day |
| Rate limit | 50 requests per second per key on the IP endpoints | "Rate limiting or throttling is NOT enabled by default. Your only constraint is the balance of credits available on your account"; an hourly lookup limit can be set per key, "based on an adaptation of the leaky bucket algorithm" |
| Official SDKs | Python, Node.js, PHP | not on the pages read |
| Data retention | No history row and no log line carrying the address. What a lookup leaves is keyed and never holds the address: the 7-day marker that makes a repeat free and, under the same keyed hash, the count behind seen_by_you; privacy: true skips both. The account's own outcome reports are kept as salted hashes for 90 days. Bulk input lists and result files are deleted with the job after 30 days. | not on the pages read |
## When Ipregistry fits better
Teams that want a credit model with pay-as-you-go packs and a security object with a dozen named flags, from a vendor that names its data sources in general terms.
- Credits do not expire on the pay-as-you-go packs, as on Spaw; the subscriptions reset monthly
- Threat data is refreshed on average every 15 minutes, geolocation at least daily
- Throttling is off by default and set per key
- The security object separates Tor from Tor exits, and relay from VPN
## When Spaw fits better
Developers who want every public IP feed compiled, dated and named for them, with a risk score whose weights are published so it can be recomputed or ignored.
- Ipregistry describes its sources as BGP tables, registry databases, geofeeds and latency tracking combined by its own algorithms; Every dataset behind a Spaw IP answer is a public feed named in the response with the date it was synced, and the risk score is a published sum of weights anyone can recompute
- Both sell credits; a Spaw credit buys five answered IP lookups, with reserved, private and unallocated ranges and seven-day repeats free, and unlike a pack the free monthly grant renews
- Ipregistry answers is_threat and is_abuser from its own data; Spaw names the list behind is_blocklisted and is_attack_source and shows the weight each carried
- Spaw applies your own allow and deny lists inside every answer (an allow-list hit scores 0, a deny-list hit 100) and counts your own lookups of an address; neither is a field the other vendor's pages describe
## Questions
**Does Spaw charge for addresses its feeds do not cover?** Spaw bills per answered address, five to the credit: the answered lookup that opens a block costs 1 credit and the next four cost nothing. Reserved, private, loopback, CGNAT, documentation and multicast ranges answer reason reserved_range and are free, unallocated space is free, and so is a repeat of the same address within seven days. A flag no feed could evaluate answers null rather than false: the address is still billed, and the null tells you the signal was not checked instead of pretending it was. Ipregistry does not publish how it bills them.
**Can I run a log file or an export through Spaw?** Yes. A bulk run takes up to 100,000 addresses from the dashboard or the API and returns one row per input address, in input order, with every flag, the risk score and the signals that made it. Five answered addresses cost a credit, repeats inside the list are looked up once, reserved ranges cost nothing, and the finish is announced on an HMAC-signed webhook.
**Is there a free tier?** Every Spaw account receives 100 free credits at the start of each month with no card on file, and unused free credits accumulate. Ipregistry offers: "20,000 free credits on sign-up" with "No credit card, no time limit".
## Sources (checked 2026-09-12)
- [Ipregistry pricing](https://ipregistry.co/pricing)
- [Ipregistry response fields](https://ipregistry.co/docs/responses)
- [Ipregistry throttling](https://ipregistry.co/docs/throttling)
Ipregistry is a trademark of its owner. Spaw is not affiliated with it.
Reference: https://spaw.co/compare/ipregistry
---
# Ipregistry alternatives for IP intelligence
IP intelligence APIs to consider instead of Ipregistry, with the pricing model, free tier and price per 1,000 lookups each vendor publishes. Figures come from public pricing pages checked on 2026-09-12.
| Service | Pricing model | Free tier | Price per 1,000 at about 10k |
| --- | --- | --- | --- |
| [Spaw](https://spaw.co) | Pay-as-you-go credit packs whose credits never expire, or optional plans, billed monthly or yearly, that deposit the same credits every month for less and roll unused plan credits over up to three months' allowance; both through Stripe Checkout. One credit buys five answered IP lookups | 100 free credits every month, no card required | $1.80 per 1,000 IP lookups at 5 to the credit (Starter pack: 1,000 credits for $9), or $1.40 on the Starter monthly plan (1,000 credits for $7 a month) |
| [AbuseIPDB](https://www.abuseipdb.com) | Plans with daily request allowances: Individual free "Forever!", Basic $25/mo or $228/year, Premium $99/mo or $1,068/year, Enterprise custom | Individual: free forever with no credit card, 1,000 check and report requests, 100 block checks and 5 bulk reports a day | not on the pages read |
| [ip-api](https://ip-api.com) | Two tiers. The free endpoint: "Our endpoints are limited to 45 HTTP requests per minute from an IP address" and "We do not allow commercial use of the free endpoint". The Pro service: "€13.3 per month, when paying for 12 months", with "Unlimited requests", "SSL (HTTPS)", "Commercial use allowed" and a "99.9% SLA" ("If the general API uptime falls below 99.9% in a month, we will automatically extend your subscription for 10 days"). | The free endpoint, for non-commercial use only: 45 requests per minute per address over HTTP ("256-bit SSL encryption is not available for this free API") | not on the pages read |
| [IP2Location](https://www.ip2location.com) | IP2Location.io API, monthly query plans: Free $0 ("50K IP Geolocation API queries/month", "11 IP Attributes", "Attribution Required"), Starter "$49/month" ("150K IP Geolocation API queries/month", "20 IP Attributes"), Plus "$249/month" ("300K IP Geolocation API queries/month", "59 IP Attributes"), Security "$499/month" ("600K IP Geolocation API queries/month", "79 IP Attributes"); overage per increment Starter $10, Plus $50, Security $100. Databases, standard licence per year: DB1 "$99", DB2 "$399", DB8 "$1,299", DB18 "$2,799", with a site licence at ten times those figures. | "50K IP Geolocation API queries/month" with 11 attributes, basic geolocation and open-proxy detection only, "Attribution Required"; without a key "up to 1,000 queries per day" | not on the pages read |
| [ipapi](https://ipapi.co) | Monthly plans by lookup quota: Starter "$15 billed monthly" for 60,000 lookups, Professional "$25 billed monthly" for 150,000, Business "$49 billed monthly" for 500,000, Enterprise "$99 billed monthly" for 2,000,000, Premium "$199 billed monthly" for 6,000,000, Ultimate "$399 billed monthly" for 15,000,000; annual billing is 20% less. | A free trial of "up-to 1000 IP lookups in a day" (about 30,000 a month), "for testing & development purposes" and "Not for production use" | not on the pages read |
| [ipdata](https://ipdata.co) | Monthly plans by daily request quota: Personal "$10 per month" with "2,500 requests daily", Lite "$30 per month" with "10,000 requests daily", Startup "$50 per month" with "50,000 requests daily", Business "$120 per month" with "100,000 requests daily"; "20% off all annual plans" at the time of reading. | "1500 free requests daily", "restricted to non-commercial use"; on the free tier, over the quota "all further requests are blocked and you receive an email notification" | not on the pages read |
| [ipgeolocation.io](https://ipgeolocation.io) | Monthly credit plans, "A standard unified IP lookup uses 1 credit": Starter "$19/month" for "150000 API Credits Per month", Plus "$49/month" for 500,000, Pro "$79/month" for 1,000,000, Business "$129/month" for 2,000,000, Premium "$249/month" for 5,000,000, Enterprise "Contact Sales"; annual billing gives "2 months free". "Paid monthly plans reset every month. Paid annual plans reset every year". | "1000 API Credits Per day for life" with no credit card, "a hard limit of 1000 requests per day" and "No overages on Free"; the free plan returns "core location without the radius and confidence fields" | not on the pages read |
| [IPinfo](https://ipinfo.io) | Monthly plans by API request quota: Lite $0/mo, Core $26/mo for 150,000 requests, Max $163/mo for 250,000, Enterprise custom for "1M+". Database downloads are listed separately, "Starting at $1,000/month". | IPinfo Lite at "$0/mo", free forever with no credit card, giving unlimited API requests at country level only | not on the pages read |
| [IPQualityScore](https://www.ipqualityscore.com) | Monthly plans by lookup quota: Free $0 (1,000 lookups a month), Startup "As low as: $99 / Month" (5,000), SMB Basic "$499" (10,000), SMB+ "$999" (75,000), Enterprise custom. "Any valid request sent to our Proxy Detection, Email Verification, or Device Fingerprinting services including lookups performed through a batch CSV file check will consume a lookup credit." | 1,000 lookups a month, capped at 35 a day | not on the pages read |
| [ipstack](https://ipstack.com) | Monthly plans by request quota: Free "100 Request/mo." at $0, Basic "50,000 Request/mo." at "$12.99/mo.", Professional "500,000 Request/mo." at "$59.99/mo.", Professional + "2,000,000 Request/mo." at "$99.99/mo.", Custom on request; yearly billing "Save up to 15%". Overage fees apply past the quota, with notifications at 75%, 90% and 100%. | "100 Request/mo." at $0, without SSL encryption, support or the paid modules (SSL, commercial use and the Location, Time Zone and Connection modules are listed for paid plans) | not on the pages read |
| [MaxMind](https://www.maxmind.com) | Per query, by service, with no minimum published: "If you estimate your volume of queries to be over $2,500 a month, contact us to discuss rates." | A no-cost trial is offered ("Experience the functionality and benefits of our GeoIP web services at no cost"); no free monthly allowance is stated on the page checked | $0.10 per 1,000 for Country ($0.0001 a query), $0.30 for City Plus ($0.0003) and $2.00 for Insights ($0.002) |
| [proxycheck.io](https://proxycheck.io) | not on the pages read | not on the pages read | not on the pages read |
| [Spur](https://spur.us) | not on the pages read | not on the pages read | not on the pages read |
## Spaw
Verification API that bills per answered lookup across six signals on one key and one balance: Email verification, Phone intelligence, IP intelligence, Address verification, Business verification against open registers, and Cross-signal consistency, which runs any subset of the first four in one call and names where they agree and where they do not. The IP side answers from local databases and compiled public feeds: where an address is, who runs it, and whether it is a visitor at all.
## AbuseIPDB
AbuseIPDB is a reputation service built on abuse reports its own users submit: a check returns an abuse confidence score with the report count, country, usage type, ISP and a Tor flag, and plans are sold as daily request allowances rather than credits.
Comparison: https://spaw.co/compare/abuseipdb
## ip-api
ip-api is a geolocation JSON API with a free, non-commercial, HTTP-only endpoint limited by request rate, and a Pro subscription that adds HTTPS, unlimited queries and commercial use; its answer carries one proxy flag, one hosting flag and one mobile flag beside the location and network fields.
Comparison: https://spaw.co/compare/ip-api
## IP2Location
IP2Location sells geolocation as downloadable databases (DB1 to DB26, licensed yearly) and, through IP2Location.io, as an API on monthly query plans whose higher tiers add proxy, VPN, Tor, datacenter, crawler and residential-proxy detection and a fraud score; the classic web service no longer takes new registrations.
Comparison: https://spaw.co/compare/ip2location
## ipapi
ipapi.co is a geolocation API sold as monthly lookup quotas, whose pricing page credits its data to DB-IP and the IP2Location LITE database; the answer carries location, country and network fields and no proxy, VPN or Tor flag.
Comparison: https://spaw.co/compare/ipapi
## ipdata
ipdata is a geolocation and threat API sold as daily request quotas, from a free non-commercial tier to a Business plan, whose pricing page says it does not keep records of the addresses looked up beyond a day; its documentation site refused automated reading, so the fields are not reproduced here.
Comparison: https://spaw.co/compare/ipdata
## ipgeolocation.io
ipgeolocation.io sells geolocation and a separate security API on monthly credit plans, with location built from its own measurements and a security answer that flags VPN, proxy, residential proxy and Tor with provider names, confidence scores and a 0 to 100 threat score.
Comparison: https://spaw.co/compare/ipgeolocation
## IPinfo
IPinfo is an IP data company that sells geolocation, ASN, privacy detection and company data as monthly API plans and as database downloads, collected through its own measurement network rather than assembled from third-party feeds.
Comparison: https://spaw.co/compare/ipinfo
## IPQualityScore
IPQualityScore sells fraud detection across several signals on one lookup credit: proxy and VPN detection, email verification and device fingerprinting all draw from the same monthly quota, and the IP answer carries a fraud score alongside the connection facts.
Comparison: https://spaw.co/compare/ipqualityscore
## ipstack
ipstack is a geolocation API on monthly request quotas from a 100-request free tier to a two-million-request plan, with a bulk endpoint and a security module reserved for its top plan; its documentation pages rendered only a header to our fetcher, so the field list is not reproduced here.
Comparison: https://spaw.co/compare/ipstack
## MaxMind
MaxMind sells GeoIP as three per-query web services — Country, City Plus and Insights — and as downloadable databases, with proxy and anonymiser detection reserved for the Insights tier.
Comparison: https://spaw.co/compare/maxmind
## proxycheck.io
proxycheck.io sells an API for checking whether an address is a proxy; every page we tried on 2026-09-12 answered 403 Forbidden to automated reading, so nothing about its pricing, limits, fields or data is reproduced here.
Comparison: https://spaw.co/compare/proxycheck
## Spur
Spur is an anonymity-infrastructure vendor: its Context API answers whether an address is a VPN, proxy or other tunnel with the operator named, and its feeds list anonymous, residential-proxy and datacenter address space; its pricing page rate-limited our fetcher, so prices are not reproduced here.
Comparison: https://spaw.co/compare/spur
Ipregistry and the other product names are trademarks of their owners. Spaw is not affiliated with them.
Reference: https://spaw.co/alternatives/ipregistry
---
# Spaw vs ipstack: IP intelligence API comparison
How Spaw and ipstack compare on pricing, billing rules, the feeds behind each flag and limits, from ipstack's public pages checked on 2026-09-12. Corrections: support@spaw.co.
| | Spaw | ipstack |
| --- | --- | --- |
| Pricing model | Pay-as-you-go credit packs whose credits never expire, or optional plans, billed monthly or yearly, that deposit the same credits every month for less and roll unused plan credits over up to three months' allowance; both through Stripe Checkout. One credit buys five answered IP lookups | Monthly plans by request quota: Free "100 Request/mo." at $0, Basic "50,000 Request/mo." at "$12.99/mo.", Professional "500,000 Request/mo." at "$59.99/mo.", Professional + "2,000,000 Request/mo." at "$99.99/mo.", Custom on request; yearly billing "Save up to 15%". Overage fees apply past the quota, with notifications at 75%, 90% and 100%. |
| Free tier | 100 free credits every month, no card required | "100 Request/mo." at $0, without SSL encryption, support or the paid modules (SSL, commercial use and the Location, Time Zone and Connection modules are listed for paid plans) |
| Price per 1,000 at about 10k | $1.80 per 1,000 IP lookups at 5 to the credit (Starter pack: 1,000 credits for $9), or $1.40 on the Starter monthly plan (1,000 credits for $7 a month) | not on the pages read |
| Price per 1,000 at about 100k | $1.56 per 1,000 IP lookups at 5 to the credit (Growth pack: 5,000 credits for $39), or $1.16 on the Growth monthly plan (5,000 credits for $29 a month) | not on the pages read |
| Addresses no feed covered billed | One credit buys five answered addresses whatever their flags say; a flag no feed could evaluate answers null rather than false, and the answer costs the same | not on the pages read |
| Reserved and private ranges billed | No: reserved, private, loopback, CGNAT, documentation and multicast ranges answer reason reserved_range and are free, unallocated space is free, and so are repeats within 7 days | not on the pages read |
| Feeds behind the flags | Public feeds, each named in the answer with the date it was synced: DB-IP Lite and RFC 8805 geofeeds for location, the five registries' delegated statistics for allocation, Spamhaus DROP and ASN-DROP and abuse.ch Feodo Tracker for blocklists, the Tor Project's exit list and Onionoo, the Mullvad, NordVPN and Private Internet Access server lists plus the open X4BNet lists, Apple's iCloud Private Relay egress list, the cloud providers' own published ranges, and the verification files Google, Microsoft, OpenAI, Perplexity, DuckDuckGo and Ahrefs publish for their crawlers. | A "Security Module" is listed for the Professional + plan only; its fields were not readable on the documentation pages we fetched, so they are not reproduced here |
| Refresh cadence | Every feed re-syncs daily, the botnet command-and-control list hourly, and the geofeed crawl weekly. Each response names every dataset it consulted with the date it was last synced, and /status lists them all. | "up to 24 database updates per day" |
| Geolocation source | The DB-IP Lite database, overridden wherever the operator publishes the location itself — an RFC 8805 geofeed, Apple's relay list, a VPN provider's server list. location_source says which answered, and location_confidence is graded from a weekly accuracy report rather than asserted. | Its own database, "over 2 million unique locations in more than 200,000 cities worldwide"; the source is not named on the pages read |
| Bulk limit | 100,000 addresses per bulk job, with an HMAC-signed completion webhook | not on the pages read |
| Batch API | 1,000 addresses per synchronous request; repeats inside the list are looked up once | A "Bulk Endpoint" is included on the Professional + plan only; its size limit was not readable |
| Rate limit | 50 requests per second per key on the IP endpoints | Monthly quotas with overage; no per-second figure is published on the pages read |
| Official SDKs | Python, Node.js, PHP | not on the pages read |
| Data retention | No history row and no log line carrying the address. What a lookup leaves is keyed and never holds the address: the 7-day marker that makes a repeat free and, under the same keyed hash, the count behind seen_by_you; privacy: true skips both. The account's own outcome reports are kept as salted hashes for 90 days. Bulk input lists and result files are deleted with the job after 30 days. | not on the pages read |
## When ipstack fits better
Teams already on the apilayer marketplace that want a high-volume monthly quota and are willing to buy the top plan for bulk and security answers.
- The free tier is 100 requests a month and, like SSL encryption, commercial use is listed under the paid plans
- The bulk endpoint and the security module are reserved for the $99.99 Professional + plan
- The database is described as updated up to 24 times a day
- docs.apilayer.com rendered only its header to automated reading on 2026-09-12, so the fields are not listed here
## When Spaw fits better
Developers who want every public IP feed compiled, dated and named for them, with a risk score whose weights are published so it can be recomputed or ignored.
- ipstack reserves its bulk endpoint and security module for the top plan; Spaw's batch, bulk and every flag are on every key at the same credit price
- ipstack sells a monthly quota that resets; Spaw sells pack credits that never expire at five IP lookups to the credit, and reserved, private, documentation and unallocated ranges are free
- Every dataset behind a Spaw IP answer is a public feed named in the response with the date it was synced, and the risk score is a published sum of weights anyone can recompute
- ipstack's field list could not be read by our fetcher; Spaw's 62 fields, weights and feeds are on its docs page and in the OpenAPI document
## Questions
**Does Spaw charge for addresses its feeds do not cover?** Spaw bills per answered address, five to the credit: the answered lookup that opens a block costs 1 credit and the next four cost nothing. Reserved, private, loopback, CGNAT, documentation and multicast ranges answer reason reserved_range and are free, unallocated space is free, and so is a repeat of the same address within seven days. A flag no feed could evaluate answers null rather than false: the address is still billed, and the null tells you the signal was not checked instead of pretending it was. ipstack does not publish how it bills them.
**Can I run a log file or an export through Spaw?** Yes. A bulk run takes up to 100,000 addresses from the dashboard or the API and returns one row per input address, in input order, with every flag, the risk score and the signals that made it. Five answered addresses cost a credit, repeats inside the list are looked up once, reserved ranges cost nothing, and the finish is announced on an HMAC-signed webhook.
**Is there a free tier?** Every Spaw account receives 100 free credits at the start of each month with no card on file, and unused free credits accumulate. ipstack offers: "100 Request/mo." at $0, without SSL encryption, support or the paid modules (SSL, commercial use and the Location, Time Zone and Connection modules are listed for paid plans).
## Sources (checked 2026-09-12)
- [ipstack pricing](https://ipstack.com/pricing)
- [ipstack documentation (only the header rendered on 2026-09-12)](https://docs.apilayer.com/ipstack/docs/api-documentation)
ipstack is a trademark of its owner. Spaw is not affiliated with it.
Reference: https://spaw.co/compare/ipstack
---
# ipstack alternatives for IP intelligence
IP intelligence APIs to consider instead of ipstack, with the pricing model, free tier and price per 1,000 lookups each vendor publishes. Figures come from public pricing pages checked on 2026-09-12.
| Service | Pricing model | Free tier | Price per 1,000 at about 10k |
| --- | --- | --- | --- |
| [Spaw](https://spaw.co) | Pay-as-you-go credit packs whose credits never expire, or optional plans, billed monthly or yearly, that deposit the same credits every month for less and roll unused plan credits over up to three months' allowance; both through Stripe Checkout. One credit buys five answered IP lookups | 100 free credits every month, no card required | $1.80 per 1,000 IP lookups at 5 to the credit (Starter pack: 1,000 credits for $9), or $1.40 on the Starter monthly plan (1,000 credits for $7 a month) |
| [AbuseIPDB](https://www.abuseipdb.com) | Plans with daily request allowances: Individual free "Forever!", Basic $25/mo or $228/year, Premium $99/mo or $1,068/year, Enterprise custom | Individual: free forever with no credit card, 1,000 check and report requests, 100 block checks and 5 bulk reports a day | not on the pages read |
| [ip-api](https://ip-api.com) | Two tiers. The free endpoint: "Our endpoints are limited to 45 HTTP requests per minute from an IP address" and "We do not allow commercial use of the free endpoint". The Pro service: "€13.3 per month, when paying for 12 months", with "Unlimited requests", "SSL (HTTPS)", "Commercial use allowed" and a "99.9% SLA" ("If the general API uptime falls below 99.9% in a month, we will automatically extend your subscription for 10 days"). | The free endpoint, for non-commercial use only: 45 requests per minute per address over HTTP ("256-bit SSL encryption is not available for this free API") | not on the pages read |
| [IP2Location](https://www.ip2location.com) | IP2Location.io API, monthly query plans: Free $0 ("50K IP Geolocation API queries/month", "11 IP Attributes", "Attribution Required"), Starter "$49/month" ("150K IP Geolocation API queries/month", "20 IP Attributes"), Plus "$249/month" ("300K IP Geolocation API queries/month", "59 IP Attributes"), Security "$499/month" ("600K IP Geolocation API queries/month", "79 IP Attributes"); overage per increment Starter $10, Plus $50, Security $100. Databases, standard licence per year: DB1 "$99", DB2 "$399", DB8 "$1,299", DB18 "$2,799", with a site licence at ten times those figures. | "50K IP Geolocation API queries/month" with 11 attributes, basic geolocation and open-proxy detection only, "Attribution Required"; without a key "up to 1,000 queries per day" | not on the pages read |
| [ipapi](https://ipapi.co) | Monthly plans by lookup quota: Starter "$15 billed monthly" for 60,000 lookups, Professional "$25 billed monthly" for 150,000, Business "$49 billed monthly" for 500,000, Enterprise "$99 billed monthly" for 2,000,000, Premium "$199 billed monthly" for 6,000,000, Ultimate "$399 billed monthly" for 15,000,000; annual billing is 20% less. | A free trial of "up-to 1000 IP lookups in a day" (about 30,000 a month), "for testing & development purposes" and "Not for production use" | not on the pages read |
| [ipdata](https://ipdata.co) | Monthly plans by daily request quota: Personal "$10 per month" with "2,500 requests daily", Lite "$30 per month" with "10,000 requests daily", Startup "$50 per month" with "50,000 requests daily", Business "$120 per month" with "100,000 requests daily"; "20% off all annual plans" at the time of reading. | "1500 free requests daily", "restricted to non-commercial use"; on the free tier, over the quota "all further requests are blocked and you receive an email notification" | not on the pages read |
| [ipgeolocation.io](https://ipgeolocation.io) | Monthly credit plans, "A standard unified IP lookup uses 1 credit": Starter "$19/month" for "150000 API Credits Per month", Plus "$49/month" for 500,000, Pro "$79/month" for 1,000,000, Business "$129/month" for 2,000,000, Premium "$249/month" for 5,000,000, Enterprise "Contact Sales"; annual billing gives "2 months free". "Paid monthly plans reset every month. Paid annual plans reset every year". | "1000 API Credits Per day for life" with no credit card, "a hard limit of 1000 requests per day" and "No overages on Free"; the free plan returns "core location without the radius and confidence fields" | not on the pages read |
| [IPinfo](https://ipinfo.io) | Monthly plans by API request quota: Lite $0/mo, Core $26/mo for 150,000 requests, Max $163/mo for 250,000, Enterprise custom for "1M+". Database downloads are listed separately, "Starting at $1,000/month". | IPinfo Lite at "$0/mo", free forever with no credit card, giving unlimited API requests at country level only | not on the pages read |
| [IPQualityScore](https://www.ipqualityscore.com) | Monthly plans by lookup quota: Free $0 (1,000 lookups a month), Startup "As low as: $99 / Month" (5,000), SMB Basic "$499" (10,000), SMB+ "$999" (75,000), Enterprise custom. "Any valid request sent to our Proxy Detection, Email Verification, or Device Fingerprinting services including lookups performed through a batch CSV file check will consume a lookup credit." | 1,000 lookups a month, capped at 35 a day | not on the pages read |
| [Ipregistry](https://ipregistry.co) | Credits, "1 lookup = 1 request = 1 credit" (batch endpoints use "one credit per lookup in the batch request, plus 1"). Subscriptions: Starter "100,000 credits / mo" at "$29 $24 / month", Professional "300,000 credits / mo" at "$79 $66 / month", Business "1,150,000 credits / mo" at "$199 $166 / month", Scale "2,450,000 credits / mo" at "$399 $333 / month" (the lower figure is the annual rate). Pay-as-you-go packs: 15,000 credits for $10, 100,000 for $50, 250,000 for $100, 1,750,000 for $500. | "20,000 free credits on sign-up" with "No credit card, no time limit" | $0.67 (the $10 pack of 15,000 credits, $0.000667 a lookup) |
| [MaxMind](https://www.maxmind.com) | Per query, by service, with no minimum published: "If you estimate your volume of queries to be over $2,500 a month, contact us to discuss rates." | A no-cost trial is offered ("Experience the functionality and benefits of our GeoIP web services at no cost"); no free monthly allowance is stated on the page checked | $0.10 per 1,000 for Country ($0.0001 a query), $0.30 for City Plus ($0.0003) and $2.00 for Insights ($0.002) |
| [proxycheck.io](https://proxycheck.io) | not on the pages read | not on the pages read | not on the pages read |
| [Spur](https://spur.us) | not on the pages read | not on the pages read | not on the pages read |
## Spaw
Verification API that bills per answered lookup across six signals on one key and one balance: Email verification, Phone intelligence, IP intelligence, Address verification, Business verification against open registers, and Cross-signal consistency, which runs any subset of the first four in one call and names where they agree and where they do not. The IP side answers from local databases and compiled public feeds: where an address is, who runs it, and whether it is a visitor at all.
## AbuseIPDB
AbuseIPDB is a reputation service built on abuse reports its own users submit: a check returns an abuse confidence score with the report count, country, usage type, ISP and a Tor flag, and plans are sold as daily request allowances rather than credits.
Comparison: https://spaw.co/compare/abuseipdb
## ip-api
ip-api is a geolocation JSON API with a free, non-commercial, HTTP-only endpoint limited by request rate, and a Pro subscription that adds HTTPS, unlimited queries and commercial use; its answer carries one proxy flag, one hosting flag and one mobile flag beside the location and network fields.
Comparison: https://spaw.co/compare/ip-api
## IP2Location
IP2Location sells geolocation as downloadable databases (DB1 to DB26, licensed yearly) and, through IP2Location.io, as an API on monthly query plans whose higher tiers add proxy, VPN, Tor, datacenter, crawler and residential-proxy detection and a fraud score; the classic web service no longer takes new registrations.
Comparison: https://spaw.co/compare/ip2location
## ipapi
ipapi.co is a geolocation API sold as monthly lookup quotas, whose pricing page credits its data to DB-IP and the IP2Location LITE database; the answer carries location, country and network fields and no proxy, VPN or Tor flag.
Comparison: https://spaw.co/compare/ipapi
## ipdata
ipdata is a geolocation and threat API sold as daily request quotas, from a free non-commercial tier to a Business plan, whose pricing page says it does not keep records of the addresses looked up beyond a day; its documentation site refused automated reading, so the fields are not reproduced here.
Comparison: https://spaw.co/compare/ipdata
## ipgeolocation.io
ipgeolocation.io sells geolocation and a separate security API on monthly credit plans, with location built from its own measurements and a security answer that flags VPN, proxy, residential proxy and Tor with provider names, confidence scores and a 0 to 100 threat score.
Comparison: https://spaw.co/compare/ipgeolocation
## IPinfo
IPinfo is an IP data company that sells geolocation, ASN, privacy detection and company data as monthly API plans and as database downloads, collected through its own measurement network rather than assembled from third-party feeds.
Comparison: https://spaw.co/compare/ipinfo
## IPQualityScore
IPQualityScore sells fraud detection across several signals on one lookup credit: proxy and VPN detection, email verification and device fingerprinting all draw from the same monthly quota, and the IP answer carries a fraud score alongside the connection facts.
Comparison: https://spaw.co/compare/ipqualityscore
## Ipregistry
Ipregistry is a geolocation and threat API sold as credits, either monthly subscriptions or pay-as-you-go packs, whose security object answers abuser, attacker, bogon, cloud provider, proxy, relay, Tor, VPN, anonymous and threat flags and whose data is described as built from BGP tables, registry databases, geofeeds and latency measurements.
Comparison: https://spaw.co/compare/ipregistry
## MaxMind
MaxMind sells GeoIP as three per-query web services — Country, City Plus and Insights — and as downloadable databases, with proxy and anonymiser detection reserved for the Insights tier.
Comparison: https://spaw.co/compare/maxmind
## proxycheck.io
proxycheck.io sells an API for checking whether an address is a proxy; every page we tried on 2026-09-12 answered 403 Forbidden to automated reading, so nothing about its pricing, limits, fields or data is reproduced here.
Comparison: https://spaw.co/compare/proxycheck
## Spur
Spur is an anonymity-infrastructure vendor: its Context API answers whether an address is a VPN, proxy or other tunnel with the operator named, and its feeds list anonymous, residential-proxy and datacenter address space; its pricing page rate-limited our fetcher, so prices are not reproduced here.
Comparison: https://spaw.co/compare/spur
ipstack and the other product names are trademarks of their owners. Spaw is not affiliated with them.
Reference: https://spaw.co/alternatives/ipstack
---
# Spaw vs Kickbox: email verification API comparison
How Spaw and Kickbox compare on pricing, billing rules, verification depth and limits, from Kickbox's public pages checked on 2026-09-03. Corrections: support@spaw.co.
| | Spaw | Kickbox |
| --- | --- | --- |
| Pricing model | Pay-as-you-go credit packs whose credits never expire, or optional plans, billed monthly or yearly, that deposit the same credits every month for less and roll unused plan credits over up to three months' allowance; both through Stripe Checkout | not on the pages read |
| Free tier | 100 free credits every month, no card required | not on the pages read |
| Price per 1,000 at about 10k | $7.80 (Growth pack: 5,000 credits for $39), or $5.80 on the Growth monthly plan (5,000 credits for $29 a month) | not on the pages read |
| Price per 1,000 at about 100k | $4.49 (Volume pack: 100,000 credits for $449), or $3.49 on the Volume monthly plan (100,000 credits for $349 a month) | not on the pages read |
| Unknown or catch-all results billed | No: an unconfirmed mailbox (reason unverified) and an answer Spaw could not obtain because its handshake partner was unavailable are free and leave no 7-day repeat marker, so a later lookup is a fresh one; a catch-all answer costs 1 credit like any risky answer, and an automatic re-check bills 1 credit only when it confirms the mailbox | not on the pages read |
| Undeliverable results billed | No, within a fair use: undeliverable answers that needed a mailbox probe are free up to max(500, your billed email answers) in a rolling 30 days, then 1 credit each; the rest are always free | not on the pages read |
| SMTP mailbox check | Yes | not on the pages read |
| Catch-all handling | A catch-all answer is remembered per domain for 7 days, and every unconfirmed mailbox carries a published 0 to 100 mailbox_confidence estimate | Results are deliverable, undeliverable, risky or unknown; how accept-all domains are billed is not published on the pages we could read |
| Bulk limit | 100,000 addresses per bulk job, with an HMAC-signed completion webhook | not on the pages read |
| Batch API | 50 addresses per synchronous request, probes sent concurrently | not on the pages read |
| Rate limit | 5 requests per second per key on the email and consistency endpoints | not on the pages read |
| Official SDKs | Python, Node.js, PHP | PHP, Node.js, Ruby, Python |
| Data retention | Only email verification keeps a history row, for 30 days, then pruned; test addresses are never logged | not on the pages read |
## When Kickbox fits better
Teams that want a single quality score next to the verdict and already build on one of Kickbox's four official libraries.
- Sendex score: a quality estimate between 0 (no quality) and 1 (perfect quality)
- Four official client libraries on GitHub
## When Spaw fits better
Developers who want one flat price per answered lookup across every signal a signup form produces, a transparent risk score, and an API that documents every field.
- Kickbox's website blocked automated reading when we checked, so its prices are not reproduced here; Spaw's packs and free grant are listed at spaw.co/pricing
- Spaw's risk score is a 0 to 100 sum of published weights; Kickbox's Sendex score is a 0 to 1 quality estimate
- Both report deliverable, undeliverable, risky and unknown-style outcomes; Spaw adds a machine-readable reason and 46 named fields per address
- Spaw bills nothing for unverified mailboxes, for undeliverable verdicts within a fair-use allowance, or for repeats within 7 days
## Questions
**Does Spaw charge for catch-all or unknown results?** Spaw bills per answer, not per probe outcome. A catch-all domain comes back risky with reason catch_all and costs 1 credit like any risky answer. A mailbox the handshake could not confirm (greylisting, a timeout, no definite answer) comes back risky with reason unverified and is free, and so is an answer Spaw could not obtain at all because its handshake partner was unavailable; neither leaves a 7-day repeat marker, so a later lookup is a fresh one (an uncertain result is reused for up to 15 minutes, then the mailbox is asked again) and bills 1 credit only once the mailbox is confirmed, and the automatic re-checks behind callback_url bill 1 credit only when they confirm it. Undeliverable verdicts are free within a fair use of max(500, your billed email answers) in a rolling 30 days, and invalid input and repeats within 7 days are free too. Kickbox does not publish how it bills them.
**Can I move a list from Kickbox to Spaw?** Yes. Upload the CSV to the dashboard or POST it to the bulk endpoint; Spaw keeps your columns, adds the verdict columns, and lets you download only the deliverable, risky or undeliverable rows. Addresses that come back undeliverable join your suppression list so later runs skip them for free.
**Is there a free tier?** Every Spaw account receives 100 free credits at the start of each month with no card on file, and unused free credits accumulate. Kickbox does not publish a free tier on the pages checked.
## Sources (checked 2026-09-03)
- [Kickbox GitHub organization](https://github.com/kickboxio)
- [Kickbox Python client README](https://github.com/kickboxio/kickbox-python)
Kickbox is a trademark of its owner. Spaw is not affiliated with it.
Reference: https://spaw.co/compare/kickbox
---
# Kickbox alternatives for email verification
Email verification APIs to consider instead of Kickbox, with the pricing model, free tier and price per 1,000 lookups each vendor publishes. Figures come from public pricing pages checked on 2026-09-03.
| Service | Pricing model | Free tier | Price per 1,000 at about 10k |
| --- | --- | --- | --- |
| [Spaw](https://spaw.co) | Pay-as-you-go credit packs whose credits never expire, or optional plans, billed monthly or yearly, that deposit the same credits every month for less and roll unused plan credits over up to three months' allowance; both through Stripe Checkout | 100 free credits every month, no card required | $7.80 (Growth pack: 5,000 credits for $39), or $5.80 on the Growth monthly plan (5,000 credits for $29 a month) |
| [Abstract API](https://www.abstractapi.com) | Monthly subscription tiers with a request slider from 5,000 to 150,000 requests a month; Starter from $17, Standard from $37, Professional from $39 a month | 100 requests on the Free plan | not on the pages read |
| [Bouncer](https://www.usebouncer.com) | Pay-as-you-go credits that never expire (from 1,000 credits for $8 up to 1,000,000 for $2,000), auto-refill, plus a Deliverability Kit subscription at $25 to $250 a month | 100 free credits, no card required | $6.00 (10,000 credits for $60) |
| [Emailable](https://emailable.com) | Pay-as-you-go credits that never expire, or a monthly subscription at a 15% discount; minimum purchase 5,000 credits | 250 free credits at signup | $7.60 (the pay-as-you-go rate printed for the 5,000-credit package, $0.0076 a credit; larger packages are priced in a calculator, and the 5,000-credit subscription is $32.30 a month after the 15% discount) |
| [Hunter](https://hunter.io) | Monthly or yearly subscriptions with unified credits shared across search, verification and enrichment; credit packs on paid plans, usable for up to 12 months from purchase according to the pricing page | Free plan with 50 credits a month; one verification costs 0.5 credit, so 100 verifications | $7.45 (Growth: $149 a month for 10,000 credits, 20,000 verifications at 0.5 credit each; $5.20 billed yearly) |
| [Mailboxlayer](https://mailboxlayer.com) | Monthly or yearly subscription (10 to 15% off yearly) with a request quota per plan and per-request overage | 100 requests a month | $3.00 for the Basic quota (5,000 requests for $14.99 a month; overage $0.012 a request, so 10,000 requests on Basic cost about $74.95, $7.50 per 1,000); Professional Plus: 50,000 for $74.99, $1.50 per 1,000 |
| [NeverBounce](https://www.neverbounce.com) | not on the pages read | not on the pages read | not on the pages read |
| [Verifalia](https://verifalia.com) | Pay-as-you-go credit packs that never expire, or monthly, quarterly and yearly subscriptions; verification depth is priced as quality levels; pack and plan prices are shown in a calculator rather than listed | 25 free credits a day on the Free plan; unused daily credits expire each day | not on the pages read |
| [ZeroBounce](https://www.zerobounce.net) | Pay-as-you-go credits from $39 for 2,000 addresses, 15% off with autopay, or the ZeroBounce ONE subscription at $99 a month with unused credits rolling over; volume pricing shown through a calculator rather than fixed packs | 100 validation credits a month on the free account | not on the pages read |
## Spaw
Verification API that bills per answered lookup across six signals on one key and one balance: Email verification, Phone intelligence, IP intelligence, Address verification, Business verification against open registers, and Cross-signal consistency, which runs any subset of the first four in one call and names where they agree and where they do not. The email side checks syntax, live MX, SPF and DMARC, disposable and role lists, and runs an SMTP mailbox handshake that never sends a message.
## Abstract API
Abstract API's email validation endpoint is one of a family of utility APIs sold as monthly request quotas with a free 100-request plan.
Comparison: https://spaw.co/compare/abstract
## Bouncer
Bouncer is an email verification service with pay-as-you-go credits that never expire, high-throughput batch APIs, a real-time form product (Bouncer Shield) and a Deliverability Kit subscription.
Comparison: https://spaw.co/compare/bouncer
## Emailable
Emailable is an email verification service with pay-as-you-go credits, a bulk verifier, a widget and an API, with Node.js, Ruby and Python libraries.
Comparison: https://spaw.co/compare/emailable
## Hunter
Hunter is an outreach platform whose Email Verifier shares unified plan credits with its email finder, enrichment and campaign tools.
Comparison: https://spaw.co/compare/hunter
## Mailboxlayer
Mailboxlayer is apilayer's email validation API, sold as monthly request quotas from a free 100-request plan up to 250,000 requests a month.
Comparison: https://spaw.co/compare/mailboxlayer
## NeverBounce
NeverBounce is a ZoomInfo-owned email verification service with a bulk list-cleaning workflow, a real-time API and official client libraries in six languages.
Comparison: https://spaw.co/compare/neverbounce
## Verifalia
Verifalia is an email verification service with selectable quality levels, credit packs that never expire, subscriptions with daily free credits, and official SDKs in six languages.
Comparison: https://spaw.co/compare/verifalia
## ZeroBounce
ZeroBounce is an email validation service with a pay-as-you-go credit model and a subscription (ZeroBounce ONE) that bundles deliverability tools such as activity data, inbox placement tests and blacklist monitoring.
Comparison: https://spaw.co/compare/zerobounce
Kickbox and the other product names are trademarks of their owners. Spaw is not affiliated with them.
Reference: https://spaw.co/alternatives/kickbox
---
# Spaw vs Lob: address verification API comparison
How Spaw and Lob compare on pricing, billing rules, the registers behind each answer and limits, from Lob's public pages checked on 2026-09-16. Corrections: support@spaw.co.
| | Spaw | Lob |
| --- | --- | --- |
| Pricing model | Pay-as-you-go credit packs whose credits never expire, or optional plans, billed monthly or yearly, that deposit the same credits every month for less and roll unused plan credits over up to three months' allowance; both through Stripe Checkout | Verifications are priced per call on top of a print-and-mail plan: on the Developer plan ($0 a month) "US Address Verifications $0.05/additional" and international $0.07; on Startup ($260 a month) $0.025 with "1,000 included for $25/mo" and international $0.06 with 500 included for $30 a month; on Growth ($550 a month) $0.009 with "50,000 included for $450/mo" and international $0.055 with 10,000 included for $550 a month; Enterprise is "Contact us for details". |
| Free tier | 100 free credits every month, no card required | The Developer plan is "Starting at $0 / month" with verifications billed per call; the site offers "Get started for free". |
| Price per 1,000 at about 10k | $7.80 (Growth pack: 5,000 credits for $39), or $5.80 on the Growth monthly plan (5,000 credits for $29 a month) | $50 per 1,000 US verifications on the Developer plan ($0.05 each) or $25 per 1,000 on Startup ($0.025 each beyond the 1,000 included for $25 a month); international $70 or $60. |
| Price per 1,000 at about 100k | $4.49 (Volume pack: 100,000 credits for $449), or $3.49 on the Volume monthly plan (100,000 credits for $349 a month) | $9 per 1,000 US verifications on the Growth plan ($0.009 each beyond the 50,000 included for $450 a month); international $55 per 1,000. |
| Addresses no register covered billed | An address that stands as written costs 1 credit whether or not a register carried the building; where no register covers the country, exists answers null rather than a verdict | not on the pages read |
| Addresses that cannot stand billed | No: an address that cannot stand as written is free whatever the reason, and so are repeats within 7 days and rows repeating an address already in the same job | not on the pages read |
| Registers held | Open address registers for 23 countries and territories, each named in the answer with its declared coverage, complete or partial. Complete, building by building: Australia, Austria, Belgium, Czechia, Latvia, Luxembourg, Netherlands, Norway and Switzerland. Partial, building by building: Brazil, Denmark, France (with French Guiana, Guadeloupe, Martinique, Mayotte, Réunion, St. Barthélemy, St. Martin and St. Pierre & Miquelon), Germany and United States. Partial, streets only: Great Britain. House-number ranges as well: United States. Absence from a partial register is never read as evidence that an address does not exist. | Postal: "USPS CASS certification", plus a deliverability likelihood "based on Lob's mail delivery data to over half of US households". |
| Country coverage | Postal formats for 207 countries and territories: the line layout for 199 of them, and the required fields and postcode pattern wherever the table records them; 52 postal operators' rules for naming a delivery point that is not a street; open address registers for 23 countries and territories; postcode directories for Great Britain from the ONS Postcode Directory, with Northern Ireland excluded because reusing BT postcodes needs a separate licence, and for 108 more countries and territories from GeoNames' postal codes. | "Access delivery data for more than 240 global territories" |
| Claims post is delivered | No, and every answer says so: is_deliverable, is_cmra, is_vacant and is_residential answer null because that check is licensed and not enabled here. exists is a match in an open address register, never a statement that post arrives. | Yes. The live demo on the address-verification page lists DPV Confirmation, DPV CMRA, DPV Vacant, DPV Active and DPV Footnotes, and a "USPS Deliverability" likelihood from Lob's own delivery data. |
| Bulk limit | 100,000 addresses per bulk job, with an HMAC-signed completion webhook | not on the pages read |
| Batch API | 100 addresses per synchronous request | Offered: the page says "validate individual or bulk addresses with our award-winning APIs" and answers a FAQ on validating a list; no limit is stated. |
| Rate limit | 50 requests per second per key on the address endpoints | not on the pages read |
| Official SDKs | Python, Node.js, PHP | not on the pages read |
| Data retention | No history row and no log line carrying the address. What a lookup leaves is the 7-day marker that makes a repeat free and the count behind seen_by_you, both kept under a digest, never the address. The address itself is kept only on the account's own suppression list and in its monitors. Bulk input lists and result files are deleted with the job after 30 days. | not on the pages read |
## When Lob fits better
Teams that already print and mail through Lob and want the same account to clean the addresses first, with the postal service's delivery-point answers for the United States.
- USPS delivery-point data: DPV confirmation, CMRA, vacancy and the footnotes
- A deliverability likelihood from Lob's own mail delivery records rather than from USPS alone
- Real-time autocomplete and standardisation on forms
- Verifications billed per call beside a print-and-mail plan, with volume included on the paid plans
## When Spaw fits better
Developers who want a written address parsed against its own country's rules and checked against open registers, where a field nobody could evaluate answers null instead of a guess.
- Lob answers whether post is delivered and whether the point is an agency or vacant; Spaw's is_deliverable, is_cmra, is_vacant and is_residential answer null because that check is licensed and not enabled
- Lob's verifications are priced per call on top of a mail plan; a Spaw credit buys one answered lookup with no plan beneath it, and an address that cannot stand as written costs nothing
- Spaw's mail-drop answer names the operator's branch list, the registered-agent list or the company cluster that decided it, from open registers rather than delivery records
- Every Spaw answer names the datasets and versions behind it in sources, so a result can be traced to the register that gave it
## Questions
**Does Spaw charge for addresses no register covers?** An address that stands as written costs 1 credit, whether or not a register carried the building. An address that cannot stand costs nothing, whatever the reason, and a repeat within seven days is free. Where no register covers the country, or where the register that answered is one whose coverage is partial and does not carry the address, exists answers null — not evaluated — rather than a verdict, and the not_in_register weight cannot fire. Lob does not publish how it bills them.
**Can I move an address list from Lob to Spaw?** Yes. Upload the CSV in the dashboard or POST it to the bulk endpoint, up to 100,000 addresses a job; Spaw keeps your columns and adds 64 result columns after them (the address as submitted, the answer, the datasets behind it, what the row cost and whether your suppression list answered it), and a country column gives each row its own country. Rows repeating an address already in the list are answered once, cost nothing, and are counted as duplicates.
**Is there a free tier?** Every Spaw account receives 100 free credits at the start of each month with no card on file, and unused free credits accumulate. Lob offers: The Developer plan is "Starting at $0 / month" with verifications billed per call; the site offers "Get started for free".
## Sources (checked 2026-09-16)
- [Lob pricing](https://www.lob.com/pricing)
- [Lob address verification](https://www.lob.com/address-verification)
Lob is a trademark of its owner. Spaw is not affiliated with it.
Reference: https://spaw.co/compare/lob
---
# Lob alternatives for address verification
Address verification APIs to consider instead of Lob, with the pricing model, free tier and price per 1,000 lookups each vendor publishes. Figures come from public pricing pages checked on 2026-09-16.
| Service | Pricing model | Free tier | Price per 1,000 at about 10k |
| --- | --- | --- | --- |
| [Spaw](https://spaw.co) | Pay-as-you-go credit packs whose credits never expire, or optional plans, billed monthly or yearly, that deposit the same credits every month for less and roll unused plan credits over up to three months' allowance; both through Stripe Checkout | 100 free credits every month, no card required | $7.80 (Growth pack: 5,000 credits for $39), or $5.80 on the Growth monthly plan (5,000 credits for $29 a month) |
| [Geoapify](https://www.geoapify.com) | Daily credit quotas on monthly subscriptions: Free (3,000 credits a day), API 10 (10,000 a day, $59 billed monthly), API 25 (25,000, $109), API 50 (50,000, $179), API 100 (100,000, $299), API 250 (250,000, $609) and Custom ("Unmetered credits / day", from $860). "1 Geocoding API request costs 1 credit"; in a batch each address counts 0.5 credits, plus 1 credit to create the job and 1 for each result request. A yearly term is offered, but its prices are not in the page text read. | A standing free plan: "3,000 credits / day", "No credit card required", up to 5 requests a second, usable for "commercial websites and apps" with the required Geoapify and data-source attribution | not on the pages read |
| [Google Address Validation API](https://mapsplatform.google.com) | "Pay-as-you-go", "charging per request according to the pricing table", under two SKUs named on the billing page, Address Validation Pro and Address Validation Enterprise; the per-request rates render inside the pricing table and are not stated in the page text as fetched, so no figure is quoted here. | not on the pages read | not on the pages read |
| [Ideal Postcodes](https://ideal-postcodes.co.uk) | Pay-as-you-go credit packs, "No Subscriptions. No Monthly Fees.": 200 credits for £9 ("£0.045 per lookup"), 1,100 for £42 (£0.038), 4,300 for £155 (£0.036), 12,800 for £420 (£0.033) and 32,100 for £900 (£0.028). "Credits are valid for 12 months"; the lookup guide adds that the timer "only starts when you begin using that batch of credits". Larger volumes and UK registered charities are priced on request. | A trial rather than a standing tier: "1-Month Trial", "50 free credits, full support and no credit card needed"; the trial searches worldwide except Ireland, "which requires separate licensing" | £33 per 1,000 lookups on the 12,800-credit pack (£420, "£0.033 per lookup"), the smallest pack that covers 10,000. |
| [Loqate](https://www.loqate.com) | Pay-as-you-go credit purchases of £50, £100, £250, £500 and £1,000 (or 75€, 150€ and 300€), each buying a published per-lookup rate for every service, plus bespoke plans; "Credit is valid for 12 months after the purchase date" and there are no setup costs. The pricing page shows two rate tables headed "New Pricing" and "Old Pricing" without saying which accounts each applies to; the figures here are from "New Pricing". Its usage-example selector also lists a £2,500 plan that neither table prices. | A trial rather than a standing tier: "Start your 45-day free trial"; the FAQ adds "You get free credits to test the service" | £82 to £86 per 1,000 Address Verify lookups on the "New Pricing" table: 8.2p a lookup on the £100, £250 and £500 tiers and 8.6p on the £50 and £1,000 tiers. Address Capture is 8.6p to 4.9p in the UK and 7.1p to 4.1p for the rest of the world. |
| [Melissa](https://www.melissa.com) | US Address Verification pay-as-you-go "Starting at $40" for 10,000 credits at "10 Credits Per Address" (the same page's FAQ says a US address equals 3 credits and a global address 10); an API subscription "Starting at $5,145" for 1,000,000 records a year; an unlimited-records plan "Starting at $16,000". Global Address Verification subscriptions from $40 a month for 1,000 records and $95 a month for 2,500. | Global Address Verification "GAV Free" at $0 for 250 records a month; 1,000 lookup credits a month after a one-time $4.97 KYC/AML verification fee, described on the pricing page. | $40 per 1,000 US addresses on the smallest pay-as-you-go pack, taking the pack's own "10 Credits Per Address" ($40 buys 10,000 credits). |
| [Postcode.eu](https://www.postcode.eu) | Annual tiers that include validations: Tier 1 €50, Tier 2 €100, Tier 3 €250, Tier 4 €500 and Tier 5 €1,000 a year, each with a per-validation rate by country group. "Up to the Tier amount you can perform a mix of validations", subscriptions "are automatically upgraded when the maximum of the Tier is reached", and past the highest Tier usage "will be invoiced in blocks of 10% and the rates of this Tier apply". A validation is "a bundle of related requests that were used to validate an address". An SLA (€1,000 + 2% a year) and a DORA addendum (€2,500 a year) are extra. | A trial rather than a standing tier: "After registering an account it is possible to try the APIs for free", and "Testing the API is without obligations" | €17.64 per 1,000 validations on Tier 3 (€250 a year, "€0.01764 each") for Austria, Belgium, Denmark, Finland, France, Luxembourg, Norway, Spain, Sweden, Switzerland and the Netherlands. Germany, Italy and the United Kingdom cost more a validation, so 10,000 of them fall in Tier 4 (€500 a year): €34.17 per 1,000 for Germany (€0.03417 each) and €43.30 for Italy and the United Kingdom (€0.0433 each). "Tier prices are including validations" and "Prices and quantities are per year", so the tier amount is paid in full: 10,000 validations cost €250 a year for the eleven countries (€25 per 1,000) and €500 for Germany, Italy or the United Kingdom (€50 per 1,000). |
| [Postcoder](https://postcoder.com) | Credit packs (500 to 200,000 credits, "valid for 12 months from purchase") or monthly plans (1,000 to 100,000 credits, with unused credits not carried over). "Your credits can be used for any Postcoder request." The pack prices themselves render as placeholders ("£--") in the pack list; the credit calculator's recommendation box shows "Credit pack £25 500 Credits" and "Monthly plan £25 1,000 Credits". Larger packs and plans with volume discounts are quoted. | A free trial is offered; its duration and allowance are not stated on the pricing page | £100 per 1,000 UK address lookups pay-as-you-go (10.00p a request, 2.0 credits) or £45 per 1,000 on a monthly plan (4.50p). This is the address comparison, so only the address rate is quoted: the same account buys email and mobile validation at their own per-request rates, which are read off the same page. |
| [PostGrid](https://www.postgrid.com) | Quoted: the pricing page asks for annual mail-piece and address-verification volumes in bands from "Less than 50,000" to "More than 1,000,000" and publishes no rate. | "Try for Free. Sign Up for Address Verification API" on the product page; no standing free volume is stated. | not on the pages read |
| [Smarty](https://www.smarty.com) | Annual lookup bundles on the Professional plan from $552 a year, sized 12K, 60K, 120K, 300K, 600K, 1M or 2M lookups; a monthly option and bespoke Custom pricing are offered. Only the $552 entry price is shown on the page. A subscription "expires when the address lookups or the time runs out, whichever comes first"; there are no overage costs, and an exhausted plan answers 402 "payment required" unless auto-renewal buys a new block. | A free trial rather than a standing tier: "42 days" and "1,000 address lookups", "No credit card required" | not on the pages read |
## Spaw
Verification API that bills per answered lookup across six signals on one key and one balance: Email verification, Phone intelligence, IP intelligence, Address verification, Business verification against open registers, and Cross-signal consistency, which runs any subset of the first four in one call and names where they agree and where they do not. The address side reads a written address against its own country's format, names the kind of delivery point behind it, checks the postcode against the national directory and follows the building into that country's own address register where one is installed.
## Geoapify
Geoapify is a location platform (maps, routing, places and geocoding) whose Geocoding API is also sold for address validation: an address is matched against open datasets such as OpenStreetMap and OpenAddresses and returned with coordinates and confidence scores for the city, the street and the building.
Comparison: https://spaw.co/compare/geoapify
## Google Address Validation API
Google's Address Validation API, part of Google Maps Platform, validates, standardises and geocodes an address, reports a confirmation level per component with inferred and replaced parts, and can run US and Puerto Rico addresses through USPS CASS on request.
Comparison: https://spaw.co/compare/google-address-validation
## Ideal Postcodes
Ideal Postcodes is a UK-first address lookup, autocomplete and cleansing API answered from Royal Mail's Postcode Address File, with Ordnance Survey AddressBase Core and Premium offered as additional datasets, sold as pay-as-you-go credit packs, with international address data for more than 240 countries and territories; its site also lists phone and email validation among its products.
Comparison: https://spaw.co/compare/ideal-postcodes
## Loqate
Loqate sells address capture and address verification alongside email, phone and bank validation, priced per lookup from a pay-as-you-go credit balance, with international data for over 245 countries and territories.
Comparison: https://spaw.co/compare/loqate
## Melissa
Melissa sells address verification for the United States and "250+ Countries & Territories" against postal data, CASS and DPV certified, with a credit model for pay-as-you-go use and annual subscriptions sized by records, alongside name, phone, email and identity verification.
Comparison: https://spaw.co/compare/melissa
## Postcode.eu
Postcode.eu, from Postcode.nl B.V., sells address autocomplete and validation APIs for fourteen European countries, each answered from the official sources its pages name (government address registers for most, the postal operator's file for others), priced per validation inside an annual tier.
Comparison: https://spaw.co/compare/postcode-eu
## Postcoder
Postcoder is a UK-first address lookup API sold as credits that any of its services can spend: address lookup, bank validation, email validation, mobile validation and OTP verification, each costing a published number of credits per request.
Comparison: https://spaw.co/compare/postcoder
## PostGrid
PostGrid sells an address verification API for the United States and Canada, CASS and SERP certified against USPS and Canada Post data, with an international service for "245+ Countries and Territories", autocomplete, parsing, batch verification and a Salesforce integration, priced by quotation.
Comparison: https://spaw.co/compare/postgrid
## Smarty
Smarty sells US address verification against postal data: a lookup validates to the secondary level and returns delivery-point metadata that open registers cannot answer, including a DPV match code, the residential indicator and vacancy status.
Comparison: https://spaw.co/compare/smarty
Lob and the other product names are trademarks of their owners. Spaw is not affiliated with them.
Reference: https://spaw.co/alternatives/lob
---
# Spaw vs Loqate: address verification API comparison
How Spaw and Loqate compare on pricing, billing rules, the registers behind each answer and limits, from Loqate's public pages checked on 2026-09-22. Corrections: support@spaw.co.
| | Spaw | Loqate |
| --- | --- | --- |
| Pricing model | Pay-as-you-go credit packs whose credits never expire, or optional plans, billed monthly or yearly, that deposit the same credits every month for less and roll unused plan credits over up to three months' allowance; both through Stripe Checkout | Pay-as-you-go credit purchases of £50, £100, £250, £500 and £1,000 (or 75€, 150€ and 300€), each buying a published per-lookup rate for every service, plus bespoke plans; "Credit is valid for 12 months after the purchase date" and there are no setup costs. The pricing page shows two rate tables headed "New Pricing" and "Old Pricing" without saying which accounts each applies to; the figures here are from "New Pricing". Its usage-example selector also lists a £2,500 plan that neither table prices. |
| Free tier | 100 free credits every month, no card required | A trial rather than a standing tier: "Start your 45-day free trial"; the FAQ adds "You get free credits to test the service" |
| Price per 1,000 at about 10k | $7.80 (Growth pack: 5,000 credits for $39), or $5.80 on the Growth monthly plan (5,000 credits for $29 a month) | £82 to £86 per 1,000 Address Verify lookups on the "New Pricing" table: 8.2p a lookup on the £100, £250 and £500 tiers and 8.6p on the £50 and £1,000 tiers. Address Capture is 8.6p to 4.9p in the UK and 7.1p to 4.1p for the rest of the world. |
| Price per 1,000 at about 100k | $4.49 (Volume pack: 100,000 credits for $449), or $3.49 on the Volume monthly plan (100,000 credits for $349 a month) | £82 to £86 per 1,000 Address Verify lookups on every published tier (8.2p on the £100, £250 and £500 tiers, 8.6p on £50 and £1,000); larger volumes are quoted bespoke ("For more credits or tailor-made plans get in touch"). |
| Addresses no register covered billed | An address that stands as written costs 1 credit whether or not a register carried the building; where no register covers the country, exists answers null rather than a verdict | not on the pages read |
| Addresses that cannot stand billed | No: an address that cannot stand as written is free whatever the reason, and so are repeats within 7 days and rows repeating an address already in the same job | not on the pages read |
| Registers held | Open address registers for 23 countries and territories, each named in the answer with its declared coverage, complete or partial. Complete, building by building: Australia, Austria, Belgium, Czechia, Latvia, Luxembourg, Netherlands, Norway and Switzerland. Partial, building by building: Brazil, Denmark, France (with French Guiana, Guadeloupe, Martinique, Mayotte, Réunion, St. Barthélemy, St. Martin and St. Pierre & Miquelon), Germany and United States. Partial, streets only: Great Britain. House-number ranges as well: United States. Absence from a partial register is never read as evidence that an address does not exist. | Not published as a register list. The Address Verify documentation describes "postal authority datasets and proprietary enhanced data covering countries worldwide", and the pricing page offers enhanced UK datasets ("Eircodes, Multi-Residence, Not Yet Built data and Property Attributes data") on request. |
| Country coverage | Postal formats for 207 countries and territories: the line layout for 199 of them, and the required fields and postcode pattern wherever the table records them; 52 postal operators' rules for naming a delivery point that is not a street; open address registers for 23 countries and territories; postcode directories for Great Britain from the ONS Postcode Directory, with Northern Ireland excluded because reusing BT postcodes needs a separate licence, and for 108 more countries and territories from GeoNames' postal codes. | The pricing page states "international data for over 245 countries and territories"; the Address Verify page states "Coverage for 250 countries and territories". |
| Claims post is delivered | No, and every answer says so: is_deliverable, is_cmra, is_vacant and is_residential answer null because that check is licensed and not enabled here. exists is a match in an open address register, never a statement that post arrives. | Not as a yes or no. Every valid Address Verify result carries an Address Verification Code giving the level it was verified to, "broadly to Administrative Area or Locality, or more closely to Thoroughfare, Premise or Delivery Point", and the documentation says some countries can be validated "all the way down to Delivery Point" while others only to a broad Administrative Area level. |
| Bulk limit | 100,000 addresses per bulk job, with an HMAC-signed completion webhook | not on the pages read |
| Batch API | 100 addresses per synchronous request | Offered: Address Verify "Supports individual address validation via API or bulk dataset processing through batch operations"; no limit is stated on the pages read. |
| Rate limit | 50 requests per second per key on the address endpoints | not on the pages read |
| Official SDKs | Python, Node.js, PHP | not on the pages read |
| Data retention | No history row and no log line carrying the address. What a lookup leaves is the 7-day marker that makes a repeat free and the count behind seen_by_you, both kept under a digest, never the address. The address itself is kept only on the account's own suppression list and in its monitors. Bulk input lists and result files are deleted with the job after 30 days. | not on the pages read |
## When Loqate fits better
Teams that want address capture in a checkout across many countries, with email, phone and bank validation billed from the same balance.
- Address, email, phone and bank validation all draw on the same credit balance
- Every rate is published per lookup, per tier, on one page, in pounds and in euros
- Address Capture charges only the Retrieve step; the Find requests made while a customer types are free
- Address Verify also runs on premises, on Kubernetes, with no part of the verification needing the internet
- A 45-day free trial
## When Spaw fits better
Developers who want a written address parsed against its own country's rules and checked against open registers, where a field nobody could evaluate answers null instead of a guess.
- Loqate prices each service separately per lookup; a Spaw credit buys an answer on any of its six signals at the same rate, or five on IP
- Loqate publishes a country count, not a register list; every Spaw address answer names the register that answered and whether its coverage is complete or partial
- Spaw returns 61 fields including the delivery-point type, the postcode's status in the national directory and how many companies are registered at the address
- Spaw charges nothing for an address that cannot stand as written, whatever the reason
## Questions
**Does Spaw charge for addresses no register covers?** An address that stands as written costs 1 credit, whether or not a register carried the building. An address that cannot stand costs nothing, whatever the reason, and a repeat within seven days is free. Where no register covers the country, or where the register that answered is one whose coverage is partial and does not carry the address, exists answers null — not evaluated — rather than a verdict, and the not_in_register weight cannot fire. Loqate does not publish how it bills them.
**Can I move an address list from Loqate to Spaw?** Yes. Upload the CSV in the dashboard or POST it to the bulk endpoint, up to 100,000 addresses a job; Spaw keeps your columns and adds 64 result columns after them (the address as submitted, the answer, the datasets behind it, what the row cost and whether your suppression list answered it), and a country column gives each row its own country. Rows repeating an address already in the list are answered once, cost nothing, and are counted as duplicates.
**Is there a free tier?** Every Spaw account receives 100 free credits at the start of each month with no card on file, and unused free credits accumulate. Loqate offers: A trial rather than a standing tier: "Start your 45-day free trial"; the FAQ adds "You get free credits to test the service".
## Sources (checked 2026-09-22)
- [Loqate pricing](https://www.loqate.com/en-gb/pricing/)
- [Loqate Address Verify](https://www.loqate.com/en-gb/address-verification/)
- [Loqate Address Verify documentation](https://docs.loqate.com/our-services/address-verify/overview)
Loqate is a trademark of its owner. Spaw is not affiliated with it.
Reference: https://spaw.co/compare/loqate
---
# Loqate alternatives for address verification
Address verification APIs to consider instead of Loqate, with the pricing model, free tier and price per 1,000 lookups each vendor publishes. Figures come from public pricing pages checked on 2026-09-22.
| Service | Pricing model | Free tier | Price per 1,000 at about 10k |
| --- | --- | --- | --- |
| [Spaw](https://spaw.co) | Pay-as-you-go credit packs whose credits never expire, or optional plans, billed monthly or yearly, that deposit the same credits every month for less and roll unused plan credits over up to three months' allowance; both through Stripe Checkout | 100 free credits every month, no card required | $7.80 (Growth pack: 5,000 credits for $39), or $5.80 on the Growth monthly plan (5,000 credits for $29 a month) |
| [Geoapify](https://www.geoapify.com) | Daily credit quotas on monthly subscriptions: Free (3,000 credits a day), API 10 (10,000 a day, $59 billed monthly), API 25 (25,000, $109), API 50 (50,000, $179), API 100 (100,000, $299), API 250 (250,000, $609) and Custom ("Unmetered credits / day", from $860). "1 Geocoding API request costs 1 credit"; in a batch each address counts 0.5 credits, plus 1 credit to create the job and 1 for each result request. A yearly term is offered, but its prices are not in the page text read. | A standing free plan: "3,000 credits / day", "No credit card required", up to 5 requests a second, usable for "commercial websites and apps" with the required Geoapify and data-source attribution | not on the pages read |
| [Google Address Validation API](https://mapsplatform.google.com) | "Pay-as-you-go", "charging per request according to the pricing table", under two SKUs named on the billing page, Address Validation Pro and Address Validation Enterprise; the per-request rates render inside the pricing table and are not stated in the page text as fetched, so no figure is quoted here. | not on the pages read | not on the pages read |
| [Ideal Postcodes](https://ideal-postcodes.co.uk) | Pay-as-you-go credit packs, "No Subscriptions. No Monthly Fees.": 200 credits for £9 ("£0.045 per lookup"), 1,100 for £42 (£0.038), 4,300 for £155 (£0.036), 12,800 for £420 (£0.033) and 32,100 for £900 (£0.028). "Credits are valid for 12 months"; the lookup guide adds that the timer "only starts when you begin using that batch of credits". Larger volumes and UK registered charities are priced on request. | A trial rather than a standing tier: "1-Month Trial", "50 free credits, full support and no credit card needed"; the trial searches worldwide except Ireland, "which requires separate licensing" | £33 per 1,000 lookups on the 12,800-credit pack (£420, "£0.033 per lookup"), the smallest pack that covers 10,000. |
| [Lob](https://www.lob.com) | Verifications are priced per call on top of a print-and-mail plan: on the Developer plan ($0 a month) "US Address Verifications $0.05/additional" and international $0.07; on Startup ($260 a month) $0.025 with "1,000 included for $25/mo" and international $0.06 with 500 included for $30 a month; on Growth ($550 a month) $0.009 with "50,000 included for $450/mo" and international $0.055 with 10,000 included for $550 a month; Enterprise is "Contact us for details". | The Developer plan is "Starting at $0 / month" with verifications billed per call; the site offers "Get started for free". | $50 per 1,000 US verifications on the Developer plan ($0.05 each) or $25 per 1,000 on Startup ($0.025 each beyond the 1,000 included for $25 a month); international $70 or $60. |
| [Melissa](https://www.melissa.com) | US Address Verification pay-as-you-go "Starting at $40" for 10,000 credits at "10 Credits Per Address" (the same page's FAQ says a US address equals 3 credits and a global address 10); an API subscription "Starting at $5,145" for 1,000,000 records a year; an unlimited-records plan "Starting at $16,000". Global Address Verification subscriptions from $40 a month for 1,000 records and $95 a month for 2,500. | Global Address Verification "GAV Free" at $0 for 250 records a month; 1,000 lookup credits a month after a one-time $4.97 KYC/AML verification fee, described on the pricing page. | $40 per 1,000 US addresses on the smallest pay-as-you-go pack, taking the pack's own "10 Credits Per Address" ($40 buys 10,000 credits). |
| [Postcode.eu](https://www.postcode.eu) | Annual tiers that include validations: Tier 1 €50, Tier 2 €100, Tier 3 €250, Tier 4 €500 and Tier 5 €1,000 a year, each with a per-validation rate by country group. "Up to the Tier amount you can perform a mix of validations", subscriptions "are automatically upgraded when the maximum of the Tier is reached", and past the highest Tier usage "will be invoiced in blocks of 10% and the rates of this Tier apply". A validation is "a bundle of related requests that were used to validate an address". An SLA (€1,000 + 2% a year) and a DORA addendum (€2,500 a year) are extra. | A trial rather than a standing tier: "After registering an account it is possible to try the APIs for free", and "Testing the API is without obligations" | €17.64 per 1,000 validations on Tier 3 (€250 a year, "€0.01764 each") for Austria, Belgium, Denmark, Finland, France, Luxembourg, Norway, Spain, Sweden, Switzerland and the Netherlands. Germany, Italy and the United Kingdom cost more a validation, so 10,000 of them fall in Tier 4 (€500 a year): €34.17 per 1,000 for Germany (€0.03417 each) and €43.30 for Italy and the United Kingdom (€0.0433 each). "Tier prices are including validations" and "Prices and quantities are per year", so the tier amount is paid in full: 10,000 validations cost €250 a year for the eleven countries (€25 per 1,000) and €500 for Germany, Italy or the United Kingdom (€50 per 1,000). |
| [Postcoder](https://postcoder.com) | Credit packs (500 to 200,000 credits, "valid for 12 months from purchase") or monthly plans (1,000 to 100,000 credits, with unused credits not carried over). "Your credits can be used for any Postcoder request." The pack prices themselves render as placeholders ("£--") in the pack list; the credit calculator's recommendation box shows "Credit pack £25 500 Credits" and "Monthly plan £25 1,000 Credits". Larger packs and plans with volume discounts are quoted. | A free trial is offered; its duration and allowance are not stated on the pricing page | £100 per 1,000 UK address lookups pay-as-you-go (10.00p a request, 2.0 credits) or £45 per 1,000 on a monthly plan (4.50p). This is the address comparison, so only the address rate is quoted: the same account buys email and mobile validation at their own per-request rates, which are read off the same page. |
| [PostGrid](https://www.postgrid.com) | Quoted: the pricing page asks for annual mail-piece and address-verification volumes in bands from "Less than 50,000" to "More than 1,000,000" and publishes no rate. | "Try for Free. Sign Up for Address Verification API" on the product page; no standing free volume is stated. | not on the pages read |
| [Smarty](https://www.smarty.com) | Annual lookup bundles on the Professional plan from $552 a year, sized 12K, 60K, 120K, 300K, 600K, 1M or 2M lookups; a monthly option and bespoke Custom pricing are offered. Only the $552 entry price is shown on the page. A subscription "expires when the address lookups or the time runs out, whichever comes first"; there are no overage costs, and an exhausted plan answers 402 "payment required" unless auto-renewal buys a new block. | A free trial rather than a standing tier: "42 days" and "1,000 address lookups", "No credit card required" | not on the pages read |
## Spaw
Verification API that bills per answered lookup across six signals on one key and one balance: Email verification, Phone intelligence, IP intelligence, Address verification, Business verification against open registers, and Cross-signal consistency, which runs any subset of the first four in one call and names where they agree and where they do not. The address side reads a written address against its own country's format, names the kind of delivery point behind it, checks the postcode against the national directory and follows the building into that country's own address register where one is installed.
## Geoapify
Geoapify is a location platform (maps, routing, places and geocoding) whose Geocoding API is also sold for address validation: an address is matched against open datasets such as OpenStreetMap and OpenAddresses and returned with coordinates and confidence scores for the city, the street and the building.
Comparison: https://spaw.co/compare/geoapify
## Google Address Validation API
Google's Address Validation API, part of Google Maps Platform, validates, standardises and geocodes an address, reports a confirmation level per component with inferred and replaced parts, and can run US and Puerto Rico addresses through USPS CASS on request.
Comparison: https://spaw.co/compare/google-address-validation
## Ideal Postcodes
Ideal Postcodes is a UK-first address lookup, autocomplete and cleansing API answered from Royal Mail's Postcode Address File, with Ordnance Survey AddressBase Core and Premium offered as additional datasets, sold as pay-as-you-go credit packs, with international address data for more than 240 countries and territories; its site also lists phone and email validation among its products.
Comparison: https://spaw.co/compare/ideal-postcodes
## Lob
Lob is a print-and-mail platform whose Address Verification API validates, standardises and enriches US and international addresses, CASS certified, with USPS delivery-point data and a deliverability likelihood drawn from Lob's own mail delivery records.
Comparison: https://spaw.co/compare/lob
## Melissa
Melissa sells address verification for the United States and "250+ Countries & Territories" against postal data, CASS and DPV certified, with a credit model for pay-as-you-go use and annual subscriptions sized by records, alongside name, phone, email and identity verification.
Comparison: https://spaw.co/compare/melissa
## Postcode.eu
Postcode.eu, from Postcode.nl B.V., sells address autocomplete and validation APIs for fourteen European countries, each answered from the official sources its pages name (government address registers for most, the postal operator's file for others), priced per validation inside an annual tier.
Comparison: https://spaw.co/compare/postcode-eu
## Postcoder
Postcoder is a UK-first address lookup API sold as credits that any of its services can spend: address lookup, bank validation, email validation, mobile validation and OTP verification, each costing a published number of credits per request.
Comparison: https://spaw.co/compare/postcoder
## PostGrid
PostGrid sells an address verification API for the United States and Canada, CASS and SERP certified against USPS and Canada Post data, with an international service for "245+ Countries and Territories", autocomplete, parsing, batch verification and a Salesforce integration, priced by quotation.
Comparison: https://spaw.co/compare/postgrid
## Smarty
Smarty sells US address verification against postal data: a lookup validates to the secondary level and returns delivery-point metadata that open registers cannot answer, including a DPV match code, the residential indicator and vacancy status.
Comparison: https://spaw.co/compare/smarty
Loqate and the other product names are trademarks of their owners. Spaw is not affiliated with them.
Reference: https://spaw.co/alternatives/loqate
---
# Spaw vs Loqate Phone Validation: phone intelligence API comparison
How Spaw and Loqate Phone Validation compare on pricing, billing rules, where the line type comes from and limits, from Loqate Phone Validation's public pages checked on 2026-09-27. Corrections: support@spaw.co.
| | Spaw | Loqate Phone Validation |
| --- | --- | --- |
| Pricing model | Pay-as-you-go credit packs whose credits never expire, or optional plans, billed monthly or yearly, that deposit the same credits every month for less and roll unused plan credits over up to three months' allowance; both through Stripe Checkout | Pay-as-you-go credit purchases of £50, £100, £250, £500 and £1,000 (or 75€, 150€ and 300€), each buying a published per-lookup rate for every service, plus bespoke plans; "Credit is valid for 12 months after the purchase date". Phone Validation on the "New Pricing" table is 7.1p, 6.7p, 5.6p, 4.7p and 4.1p a lookup across the pound tiers and 8.6c, 7.8c and 6.6c across the euro ones. The page also shows an "Old Pricing" table without saying which accounts each applies to; the figures here are from "New Pricing". Its usage-example selector also lists a £2,500 plan that neither table prices, and all prices exclude VAT. |
| Free tier | 100 free credits every month, no card required | A trial rather than a standing tier: "Start your 45-day free trial"; the FAQ adds "You get free credits to test the service" |
| Price per 1,000 at about 10k | $7.80 (Growth pack: 5,000 credits for $39), or $5.80 on the Growth monthly plan (5,000 credits for $29 a month) | £47 per 1,000 (4.7p a lookup) on the £500 credit, which buys about 10,600 lookups; the smaller credits run from 7.1p (£50) to 5.6p (£250) |
| Price per 1,000 at about 100k | $4.49 (Volume pack: 100,000 credits for $449), or $3.49 on the Volume monthly plan (100,000 credits for $349 a month) | £41 per 1,000 (4.1p a lookup) on the £1,000 credit, the largest either rate table prices, which buys about 24,400 lookups; larger volumes are quoted bespoke ("For more credits or tailor-made plans get in touch") |
| Numbers the vendor could not settle billed | A valid number costs 1 credit whatever its flags say. The live carrier-network check (hlr: true) is not enabled on the service: hlr_checked answers false and nothing extra is charged. | The validate method "will consume credit", with no exception stated in the API reference; IsValid answers Maybe where "full validation couldn't be performed", for example on cellular network issues |
| Invalid numbers billed | No: invalid numbers are free, including a number in a block the regulator lists as unassigned, and so are repeats within 7 days | not on the pages read |
| Line type source | The numbering plan metadata everywhere except the North American plan, where mobiles and landlines share blocks and the plan can only say fixed_line_or_mobile; there the regulator's block holder settles it — a wireless carrier's block is mobile, a virtual-number wholesaler's is voip. line_type_source names which of the two answered. | Loqate's "own custom libraries alongside the Home Location Register (HLR) database": the first step checks "length, format, digit count, country of origin, and whether it is a landline, mobile or VoIP number", the second the carrier and the third an HLR lookup. NumberType answers MOBILE, LANDLINE or VOIP, and the product detail page says "Full validation for Mobile numbers. Global formatting and carrier identification only for non-UK Landline and VoIP numbers." |
| Numbering plan coverage | libphonenumber metadata for every region it carries, plus the regulators' own allocation tables, each named in the answer with the date of the copy it read. Complete tables, where a geographic number in a block the table leaves out answers unassigned_block: NANPA for the United States (with Puerto Rico, the U.S. Virgin Islands, Guam, American Samoa and the Northern Mariana Islands), the CNAC for Canada, ARCEP for France (with Guadeloupe, St. Barthélemy, St. Martin, Martinique, French Guiana, Réunion, Mayotte and St. Pierre & Miquelon), ACM for the Netherlands and the CRT (formerly the IFT) for Mexico. Partial tables, answered only from the ranges they list (a range listed as free, spare or held in reserve answers unassigned_block), so block_status answers null for a number outside them: Ofcom for the United Kingdom (with Guernsey, Jersey and the Isle of Man), ACMA for Australia, the NAD for New Zealand, UKE for Poland and MIMIT for Italy. Re-synced weekly, NANPA daily; /status lists each table's age. Outside those regions, block_status and is_virtual answer null rather than a guess. | The product page states "Coverage for 241 countries & territories"; its FAQ, the documentation and the API reference say 237. No numbering plan or regulator table is named. |
| Live network check | Reserved behind hlr: true: 4 credits on top of the base credit, only where the network answers, reported in its own reachable, ported, current_carrier and roaming fields. The live carrier-network check (hlr: true) is not enabled on the service: hlr_checked answers false and nothing extra is charged. | Part of the standard three-step check, with no separate price on the pricing page: the third step is an HLR lookup "to verify active and connected numbers", for mobile numbers. |
| Ported numbers | None from the tables, and the docs say so: carrier and line_type describe the block the number was allocated in, never a later port. Only a live carrier-network check can say who serves a number today. | Yes, from the HLR step: NetworkName is "the current operator serving the supplied number" ("We take the network that the phone number has been ported to and display that at the network name"), and optional fields return Ported, PortedNetworkName, Roaming and OriginalNetworkName. |
| SMS pumping controls | Flags in the risk score, and an SMS policy that turns them into ok_to_send and blocked_by: a fraud and deliverability decision, not consent and not a do-not-call scrub. allowed_countries limits sending to the regions you serve; it is empty (any country) until you set it. range_burst (+40) flags a number once your account has looked up 5 distinct numbers in its 1,000-number range within 60 minutes, and numbers_per_client (+30) once one client address (the client_ip you send, or a browser visitor's own) has tried 5 within 60 minutes, unless Spaw's own IP data places it on a mobile carrier or a corporate proxy, or it is private or reserved. Both count only your own single and browser lookups, so a new account has no history. The range is a heuristic, and in North America a burst more likely means someone testing numbers against a form; the client count does not stop an attack rotated through residential proxies. disposable (+70) flags a number SMS-receiving websites publish and disposable_stale (+40) an old listing; disposable_range (+30) flags an unlisted number in a 1,000-number range holding 10 or more fresh listings. The lists are public and synced daily. The default policy blocks the fictional, disposable, reported_abuse, reported_abuse_widely and premium_rate signals and a risk_score above 60, so neither traffic signal blocks on its own. | not on the pages read |
| Bulk limit | 100,000 numbers per bulk job, with an HMAC-signed completion webhook | not on the pages read |
| Batch API | 1,000 numbers per synchronous request, answered in input order | Not documented for phone validation: the best-practices guide's "Batch Validation" example loops the single-number endpoint with a 100 ms pause between requests, and the product page lists "Real-time and bulk verification" without a limit. |
| Rate limit | 50 requests per second per key on the phone endpoints | No figure on the pages read: each key carries a daily limit on the credit it can consume, "a security setting to help protect your credit", and a key can be restricted by website address or IP |
| Official SDKs | Python, Node.js, PHP | not on the pages read |
| Data retention | No history row and no log line carrying the number. What a lookup leaves is keyed and never holds the number: the 7-day marker that makes a repeat free (a second one when the account paid for a live check); for single and browser lookups, a truncated keyed hash of the number filed under keyed hashes of its range and client address for 60 minutes, and the account's lookups per country and hour as counts for 30 days; and a live check's answer, held for a day under a hash of the number. The number itself is kept only in the account's own outcome reports, phone suppression list and monitors. Bulk input lists and result files are deleted with the job after 30 days. | Numbers validated through the API are "Stored in infrastructure logs for 30 days for operational purposes", per the phone validation implementation guide |
## When Loqate Phone Validation fits better
Teams already using Loqate for address capture that want a live network check on mobile numbers at the form, billed from the same credit balance.
- Every lookup runs a three-step check whose last step is an HLR lookup on mobile numbers, returning the current operator after a port and optional roaming fields
- IsValid answers Yes, No or Maybe; Maybe marks a lookup the network check could not complete
- Phone, address, email and bank validation draw on the same credit balance, valid for 12 months
- Also offered as a Tag snippet for web forms and in pre-built integrations such as Adobe Commerce, commercetools and Microsoft Dynamics 365
## When Spaw fits better
Developers who want the numbering plan and the regulators' own allocation tables read for them, the dataset behind every field named in the answer, and an SMS policy that turns the evidence into ok_to_send and blocked_by — a fraud and deliverability decision, not consent and not a do-not-call scrub. Every answer is made on the server: the opt-in live carrier-network check, the one call that would send a number to a partner, is not enabled on the service.
- Loqate's lookup includes an HLR check that reports the current operator after a port; Spaw's carrier and line_type describe the original allocation and never a later port. The live carrier-network check (hlr: true) is not enabled on the service: hlr_checked answers false and nothing extra is charged.
- Loqate describes its own libraries and an HLR lookup without naming a numbering dataset; Spaw's line_type_source says whether the numbering plan or the regulator's block table decided the line type, and the sources block names each dataset an answer consulted, with its version or sync date
- Loqate's phone API takes one number per request, and its pages name bulk verification without a limit; Spaw takes 1,000 numbers per batch request and 100,000 per bulk job
- Loqate charges per lookup from credit that lasts 12 months; Spaw's pack credits never expire, an invalid number costs nothing, and 100 free credits arrive every month
## Questions
**Does Spaw charge for numbers it cannot settle?** Spaw bills per answer. A number the numbering plan and, where one is installed, the regulator's own table agree is assignable costs 1 credit whatever its flags say. An invalid number is free — including one in a block the regulator lists as unassigned — and so is a repeat of the same number within seven days. The live carrier-network check (hlr: true) is not enabled on the service: hlr_checked answers false and nothing extra is charged. Loqate Phone Validation's pages say: The validate method "will consume credit", with no exception stated in the API reference; IsValid answers Maybe where "full validation couldn't be performed", for example on cellular network issues.
**Can I move a list of numbers from Loqate Phone Validation to Spaw?** Yes. Upload the CSV in the dashboard or POST it to the bulk endpoint, up to 100,000 numbers a job; Spaw keeps your columns, adds the answer columns, and lets you download the whole file or only the valid, invalid, low, medium or high-risk rows. A country column gives each row its own region, so a mixed international list needs no splitting.
**Is there a free tier?** Every Spaw account receives 100 free credits at the start of each month with no card on file, and unused free credits accumulate. Loqate Phone Validation offers: A trial rather than a standing tier: "Start your 45-day free trial"; the FAQ adds "You get free credits to test the service".
## Sources (checked 2026-09-27)
- [Loqate Phone Validation](https://www.loqate.com/en-gb/phone-validation/)
- [Loqate Phone Validation product detail](https://www.loqate.com/en-gb/phone-validation/product-detail/)
- [Loqate pricing](https://www.loqate.com/en-gb/pricing/)
- [Loqate Phone Validation documentation overview](https://docs.loqate.com/our-services/phone-validation/overview)
- [Loqate Individual Phone Validation API reference](https://docs.loqate.com/api-reference/phone-validation/individual-validate)
- [Loqate Phone Validation implementation guide](https://docs.loqate.com/our-services/phone-validation/implementation-guide)
- [Loqate Phone Validation best practices](https://docs.loqate.com/our-services/phone-validation/best-practices)
- [Loqate FAQ](https://docs.loqate.com/loqate-basics/faq)
Loqate Phone Validation is a trademark of its owner. Spaw is not affiliated with it.
Reference: https://spaw.co/compare/loqate-phone
---
# Loqate Phone Validation alternatives for phone intelligence
Phone intelligence APIs to consider instead of Loqate Phone Validation, with the pricing model, free tier and price per 1,000 lookups each vendor publishes. Figures come from public pricing pages checked on 2026-09-27.
| Service | Pricing model | Free tier | Price per 1,000 at about 10k |
| --- | --- | --- | --- |
| [Spaw](https://spaw.co) | Pay-as-you-go credit packs whose credits never expire, or optional plans, billed monthly or yearly, that deposit the same credits every month for less and roll unused plan credits over up to three months' allowance; both through Stripe Checkout | 100 free credits every month, no card required | $7.80 (Growth pack: 5,000 credits for $39), or $5.80 on the Growth monthly plan (5,000 credits for $29 a month) |
| [Abstract Phone Validation](https://www.abstractapi.com/api/phone-validation-api) | Monthly request quotas on three plans that differ by the data returned: Starter (validation, location, carrier) from $19 a month for 5,000 requests to $199 for 150,000; Standard (adds messaging) from $19 for 3,000 to $199 for 100,000; Professional (adds registration and risk) from $19 for 2,000 to $199 for 50,000. Annual billing saves 10%; the pricing page quotes $3.40 per 1,000 requests at Starter's entry volume, billed annually | 100 requests a month with no card, on the full response including registration and risk | not on the pages read |
| [ClearoutPhone](https://clearoutphone.io) | Credits that never expire, bought one time or through monthly or annual subscriptions that add them each period and carry them forward. One Time, each shown as a starting price: 5,000 credits from $35, 10,000 from $58, 100,000 from $400 and 1,000,000 from $1,200. Standard validation is 1 credit; Smart validation is 1 credit for most numbers and up to 5 when it runs a deeper real-time check | 100 free credits on sign-up, one validation each | $5.80 per 1,000 one time (10,000 credits for $58), $5.20 on the monthly plan (10,000 credits for $52 a month) or $3.20 on the annual plan as its tab rendered ("Buy 120,000 credits for $384 billed yearly", 10,000 a month), although the page labels annual billing "Save 20%" against one-time prices; a Smart validation that runs the deeper check costs up to 5 credits |
| [IPQualityScore Phone Validation](https://www.ipqualityscore.com/solutions/phone-validation) | Monthly plans shared with IPQualityScore's other checks: Free $0 (1,000 lookups a month), Startup "As low as: $99 / Month" (5,000), SMB Basic "As low as: $499 / Month" (10,000), SMB+ "As low as: $999 / Month" (75,000), Enterprise custom; "Each plan requires a minimum monthly commitment". The phone validation page says "Score phone numbers on average from $0.0005 per user with no monthly contracts", on "tiered flat rate pricing across all countries that reduces as your monthly volume increases", without a tier table. Add-ons are enabled through the account manager or Support at no stated price. | 1,000 lookups a month on the Free plan, capped at 35 a day; the plan also covers IP, email and URL checks | not on the pages read |
| [numlookupapi](https://numlookupapi.com) | Monthly subscription tiers by request quota: Free $0 for 100 requests, Basic $9.99 for 5,000, Pro $49.99 for 50,000, Scale $109.99 for 250,000, and Custom by contact; the page offers yearly billing at 20% off. Scale and Custom list "Overages" with no overage price shown, and the quota documentation says a request over the monthly quota answers 429 until the plan is upgraded or the month ends. | 100 requests a month on one API key, at 10 requests a minute | not on the pages read |
| [numverify](https://numverify.com) | Monthly subscription tiers by request quota: Free 100 requests, Basic $14.99 for 5,000, Professional $59.99 for 50,000, Enterprise $129.99 for 250,000, each with a published overage price per call ($0.011992, $0.0047992 and $0.00207984 respectively); annual billing saves up to 15%, and custom pricing is offered above the Enterprise quota | 100 API requests a month, for non-commercial use | not on the pages read |
| [Telnyx Number Lookup](https://telnyx.com/products/number-lookup) | Per query, priced per lookup type, with "$0 platform fee"; pay as you go gets "Automatic tier discounts as usage crosses thresholds", committed plans start at $500 a month, and inbound CNAM is $0.40 per number per month | An outbound CNAM listing at no charge; the pages read list no free query allowance | $1.50 per 1,000 for a local routing number query ($0.0015 a query), $2.50 for MCC/MNC ($0.0025) and $3.00 for CNAM ($0.003) |
| [Twilio Lookup](https://www.twilio.com/en-us/user-authentication-identity/lookup) | Per request, priced per data package on a Twilio account ("Pricing current as of August 2026"); line status and reassigned number risk are tiered by query volume, and SIM swap and call forwarding are contact sales | Formatting and validation are listed as Free, and so is the SMS Pumping Risk Score for North American numbers; no free allowance is published for the other packages | $8.00 per 1,000 for line type intelligence ($0.008 per request), $10.00 for inbound caller identification ($0.01, US only), $100.00 for identity match ($0.1, which the page says varies by country) |
| [Veriphone](https://veriphone.io) | One-time credit packs that never expire, from 10,000 credits for $24 to 5,000,000 for $999, or monthly plans whose credits reset each month: Starter $12.99 for 10,000, Professional $39 for 50,000, Business $99 for 250,000 and Enterprise $249 for 1,000,000. A standard validation is 1 credit and a Current Carrier Lookup 10 | 1,000 free credits a month with no card, not carried over; on the free plan a Current Carrier Lookup draws on purchased credits only | $2.40 per 1,000 on the 10,000-credit pack ($24, credits never expire), or $1.30 on the Starter plan (10,000 credits a month for $12.99, reset each month). A Current Carrier Lookup takes 10 credits, so ten times those figures |
## Spaw
Verification API that bills per answered lookup across six signals on one key and one balance: Email verification, Phone intelligence, IP intelligence, Address verification, Business verification against open registers, and Cross-signal consistency, which runs any subset of the first four in one call and names where they agree and where they do not. The phone side reads the numbering plan and the regulators' own allocation tables to name the line type, the block holder and the blocks nobody holds, and flags the numbers that are not a person's handset.
## Abstract Phone Validation
Abstract Phone Validation is a phone API from Abstract sold as monthly request quotas on three data tiers, returning carrier, line type and location, with messaging from Standard up and registration and risk on Professional and the Free plan; the Professional example response also carries breach history.
Comparison: https://spaw.co/compare/abstract-phone
## ClearoutPhone
ClearoutPhone is a phone validation service from Clearout sold as credits that never expire, with a 1-credit Standard mode and a Smart mode that runs a real-time telecom query, at up to 5 credits, where static data is uncertain.
Comparison: https://spaw.co/compare/clearoutphone
## IPQualityScore Phone Validation
IPQualityScore Phone Validation is the phone side of a fraud-scoring service: one API returns validity, carrier, line type and a 0 to 100 fraud score, with live-line, identity, do-not-call and SMS-pumping data as add-ons, on the same monthly plans as its IP, email and URL checks.
Comparison: https://spaw.co/compare/ipqualityscore-phone
## numlookupapi
numlookupapi is a phone number validation API sold as a monthly request quota: one validate endpoint returns validity, formats, country, location, carrier and line type.
Comparison: https://spaw.co/compare/numlookupapi
## numverify
numverify is a phone number validation API sold as a monthly request quota, returning validity, country and location, carrier and line type.
Comparison: https://spaw.co/compare/numverify
## Telnyx Number Lookup
Telnyx Number Lookup is a per-query number API sold beside Telnyx's carrier services, priced separately for local routing number (portability and carrier), MCC/MNC, and CNAM caller name.
Comparison: https://spaw.co/compare/telnyx-lookup
## Twilio Lookup
Twilio Lookup is a phone number API sold as separately priced data packages on top of a Twilio account: formatting and validation are free, and line type, caller identity, line status and the risk packages are each priced on their own, per request (the SMS Pumping Risk Score is free for North American numbers) or through sales for SIM swap and call forwarding.
Comparison: https://spaw.co/compare/twilio-lookup
## Veriphone
Veriphone is a phone validation API sold as credit packs or monthly plans, returning validity, carrier, line type and region from national numbering plans, with a 10-credit Current Carrier Lookup read from national portability registries.
Comparison: https://spaw.co/compare/veriphone
Loqate Phone Validation and the other product names are trademarks of their owners. Spaw is not affiliated with them.
Reference: https://spaw.co/alternatives/loqate-phone
---
# Spaw vs Mailboxlayer: email verification API comparison
How Spaw and Mailboxlayer compare on pricing, billing rules, verification depth and limits, from Mailboxlayer's public pages checked on 2026-09-13. Corrections: support@spaw.co.
| | Spaw | Mailboxlayer |
| --- | --- | --- |
| Pricing model | Pay-as-you-go credit packs whose credits never expire, or optional plans, billed monthly or yearly, that deposit the same credits every month for less and roll unused plan credits over up to three months' allowance; both through Stripe Checkout | Monthly or yearly subscription (10 to 15% off yearly) with a request quota per plan and per-request overage |
| Free tier | 100 free credits every month, no card required | 100 requests a month |
| Price per 1,000 at about 10k | $7.80 (Growth pack: 5,000 credits for $39), or $5.80 on the Growth monthly plan (5,000 credits for $29 a month) | $3.00 for the Basic quota (5,000 requests for $14.99 a month; overage $0.012 a request, so 10,000 requests on Basic cost about $74.95, $7.50 per 1,000); Professional Plus: 50,000 for $74.99, $1.50 per 1,000 |
| Price per 1,000 at about 100k | $4.49 (Volume pack: 100,000 credits for $449), or $3.49 on the Volume monthly plan (100,000 credits for $349 a month) | $1.50 to $1.00 (Professional Plus: 50,000 requests for $74.99, overage $0.006 a request; Enterprise Plus: 250,000 for $249.99, overage $0.004) |
| Unknown or catch-all results billed | No: an unconfirmed mailbox (reason unverified) and an answer Spaw could not obtain because its handshake partner was unavailable are free and leave no 7-day repeat marker, so a later lookup is a fresh one; a catch-all answer costs 1 credit like any risky answer, and an automatic re-check bills 1 credit only when it confirms the mailbox | Yes |
| Undeliverable results billed | No, within a fair use: undeliverable answers that needed a mailbox probe are free up to max(500, your billed email answers) in a rolling 30 days, then 1 credit each; the rest are always free | Yes |
| SMTP mailbox check | Yes | Yes |
| Catch-all handling | A catch-all answer is remembered per domain for 7 days, and every unconfirmed mailbox carries a published 0 to 100 mailbox_confidence estimate | Catch-all detection from the Basic plan up, not on Free; every request counts against the quota |
| Bulk limit | 100,000 addresses per bulk job, with an HMAC-signed completion webhook | Bulk endpoint: 25 emails per request on Professional Plus, 100 on Enterprise Plus |
| Batch API | 50 addresses per synchronous request, probes sent concurrently | 25 or 100 emails per request depending on plan |
| Rate limit | 5 requests per second per key on the email and consistency endpoints | not on the pages read |
| Official SDKs | Python, Node.js, PHP | not on the pages read |
| Data retention | Only email verification keeps a history row, for 30 days, then pruned; test addresses are never logged | not on the pages read |
## When Mailboxlayer fits better
Developers who prefer a flat monthly quota and already use other apilayer APIs.
- SMTP check, MX records, syntax and typo validation, and the free and disposable databases on every plan including Free; catch-all detection from Basic up
- 256-bit HTTPS encryption listed for paid plans
- Part of the apilayer marketplace
## When Spaw fits better
Developers who want one flat price per answered lookup across every signal a signup form produces, a transparent risk score, and an API that documents every field.
- Mailboxlayer counts every request against a monthly quota; Spaw bills only answered lookups and leaves unverified mailboxes free, and undeliverable verdicts free within a fair-use allowance
- Spaw's bulk jobs take 100,000 addresses; mailboxlayer's bulk endpoint takes 25 or 100 per request depending on plan
- Spaw includes the SMTP handshake on every lookup including the free monthly grant; mailboxlayer's SMTP check is on every plan too, while its catch-all detection starts at Basic
- At high volume mailboxlayer's quota price of $1.00 to $1.50 per 1,000 is below Spaw's $4.49 on the 100,000 pack and $3.49 on the Volume plan
## Questions
**Does Spaw charge for catch-all or unknown results?** Spaw bills per answer, not per probe outcome. A catch-all domain comes back risky with reason catch_all and costs 1 credit like any risky answer. A mailbox the handshake could not confirm (greylisting, a timeout, no definite answer) comes back risky with reason unverified and is free, and so is an answer Spaw could not obtain at all because its handshake partner was unavailable; neither leaves a 7-day repeat marker, so a later lookup is a fresh one (an uncertain result is reused for up to 15 minutes, then the mailbox is asked again) and bills 1 credit only once the mailbox is confirmed, and the automatic re-checks behind callback_url bill 1 credit only when they confirm it. Undeliverable verdicts are free within a fair use of max(500, your billed email answers) in a rolling 30 days, and invalid input and repeats within 7 days are free too. Mailboxlayer counts them according to its public pages.
**Can I move a list from Mailboxlayer to Spaw?** Yes. Upload the CSV to the dashboard or POST it to the bulk endpoint; Spaw keeps your columns, adds the verdict columns, and lets you download only the deliverable, risky or undeliverable rows. Addresses that come back undeliverable join your suppression list so later runs skip them for free.
**Is there a free tier?** Every Spaw account receives 100 free credits at the start of each month with no card on file, and unused free credits accumulate. Mailboxlayer offers: 100 requests a month.
## Sources (checked 2026-09-13)
- [Mailboxlayer product and pricing](https://mailboxlayer.com/product)
Mailboxlayer is a trademark of its owner. Spaw is not affiliated with it.
Reference: https://spaw.co/compare/mailboxlayer
---
# Mailboxlayer alternatives for email verification
Email verification APIs to consider instead of Mailboxlayer, with the pricing model, free tier and price per 1,000 lookups each vendor publishes. Figures come from public pricing pages checked on 2026-09-13.
| Service | Pricing model | Free tier | Price per 1,000 at about 10k |
| --- | --- | --- | --- |
| [Spaw](https://spaw.co) | Pay-as-you-go credit packs whose credits never expire, or optional plans, billed monthly or yearly, that deposit the same credits every month for less and roll unused plan credits over up to three months' allowance; both through Stripe Checkout | 100 free credits every month, no card required | $7.80 (Growth pack: 5,000 credits for $39), or $5.80 on the Growth monthly plan (5,000 credits for $29 a month) |
| [Abstract API](https://www.abstractapi.com) | Monthly subscription tiers with a request slider from 5,000 to 150,000 requests a month; Starter from $17, Standard from $37, Professional from $39 a month | 100 requests on the Free plan | not on the pages read |
| [Bouncer](https://www.usebouncer.com) | Pay-as-you-go credits that never expire (from 1,000 credits for $8 up to 1,000,000 for $2,000), auto-refill, plus a Deliverability Kit subscription at $25 to $250 a month | 100 free credits, no card required | $6.00 (10,000 credits for $60) |
| [Emailable](https://emailable.com) | Pay-as-you-go credits that never expire, or a monthly subscription at a 15% discount; minimum purchase 5,000 credits | 250 free credits at signup | $7.60 (the pay-as-you-go rate printed for the 5,000-credit package, $0.0076 a credit; larger packages are priced in a calculator, and the 5,000-credit subscription is $32.30 a month after the 15% discount) |
| [Hunter](https://hunter.io) | Monthly or yearly subscriptions with unified credits shared across search, verification and enrichment; credit packs on paid plans, usable for up to 12 months from purchase according to the pricing page | Free plan with 50 credits a month; one verification costs 0.5 credit, so 100 verifications | $7.45 (Growth: $149 a month for 10,000 credits, 20,000 verifications at 0.5 credit each; $5.20 billed yearly) |
| [Kickbox](https://kickbox.com) | not on the pages read | not on the pages read | not on the pages read |
| [NeverBounce](https://www.neverbounce.com) | not on the pages read | not on the pages read | not on the pages read |
| [Verifalia](https://verifalia.com) | Pay-as-you-go credit packs that never expire, or monthly, quarterly and yearly subscriptions; verification depth is priced as quality levels; pack and plan prices are shown in a calculator rather than listed | 25 free credits a day on the Free plan; unused daily credits expire each day | not on the pages read |
| [ZeroBounce](https://www.zerobounce.net) | Pay-as-you-go credits from $39 for 2,000 addresses, 15% off with autopay, or the ZeroBounce ONE subscription at $99 a month with unused credits rolling over; volume pricing shown through a calculator rather than fixed packs | 100 validation credits a month on the free account | not on the pages read |
## Spaw
Verification API that bills per answered lookup across six signals on one key and one balance: Email verification, Phone intelligence, IP intelligence, Address verification, Business verification against open registers, and Cross-signal consistency, which runs any subset of the first four in one call and names where they agree and where they do not. The email side checks syntax, live MX, SPF and DMARC, disposable and role lists, and runs an SMTP mailbox handshake that never sends a message.
## Abstract API
Abstract API's email validation endpoint is one of a family of utility APIs sold as monthly request quotas with a free 100-request plan.
Comparison: https://spaw.co/compare/abstract
## Bouncer
Bouncer is an email verification service with pay-as-you-go credits that never expire, high-throughput batch APIs, a real-time form product (Bouncer Shield) and a Deliverability Kit subscription.
Comparison: https://spaw.co/compare/bouncer
## Emailable
Emailable is an email verification service with pay-as-you-go credits, a bulk verifier, a widget and an API, with Node.js, Ruby and Python libraries.
Comparison: https://spaw.co/compare/emailable
## Hunter
Hunter is an outreach platform whose Email Verifier shares unified plan credits with its email finder, enrichment and campaign tools.
Comparison: https://spaw.co/compare/hunter
## Kickbox
Kickbox is an email verification API that returns deliverable, undeliverable, risky or unknown plus a 0 to 1 Sendex quality score, with official libraries for PHP, Node.js, Ruby and Python.
Comparison: https://spaw.co/compare/kickbox
## NeverBounce
NeverBounce is a ZoomInfo-owned email verification service with a bulk list-cleaning workflow, a real-time API and official client libraries in six languages.
Comparison: https://spaw.co/compare/neverbounce
## Verifalia
Verifalia is an email verification service with selectable quality levels, credit packs that never expire, subscriptions with daily free credits, and official SDKs in six languages.
Comparison: https://spaw.co/compare/verifalia
## ZeroBounce
ZeroBounce is an email validation service with a pay-as-you-go credit model and a subscription (ZeroBounce ONE) that bundles deliverability tools such as activity data, inbox placement tests and blacklist monitoring.
Comparison: https://spaw.co/compare/zerobounce
Mailboxlayer and the other product names are trademarks of their owners. Spaw is not affiliated with them.
Reference: https://spaw.co/alternatives/mailboxlayer
---
# Spaw vs MaxMind: IP intelligence API comparison
How Spaw and MaxMind compare on pricing, billing rules, the feeds behind each flag and limits, from MaxMind's public pages checked on 2026-09-10. Corrections: support@spaw.co.
| | Spaw | MaxMind |
| --- | --- | --- |
| Pricing model | Pay-as-you-go credit packs whose credits never expire, or optional plans, billed monthly or yearly, that deposit the same credits every month for less and roll unused plan credits over up to three months' allowance; both through Stripe Checkout. One credit buys five answered IP lookups | Per query, by service, with no minimum published: "If you estimate your volume of queries to be over $2,500 a month, contact us to discuss rates." |
| Free tier | 100 free credits every month, no card required | A no-cost trial is offered ("Experience the functionality and benefits of our GeoIP web services at no cost"); no free monthly allowance is stated on the page checked |
| Price per 1,000 at about 10k | $1.80 per 1,000 IP lookups at 5 to the credit (Starter pack: 1,000 credits for $9), or $1.40 on the Starter monthly plan (1,000 credits for $7 a month) | $0.10 per 1,000 for Country ($0.0001 a query), $0.30 for City Plus ($0.0003) and $2.00 for Insights ($0.002) |
| Price per 1,000 at about 100k | $1.56 per 1,000 IP lookups at 5 to the credit (Growth pack: 5,000 credits for $39), or $1.16 on the Growth monthly plan (5,000 credits for $29 a month) | The same rates: the page publishes no volume tiers, and asks you to contact them above $2,500 a month |
| Addresses no feed covered billed | One credit buys five answered addresses whatever their flags say; a flag no feed could evaluate answers null rather than false, and the answer costs the same | not on the pages read |
| Reserved and private ranges billed | No: reserved, private, loopback, CGNAT, documentation and multicast ranges answer reason reserved_range and are free, unallocated space is free, and so are repeats within 7 days | not on the pages read |
| Feeds behind the flags | Public feeds, each named in the answer with the date it was synced: DB-IP Lite and RFC 8805 geofeeds for location, the five registries' delegated statistics for allocation, Spamhaus DROP and ASN-DROP and abuse.ch Feodo Tracker for blocklists, the Tor Project's exit list and Onionoo, the Mullvad, NordVPN and Private Internet Access server lists plus the open X4BNet lists, Apple's iCloud Private Relay egress list, the cloud providers' own published ranges, and the verification files Google, Microsoft, OpenAI, Perplexity, DuckDuckGo and Ahrefs publish for their crawlers. | Not described on the pages checked. Insights is documented as "Everything in Country and City Plus, + Proxy Detection, and confidence factors, user count and type, static IP score, demographic data, and anonymizers", so anonymiser detection is the top tier only. |
| Refresh cadence | Every feed re-syncs daily, the botnet command-and-control list hourly, and the geofeed crawl weekly. Each response names every dataset it consulted with the date it was last synced, and /status lists them all. | Not published on the pages checked: the database-update guide tells you to "check periodically throughout the day for a release" rather than naming a schedule. |
| Geolocation source | The DB-IP Lite database, overridden wherever the operator publishes the location itself — an RFC 8805 geofeed, Apple's relay list, a VPN provider's server list. location_source says which answered, and location_confidence is graded from a weekly accuracy report rather than asserted. | Its own database: Country returns "Data associated with the country of an IP address", City Plus locates "to the zip or postal code level and the associated ISP or organization". |
| Bulk limit | 100,000 addresses per bulk job, with an HMAC-signed completion webhook | not on the pages read |
| Batch API | 1,000 addresses per synchronous request; repeats inside the list are looked up once | not on the pages read |
| Rate limit | 50 requests per second per key on the IP endpoints | not on the pages read |
| Official SDKs | Python, Node.js, PHP | not on the pages read |
| Data retention | No history row and no log line carrying the address. What a lookup leaves is keyed and never holds the address: the 7-day marker that makes a repeat free and, under the same keyed hash, the count behind seen_by_you; privacy: true skips both. The account's own outcome reports are kept as salted hashes for 90 days. Bulk input lists and result files are deleted with the job after 30 days. | not on the pages read |
## When MaxMind fits better
Teams that want geolocation at the lowest published per-query price and are content to buy Insights, at twenty times the City Plus rate, for anonymiser detection.
- The only per-query price among the IP services compared here, and a low one: $0.0001 for a country answer
- Confidence factors ship with Insights rather than with every answer
- Databases are sold for local use as well as the web services
## When Spaw fits better
Developers who want every public IP feed compiled, dated and named for them, with a risk score whose weights are published so it can be recomputed or ignored.
- MaxMind puts proxy and anonymiser detection on its most expensive tier; Spaw's Tor, VPN, relay, datacenter, crawler and blocklist flags are in every answer, five to the credit
- MaxMind's Country and City Plus rates are lower than Spaw's price per IP lookup and its Insights rate is higher; a Spaw lookup buys every signal at once rather than one service
- Spaw names the dataset and its sync date behind each flag; MaxMind publishes no refresh schedule on the pages checked
- Spaw's flags are tri-state: null means a feed did not evaluate the address, never that the answer is no
## Questions
**Does Spaw charge for addresses its feeds do not cover?** Spaw bills per answered address, five to the credit: the answered lookup that opens a block costs 1 credit and the next four cost nothing. Reserved, private, loopback, CGNAT, documentation and multicast ranges answer reason reserved_range and are free, unallocated space is free, and so is a repeat of the same address within seven days. A flag no feed could evaluate answers null rather than false: the address is still billed, and the null tells you the signal was not checked instead of pretending it was. MaxMind does not publish how it bills them.
**Can I run a log file or an export through Spaw?** Yes. A bulk run takes up to 100,000 addresses from the dashboard or the API and returns one row per input address, in input order, with every flag, the risk score and the signals that made it. Five answered addresses cost a credit, repeats inside the list are looked up once, reserved ranges cost nothing, and the finish is announced on an HMAC-signed webhook.
**Is there a free tier?** Every Spaw account receives 100 free credits at the start of each month with no card on file, and unused free credits accumulate. MaxMind offers: A no-cost trial is offered ("Experience the functionality and benefits of our GeoIP web services at no cost"); no free monthly allowance is stated on the page checked.
## Sources (checked 2026-09-10)
- [MaxMind GeoIP web services](https://www.maxmind.com/en/geoip-api-web-services)
- [MaxMind database update guide](https://dev.maxmind.com/geoip/updating-databases)
MaxMind is a trademark of its owner. Spaw is not affiliated with it.
Reference: https://spaw.co/compare/maxmind
---
# MaxMind alternatives for IP intelligence
IP intelligence APIs to consider instead of MaxMind, with the pricing model, free tier and price per 1,000 lookups each vendor publishes. Figures come from public pricing pages checked on 2026-09-10.
| Service | Pricing model | Free tier | Price per 1,000 at about 10k |
| --- | --- | --- | --- |
| [Spaw](https://spaw.co) | Pay-as-you-go credit packs whose credits never expire, or optional plans, billed monthly or yearly, that deposit the same credits every month for less and roll unused plan credits over up to three months' allowance; both through Stripe Checkout. One credit buys five answered IP lookups | 100 free credits every month, no card required | $1.80 per 1,000 IP lookups at 5 to the credit (Starter pack: 1,000 credits for $9), or $1.40 on the Starter monthly plan (1,000 credits for $7 a month) |
| [AbuseIPDB](https://www.abuseipdb.com) | Plans with daily request allowances: Individual free "Forever!", Basic $25/mo or $228/year, Premium $99/mo or $1,068/year, Enterprise custom | Individual: free forever with no credit card, 1,000 check and report requests, 100 block checks and 5 bulk reports a day | not on the pages read |
| [ip-api](https://ip-api.com) | Two tiers. The free endpoint: "Our endpoints are limited to 45 HTTP requests per minute from an IP address" and "We do not allow commercial use of the free endpoint". The Pro service: "€13.3 per month, when paying for 12 months", with "Unlimited requests", "SSL (HTTPS)", "Commercial use allowed" and a "99.9% SLA" ("If the general API uptime falls below 99.9% in a month, we will automatically extend your subscription for 10 days"). | The free endpoint, for non-commercial use only: 45 requests per minute per address over HTTP ("256-bit SSL encryption is not available for this free API") | not on the pages read |
| [IP2Location](https://www.ip2location.com) | IP2Location.io API, monthly query plans: Free $0 ("50K IP Geolocation API queries/month", "11 IP Attributes", "Attribution Required"), Starter "$49/month" ("150K IP Geolocation API queries/month", "20 IP Attributes"), Plus "$249/month" ("300K IP Geolocation API queries/month", "59 IP Attributes"), Security "$499/month" ("600K IP Geolocation API queries/month", "79 IP Attributes"); overage per increment Starter $10, Plus $50, Security $100. Databases, standard licence per year: DB1 "$99", DB2 "$399", DB8 "$1,299", DB18 "$2,799", with a site licence at ten times those figures. | "50K IP Geolocation API queries/month" with 11 attributes, basic geolocation and open-proxy detection only, "Attribution Required"; without a key "up to 1,000 queries per day" | not on the pages read |
| [ipapi](https://ipapi.co) | Monthly plans by lookup quota: Starter "$15 billed monthly" for 60,000 lookups, Professional "$25 billed monthly" for 150,000, Business "$49 billed monthly" for 500,000, Enterprise "$99 billed monthly" for 2,000,000, Premium "$199 billed monthly" for 6,000,000, Ultimate "$399 billed monthly" for 15,000,000; annual billing is 20% less. | A free trial of "up-to 1000 IP lookups in a day" (about 30,000 a month), "for testing & development purposes" and "Not for production use" | not on the pages read |
| [ipdata](https://ipdata.co) | Monthly plans by daily request quota: Personal "$10 per month" with "2,500 requests daily", Lite "$30 per month" with "10,000 requests daily", Startup "$50 per month" with "50,000 requests daily", Business "$120 per month" with "100,000 requests daily"; "20% off all annual plans" at the time of reading. | "1500 free requests daily", "restricted to non-commercial use"; on the free tier, over the quota "all further requests are blocked and you receive an email notification" | not on the pages read |
| [ipgeolocation.io](https://ipgeolocation.io) | Monthly credit plans, "A standard unified IP lookup uses 1 credit": Starter "$19/month" for "150000 API Credits Per month", Plus "$49/month" for 500,000, Pro "$79/month" for 1,000,000, Business "$129/month" for 2,000,000, Premium "$249/month" for 5,000,000, Enterprise "Contact Sales"; annual billing gives "2 months free". "Paid monthly plans reset every month. Paid annual plans reset every year". | "1000 API Credits Per day for life" with no credit card, "a hard limit of 1000 requests per day" and "No overages on Free"; the free plan returns "core location without the radius and confidence fields" | not on the pages read |
| [IPinfo](https://ipinfo.io) | Monthly plans by API request quota: Lite $0/mo, Core $26/mo for 150,000 requests, Max $163/mo for 250,000, Enterprise custom for "1M+". Database downloads are listed separately, "Starting at $1,000/month". | IPinfo Lite at "$0/mo", free forever with no credit card, giving unlimited API requests at country level only | not on the pages read |
| [IPQualityScore](https://www.ipqualityscore.com) | Monthly plans by lookup quota: Free $0 (1,000 lookups a month), Startup "As low as: $99 / Month" (5,000), SMB Basic "$499" (10,000), SMB+ "$999" (75,000), Enterprise custom. "Any valid request sent to our Proxy Detection, Email Verification, or Device Fingerprinting services including lookups performed through a batch CSV file check will consume a lookup credit." | 1,000 lookups a month, capped at 35 a day | not on the pages read |
| [Ipregistry](https://ipregistry.co) | Credits, "1 lookup = 1 request = 1 credit" (batch endpoints use "one credit per lookup in the batch request, plus 1"). Subscriptions: Starter "100,000 credits / mo" at "$29 $24 / month", Professional "300,000 credits / mo" at "$79 $66 / month", Business "1,150,000 credits / mo" at "$199 $166 / month", Scale "2,450,000 credits / mo" at "$399 $333 / month" (the lower figure is the annual rate). Pay-as-you-go packs: 15,000 credits for $10, 100,000 for $50, 250,000 for $100, 1,750,000 for $500. | "20,000 free credits on sign-up" with "No credit card, no time limit" | $0.67 (the $10 pack of 15,000 credits, $0.000667 a lookup) |
| [ipstack](https://ipstack.com) | Monthly plans by request quota: Free "100 Request/mo." at $0, Basic "50,000 Request/mo." at "$12.99/mo.", Professional "500,000 Request/mo." at "$59.99/mo.", Professional + "2,000,000 Request/mo." at "$99.99/mo.", Custom on request; yearly billing "Save up to 15%". Overage fees apply past the quota, with notifications at 75%, 90% and 100%. | "100 Request/mo." at $0, without SSL encryption, support or the paid modules (SSL, commercial use and the Location, Time Zone and Connection modules are listed for paid plans) | not on the pages read |
| [proxycheck.io](https://proxycheck.io) | not on the pages read | not on the pages read | not on the pages read |
| [Spur](https://spur.us) | not on the pages read | not on the pages read | not on the pages read |
## Spaw
Verification API that bills per answered lookup across six signals on one key and one balance: Email verification, Phone intelligence, IP intelligence, Address verification, Business verification against open registers, and Cross-signal consistency, which runs any subset of the first four in one call and names where they agree and where they do not. The IP side answers from local databases and compiled public feeds: where an address is, who runs it, and whether it is a visitor at all.
## AbuseIPDB
AbuseIPDB is a reputation service built on abuse reports its own users submit: a check returns an abuse confidence score with the report count, country, usage type, ISP and a Tor flag, and plans are sold as daily request allowances rather than credits.
Comparison: https://spaw.co/compare/abuseipdb
## ip-api
ip-api is a geolocation JSON API with a free, non-commercial, HTTP-only endpoint limited by request rate, and a Pro subscription that adds HTTPS, unlimited queries and commercial use; its answer carries one proxy flag, one hosting flag and one mobile flag beside the location and network fields.
Comparison: https://spaw.co/compare/ip-api
## IP2Location
IP2Location sells geolocation as downloadable databases (DB1 to DB26, licensed yearly) and, through IP2Location.io, as an API on monthly query plans whose higher tiers add proxy, VPN, Tor, datacenter, crawler and residential-proxy detection and a fraud score; the classic web service no longer takes new registrations.
Comparison: https://spaw.co/compare/ip2location
## ipapi
ipapi.co is a geolocation API sold as monthly lookup quotas, whose pricing page credits its data to DB-IP and the IP2Location LITE database; the answer carries location, country and network fields and no proxy, VPN or Tor flag.
Comparison: https://spaw.co/compare/ipapi
## ipdata
ipdata is a geolocation and threat API sold as daily request quotas, from a free non-commercial tier to a Business plan, whose pricing page says it does not keep records of the addresses looked up beyond a day; its documentation site refused automated reading, so the fields are not reproduced here.
Comparison: https://spaw.co/compare/ipdata
## ipgeolocation.io
ipgeolocation.io sells geolocation and a separate security API on monthly credit plans, with location built from its own measurements and a security answer that flags VPN, proxy, residential proxy and Tor with provider names, confidence scores and a 0 to 100 threat score.
Comparison: https://spaw.co/compare/ipgeolocation
## IPinfo
IPinfo is an IP data company that sells geolocation, ASN, privacy detection and company data as monthly API plans and as database downloads, collected through its own measurement network rather than assembled from third-party feeds.
Comparison: https://spaw.co/compare/ipinfo
## IPQualityScore
IPQualityScore sells fraud detection across several signals on one lookup credit: proxy and VPN detection, email verification and device fingerprinting all draw from the same monthly quota, and the IP answer carries a fraud score alongside the connection facts.
Comparison: https://spaw.co/compare/ipqualityscore
## Ipregistry
Ipregistry is a geolocation and threat API sold as credits, either monthly subscriptions or pay-as-you-go packs, whose security object answers abuser, attacker, bogon, cloud provider, proxy, relay, Tor, VPN, anonymous and threat flags and whose data is described as built from BGP tables, registry databases, geofeeds and latency measurements.
Comparison: https://spaw.co/compare/ipregistry
## ipstack
ipstack is a geolocation API on monthly request quotas from a 100-request free tier to a two-million-request plan, with a bulk endpoint and a security module reserved for its top plan; its documentation pages rendered only a header to our fetcher, so the field list is not reproduced here.
Comparison: https://spaw.co/compare/ipstack
## proxycheck.io
proxycheck.io sells an API for checking whether an address is a proxy; every page we tried on 2026-09-12 answered 403 Forbidden to automated reading, so nothing about its pricing, limits, fields or data is reproduced here.
Comparison: https://spaw.co/compare/proxycheck
## Spur
Spur is an anonymity-infrastructure vendor: its Context API answers whether an address is a VPN, proxy or other tunnel with the operator named, and its feeds list anonymous, residential-proxy and datacenter address space; its pricing page rate-limited our fetcher, so prices are not reproduced here.
Comparison: https://spaw.co/compare/spur
MaxMind and the other product names are trademarks of their owners. Spaw is not affiliated with them.
Reference: https://spaw.co/alternatives/maxmind
---
# Spaw vs Melissa: address verification API comparison
How Spaw and Melissa compare on pricing, billing rules, the registers behind each answer and limits, from Melissa's public pages checked on 2026-09-16. Corrections: support@spaw.co.
| | Spaw | Melissa |
| --- | --- | --- |
| Pricing model | Pay-as-you-go credit packs whose credits never expire, or optional plans, billed monthly or yearly, that deposit the same credits every month for less and roll unused plan credits over up to three months' allowance; both through Stripe Checkout | US Address Verification pay-as-you-go "Starting at $40" for 10,000 credits at "10 Credits Per Address" (the same page's FAQ says a US address equals 3 credits and a global address 10); an API subscription "Starting at $5,145" for 1,000,000 records a year; an unlimited-records plan "Starting at $16,000". Global Address Verification subscriptions from $40 a month for 1,000 records and $95 a month for 2,500. |
| Free tier | 100 free credits every month, no card required | Global Address Verification "GAV Free" at $0 for 250 records a month; 1,000 lookup credits a month after a one-time $4.97 KYC/AML verification fee, described on the pricing page. |
| Price per 1,000 at about 10k | $7.80 (Growth pack: 5,000 credits for $39), or $5.80 on the Growth monthly plan (5,000 credits for $29 a month) | $40 per 1,000 US addresses on the smallest pay-as-you-go pack, taking the pack's own "10 Credits Per Address" ($40 buys 10,000 credits). |
| Price per 1,000 at about 100k | $4.49 (Volume pack: 100,000 credits for $449), or $3.49 on the Volume monthly plan (100,000 credits for $349 a month) | not on the pages read |
| Addresses no register covered billed | An address that stands as written costs 1 credit whether or not a register carried the building; where no register covers the country, exists answers null rather than a verdict | not on the pages read |
| Addresses that cannot stand billed | No: an address that cannot stand as written is free whatever the reason, and so are repeats within 7 days and rows repeating an address already in the same job | not on the pages read |
| Registers held | Open address registers for 23 countries and territories, each named in the answer with its declared coverage, complete or partial. Complete, building by building: Australia, Austria, Belgium, Czechia, Latvia, Luxembourg, Netherlands, Norway and Switzerland. Partial, building by building: Brazil, Denmark, France (with French Guiana, Guadeloupe, Martinique, Mayotte, Réunion, St. Barthélemy, St. Martin and St. Pierre & Miquelon), Germany and United States. Partial, streets only: Great Britain. House-number ranges as well: United States. Absence from a partial register is never read as evidence that an address does not exist. | Postal, not cadastral: "a postal certified address engine using official government data including CASS (USPS), SERP (Canada Post), PAF (Royal Mail) and more"; no national address register is named. |
| Country coverage | Postal formats for 207 countries and territories: the line layout for 199 of them, and the required fields and postcode pattern wherever the table records them; 52 postal operators' rules for naming a delivery point that is not a street; open address registers for 23 countries and territories; postcode directories for Great Britain from the ONS Postcode Directory, with Northern Ireland excluded because reusing BT postcodes needs a separate licence, and for 108 more countries and territories from GeoNames' postal codes. | "Global Address Verification & Validation for 250+ Countries & Territories" |
| Claims post is delivered | No, and every answer says so: is_deliverable, is_cmra, is_vacant and is_residential answer null because that check is licensed and not enabled here. exists is a match in an open address register, never a statement that post arrives. | Yes. The address-verification page names DPV ("to verify the address is deliverable"), SuiteLink for missing suite numbers, RBDI (a residential or business delivery indicator), AddressPlus for apartment numbers, "Non-USPS data (5+ million addresses not supported by USPS)" and coordinates "up to the rooftop (delivery point) level". |
| Bulk limit | 100,000 addresses per bulk job, with an HMAC-signed completion webhook | not on the pages read |
| Batch API | 100 addresses per synchronous request | not on the pages read |
| Rate limit | 50 requests per second per key on the address endpoints | not on the pages read |
| Official SDKs | Python, Node.js, PHP | not on the pages read |
| Data retention | No history row and no log line carrying the address. What a lookup leaves is the 7-day marker that makes a repeat free and the count behind seen_by_you, both kept under a digest, never the address. The address itself is kept only on the account's own suppression list and in its monitors. Bulk input lists and result files are deleted with the job after 30 days. | not on the pages read |
## When Melissa fits better
Mailers and CRM teams that want the postal services' own delivery-point answers for the United States, Canada and the United Kingdom, from one vendor that also sells change-of-address and identity checks.
- CASS and DPV certified for the United States, SERP for Canada, PAF for the United Kingdom
- Delivery-point answers licensed postal data can give: DPV, a residential or business indicator, missing suite numbers
- Rooftop geocodes and transliteration to and from native scripts
- A free global tier of 250 records a month
## When Spaw fits better
Developers who want a written address parsed against its own country's rules and checked against open registers, where a field nobody could evaluate answers null instead of a guess.
- Melissa answers whether post is delivered to an address; Spaw does not and says so: is_deliverable, is_cmra, is_vacant and is_residential answer null because that check is licensed and not enabled
- Spaw's confirmation map says which open dataset stood behind each part of the address, and building_use and coordinates come from the national registers themselves, never from a postal file
- Melissa prices the United States and the rest of the world on different scales; a Spaw credit buys one answered address lookup wherever the address is, and an address that cannot stand as written costs nothing
- Spaw names the register that answered and whether its coverage is complete or partial, and never fills a gap in open data with a guess
## Questions
**Does Spaw charge for addresses no register covers?** An address that stands as written costs 1 credit, whether or not a register carried the building. An address that cannot stand costs nothing, whatever the reason, and a repeat within seven days is free. Where no register covers the country, or where the register that answered is one whose coverage is partial and does not carry the address, exists answers null — not evaluated — rather than a verdict, and the not_in_register weight cannot fire. Melissa does not publish how it bills them.
**Can I move an address list from Melissa to Spaw?** Yes. Upload the CSV in the dashboard or POST it to the bulk endpoint, up to 100,000 addresses a job; Spaw keeps your columns and adds 64 result columns after them (the address as submitted, the answer, the datasets behind it, what the row cost and whether your suppression list answered it), and a country column gives each row its own country. Rows repeating an address already in the list are answered once, cost nothing, and are counted as duplicates.
**Is there a free tier?** Every Spaw account receives 100 free credits at the start of each month with no card on file, and unused free credits accumulate. Melissa offers: Global Address Verification "GAV Free" at $0 for 250 records a month; 1,000 lookup credits a month after a one-time $4.97 KYC/AML verification fee, described on the pricing page.
## Sources (checked 2026-09-16)
- [Melissa pricing](https://www.melissa.com/pricing)
- [Melissa address verification](https://www.melissa.com/address-verification)
Melissa is a trademark of its owner. Spaw is not affiliated with it.
Reference: https://spaw.co/compare/melissa
---
# Melissa alternatives for address verification
Address verification APIs to consider instead of Melissa, with the pricing model, free tier and price per 1,000 lookups each vendor publishes. Figures come from public pricing pages checked on 2026-09-16.
| Service | Pricing model | Free tier | Price per 1,000 at about 10k |
| --- | --- | --- | --- |
| [Spaw](https://spaw.co) | Pay-as-you-go credit packs whose credits never expire, or optional plans, billed monthly or yearly, that deposit the same credits every month for less and roll unused plan credits over up to three months' allowance; both through Stripe Checkout | 100 free credits every month, no card required | $7.80 (Growth pack: 5,000 credits for $39), or $5.80 on the Growth monthly plan (5,000 credits for $29 a month) |
| [Geoapify](https://www.geoapify.com) | Daily credit quotas on monthly subscriptions: Free (3,000 credits a day), API 10 (10,000 a day, $59 billed monthly), API 25 (25,000, $109), API 50 (50,000, $179), API 100 (100,000, $299), API 250 (250,000, $609) and Custom ("Unmetered credits / day", from $860). "1 Geocoding API request costs 1 credit"; in a batch each address counts 0.5 credits, plus 1 credit to create the job and 1 for each result request. A yearly term is offered, but its prices are not in the page text read. | A standing free plan: "3,000 credits / day", "No credit card required", up to 5 requests a second, usable for "commercial websites and apps" with the required Geoapify and data-source attribution | not on the pages read |
| [Google Address Validation API](https://mapsplatform.google.com) | "Pay-as-you-go", "charging per request according to the pricing table", under two SKUs named on the billing page, Address Validation Pro and Address Validation Enterprise; the per-request rates render inside the pricing table and are not stated in the page text as fetched, so no figure is quoted here. | not on the pages read | not on the pages read |
| [Ideal Postcodes](https://ideal-postcodes.co.uk) | Pay-as-you-go credit packs, "No Subscriptions. No Monthly Fees.": 200 credits for £9 ("£0.045 per lookup"), 1,100 for £42 (£0.038), 4,300 for £155 (£0.036), 12,800 for £420 (£0.033) and 32,100 for £900 (£0.028). "Credits are valid for 12 months"; the lookup guide adds that the timer "only starts when you begin using that batch of credits". Larger volumes and UK registered charities are priced on request. | A trial rather than a standing tier: "1-Month Trial", "50 free credits, full support and no credit card needed"; the trial searches worldwide except Ireland, "which requires separate licensing" | £33 per 1,000 lookups on the 12,800-credit pack (£420, "£0.033 per lookup"), the smallest pack that covers 10,000. |
| [Lob](https://www.lob.com) | Verifications are priced per call on top of a print-and-mail plan: on the Developer plan ($0 a month) "US Address Verifications $0.05/additional" and international $0.07; on Startup ($260 a month) $0.025 with "1,000 included for $25/mo" and international $0.06 with 500 included for $30 a month; on Growth ($550 a month) $0.009 with "50,000 included for $450/mo" and international $0.055 with 10,000 included for $550 a month; Enterprise is "Contact us for details". | The Developer plan is "Starting at $0 / month" with verifications billed per call; the site offers "Get started for free". | $50 per 1,000 US verifications on the Developer plan ($0.05 each) or $25 per 1,000 on Startup ($0.025 each beyond the 1,000 included for $25 a month); international $70 or $60. |
| [Loqate](https://www.loqate.com) | Pay-as-you-go credit purchases of £50, £100, £250, £500 and £1,000 (or 75€, 150€ and 300€), each buying a published per-lookup rate for every service, plus bespoke plans; "Credit is valid for 12 months after the purchase date" and there are no setup costs. The pricing page shows two rate tables headed "New Pricing" and "Old Pricing" without saying which accounts each applies to; the figures here are from "New Pricing". Its usage-example selector also lists a £2,500 plan that neither table prices. | A trial rather than a standing tier: "Start your 45-day free trial"; the FAQ adds "You get free credits to test the service" | £82 to £86 per 1,000 Address Verify lookups on the "New Pricing" table: 8.2p a lookup on the £100, £250 and £500 tiers and 8.6p on the £50 and £1,000 tiers. Address Capture is 8.6p to 4.9p in the UK and 7.1p to 4.1p for the rest of the world. |
| [Postcode.eu](https://www.postcode.eu) | Annual tiers that include validations: Tier 1 €50, Tier 2 €100, Tier 3 €250, Tier 4 €500 and Tier 5 €1,000 a year, each with a per-validation rate by country group. "Up to the Tier amount you can perform a mix of validations", subscriptions "are automatically upgraded when the maximum of the Tier is reached", and past the highest Tier usage "will be invoiced in blocks of 10% and the rates of this Tier apply". A validation is "a bundle of related requests that were used to validate an address". An SLA (€1,000 + 2% a year) and a DORA addendum (€2,500 a year) are extra. | A trial rather than a standing tier: "After registering an account it is possible to try the APIs for free", and "Testing the API is without obligations" | €17.64 per 1,000 validations on Tier 3 (€250 a year, "€0.01764 each") for Austria, Belgium, Denmark, Finland, France, Luxembourg, Norway, Spain, Sweden, Switzerland and the Netherlands. Germany, Italy and the United Kingdom cost more a validation, so 10,000 of them fall in Tier 4 (€500 a year): €34.17 per 1,000 for Germany (€0.03417 each) and €43.30 for Italy and the United Kingdom (€0.0433 each). "Tier prices are including validations" and "Prices and quantities are per year", so the tier amount is paid in full: 10,000 validations cost €250 a year for the eleven countries (€25 per 1,000) and €500 for Germany, Italy or the United Kingdom (€50 per 1,000). |
| [Postcoder](https://postcoder.com) | Credit packs (500 to 200,000 credits, "valid for 12 months from purchase") or monthly plans (1,000 to 100,000 credits, with unused credits not carried over). "Your credits can be used for any Postcoder request." The pack prices themselves render as placeholders ("£--") in the pack list; the credit calculator's recommendation box shows "Credit pack £25 500 Credits" and "Monthly plan £25 1,000 Credits". Larger packs and plans with volume discounts are quoted. | A free trial is offered; its duration and allowance are not stated on the pricing page | £100 per 1,000 UK address lookups pay-as-you-go (10.00p a request, 2.0 credits) or £45 per 1,000 on a monthly plan (4.50p). This is the address comparison, so only the address rate is quoted: the same account buys email and mobile validation at their own per-request rates, which are read off the same page. |
| [PostGrid](https://www.postgrid.com) | Quoted: the pricing page asks for annual mail-piece and address-verification volumes in bands from "Less than 50,000" to "More than 1,000,000" and publishes no rate. | "Try for Free. Sign Up for Address Verification API" on the product page; no standing free volume is stated. | not on the pages read |
| [Smarty](https://www.smarty.com) | Annual lookup bundles on the Professional plan from $552 a year, sized 12K, 60K, 120K, 300K, 600K, 1M or 2M lookups; a monthly option and bespoke Custom pricing are offered. Only the $552 entry price is shown on the page. A subscription "expires when the address lookups or the time runs out, whichever comes first"; there are no overage costs, and an exhausted plan answers 402 "payment required" unless auto-renewal buys a new block. | A free trial rather than a standing tier: "42 days" and "1,000 address lookups", "No credit card required" | not on the pages read |
## Spaw
Verification API that bills per answered lookup across six signals on one key and one balance: Email verification, Phone intelligence, IP intelligence, Address verification, Business verification against open registers, and Cross-signal consistency, which runs any subset of the first four in one call and names where they agree and where they do not. The address side reads a written address against its own country's format, names the kind of delivery point behind it, checks the postcode against the national directory and follows the building into that country's own address register where one is installed.
## Geoapify
Geoapify is a location platform (maps, routing, places and geocoding) whose Geocoding API is also sold for address validation: an address is matched against open datasets such as OpenStreetMap and OpenAddresses and returned with coordinates and confidence scores for the city, the street and the building.
Comparison: https://spaw.co/compare/geoapify
## Google Address Validation API
Google's Address Validation API, part of Google Maps Platform, validates, standardises and geocodes an address, reports a confirmation level per component with inferred and replaced parts, and can run US and Puerto Rico addresses through USPS CASS on request.
Comparison: https://spaw.co/compare/google-address-validation
## Ideal Postcodes
Ideal Postcodes is a UK-first address lookup, autocomplete and cleansing API answered from Royal Mail's Postcode Address File, with Ordnance Survey AddressBase Core and Premium offered as additional datasets, sold as pay-as-you-go credit packs, with international address data for more than 240 countries and territories; its site also lists phone and email validation among its products.
Comparison: https://spaw.co/compare/ideal-postcodes
## Lob
Lob is a print-and-mail platform whose Address Verification API validates, standardises and enriches US and international addresses, CASS certified, with USPS delivery-point data and a deliverability likelihood drawn from Lob's own mail delivery records.
Comparison: https://spaw.co/compare/lob
## Loqate
Loqate sells address capture and address verification alongside email, phone and bank validation, priced per lookup from a pay-as-you-go credit balance, with international data for over 245 countries and territories.
Comparison: https://spaw.co/compare/loqate
## Postcode.eu
Postcode.eu, from Postcode.nl B.V., sells address autocomplete and validation APIs for fourteen European countries, each answered from the official sources its pages name (government address registers for most, the postal operator's file for others), priced per validation inside an annual tier.
Comparison: https://spaw.co/compare/postcode-eu
## Postcoder
Postcoder is a UK-first address lookup API sold as credits that any of its services can spend: address lookup, bank validation, email validation, mobile validation and OTP verification, each costing a published number of credits per request.
Comparison: https://spaw.co/compare/postcoder
## PostGrid
PostGrid sells an address verification API for the United States and Canada, CASS and SERP certified against USPS and Canada Post data, with an international service for "245+ Countries and Territories", autocomplete, parsing, batch verification and a Salesforce integration, priced by quotation.
Comparison: https://spaw.co/compare/postgrid
## Smarty
Smarty sells US address verification against postal data: a lookup validates to the secondary level and returns delivery-point metadata that open registers cannot answer, including a DPV match code, the residential indicator and vacancy status.
Comparison: https://spaw.co/compare/smarty
Melissa and the other product names are trademarks of their owners. Spaw is not affiliated with them.
Reference: https://spaw.co/alternatives/melissa
---
# Spaw vs NeverBounce: email verification API comparison
How Spaw and NeverBounce compare on pricing, billing rules, verification depth and limits, from NeverBounce's public pages checked on 2026-09-03. Corrections: support@spaw.co.
| | Spaw | NeverBounce |
| --- | --- | --- |
| Pricing model | Pay-as-you-go credit packs whose credits never expire, or optional plans, billed monthly or yearly, that deposit the same credits every month for less and roll unused plan credits over up to three months' allowance; both through Stripe Checkout | not on the pages read |
| Free tier | 100 free credits every month, no card required | not on the pages read |
| Price per 1,000 at about 10k | $7.80 (Growth pack: 5,000 credits for $39), or $5.80 on the Growth monthly plan (5,000 credits for $29 a month) | not on the pages read |
| Price per 1,000 at about 100k | $4.49 (Volume pack: 100,000 credits for $449), or $3.49 on the Volume monthly plan (100,000 credits for $349 a month) | not on the pages read |
| Unknown or catch-all results billed | No: an unconfirmed mailbox (reason unverified) and an answer Spaw could not obtain because its handshake partner was unavailable are free and leave no 7-day repeat marker, so a later lookup is a fresh one; a catch-all answer costs 1 credit like any risky answer, and an automatic re-check bills 1 credit only when it confirms the mailbox | not on the pages read |
| Undeliverable results billed | No, within a fair use: undeliverable answers that needed a mailbox probe are free up to max(500, your billed email answers) in a rolling 30 days, then 1 credit each; the rest are always free | not on the pages read |
| SMTP mailbox check | Yes | not on the pages read |
| Catch-all handling | A catch-all answer is remembered per domain for 7 days, and every unconfirmed mailbox carries a published 0 to 100 mailbox_confidence estimate | catchall is one of the result codes alongside valid, invalid, disposable and unknown; jobs with a high rate of unknowns can be sent for a manual review |
| Bulk limit | 100,000 addresses per bulk job, with an HMAC-signed completion webhook | Lists of millions should be split into jobs of 1,000,000 addresses; 10 concurrent jobs and at most 50 runs a day per account, and no more than 10 jobs per 100,000 items an hour |
| Batch API | 50 addresses per synchronous request, probes sent concurrently | Bulk jobs through the jobs API (create, status, download) |
| Rate limit | 5 requests per second per key on the email and consistency endpoints | Job creation limits as above; per-request limits for the single-check endpoint are not published on the pages checked |
| Official SDKs | Python, Node.js, PHP | PHP, Node.js, Python, Ruby, Go, .NET |
| Data retention | Only email verification keeps a history row, for 30 days, then pruned; test addresses are never logged | not on the pages read |
## When NeverBounce fits better
Teams that clean large lists through jobs and want official wrappers in six languages plus an n8n node.
- Official n8n node
- Manual review process for jobs with many unknown results
- Owned by ZoomInfo
## When Spaw fits better
Developers who want one flat price per answered lookup across every signal a signup form produces, a transparent risk score, and an API that documents every field.
- NeverBounce's pricing pages blocked automated reading when we checked, so its rates are not reproduced here; Spaw's packs and free grant are listed at spaw.co/pricing
- Spaw runs the SMTP handshake on every lookup and bills nothing for undeliverable verdicts within a fair-use allowance
- Spaw's bulk jobs take 100,000 addresses with a signed completion webhook; NeverBounce asks for 1,000,000-address chunks, 10 concurrent jobs and 50 runs a day
- Spaw returns 46 named fields and a machine-readable reason per address
## Questions
**Does Spaw charge for catch-all or unknown results?** Spaw bills per answer, not per probe outcome. A catch-all domain comes back risky with reason catch_all and costs 1 credit like any risky answer. A mailbox the handshake could not confirm (greylisting, a timeout, no definite answer) comes back risky with reason unverified and is free, and so is an answer Spaw could not obtain at all because its handshake partner was unavailable; neither leaves a 7-day repeat marker, so a later lookup is a fresh one (an uncertain result is reused for up to 15 minutes, then the mailbox is asked again) and bills 1 credit only once the mailbox is confirmed, and the automatic re-checks behind callback_url bill 1 credit only when they confirm it. Undeliverable verdicts are free within a fair use of max(500, your billed email answers) in a rolling 30 days, and invalid input and repeats within 7 days are free too. NeverBounce does not publish how it bills them.
**Can I move a list from NeverBounce to Spaw?** Yes. Upload the CSV to the dashboard or POST it to the bulk endpoint; Spaw keeps your columns, adds the verdict columns, and lets you download only the deliverable, risky or undeliverable rows. Addresses that come back undeliverable join your suppression list so later runs skip them for free.
**Is there a free tier?** Every Spaw account receives 100 free credits at the start of each month with no card on file, and unused free credits accumulate. NeverBounce does not publish a free tier on the pages checked.
## Sources (checked 2026-09-03)
- [NeverBounce usage guidelines](https://developers.neverbounce.com/reference/usage-guidelines)
- [NeverBounce verifying a list](https://developers.neverbounce.com/docs/verifying-a-list)
- [NeverBounce GitHub organization](https://github.com/NeverBounce)
NeverBounce is a trademark of its owner. Spaw is not affiliated with it.
Reference: https://spaw.co/compare/neverbounce
---
# NeverBounce alternatives for email verification
Email verification APIs to consider instead of NeverBounce, with the pricing model, free tier and price per 1,000 lookups each vendor publishes. Figures come from public pricing pages checked on 2026-09-03.
| Service | Pricing model | Free tier | Price per 1,000 at about 10k |
| --- | --- | --- | --- |
| [Spaw](https://spaw.co) | Pay-as-you-go credit packs whose credits never expire, or optional plans, billed monthly or yearly, that deposit the same credits every month for less and roll unused plan credits over up to three months' allowance; both through Stripe Checkout | 100 free credits every month, no card required | $7.80 (Growth pack: 5,000 credits for $39), or $5.80 on the Growth monthly plan (5,000 credits for $29 a month) |
| [Abstract API](https://www.abstractapi.com) | Monthly subscription tiers with a request slider from 5,000 to 150,000 requests a month; Starter from $17, Standard from $37, Professional from $39 a month | 100 requests on the Free plan | not on the pages read |
| [Bouncer](https://www.usebouncer.com) | Pay-as-you-go credits that never expire (from 1,000 credits for $8 up to 1,000,000 for $2,000), auto-refill, plus a Deliverability Kit subscription at $25 to $250 a month | 100 free credits, no card required | $6.00 (10,000 credits for $60) |
| [Emailable](https://emailable.com) | Pay-as-you-go credits that never expire, or a monthly subscription at a 15% discount; minimum purchase 5,000 credits | 250 free credits at signup | $7.60 (the pay-as-you-go rate printed for the 5,000-credit package, $0.0076 a credit; larger packages are priced in a calculator, and the 5,000-credit subscription is $32.30 a month after the 15% discount) |
| [Hunter](https://hunter.io) | Monthly or yearly subscriptions with unified credits shared across search, verification and enrichment; credit packs on paid plans, usable for up to 12 months from purchase according to the pricing page | Free plan with 50 credits a month; one verification costs 0.5 credit, so 100 verifications | $7.45 (Growth: $149 a month for 10,000 credits, 20,000 verifications at 0.5 credit each; $5.20 billed yearly) |
| [Kickbox](https://kickbox.com) | not on the pages read | not on the pages read | not on the pages read |
| [Mailboxlayer](https://mailboxlayer.com) | Monthly or yearly subscription (10 to 15% off yearly) with a request quota per plan and per-request overage | 100 requests a month | $3.00 for the Basic quota (5,000 requests for $14.99 a month; overage $0.012 a request, so 10,000 requests on Basic cost about $74.95, $7.50 per 1,000); Professional Plus: 50,000 for $74.99, $1.50 per 1,000 |
| [Verifalia](https://verifalia.com) | Pay-as-you-go credit packs that never expire, or monthly, quarterly and yearly subscriptions; verification depth is priced as quality levels; pack and plan prices are shown in a calculator rather than listed | 25 free credits a day on the Free plan; unused daily credits expire each day | not on the pages read |
| [ZeroBounce](https://www.zerobounce.net) | Pay-as-you-go credits from $39 for 2,000 addresses, 15% off with autopay, or the ZeroBounce ONE subscription at $99 a month with unused credits rolling over; volume pricing shown through a calculator rather than fixed packs | 100 validation credits a month on the free account | not on the pages read |
## Spaw
Verification API that bills per answered lookup across six signals on one key and one balance: Email verification, Phone intelligence, IP intelligence, Address verification, Business verification against open registers, and Cross-signal consistency, which runs any subset of the first four in one call and names where they agree and where they do not. The email side checks syntax, live MX, SPF and DMARC, disposable and role lists, and runs an SMTP mailbox handshake that never sends a message.
## Abstract API
Abstract API's email validation endpoint is one of a family of utility APIs sold as monthly request quotas with a free 100-request plan.
Comparison: https://spaw.co/compare/abstract
## Bouncer
Bouncer is an email verification service with pay-as-you-go credits that never expire, high-throughput batch APIs, a real-time form product (Bouncer Shield) and a Deliverability Kit subscription.
Comparison: https://spaw.co/compare/bouncer
## Emailable
Emailable is an email verification service with pay-as-you-go credits, a bulk verifier, a widget and an API, with Node.js, Ruby and Python libraries.
Comparison: https://spaw.co/compare/emailable
## Hunter
Hunter is an outreach platform whose Email Verifier shares unified plan credits with its email finder, enrichment and campaign tools.
Comparison: https://spaw.co/compare/hunter
## Kickbox
Kickbox is an email verification API that returns deliverable, undeliverable, risky or unknown plus a 0 to 1 Sendex quality score, with official libraries for PHP, Node.js, Ruby and Python.
Comparison: https://spaw.co/compare/kickbox
## Mailboxlayer
Mailboxlayer is apilayer's email validation API, sold as monthly request quotas from a free 100-request plan up to 250,000 requests a month.
Comparison: https://spaw.co/compare/mailboxlayer
## Verifalia
Verifalia is an email verification service with selectable quality levels, credit packs that never expire, subscriptions with daily free credits, and official SDKs in six languages.
Comparison: https://spaw.co/compare/verifalia
## ZeroBounce
ZeroBounce is an email validation service with a pay-as-you-go credit model and a subscription (ZeroBounce ONE) that bundles deliverability tools such as activity data, inbox placement tests and blacklist monitoring.
Comparison: https://spaw.co/compare/zerobounce
NeverBounce and the other product names are trademarks of their owners. Spaw is not affiliated with them.
Reference: https://spaw.co/alternatives/neverbounce
---
# Spaw vs numlookupapi: phone intelligence API comparison
How Spaw and numlookupapi compare on pricing, billing rules, where the line type comes from and limits, from numlookupapi's public pages checked on 2026-09-27. Corrections: support@spaw.co.
| | Spaw | numlookupapi |
| --- | --- | --- |
| Pricing model | Pay-as-you-go credit packs whose credits never expire, or optional plans, billed monthly or yearly, that deposit the same credits every month for less and roll unused plan credits over up to three months' allowance; both through Stripe Checkout | Monthly subscription tiers by request quota: Free $0 for 100 requests, Basic $9.99 for 5,000, Pro $49.99 for 50,000, Scale $109.99 for 250,000, and Custom by contact; the page offers yearly billing at 20% off. Scale and Custom list "Overages" with no overage price shown, and the quota documentation says a request over the monthly quota answers 429 until the plan is upgraded or the month ends. |
| Free tier | 100 free credits every month, no card required | 100 requests a month on one API key, at 10 requests a minute |
| Price per 1,000 at about 10k | $7.80 (Growth pack: 5,000 credits for $39), or $5.80 on the Growth monthly plan (5,000 credits for $29 a month) | not on the pages read |
| Price per 1,000 at about 100k | $4.49 (Volume pack: 100,000 credits for $449), or $3.49 on the Volume monthly plan (100,000 credits for $349 a month) | not on the pages read |
| Numbers the vendor could not settle billed | A valid number costs 1 credit whatever its flags say. The live carrier-network check (hlr: true) is not enabled on the service: hlr_checked answers false and nothing extra is charged. | not on the pages read |
| Invalid numbers billed | No: invalid numbers are free, including a number in a block the regulator lists as unassigned, and so are repeats within 7 days | Only successful calls count against the quota; "validation errors (e.g., wrong parameter)" and errors on its side do not, so an empty or unparseable number (422 invalid_number, "The phone number is empty or could not be parsed") is free. A well-formed number it does not recognise answers 200 with "valid": false, and the status-codes page calls a 200 "a successful response", noting that "a well-formed but unrecognised phone number also returns a 200" |
| Line type source | The numbering plan metadata everywhere except the North American plan, where mobiles and landlines share blocks and the plan can only say fixed_line_or_mobile; there the regulator's block holder settles it — a wireless carrier's block is mobile, a virtual-number wholesaler's is voip. line_type_source names which of the two answered. | Not stated beyond the pricing FAQ: "All number data delivered are sourced from multiple data providers and international carriers." The documented line_type values are landline, mobile, satellite, paging, special_services, premium_rate, toll_free and N/A; none of them is VoIP. |
| Numbering plan coverage | libphonenumber metadata for every region it carries, plus the regulators' own allocation tables, each named in the answer with the date of the copy it read. Complete tables, where a geographic number in a block the table leaves out answers unassigned_block: NANPA for the United States (with Puerto Rico, the U.S. Virgin Islands, Guam, American Samoa and the Northern Mariana Islands), the CNAC for Canada, ARCEP for France (with Guadeloupe, St. Barthélemy, St. Martin, Martinique, French Guiana, Réunion, Mayotte and St. Pierre & Miquelon), ACM for the Netherlands and the CRT (formerly the IFT) for Mexico. Partial tables, answered only from the ranges they list (a range listed as free, spare or held in reserve answers unassigned_block), so block_status answers null for a number outside them: Ofcom for the United Kingdom (with Guernsey, Jersey and the Isle of Man), ACMA for Australia, the NAD for New Zealand, UKE for Poland and MIMIT for Italy. Re-synced weekly, NANPA daily; /status lists each table's age. Outside those regions, block_status and is_virtual answer null rather than a guess. | "Phone data for 230+ countries", per the site footer; the pages read name no numbering plan or regulator table. |
| Live network check | Reserved behind hlr: true: 4 credits on top of the base credit, only where the network answers, reported in its own reachable, ported, current_carrier and roaming fields. The live carrier-network check (hlr: true) is not enabled on the service: hlr_checked answers false and nothing extra is charged. | not on the pages read |
| Ported numbers | None from the tables, and the docs say so: carrier and line_type describe the block the number was allocated in, never a later port. Only a live carrier-network check can say who serves a number today. | not on the pages read |
| SMS pumping controls | Flags in the risk score, and an SMS policy that turns them into ok_to_send and blocked_by: a fraud and deliverability decision, not consent and not a do-not-call scrub. allowed_countries limits sending to the regions you serve; it is empty (any country) until you set it. range_burst (+40) flags a number once your account has looked up 5 distinct numbers in its 1,000-number range within 60 minutes, and numbers_per_client (+30) once one client address (the client_ip you send, or a browser visitor's own) has tried 5 within 60 minutes, unless Spaw's own IP data places it on a mobile carrier or a corporate proxy, or it is private or reserved. Both count only your own single and browser lookups, so a new account has no history. The range is a heuristic, and in North America a burst more likely means someone testing numbers against a form; the client count does not stop an attack rotated through residential proxies. disposable (+70) flags a number SMS-receiving websites publish and disposable_stale (+40) an old listing; disposable_range (+30) flags an unlisted number in a 1,000-number range holding 10 or more fresh listings. The lists are public and synced daily. The default policy blocks the fictional, disposable, reported_abuse, reported_abuse_widely and premium_rate signals and a risk_score above 60, so neither traffic signal blocks on its own. | not on the pages read |
| Bulk limit | 100,000 numbers per bulk job, with an HMAC-signed completion webhook | not on the pages read |
| Batch API | 1,000 numbers per synchronous request, answered in input order | Not offered on the pages read: the API documents two endpoints, /v1/validate/{number} for one number and /v1/status for the quota. |
| Rate limit | 50 requests per second per key on the phone endpoints | 10 requests a minute on Free; the paid plans list "No Rate Limit" and the home page says "All paid plans come along with an unlimited amount of API connections"; a request over the minute limit or the monthly quota answers 429 |
| Official SDKs | Python, Node.js, PHP | PHP, Ruby, Node.js, Python |
| Data retention | No history row and no log line carrying the number. What a lookup leaves is keyed and never holds the number: the 7-day marker that makes a repeat free (a second one when the account paid for a live check); for single and browser lookups, a truncated keyed hash of the number filed under keyed hashes of its range and client address for 60 minutes, and the account's lookups per country and hour as counts for 30 days; and a live check's answer, held for a day under a hash of the number. The number itself is kept only in the account's own outcome reports, phone suppression list and monitors. Bulk input lists and result files are deleted with the job after 30 days. | not on the pages read |
## When numlookupapi fits better
A form or a script that needs validity, formats, country, carrier and line type at a small monthly volume on a fixed bill, or an AI agent calling it over MCP.
- A hosted MCP server at api.numlookupapi.com/mcp exposes validateNumber and getStatus to AI agents, metered like REST calls
- A well-formed number it does not recognise answers 200 with "valid": false rather than an error, so the valid field has to be read
- Sandbox API keys on Pro and above answer dummy data and do not count against the quota
- Successful lookup responses carry an X-Cost header with the number of requests the call counted against the quota
## When Spaw fits better
Developers who want the numbering plan and the regulators' own allocation tables read for them, the dataset behind every field named in the answer, and an SMS policy that turns the evidence into ok_to_send and blocked_by — a fraud and deliverability decision, not consent and not a do-not-call scrub. Every answer is made on the server: the opt-in live carrier-network check, the one call that would send a number to a partner, is not enabled on the service.
- numlookupapi sells a monthly quota that resets; Spaw sells pack credits that never expire and grants 100 free every month
- numlookupapi says its data comes from "multiple data providers and international carriers" without naming them; Spaw's line_type_source says whether the numbering plan or the regulator's block table decided the line type, and the sources block names each dataset an answer consulted, with its version or sync date
- numlookupapi documents one number per request; Spaw takes 1,000 numbers per batch request and 100,000 per bulk job
- Spaw returns 49 fields, including the regulator's block status, flags for fictional, virtual-number and SMS-receiving numbers, and ok_to_send from your own SMS policy, a fraud and deliverability decision rather than consent or a do-not-call scrub
## Questions
**Does Spaw charge for numbers it cannot settle?** Spaw bills per answer. A number the numbering plan and, where one is installed, the regulator's own table agree is assignable costs 1 credit whatever its flags say. An invalid number is free — including one in a block the regulator lists as unassigned — and so is a repeat of the same number within seven days. The live carrier-network check (hlr: true) is not enabled on the service: hlr_checked answers false and nothing extra is charged. numlookupapi does not publish how it bills them.
**Can I move a list of numbers from numlookupapi to Spaw?** Yes. Upload the CSV in the dashboard or POST it to the bulk endpoint, up to 100,000 numbers a job; Spaw keeps your columns, adds the answer columns, and lets you download the whole file or only the valid, invalid, low, medium or high-risk rows. A country column gives each row its own region, so a mixed international list needs no splitting.
**Is there a free tier?** Every Spaw account receives 100 free credits at the start of each month with no card on file, and unused free credits accumulate. numlookupapi offers: 100 requests a month on one API key, at 10 requests a minute.
## Sources (checked 2026-09-27)
- [numlookupapi pricing and plans](https://numlookupapi.com/pricing/)
- [numlookupapi home page (coverage)](https://numlookupapi.com/)
- [numlookupapi validate endpoint documentation](https://numlookupapi.com/docs/validate/)
- [numlookupapi rate limit and quotas](https://numlookupapi.com/docs/rate-limit/)
- [numlookupapi request status codes](https://numlookupapi.com/docs/status-codes/)
- [numlookupapi SDKs](https://numlookupapi.com/docs/sdks/)
- [numlookupapi MCP server](https://numlookupapi.com/docs/mcp/)
- [numlookupapi testing and sandbox keys](https://numlookupapi.com/docs/testing/)
numlookupapi is a trademark of its owner. Spaw is not affiliated with it.
Reference: https://spaw.co/compare/numlookupapi
---
# numlookupapi alternatives for phone intelligence
Phone intelligence APIs to consider instead of numlookupapi, with the pricing model, free tier and price per 1,000 lookups each vendor publishes. Figures come from public pricing pages checked on 2026-09-27.
| Service | Pricing model | Free tier | Price per 1,000 at about 10k |
| --- | --- | --- | --- |
| [Spaw](https://spaw.co) | Pay-as-you-go credit packs whose credits never expire, or optional plans, billed monthly or yearly, that deposit the same credits every month for less and roll unused plan credits over up to three months' allowance; both through Stripe Checkout | 100 free credits every month, no card required | $7.80 (Growth pack: 5,000 credits for $39), or $5.80 on the Growth monthly plan (5,000 credits for $29 a month) |
| [Abstract Phone Validation](https://www.abstractapi.com/api/phone-validation-api) | Monthly request quotas on three plans that differ by the data returned: Starter (validation, location, carrier) from $19 a month for 5,000 requests to $199 for 150,000; Standard (adds messaging) from $19 for 3,000 to $199 for 100,000; Professional (adds registration and risk) from $19 for 2,000 to $199 for 50,000. Annual billing saves 10%; the pricing page quotes $3.40 per 1,000 requests at Starter's entry volume, billed annually | 100 requests a month with no card, on the full response including registration and risk | not on the pages read |
| [ClearoutPhone](https://clearoutphone.io) | Credits that never expire, bought one time or through monthly or annual subscriptions that add them each period and carry them forward. One Time, each shown as a starting price: 5,000 credits from $35, 10,000 from $58, 100,000 from $400 and 1,000,000 from $1,200. Standard validation is 1 credit; Smart validation is 1 credit for most numbers and up to 5 when it runs a deeper real-time check | 100 free credits on sign-up, one validation each | $5.80 per 1,000 one time (10,000 credits for $58), $5.20 on the monthly plan (10,000 credits for $52 a month) or $3.20 on the annual plan as its tab rendered ("Buy 120,000 credits for $384 billed yearly", 10,000 a month), although the page labels annual billing "Save 20%" against one-time prices; a Smart validation that runs the deeper check costs up to 5 credits |
| [IPQualityScore Phone Validation](https://www.ipqualityscore.com/solutions/phone-validation) | Monthly plans shared with IPQualityScore's other checks: Free $0 (1,000 lookups a month), Startup "As low as: $99 / Month" (5,000), SMB Basic "As low as: $499 / Month" (10,000), SMB+ "As low as: $999 / Month" (75,000), Enterprise custom; "Each plan requires a minimum monthly commitment". The phone validation page says "Score phone numbers on average from $0.0005 per user with no monthly contracts", on "tiered flat rate pricing across all countries that reduces as your monthly volume increases", without a tier table. Add-ons are enabled through the account manager or Support at no stated price. | 1,000 lookups a month on the Free plan, capped at 35 a day; the plan also covers IP, email and URL checks | not on the pages read |
| [Loqate Phone Validation](https://www.loqate.com/en-gb/phone-validation/) | Pay-as-you-go credit purchases of £50, £100, £250, £500 and £1,000 (or 75€, 150€ and 300€), each buying a published per-lookup rate for every service, plus bespoke plans; "Credit is valid for 12 months after the purchase date". Phone Validation on the "New Pricing" table is 7.1p, 6.7p, 5.6p, 4.7p and 4.1p a lookup across the pound tiers and 8.6c, 7.8c and 6.6c across the euro ones. The page also shows an "Old Pricing" table without saying which accounts each applies to; the figures here are from "New Pricing". Its usage-example selector also lists a £2,500 plan that neither table prices, and all prices exclude VAT. | A trial rather than a standing tier: "Start your 45-day free trial"; the FAQ adds "You get free credits to test the service" | £47 per 1,000 (4.7p a lookup) on the £500 credit, which buys about 10,600 lookups; the smaller credits run from 7.1p (£50) to 5.6p (£250) |
| [numverify](https://numverify.com) | Monthly subscription tiers by request quota: Free 100 requests, Basic $14.99 for 5,000, Professional $59.99 for 50,000, Enterprise $129.99 for 250,000, each with a published overage price per call ($0.011992, $0.0047992 and $0.00207984 respectively); annual billing saves up to 15%, and custom pricing is offered above the Enterprise quota | 100 API requests a month, for non-commercial use | not on the pages read |
| [Telnyx Number Lookup](https://telnyx.com/products/number-lookup) | Per query, priced per lookup type, with "$0 platform fee"; pay as you go gets "Automatic tier discounts as usage crosses thresholds", committed plans start at $500 a month, and inbound CNAM is $0.40 per number per month | An outbound CNAM listing at no charge; the pages read list no free query allowance | $1.50 per 1,000 for a local routing number query ($0.0015 a query), $2.50 for MCC/MNC ($0.0025) and $3.00 for CNAM ($0.003) |
| [Twilio Lookup](https://www.twilio.com/en-us/user-authentication-identity/lookup) | Per request, priced per data package on a Twilio account ("Pricing current as of August 2026"); line status and reassigned number risk are tiered by query volume, and SIM swap and call forwarding are contact sales | Formatting and validation are listed as Free, and so is the SMS Pumping Risk Score for North American numbers; no free allowance is published for the other packages | $8.00 per 1,000 for line type intelligence ($0.008 per request), $10.00 for inbound caller identification ($0.01, US only), $100.00 for identity match ($0.1, which the page says varies by country) |
| [Veriphone](https://veriphone.io) | One-time credit packs that never expire, from 10,000 credits for $24 to 5,000,000 for $999, or monthly plans whose credits reset each month: Starter $12.99 for 10,000, Professional $39 for 50,000, Business $99 for 250,000 and Enterprise $249 for 1,000,000. A standard validation is 1 credit and a Current Carrier Lookup 10 | 1,000 free credits a month with no card, not carried over; on the free plan a Current Carrier Lookup draws on purchased credits only | $2.40 per 1,000 on the 10,000-credit pack ($24, credits never expire), or $1.30 on the Starter plan (10,000 credits a month for $12.99, reset each month). A Current Carrier Lookup takes 10 credits, so ten times those figures |
## Spaw
Verification API that bills per answered lookup across six signals on one key and one balance: Email verification, Phone intelligence, IP intelligence, Address verification, Business verification against open registers, and Cross-signal consistency, which runs any subset of the first four in one call and names where they agree and where they do not. The phone side reads the numbering plan and the regulators' own allocation tables to name the line type, the block holder and the blocks nobody holds, and flags the numbers that are not a person's handset.
## Abstract Phone Validation
Abstract Phone Validation is a phone API from Abstract sold as monthly request quotas on three data tiers, returning carrier, line type and location, with messaging from Standard up and registration and risk on Professional and the Free plan; the Professional example response also carries breach history.
Comparison: https://spaw.co/compare/abstract-phone
## ClearoutPhone
ClearoutPhone is a phone validation service from Clearout sold as credits that never expire, with a 1-credit Standard mode and a Smart mode that runs a real-time telecom query, at up to 5 credits, where static data is uncertain.
Comparison: https://spaw.co/compare/clearoutphone
## IPQualityScore Phone Validation
IPQualityScore Phone Validation is the phone side of a fraud-scoring service: one API returns validity, carrier, line type and a 0 to 100 fraud score, with live-line, identity, do-not-call and SMS-pumping data as add-ons, on the same monthly plans as its IP, email and URL checks.
Comparison: https://spaw.co/compare/ipqualityscore-phone
## Loqate Phone Validation
Loqate Phone Validation checks a number's format, carrier and, for mobile numbers, its live network status through an HLR lookup, priced per lookup from the same pay-as-you-go credit balance as Loqate's address, email and bank validation.
Comparison: https://spaw.co/compare/loqate-phone
## numverify
numverify is a phone number validation API sold as a monthly request quota, returning validity, country and location, carrier and line type.
Comparison: https://spaw.co/compare/numverify
## Telnyx Number Lookup
Telnyx Number Lookup is a per-query number API sold beside Telnyx's carrier services, priced separately for local routing number (portability and carrier), MCC/MNC, and CNAM caller name.
Comparison: https://spaw.co/compare/telnyx-lookup
## Twilio Lookup
Twilio Lookup is a phone number API sold as separately priced data packages on top of a Twilio account: formatting and validation are free, and line type, caller identity, line status and the risk packages are each priced on their own, per request (the SMS Pumping Risk Score is free for North American numbers) or through sales for SIM swap and call forwarding.
Comparison: https://spaw.co/compare/twilio-lookup
## Veriphone
Veriphone is a phone validation API sold as credit packs or monthly plans, returning validity, carrier, line type and region from national numbering plans, with a 10-credit Current Carrier Lookup read from national portability registries.
Comparison: https://spaw.co/compare/veriphone
numlookupapi and the other product names are trademarks of their owners. Spaw is not affiliated with them.
Reference: https://spaw.co/alternatives/numlookupapi
---
# Spaw vs numverify: phone intelligence API comparison
How Spaw and numverify compare on pricing, billing rules, where the line type comes from and limits, from numverify's public pages checked on 2026-09-27. Corrections: support@spaw.co.
| | Spaw | numverify |
| --- | --- | --- |
| Pricing model | Pay-as-you-go credit packs whose credits never expire, or optional plans, billed monthly or yearly, that deposit the same credits every month for less and roll unused plan credits over up to three months' allowance; both through Stripe Checkout | Monthly subscription tiers by request quota: Free 100 requests, Basic $14.99 for 5,000, Professional $59.99 for 50,000, Enterprise $129.99 for 250,000, each with a published overage price per call ($0.011992, $0.0047992 and $0.00207984 respectively); annual billing saves up to 15%, and custom pricing is offered above the Enterprise quota |
| Free tier | 100 free credits every month, no card required | 100 API requests a month, for non-commercial use |
| Price per 1,000 at about 10k | $7.80 (Growth pack: 5,000 credits for $39), or $5.80 on the Growth monthly plan (5,000 credits for $29 a month) | not on the pages read |
| Price per 1,000 at about 100k | $4.49 (Volume pack: 100,000 credits for $449), or $3.49 on the Volume monthly plan (100,000 credits for $349 a month) | not on the pages read |
| Numbers the vendor could not settle billed | A valid number costs 1 credit whatever its flags say. The live carrier-network check (hlr: true) is not enabled on the service: hlr_checked answers false and nothing extra is charged. | not on the pages read |
| Invalid numbers billed | No: invalid numbers are free, including a number in a block the regulator lists as unassigned, and so are repeats within 7 days | not on the pages read |
| Line type source | The numbering plan metadata everywhere except the North American plan, where mobiles and landlines share blocks and the plan can only say fixed_line_or_mobile; there the regulator's block holder settles it — a wireless carrier's block is mobile, a virtual-number wholesaler's is voip. line_type_source names which of the two answered. | Not named. The pricing FAQ says validation draws on "international numbering plans containing complex syntax rules, carrier data, line type data and location information", without saying whose plans or which carrier data. |
| Numbering plan coverage | libphonenumber metadata for every region it carries, plus the regulators' own allocation tables, each named in the answer with the date of the copy it read. Complete tables, where a geographic number in a block the table leaves out answers unassigned_block: NANPA for the United States (with Puerto Rico, the U.S. Virgin Islands, Guam, American Samoa and the Northern Mariana Islands), the CNAC for Canada, ARCEP for France (with Guadeloupe, St. Barthélemy, St. Martin, Martinique, French Guiana, Réunion, Mayotte and St. Pierre & Miquelon), ACM for the Netherlands and the CRT (formerly the IFT) for Mexico. Partial tables, answered only from the ranges they list (a range listed as free, spare or held in reserve answers unassigned_block), so block_status answers null for a number outside them: Ofcom for the United Kingdom (with Guernsey, Jersey and the Isle of Man), ACMA for Australia, the NAD for New Zealand, UKE for Poland and MIMIT for Italy. Re-synced weekly, NANPA daily; /status lists each table's age. Outside those regions, block_status and is_virtual answer null rather than a guess. | "232 countries, areas and territories" on the pricing page, with a list of two-letter ISO country codes on the home page; the pricing FAQ credits "the most up-to-date international numbering plans" without naming a plan or a regulator table. |
| Live network check | Reserved behind hlr: true: 4 credits on top of the base credit, only where the network answers, reported in its own reachable, ported, current_carrier and roaming fields. The live carrier-network check (hlr: true) is not enabled on the service: hlr_checked answers false and nothing extra is charged. | not on the pages read |
| Ported numbers | None from the tables, and the docs say so: carrier and line_type describe the block the number was allocated in, never a later port. Only a live carrier-network check can say who serves a number today. | not on the pages read |
| SMS pumping controls | Flags in the risk score, and an SMS policy that turns them into ok_to_send and blocked_by: a fraud and deliverability decision, not consent and not a do-not-call scrub. allowed_countries limits sending to the regions you serve; it is empty (any country) until you set it. range_burst (+40) flags a number once your account has looked up 5 distinct numbers in its 1,000-number range within 60 minutes, and numbers_per_client (+30) once one client address (the client_ip you send, or a browser visitor's own) has tried 5 within 60 minutes, unless Spaw's own IP data places it on a mobile carrier or a corporate proxy, or it is private or reserved. Both count only your own single and browser lookups, so a new account has no history. The range is a heuristic, and in North America a burst more likely means someone testing numbers against a form; the client count does not stop an attack rotated through residential proxies. disposable (+70) flags a number SMS-receiving websites publish and disposable_stale (+40) an old listing; disposable_range (+30) flags an unlisted number in a 1,000-number range holding 10 or more fresh listings. The lists are public and synced daily. The default policy blocks the fictional, disposable, reported_abuse, reported_abuse_widely and premium_rate signals and a risk_score above 60, so neither traffic signal blocks on its own. | not on the pages read |
| Bulk limit | 100,000 numbers per bulk job, with an HMAC-signed completion webhook | not on the pages read |
| Batch API | 1,000 numbers per synchronous request, answered in input order | No batch endpoint: the docs' endpoint summary lists /validate (or /check) and /countries |
| Rate limit | 50 requests per second per key on the phone endpoints | not on the pages read |
| Official SDKs | Python, Node.js, PHP | not on the pages read |
| Data retention | No history row and no log line carrying the number. What a lookup leaves is keyed and never holds the number: the 7-day marker that makes a repeat free (a second one when the account paid for a live check); for single and browser lookups, a truncated keyed hash of the number filed under keyed hashes of its range and client address for 60 minutes, and the account's lookups per country and hour as counts for 30 days; and a live check's answer, held for a day under a hash of the number. The number itself is kept only in the account's own outcome reports, phone suppression list and monitors. Bulk input lists and result files are deleted with the job after 30 days. | No period is given; the pricing FAQ says "We do not process or store phone numbers for any purpose other than validation" |
## When numverify fits better
A form or a script that needs validity, country, carrier and line type at a small monthly volume, on a monthly plan with per-call overage.
- Paid plans start at $14.99 a month for 5,000 requests
- Overage is priced per call rather than refusing the request
- The free plan is licensed for non-commercial use only
## When Spaw fits better
Developers who want the numbering plan and the regulators' own allocation tables read for them, the dataset behind every field named in the answer, and an SMS policy that turns the evidence into ok_to_send and blocked_by — a fraud and deliverability decision, not consent and not a do-not-call scrub. Every answer is made on the server: the opt-in live carrier-network check, the one call that would send a number to a partner, is not enabled on the service.
- numverify sells a monthly quota that resets; Spaw sells pack credits that never expire and grants 100 free every month
- numverify names its data only as international numbering plans; every Spaw answer's sources block names each dataset it consulted, with its version
- Spaw reads the regulator's allocation table where one is installed, so a number in a block the table shows nobody holds answers invalid and free rather than valid
- numverify's docs list two endpoints, /validate and /countries, and no batch or bulk endpoint; Spaw takes 1,000 numbers per batch and 100,000 per bulk job
- Spaw ends every answer with ok_to_send and blocked_by from the account's SMS policy, a fraud and deliverability decision rather than consent or a do-not-call scrub, and flags numbers SMS-receiving websites publish, from public lists synced daily
## Questions
**Does Spaw charge for numbers it cannot settle?** Spaw bills per answer. A number the numbering plan and, where one is installed, the regulator's own table agree is assignable costs 1 credit whatever its flags say. An invalid number is free — including one in a block the regulator lists as unassigned — and so is a repeat of the same number within seven days. The live carrier-network check (hlr: true) is not enabled on the service: hlr_checked answers false and nothing extra is charged. numverify does not publish how it bills them.
**Can I move a list of numbers from numverify to Spaw?** Yes. Upload the CSV in the dashboard or POST it to the bulk endpoint, up to 100,000 numbers a job; Spaw keeps your columns, adds the answer columns, and lets you download the whole file or only the valid, invalid, low, medium or high-risk rows. A country column gives each row its own region, so a mixed international list needs no splitting.
**Is there a free tier?** Every Spaw account receives 100 free credits at the start of each month with no card on file, and unused free credits accumulate. numverify offers: 100 API requests a month, for non-commercial use.
## Sources (checked 2026-09-27)
- [numverify pricing and FAQ](https://numverify.com/pricing)
- [numverify home page](https://numverify.com)
- [numverify API documentation](https://docs.apilayer.com/numverify/docs/api-documentation)
numverify is a trademark of its owner. Spaw is not affiliated with it.
Reference: https://spaw.co/compare/numverify
---
# numverify alternatives for phone intelligence
Phone intelligence APIs to consider instead of numverify, with the pricing model, free tier and price per 1,000 lookups each vendor publishes. Figures come from public pricing pages checked on 2026-09-27.
| Service | Pricing model | Free tier | Price per 1,000 at about 10k |
| --- | --- | --- | --- |
| [Spaw](https://spaw.co) | Pay-as-you-go credit packs whose credits never expire, or optional plans, billed monthly or yearly, that deposit the same credits every month for less and roll unused plan credits over up to three months' allowance; both through Stripe Checkout | 100 free credits every month, no card required | $7.80 (Growth pack: 5,000 credits for $39), or $5.80 on the Growth monthly plan (5,000 credits for $29 a month) |
| [Abstract Phone Validation](https://www.abstractapi.com/api/phone-validation-api) | Monthly request quotas on three plans that differ by the data returned: Starter (validation, location, carrier) from $19 a month for 5,000 requests to $199 for 150,000; Standard (adds messaging) from $19 for 3,000 to $199 for 100,000; Professional (adds registration and risk) from $19 for 2,000 to $199 for 50,000. Annual billing saves 10%; the pricing page quotes $3.40 per 1,000 requests at Starter's entry volume, billed annually | 100 requests a month with no card, on the full response including registration and risk | not on the pages read |
| [ClearoutPhone](https://clearoutphone.io) | Credits that never expire, bought one time or through monthly or annual subscriptions that add them each period and carry them forward. One Time, each shown as a starting price: 5,000 credits from $35, 10,000 from $58, 100,000 from $400 and 1,000,000 from $1,200. Standard validation is 1 credit; Smart validation is 1 credit for most numbers and up to 5 when it runs a deeper real-time check | 100 free credits on sign-up, one validation each | $5.80 per 1,000 one time (10,000 credits for $58), $5.20 on the monthly plan (10,000 credits for $52 a month) or $3.20 on the annual plan as its tab rendered ("Buy 120,000 credits for $384 billed yearly", 10,000 a month), although the page labels annual billing "Save 20%" against one-time prices; a Smart validation that runs the deeper check costs up to 5 credits |
| [IPQualityScore Phone Validation](https://www.ipqualityscore.com/solutions/phone-validation) | Monthly plans shared with IPQualityScore's other checks: Free $0 (1,000 lookups a month), Startup "As low as: $99 / Month" (5,000), SMB Basic "As low as: $499 / Month" (10,000), SMB+ "As low as: $999 / Month" (75,000), Enterprise custom; "Each plan requires a minimum monthly commitment". The phone validation page says "Score phone numbers on average from $0.0005 per user with no monthly contracts", on "tiered flat rate pricing across all countries that reduces as your monthly volume increases", without a tier table. Add-ons are enabled through the account manager or Support at no stated price. | 1,000 lookups a month on the Free plan, capped at 35 a day; the plan also covers IP, email and URL checks | not on the pages read |
| [Loqate Phone Validation](https://www.loqate.com/en-gb/phone-validation/) | Pay-as-you-go credit purchases of £50, £100, £250, £500 and £1,000 (or 75€, 150€ and 300€), each buying a published per-lookup rate for every service, plus bespoke plans; "Credit is valid for 12 months after the purchase date". Phone Validation on the "New Pricing" table is 7.1p, 6.7p, 5.6p, 4.7p and 4.1p a lookup across the pound tiers and 8.6c, 7.8c and 6.6c across the euro ones. The page also shows an "Old Pricing" table without saying which accounts each applies to; the figures here are from "New Pricing". Its usage-example selector also lists a £2,500 plan that neither table prices, and all prices exclude VAT. | A trial rather than a standing tier: "Start your 45-day free trial"; the FAQ adds "You get free credits to test the service" | £47 per 1,000 (4.7p a lookup) on the £500 credit, which buys about 10,600 lookups; the smaller credits run from 7.1p (£50) to 5.6p (£250) |
| [numlookupapi](https://numlookupapi.com) | Monthly subscription tiers by request quota: Free $0 for 100 requests, Basic $9.99 for 5,000, Pro $49.99 for 50,000, Scale $109.99 for 250,000, and Custom by contact; the page offers yearly billing at 20% off. Scale and Custom list "Overages" with no overage price shown, and the quota documentation says a request over the monthly quota answers 429 until the plan is upgraded or the month ends. | 100 requests a month on one API key, at 10 requests a minute | not on the pages read |
| [Telnyx Number Lookup](https://telnyx.com/products/number-lookup) | Per query, priced per lookup type, with "$0 platform fee"; pay as you go gets "Automatic tier discounts as usage crosses thresholds", committed plans start at $500 a month, and inbound CNAM is $0.40 per number per month | An outbound CNAM listing at no charge; the pages read list no free query allowance | $1.50 per 1,000 for a local routing number query ($0.0015 a query), $2.50 for MCC/MNC ($0.0025) and $3.00 for CNAM ($0.003) |
| [Twilio Lookup](https://www.twilio.com/en-us/user-authentication-identity/lookup) | Per request, priced per data package on a Twilio account ("Pricing current as of August 2026"); line status and reassigned number risk are tiered by query volume, and SIM swap and call forwarding are contact sales | Formatting and validation are listed as Free, and so is the SMS Pumping Risk Score for North American numbers; no free allowance is published for the other packages | $8.00 per 1,000 for line type intelligence ($0.008 per request), $10.00 for inbound caller identification ($0.01, US only), $100.00 for identity match ($0.1, which the page says varies by country) |
| [Veriphone](https://veriphone.io) | One-time credit packs that never expire, from 10,000 credits for $24 to 5,000,000 for $999, or monthly plans whose credits reset each month: Starter $12.99 for 10,000, Professional $39 for 50,000, Business $99 for 250,000 and Enterprise $249 for 1,000,000. A standard validation is 1 credit and a Current Carrier Lookup 10 | 1,000 free credits a month with no card, not carried over; on the free plan a Current Carrier Lookup draws on purchased credits only | $2.40 per 1,000 on the 10,000-credit pack ($24, credits never expire), or $1.30 on the Starter plan (10,000 credits a month for $12.99, reset each month). A Current Carrier Lookup takes 10 credits, so ten times those figures |
## Spaw
Verification API that bills per answered lookup across six signals on one key and one balance: Email verification, Phone intelligence, IP intelligence, Address verification, Business verification against open registers, and Cross-signal consistency, which runs any subset of the first four in one call and names where they agree and where they do not. The phone side reads the numbering plan and the regulators' own allocation tables to name the line type, the block holder and the blocks nobody holds, and flags the numbers that are not a person's handset.
## Abstract Phone Validation
Abstract Phone Validation is a phone API from Abstract sold as monthly request quotas on three data tiers, returning carrier, line type and location, with messaging from Standard up and registration and risk on Professional and the Free plan; the Professional example response also carries breach history.
Comparison: https://spaw.co/compare/abstract-phone
## ClearoutPhone
ClearoutPhone is a phone validation service from Clearout sold as credits that never expire, with a 1-credit Standard mode and a Smart mode that runs a real-time telecom query, at up to 5 credits, where static data is uncertain.
Comparison: https://spaw.co/compare/clearoutphone
## IPQualityScore Phone Validation
IPQualityScore Phone Validation is the phone side of a fraud-scoring service: one API returns validity, carrier, line type and a 0 to 100 fraud score, with live-line, identity, do-not-call and SMS-pumping data as add-ons, on the same monthly plans as its IP, email and URL checks.
Comparison: https://spaw.co/compare/ipqualityscore-phone
## Loqate Phone Validation
Loqate Phone Validation checks a number's format, carrier and, for mobile numbers, its live network status through an HLR lookup, priced per lookup from the same pay-as-you-go credit balance as Loqate's address, email and bank validation.
Comparison: https://spaw.co/compare/loqate-phone
## numlookupapi
numlookupapi is a phone number validation API sold as a monthly request quota: one validate endpoint returns validity, formats, country, location, carrier and line type.
Comparison: https://spaw.co/compare/numlookupapi
## Telnyx Number Lookup
Telnyx Number Lookup is a per-query number API sold beside Telnyx's carrier services, priced separately for local routing number (portability and carrier), MCC/MNC, and CNAM caller name.
Comparison: https://spaw.co/compare/telnyx-lookup
## Twilio Lookup
Twilio Lookup is a phone number API sold as separately priced data packages on top of a Twilio account: formatting and validation are free, and line type, caller identity, line status and the risk packages are each priced on their own, per request (the SMS Pumping Risk Score is free for North American numbers) or through sales for SIM swap and call forwarding.
Comparison: https://spaw.co/compare/twilio-lookup
## Veriphone
Veriphone is a phone validation API sold as credit packs or monthly plans, returning validity, carrier, line type and region from national numbering plans, with a 10-credit Current Carrier Lookup read from national portability registries.
Comparison: https://spaw.co/compare/veriphone
numverify and the other product names are trademarks of their owners. Spaw is not affiliated with them.
Reference: https://spaw.co/alternatives/numverify
---
# Spaw vs OpenCorporates: business verification API comparison
How Spaw and OpenCorporates compare on pricing, billing rules, the registers behind each answer and limits, from OpenCorporates' public pages checked on 2026-09-10. Corrections: support@spaw.co.
| | Spaw | OpenCorporates |
| --- | --- | --- |
| Pricing model | Pay-as-you-go credit packs whose credits never expire, or optional plans, billed monthly or yearly, that deposit the same credits every month for less and roll unused plan credits over up to three months' allowance; both through Stripe Checkout | Annual or monthly API plans capped by call count rather than sold by volume: Essentials "£ 2,250/year" or "£225 per month" ("Up to 500 API calls/mth", "Up to 200 API calls/day"), Starter "£ 6,600/year" (2,500 a month, 500 a day), Basic "£ 12,000/year" (5,000 a month, 1,000 a day), Enterprise by consultation with bulk-data delivery |
| Free tier | 100 free credits every month, no card required | Free at-scale access for investigative journalists, NGOs, universities and anti-crime-and-corruption research groups rather than a free plan, in the site's own words |
| Price per 1,000 at about 10k | $7.80 (Growth pack: 5,000 credits for $39), or $5.80 on the Growth monthly plan (5,000 credits for $29 a month) | not on the pages read |
| Price per 1,000 at about 100k | $4.49 (Volume pack: 100,000 credits for $449), or $3.49 on the Volume monthly plan (100,000 credits for $349 a month) | not on the pages read |
| Identifiers no register carried billed | No: an identifier no installed register carries answers unknown_identifier or unsupported_register and costs nothing | not on the pages read |
| Malformed identifiers billed | No: a malformed identifier and a register that has not synced are free too, as are repeats within 7 days | not on the pages read |
| Registers held | Two, synced as bulk files onto the server so no third party sits in the request path: the GLEIF Level 1 golden copy of Legal Entity Identifiers, refreshed daily by its publisher, and the Companies House free company data product for the United Kingdom, republished monthly. Every answer carries the date of the copy it read. | Company registers, aggregated. The pricing page does not list which ones; choosing jurisdictions is an Enterprise plan feature. |
| Jurisdiction coverage | A LEI is global by construction; company numbers are GB only today, because a company number is unique only inside its own register. Anything else answers unsupported_register, free. | Not published on the pricing page checked. |
| Cross-register comparison | Yes: where a LEI record names a Companies House number and both files are installed, the legal name and the status are compared and both sides are quoted in agreements, with flags for a disagreement or for a company the national register records as dissolved. | not on the pages read |
| Registered address returned | No, deliberately: a registered address is very often somebody's home. The response carries country and jurisdiction, which are enough to answer whether the entity is real and live. | not on the pages read |
| Bulk limit | None: there is no bulk upload for this product, because nobody types a company number into a signup form | Bulk-data delivery is listed as an Enterprise plan feature |
| Batch API | 500 identifiers per synchronous request | not on the pages read |
| Rate limit | 50 requests per second per key on the business endpoints | Daily and monthly call caps rather than a per-second limit: 200 calls a day on Essentials, 500 on Starter, 1,000 on Basic |
| Official SDKs | Python, Node.js, PHP | not on the pages read |
| Data retention | Nothing beyond the credit ledger entry that bills the lookup; a monitored list stores the identifiers you saved and each one's status from its last run, and nothing else. | not on the pages read |
## When OpenCorporates fits better
Investigators and data teams that need company records across many jurisdictions, and can work within a plan capped at a few thousand calls a month.
- Far more jurisdictions than a two-register lookup can reach, and the enterprise plan lets you choose them
- Free at-scale access for journalists, NGOs, universities and anti-corruption researchers
- Bulk-data delivery for buyers who would rather hold the data than call an API
- The plans are priced by access rather than by lookup, so the cost does not move with volume
## When Spaw fits better
Developers who want to know an identifier belongs to a real, live entity, and want the LEI record and the national register checked against each other rather than trusted separately.
- OpenCorporates covers many more jurisdictions; Spaw holds two registers — the GLEIF LEI golden copy, which is global, and Companies House for the United Kingdom — and answers unsupported_register, free, for anything else
- OpenCorporates sells access capped per day and per month; Spaw bills 1 credit only when a register answers, and nothing when it does not
- Where a LEI record names a Companies House number, Spaw compares the two records and quotes both sides, so a disagreement is something you can read
- Spaw returns no registered address, deliberately: a registered address is very often somebody's home
## Questions
**Does Spaw charge for identifiers it cannot find?** No. A lookup costs 1 credit only when a register answers. A malformed identifier, one no register carries, an identifier from a register this server does not hold, and a register that has not synced are all free, because none of them told you anything you could not have worked out. A repeat of the same identifier within seven days is free too. OpenCorporates does not publish how it bills them.
**Can I check a list of identifiers?** In batches, and on a schedule. There is deliberately no bulk upload for this product: POST /v1/entity/batch takes 500 identifiers a call under the single-lookup billing rules, and a monitored list keeps up to 500 identifiers under a weekly or monthly re-check that emails you when one leaves a register, is recorded as dissolved, or stops agreeing with the other register.
**Is there a free tier?** Every Spaw account receives 100 free credits at the start of each month with no card on file, and unused free credits accumulate. OpenCorporates offers: Free at-scale access for investigative journalists, NGOs, universities and anti-crime-and-corruption research groups rather than a free plan, in the site's own words.
## Sources (checked 2026-09-10)
- [OpenCorporates pricing](https://opencorporates.com/pricing/)
OpenCorporates is a trademark of its owner. Spaw is not affiliated with it.
Reference: https://spaw.co/compare/opencorporates
---
# OpenCorporates alternatives for business verification
Business verification APIs to consider instead of OpenCorporates, with the pricing model, free tier and price per 1,000 lookups each vendor publishes. Figures come from public pricing pages checked on 2026-09-10.
| Service | Pricing model | Free tier | Price per 1,000 at about 10k |
| --- | --- | --- | --- |
| [Spaw](https://spaw.co) | Pay-as-you-go credit packs whose credits never expire, or optional plans, billed monthly or yearly, that deposit the same credits every month for less and roll unused plan credits over up to three months' allowance; both through Stripe Checkout | 100 free credits every month, no card required | $7.80 (Growth pack: 5,000 credits for $39), or $5.80 on the Growth monthly plan (5,000 credits for $29 a month) |
| [Companies House API](https://developer.company-information.service.gov.uk) | No price is published on the developer pages checked; access is through a registered application and its key. | not on the pages read | not on the pages read |
## Spaw
Verification API that bills per answered lookup across six signals on one key and one balance: Email verification, Phone intelligence, IP intelligence, Address verification, Business verification against open registers, and Cross-signal consistency, which runs any subset of the first four in one call and names where they agree and where they do not. The business side reads a Legal Entity Identifier or a UK company number against register files synced onto the server, and compares the two records where a LEI names a UK company.
## Companies House API
The Companies House Public Data API is the UK registrar's own service: "The Companies House API lets you retrieve information about limited companies (and other companies that fall within the Companies Act 2006). The data returned is live and real-time, and is simple to use and understand."
Comparison: https://spaw.co/compare/companies-house-api
OpenCorporates and the other product names are trademarks of their owners. Spaw is not affiliated with them.
Reference: https://spaw.co/alternatives/opencorporates
---
# Spaw vs Postcode.eu: address verification API comparison
How Spaw and Postcode.eu compare on pricing, billing rules, the registers behind each answer and limits, from Postcode.eu's public pages checked on 2026-09-22. Corrections: support@spaw.co.
| | Spaw | Postcode.eu |
| --- | --- | --- |
| Pricing model | Pay-as-you-go credit packs whose credits never expire, or optional plans, billed monthly or yearly, that deposit the same credits every month for less and roll unused plan credits over up to three months' allowance; both through Stripe Checkout | Annual tiers that include validations: Tier 1 €50, Tier 2 €100, Tier 3 €250, Tier 4 €500 and Tier 5 €1,000 a year, each with a per-validation rate by country group. "Up to the Tier amount you can perform a mix of validations", subscriptions "are automatically upgraded when the maximum of the Tier is reached", and past the highest Tier usage "will be invoiced in blocks of 10% and the rates of this Tier apply". A validation is "a bundle of related requests that were used to validate an address". An SLA (€1,000 + 2% a year) and a DORA addendum (€2,500 a year) are extra. |
| Free tier | 100 free credits every month, no card required | A trial rather than a standing tier: "After registering an account it is possible to try the APIs for free", and "Testing the API is without obligations" |
| Price per 1,000 at about 10k | $7.80 (Growth pack: 5,000 credits for $39), or $5.80 on the Growth monthly plan (5,000 credits for $29 a month) | €17.64 per 1,000 validations on Tier 3 (€250 a year, "€0.01764 each") for Austria, Belgium, Denmark, Finland, France, Luxembourg, Norway, Spain, Sweden, Switzerland and the Netherlands. Germany, Italy and the United Kingdom cost more a validation, so 10,000 of them fall in Tier 4 (€500 a year): €34.17 per 1,000 for Germany (€0.03417 each) and €43.30 for Italy and the United Kingdom (€0.0433 each). "Tier prices are including validations" and "Prices and quantities are per year", so the tier amount is paid in full: 10,000 validations cost €250 a year for the eleven countries (€25 per 1,000) and €500 for Germany, Italy or the United Kingdom (€50 per 1,000). |
| Price per 1,000 at about 100k | $4.49 (Volume pack: 100,000 credits for $449), or $3.49 on the Volume monthly plan (100,000 credits for $349 a month) | Tier 5 rates (€1,000 a year), which also apply to usage past it: €11.76 per 1,000 ("€0.01176 each") for the same eleven countries, €31.29 for Germany (€0.03129 each) and €39.64 for Italy and the United Kingdom (€0.03964 each). |
| Addresses no register covered billed | An address that stands as written costs 1 credit whether or not a register carried the building; where no register covers the country, exists answers null rather than a verdict | not on the pages read |
| Addresses that cannot stand billed | No: an address that cannot stand as written is free whatever the reason, and so are repeats within 7 days and rows repeating an address already in the same job | not on the pages read |
| Registers held | Open address registers for 23 countries and territories, each named in the answer with its declared coverage, complete or partial. Complete, building by building: Australia, Austria, Belgium, Czechia, Latvia, Luxembourg, Netherlands, Norway and Switzerland. Partial, building by building: Brazil, Denmark, France (with French Guiana, Guadeloupe, Martinique, Mayotte, Réunion, St. Barthélemy, St. Martin and St. Pierre & Miquelon), Germany and United States. Partial, streets only: Great Britain. House-number ranges as well: United States. Absence from a partial register is never read as evidence that an address does not exist. | Named per country, "only official sources": the Dutch government's Basisregistraties Adressen en Gebouwen (BAG); for Belgium the Flemish CRAB, the Brussels UrbIS and the Walloon ICAR; Luxembourg's Administration du cadastre et de la topographie; Austria's federal Adressregister; the Swiss Gebäude- und Wohnungsregister; France's Base Adresse Nationale; Royal Mail's Postcode Address File for the United Kingdom; Spain's Catastro in CartoCiudad; Denmark's Adresseregister (DAR); GeoNorge for Norway; DVV and Posti for Finland; Lantmäteriet for Sweden; Poste Italiane for Italy; and Deutsche Post, with OpenStreetMap for locations, for Germany. |
| Country coverage | Postal formats for 207 countries and territories: the line layout for 199 of them, and the required fields and postcode pattern wherever the table records them; 52 postal operators' rules for naming a delivery point that is not a street; open address registers for 23 countries and territories; postcode directories for Great Britain from the ONS Postcode Directory, with Northern Ireland excluded because reusing BT postcodes needs a separate licence, and for 108 more countries and territories from GeoNames' postal codes. | Austria, Belgium, Denmark, Finland, France, Germany, Italy, Luxembourg, Norway, Spain, Sweden, Switzerland, the Netherlands and the United Kingdom; "We are constantly working to further expand the number of countries." |
| Claims post is delivered | No, and every answer says so: is_deliverable, is_cmra, is_vacant and is_residential answer null because that check is licensed and not enabled here. exists is a match in an open address register, never a statement that post arrives. | Not claimed. The Validate API grades each match from A ("A perfect match") to F and reports the element it validated to (Building, BuildingPartial, Street, Locality or None), with a flag when several matches are equally close; the pages read say nothing about whether post is delivered. |
| Bulk limit | 100,000 addresses per bulk job, with an HMAC-signed completion webhook | No limit is stated; the Validate API page offers file validation as a service ("let us validate a file!"). |
| Batch API | 100 addresses per synchronous request | not on the pages read |
| Rate limit | 50 requests per second per key on the address endpoints | The prices page lists "Speed 35 requests per second" for the Autocomplete and Validate APIs on every tier and 150 for the Dutch Postcode API; the developer FAQ's defaults are 35 requests per second for Autocomplete, 15 for Validate and 150 for the Dutch Postcode API, higher on request. |
| Official SDKs | Python, Node.js, PHP | PHP, Python |
| Data retention | No history row and no log line carrying the address. What a lookup leaves is the 7-day marker that makes a repeat free and the count behind seen_by_you, both kept under a digest, never the address. The address itself is kept only on the account's own suppression list and in its monitors. Bulk input lists and result files are deleted with the job after 30 days. | not on the pages read |
## When Postcode.eu fits better
European checkouts and CRMs that want autocomplete and validation answered from each country's official address source under one annual subscription.
- Each country is answered from the official sources the page names, most of them government address registers
- One subscription covers every country, and the tier upgrades itself as usage grows
- A separate Dutch Postcode API for postcode and house-number lookups at €0.005 to €0.00155 each
- Free Magento and WooCommerce plugins and PHP and Python modules from Postcode.nl
- Validation matches are graded A to F with the level validated to and an ambiguity flag
## When Spaw fits better
Developers who want a written address parsed against its own country's rules and checked against open registers, where a field nobody could evaluate answers null instead of a guess.
- Postcode.eu names the official sources behind each country on its pages; every Spaw answer names the register that answered it and whether that register's coverage is complete or partial, and absence from a partial register answers null rather than a verdict
- Postcode.eu prices a validation by country group inside an annual tier; a Spaw credit costs the same for every country and a pack credit never expires, and an address that cannot stand as written costs nothing
- Postcode.eu's United Kingdom answers come from the Royal Mail Postcode Address File; Spaw holds no licensed British file, so an address in Great Britain is checked against the postcode directory and the streets Ordnance Survey lists, and a listed street never means a building exists
- Spaw reads an address against its own country's postal format and adds the kind of delivery point it names, the postcode's status in the national directory and how many companies are registered at the address
## Questions
**Does Spaw charge for addresses no register covers?** An address that stands as written costs 1 credit, whether or not a register carried the building. An address that cannot stand costs nothing, whatever the reason, and a repeat within seven days is free. Where no register covers the country, or where the register that answered is one whose coverage is partial and does not carry the address, exists answers null — not evaluated — rather than a verdict, and the not_in_register weight cannot fire. Postcode.eu does not publish how it bills them.
**Can I move an address list from Postcode.eu to Spaw?** Yes. Upload the CSV in the dashboard or POST it to the bulk endpoint, up to 100,000 addresses a job; Spaw keeps your columns and adds 64 result columns after them (the address as submitted, the answer, the datasets behind it, what the row cost and whether your suppression list answered it), and a country column gives each row its own country. Rows repeating an address already in the list are answered once, cost nothing, and are counted as duplicates.
**Is there a free tier?** Every Spaw account receives 100 free credits at the start of each month with no card on file, and unused free credits accumulate. Postcode.eu offers: A trial rather than a standing tier: "After registering an account it is possible to try the APIs for free", and "Testing the API is without obligations".
## Sources (checked 2026-09-22)
- [Postcode.eu API prices](https://www.postcode.eu/products/address-api/prices)
- [Postcode.eu Address Validate API](https://www.postcode.eu/products/validate-api)
- [Postcode.eu implementation and modules](https://www.postcode.eu/products/address-api/implementation)
- [Postcode.eu developer FAQ](https://developer.postcode.eu/documentation/faq)
- [Postcode.eu Validate API documentation](https://developer.postcode.eu/documentation/international/v1/Validate/validate)
Postcode.eu is a trademark of its owner. Spaw is not affiliated with it.
Reference: https://spaw.co/compare/postcode-eu
---
# Postcode.eu alternatives for address verification
Address verification APIs to consider instead of Postcode.eu, with the pricing model, free tier and price per 1,000 lookups each vendor publishes. Figures come from public pricing pages checked on 2026-09-22.
| Service | Pricing model | Free tier | Price per 1,000 at about 10k |
| --- | --- | --- | --- |
| [Spaw](https://spaw.co) | Pay-as-you-go credit packs whose credits never expire, or optional plans, billed monthly or yearly, that deposit the same credits every month for less and roll unused plan credits over up to three months' allowance; both through Stripe Checkout | 100 free credits every month, no card required | $7.80 (Growth pack: 5,000 credits for $39), or $5.80 on the Growth monthly plan (5,000 credits for $29 a month) |
| [Geoapify](https://www.geoapify.com) | Daily credit quotas on monthly subscriptions: Free (3,000 credits a day), API 10 (10,000 a day, $59 billed monthly), API 25 (25,000, $109), API 50 (50,000, $179), API 100 (100,000, $299), API 250 (250,000, $609) and Custom ("Unmetered credits / day", from $860). "1 Geocoding API request costs 1 credit"; in a batch each address counts 0.5 credits, plus 1 credit to create the job and 1 for each result request. A yearly term is offered, but its prices are not in the page text read. | A standing free plan: "3,000 credits / day", "No credit card required", up to 5 requests a second, usable for "commercial websites and apps" with the required Geoapify and data-source attribution | not on the pages read |
| [Google Address Validation API](https://mapsplatform.google.com) | "Pay-as-you-go", "charging per request according to the pricing table", under two SKUs named on the billing page, Address Validation Pro and Address Validation Enterprise; the per-request rates render inside the pricing table and are not stated in the page text as fetched, so no figure is quoted here. | not on the pages read | not on the pages read |
| [Ideal Postcodes](https://ideal-postcodes.co.uk) | Pay-as-you-go credit packs, "No Subscriptions. No Monthly Fees.": 200 credits for £9 ("£0.045 per lookup"), 1,100 for £42 (£0.038), 4,300 for £155 (£0.036), 12,800 for £420 (£0.033) and 32,100 for £900 (£0.028). "Credits are valid for 12 months"; the lookup guide adds that the timer "only starts when you begin using that batch of credits". Larger volumes and UK registered charities are priced on request. | A trial rather than a standing tier: "1-Month Trial", "50 free credits, full support and no credit card needed"; the trial searches worldwide except Ireland, "which requires separate licensing" | £33 per 1,000 lookups on the 12,800-credit pack (£420, "£0.033 per lookup"), the smallest pack that covers 10,000. |
| [Lob](https://www.lob.com) | Verifications are priced per call on top of a print-and-mail plan: on the Developer plan ($0 a month) "US Address Verifications $0.05/additional" and international $0.07; on Startup ($260 a month) $0.025 with "1,000 included for $25/mo" and international $0.06 with 500 included for $30 a month; on Growth ($550 a month) $0.009 with "50,000 included for $450/mo" and international $0.055 with 10,000 included for $550 a month; Enterprise is "Contact us for details". | The Developer plan is "Starting at $0 / month" with verifications billed per call; the site offers "Get started for free". | $50 per 1,000 US verifications on the Developer plan ($0.05 each) or $25 per 1,000 on Startup ($0.025 each beyond the 1,000 included for $25 a month); international $70 or $60. |
| [Loqate](https://www.loqate.com) | Pay-as-you-go credit purchases of £50, £100, £250, £500 and £1,000 (or 75€, 150€ and 300€), each buying a published per-lookup rate for every service, plus bespoke plans; "Credit is valid for 12 months after the purchase date" and there are no setup costs. The pricing page shows two rate tables headed "New Pricing" and "Old Pricing" without saying which accounts each applies to; the figures here are from "New Pricing". Its usage-example selector also lists a £2,500 plan that neither table prices. | A trial rather than a standing tier: "Start your 45-day free trial"; the FAQ adds "You get free credits to test the service" | £82 to £86 per 1,000 Address Verify lookups on the "New Pricing" table: 8.2p a lookup on the £100, £250 and £500 tiers and 8.6p on the £50 and £1,000 tiers. Address Capture is 8.6p to 4.9p in the UK and 7.1p to 4.1p for the rest of the world. |
| [Melissa](https://www.melissa.com) | US Address Verification pay-as-you-go "Starting at $40" for 10,000 credits at "10 Credits Per Address" (the same page's FAQ says a US address equals 3 credits and a global address 10); an API subscription "Starting at $5,145" for 1,000,000 records a year; an unlimited-records plan "Starting at $16,000". Global Address Verification subscriptions from $40 a month for 1,000 records and $95 a month for 2,500. | Global Address Verification "GAV Free" at $0 for 250 records a month; 1,000 lookup credits a month after a one-time $4.97 KYC/AML verification fee, described on the pricing page. | $40 per 1,000 US addresses on the smallest pay-as-you-go pack, taking the pack's own "10 Credits Per Address" ($40 buys 10,000 credits). |
| [Postcoder](https://postcoder.com) | Credit packs (500 to 200,000 credits, "valid for 12 months from purchase") or monthly plans (1,000 to 100,000 credits, with unused credits not carried over). "Your credits can be used for any Postcoder request." The pack prices themselves render as placeholders ("£--") in the pack list; the credit calculator's recommendation box shows "Credit pack £25 500 Credits" and "Monthly plan £25 1,000 Credits". Larger packs and plans with volume discounts are quoted. | A free trial is offered; its duration and allowance are not stated on the pricing page | £100 per 1,000 UK address lookups pay-as-you-go (10.00p a request, 2.0 credits) or £45 per 1,000 on a monthly plan (4.50p). This is the address comparison, so only the address rate is quoted: the same account buys email and mobile validation at their own per-request rates, which are read off the same page. |
| [PostGrid](https://www.postgrid.com) | Quoted: the pricing page asks for annual mail-piece and address-verification volumes in bands from "Less than 50,000" to "More than 1,000,000" and publishes no rate. | "Try for Free. Sign Up for Address Verification API" on the product page; no standing free volume is stated. | not on the pages read |
| [Smarty](https://www.smarty.com) | Annual lookup bundles on the Professional plan from $552 a year, sized 12K, 60K, 120K, 300K, 600K, 1M or 2M lookups; a monthly option and bespoke Custom pricing are offered. Only the $552 entry price is shown on the page. A subscription "expires when the address lookups or the time runs out, whichever comes first"; there are no overage costs, and an exhausted plan answers 402 "payment required" unless auto-renewal buys a new block. | A free trial rather than a standing tier: "42 days" and "1,000 address lookups", "No credit card required" | not on the pages read |
## Spaw
Verification API that bills per answered lookup across six signals on one key and one balance: Email verification, Phone intelligence, IP intelligence, Address verification, Business verification against open registers, and Cross-signal consistency, which runs any subset of the first four in one call and names where they agree and where they do not. The address side reads a written address against its own country's format, names the kind of delivery point behind it, checks the postcode against the national directory and follows the building into that country's own address register where one is installed.
## Geoapify
Geoapify is a location platform (maps, routing, places and geocoding) whose Geocoding API is also sold for address validation: an address is matched against open datasets such as OpenStreetMap and OpenAddresses and returned with coordinates and confidence scores for the city, the street and the building.
Comparison: https://spaw.co/compare/geoapify
## Google Address Validation API
Google's Address Validation API, part of Google Maps Platform, validates, standardises and geocodes an address, reports a confirmation level per component with inferred and replaced parts, and can run US and Puerto Rico addresses through USPS CASS on request.
Comparison: https://spaw.co/compare/google-address-validation
## Ideal Postcodes
Ideal Postcodes is a UK-first address lookup, autocomplete and cleansing API answered from Royal Mail's Postcode Address File, with Ordnance Survey AddressBase Core and Premium offered as additional datasets, sold as pay-as-you-go credit packs, with international address data for more than 240 countries and territories; its site also lists phone and email validation among its products.
Comparison: https://spaw.co/compare/ideal-postcodes
## Lob
Lob is a print-and-mail platform whose Address Verification API validates, standardises and enriches US and international addresses, CASS certified, with USPS delivery-point data and a deliverability likelihood drawn from Lob's own mail delivery records.
Comparison: https://spaw.co/compare/lob
## Loqate
Loqate sells address capture and address verification alongside email, phone and bank validation, priced per lookup from a pay-as-you-go credit balance, with international data for over 245 countries and territories.
Comparison: https://spaw.co/compare/loqate
## Melissa
Melissa sells address verification for the United States and "250+ Countries & Territories" against postal data, CASS and DPV certified, with a credit model for pay-as-you-go use and annual subscriptions sized by records, alongside name, phone, email and identity verification.
Comparison: https://spaw.co/compare/melissa
## Postcoder
Postcoder is a UK-first address lookup API sold as credits that any of its services can spend: address lookup, bank validation, email validation, mobile validation and OTP verification, each costing a published number of credits per request.
Comparison: https://spaw.co/compare/postcoder
## PostGrid
PostGrid sells an address verification API for the United States and Canada, CASS and SERP certified against USPS and Canada Post data, with an international service for "245+ Countries and Territories", autocomplete, parsing, batch verification and a Salesforce integration, priced by quotation.
Comparison: https://spaw.co/compare/postgrid
## Smarty
Smarty sells US address verification against postal data: a lookup validates to the secondary level and returns delivery-point metadata that open registers cannot answer, including a DPV match code, the residential indicator and vacancy status.
Comparison: https://spaw.co/compare/smarty
Postcode.eu and the other product names are trademarks of their owners. Spaw is not affiliated with them.
Reference: https://spaw.co/alternatives/postcode-eu
---
# Spaw vs Postcoder: address verification API comparison
How Spaw and Postcoder compare on pricing, billing rules, the registers behind each answer and limits, from Postcoder's public pages checked on 2026-09-22. Corrections: support@spaw.co.
| | Spaw | Postcoder |
| --- | --- | --- |
| Pricing model | Pay-as-you-go credit packs whose credits never expire, or optional plans, billed monthly or yearly, that deposit the same credits every month for less and roll unused plan credits over up to three months' allowance; both through Stripe Checkout | Credit packs (500 to 200,000 credits, "valid for 12 months from purchase") or monthly plans (1,000 to 100,000 credits, with unused credits not carried over). "Your credits can be used for any Postcoder request." The pack prices themselves render as placeholders ("£--") in the pack list; the credit calculator's recommendation box shows "Credit pack £25 500 Credits" and "Monthly plan £25 1,000 Credits". Larger packs and plans with volume discounts are quoted. |
| Free tier | 100 free credits every month, no card required | A free trial is offered; its duration and allowance are not stated on the pricing page |
| Price per 1,000 at about 10k | $7.80 (Growth pack: 5,000 credits for $39), or $5.80 on the Growth monthly plan (5,000 credits for $29 a month) | £100 per 1,000 UK address lookups pay-as-you-go (10.00p a request, 2.0 credits) or £45 per 1,000 on a monthly plan (4.50p). This is the address comparison, so only the address rate is quoted: the same account buys email and mobile validation at their own per-request rates, which are read off the same page. |
| Price per 1,000 at about 100k | $4.49 (Volume pack: 100,000 credits for $449), or $3.49 on the Volume monthly plan (100,000 credits for $349 a month) | The same published per-request rates; the page shows no volume tier below them, and the pack prices that would set the effective rate are not rendered. |
| Addresses no register covered billed | An address that stands as written costs 1 credit whether or not a register carried the building; where no register covers the country, exists answers null rather than a verdict | not on the pages read |
| Addresses that cannot stand billed | No: an address that cannot stand as written is free whatever the reason, and so are repeats within 7 days and rows repeating an address already in the same job | not on the pages read |
| Registers held | Open address registers for 23 countries and territories, each named in the answer with its declared coverage, complete or partial. Complete, building by building: Australia, Austria, Belgium, Czechia, Latvia, Luxembourg, Netherlands, Norway and Switzerland. Partial, building by building: Brazil, Denmark, France (with French Guiana, Guadeloupe, Martinique, Mayotte, Réunion, St. Barthélemy, St. Martin and St. Pierre & Miquelon), Germany and United States. Partial, streets only: Great Britain. House-number ranges as well: United States. Absence from a partial register is never read as evidence that an address does not exist. | Not published as a register list. The pricing page separates UK, Ireland and "Rest of world" address lookups and mentions a "PAF Public Sector Licence" for public sector discounts. |
| Country coverage | Postal formats for 207 countries and territories: the line layout for 199 of them, and the required fields and postcode pattern wherever the table records them; 52 postal operators' rules for naming a delivery point that is not a street; open address registers for 23 countries and territories; postcode directories for Great Britain from the ONS Postcode Directory, with Northern Ireland excluded because reusing BT postcodes needs a separate licence, and for 108 more countries and territories from GeoNames' postal codes. | UK, Ireland and "Rest of world" are priced separately; the page publishes no country count. |
| Claims post is delivered | No, and every answer says so: is_deliverable, is_cmra, is_vacant and is_residential answer null because that check is licensed and not enabled here. exists is a match in an open address register, never a statement that post arrives. | not on the pages read |
| Bulk limit | 100,000 addresses per bulk job, with an HMAC-signed completion webhook | not on the pages read |
| Batch API | 100 addresses per synchronous request | not on the pages read |
| Rate limit | 50 requests per second per key on the address endpoints | not on the pages read |
| Official SDKs | Python, Node.js, PHP | not on the pages read |
| Data retention | No history row and no log line carrying the address. What a lookup leaves is the 7-day marker that makes a repeat free and the count behind seen_by_you, both kept under a digest, never the address. The address itself is kept only on the account's own suppression list and in its monitors. Bulk input lists and result files are deleted with the job after 30 days. | not on the pages read |
## When Postcoder fits better
UK checkouts that want a postcode-to-address picker from a licensed UK dataset, with email and mobile validation on the same balance.
- One credit balance across address, bank, email and mobile validation and OTP
- Credit packs last 12 months; monthly plan credits do not carry over
- A public sector discount tied to a PAF Public Sector Licence
- Monthly plans cost well under half the pay-as-you-go rate per request
## When Spaw fits better
Developers who want a written address parsed against its own country's rules and checked against open registers, where a field nobody could evaluate answers null instead of a guess.
- Postcoder can list the addresses at a UK postcode; Spaw cannot, because every full UK list derives from the licensed Postcode Address File: for a postcode in Great Britain its postcode endpoint answers the streets Ordnance Survey's Open Names files under the postcode's district, with no house numbers
- Spaw reads a written address against its own country's format and follows the building into that country's open address register where one is installed; Great Britain has no open building register, so an address in Great Britain is checked against the postcode directory and the streets Ordnance Survey lists, and a listed street never means a building exists
- Postcoder's monthly plan credits expire each month; Spaw's pack credits never expire and 100 arrive free every month
- Spaw charges nothing for an address that cannot stand as written, and nothing for a repeat within seven days
## Questions
**Does Spaw charge for addresses no register covers?** An address that stands as written costs 1 credit, whether or not a register carried the building. An address that cannot stand costs nothing, whatever the reason, and a repeat within seven days is free. Where no register covers the country, or where the register that answered is one whose coverage is partial and does not carry the address, exists answers null — not evaluated — rather than a verdict, and the not_in_register weight cannot fire. Postcoder does not publish how it bills them.
**Can I move an address list from Postcoder to Spaw?** Yes. Upload the CSV in the dashboard or POST it to the bulk endpoint, up to 100,000 addresses a job; Spaw keeps your columns and adds 64 result columns after them (the address as submitted, the answer, the datasets behind it, what the row cost and whether your suppression list answered it), and a country column gives each row its own country. Rows repeating an address already in the list are answered once, cost nothing, and are counted as duplicates.
**Is there a free tier?** Every Spaw account receives 100 free credits at the start of each month with no card on file, and unused free credits accumulate. Postcoder offers: A free trial is offered; its duration and allowance are not stated on the pricing page.
## Sources (checked 2026-09-22)
- [Postcoder pricing](https://postcoder.com/pricing)
Postcoder is a trademark of its owner. Spaw is not affiliated with it.
Reference: https://spaw.co/compare/postcoder
---
# Postcoder alternatives for address verification
Address verification APIs to consider instead of Postcoder, with the pricing model, free tier and price per 1,000 lookups each vendor publishes. Figures come from public pricing pages checked on 2026-09-22.
| Service | Pricing model | Free tier | Price per 1,000 at about 10k |
| --- | --- | --- | --- |
| [Spaw](https://spaw.co) | Pay-as-you-go credit packs whose credits never expire, or optional plans, billed monthly or yearly, that deposit the same credits every month for less and roll unused plan credits over up to three months' allowance; both through Stripe Checkout | 100 free credits every month, no card required | $7.80 (Growth pack: 5,000 credits for $39), or $5.80 on the Growth monthly plan (5,000 credits for $29 a month) |
| [Geoapify](https://www.geoapify.com) | Daily credit quotas on monthly subscriptions: Free (3,000 credits a day), API 10 (10,000 a day, $59 billed monthly), API 25 (25,000, $109), API 50 (50,000, $179), API 100 (100,000, $299), API 250 (250,000, $609) and Custom ("Unmetered credits / day", from $860). "1 Geocoding API request costs 1 credit"; in a batch each address counts 0.5 credits, plus 1 credit to create the job and 1 for each result request. A yearly term is offered, but its prices are not in the page text read. | A standing free plan: "3,000 credits / day", "No credit card required", up to 5 requests a second, usable for "commercial websites and apps" with the required Geoapify and data-source attribution | not on the pages read |
| [Google Address Validation API](https://mapsplatform.google.com) | "Pay-as-you-go", "charging per request according to the pricing table", under two SKUs named on the billing page, Address Validation Pro and Address Validation Enterprise; the per-request rates render inside the pricing table and are not stated in the page text as fetched, so no figure is quoted here. | not on the pages read | not on the pages read |
| [Ideal Postcodes](https://ideal-postcodes.co.uk) | Pay-as-you-go credit packs, "No Subscriptions. No Monthly Fees.": 200 credits for £9 ("£0.045 per lookup"), 1,100 for £42 (£0.038), 4,300 for £155 (£0.036), 12,800 for £420 (£0.033) and 32,100 for £900 (£0.028). "Credits are valid for 12 months"; the lookup guide adds that the timer "only starts when you begin using that batch of credits". Larger volumes and UK registered charities are priced on request. | A trial rather than a standing tier: "1-Month Trial", "50 free credits, full support and no credit card needed"; the trial searches worldwide except Ireland, "which requires separate licensing" | £33 per 1,000 lookups on the 12,800-credit pack (£420, "£0.033 per lookup"), the smallest pack that covers 10,000. |
| [Lob](https://www.lob.com) | Verifications are priced per call on top of a print-and-mail plan: on the Developer plan ($0 a month) "US Address Verifications $0.05/additional" and international $0.07; on Startup ($260 a month) $0.025 with "1,000 included for $25/mo" and international $0.06 with 500 included for $30 a month; on Growth ($550 a month) $0.009 with "50,000 included for $450/mo" and international $0.055 with 10,000 included for $550 a month; Enterprise is "Contact us for details". | The Developer plan is "Starting at $0 / month" with verifications billed per call; the site offers "Get started for free". | $50 per 1,000 US verifications on the Developer plan ($0.05 each) or $25 per 1,000 on Startup ($0.025 each beyond the 1,000 included for $25 a month); international $70 or $60. |
| [Loqate](https://www.loqate.com) | Pay-as-you-go credit purchases of £50, £100, £250, £500 and £1,000 (or 75€, 150€ and 300€), each buying a published per-lookup rate for every service, plus bespoke plans; "Credit is valid for 12 months after the purchase date" and there are no setup costs. The pricing page shows two rate tables headed "New Pricing" and "Old Pricing" without saying which accounts each applies to; the figures here are from "New Pricing". Its usage-example selector also lists a £2,500 plan that neither table prices. | A trial rather than a standing tier: "Start your 45-day free trial"; the FAQ adds "You get free credits to test the service" | £82 to £86 per 1,000 Address Verify lookups on the "New Pricing" table: 8.2p a lookup on the £100, £250 and £500 tiers and 8.6p on the £50 and £1,000 tiers. Address Capture is 8.6p to 4.9p in the UK and 7.1p to 4.1p for the rest of the world. |
| [Melissa](https://www.melissa.com) | US Address Verification pay-as-you-go "Starting at $40" for 10,000 credits at "10 Credits Per Address" (the same page's FAQ says a US address equals 3 credits and a global address 10); an API subscription "Starting at $5,145" for 1,000,000 records a year; an unlimited-records plan "Starting at $16,000". Global Address Verification subscriptions from $40 a month for 1,000 records and $95 a month for 2,500. | Global Address Verification "GAV Free" at $0 for 250 records a month; 1,000 lookup credits a month after a one-time $4.97 KYC/AML verification fee, described on the pricing page. | $40 per 1,000 US addresses on the smallest pay-as-you-go pack, taking the pack's own "10 Credits Per Address" ($40 buys 10,000 credits). |
| [Postcode.eu](https://www.postcode.eu) | Annual tiers that include validations: Tier 1 €50, Tier 2 €100, Tier 3 €250, Tier 4 €500 and Tier 5 €1,000 a year, each with a per-validation rate by country group. "Up to the Tier amount you can perform a mix of validations", subscriptions "are automatically upgraded when the maximum of the Tier is reached", and past the highest Tier usage "will be invoiced in blocks of 10% and the rates of this Tier apply". A validation is "a bundle of related requests that were used to validate an address". An SLA (€1,000 + 2% a year) and a DORA addendum (€2,500 a year) are extra. | A trial rather than a standing tier: "After registering an account it is possible to try the APIs for free", and "Testing the API is without obligations" | €17.64 per 1,000 validations on Tier 3 (€250 a year, "€0.01764 each") for Austria, Belgium, Denmark, Finland, France, Luxembourg, Norway, Spain, Sweden, Switzerland and the Netherlands. Germany, Italy and the United Kingdom cost more a validation, so 10,000 of them fall in Tier 4 (€500 a year): €34.17 per 1,000 for Germany (€0.03417 each) and €43.30 for Italy and the United Kingdom (€0.0433 each). "Tier prices are including validations" and "Prices and quantities are per year", so the tier amount is paid in full: 10,000 validations cost €250 a year for the eleven countries (€25 per 1,000) and €500 for Germany, Italy or the United Kingdom (€50 per 1,000). |
| [PostGrid](https://www.postgrid.com) | Quoted: the pricing page asks for annual mail-piece and address-verification volumes in bands from "Less than 50,000" to "More than 1,000,000" and publishes no rate. | "Try for Free. Sign Up for Address Verification API" on the product page; no standing free volume is stated. | not on the pages read |
| [Smarty](https://www.smarty.com) | Annual lookup bundles on the Professional plan from $552 a year, sized 12K, 60K, 120K, 300K, 600K, 1M or 2M lookups; a monthly option and bespoke Custom pricing are offered. Only the $552 entry price is shown on the page. A subscription "expires when the address lookups or the time runs out, whichever comes first"; there are no overage costs, and an exhausted plan answers 402 "payment required" unless auto-renewal buys a new block. | A free trial rather than a standing tier: "42 days" and "1,000 address lookups", "No credit card required" | not on the pages read |
## Spaw
Verification API that bills per answered lookup across six signals on one key and one balance: Email verification, Phone intelligence, IP intelligence, Address verification, Business verification against open registers, and Cross-signal consistency, which runs any subset of the first four in one call and names where they agree and where they do not. The address side reads a written address against its own country's format, names the kind of delivery point behind it, checks the postcode against the national directory and follows the building into that country's own address register where one is installed.
## Geoapify
Geoapify is a location platform (maps, routing, places and geocoding) whose Geocoding API is also sold for address validation: an address is matched against open datasets such as OpenStreetMap and OpenAddresses and returned with coordinates and confidence scores for the city, the street and the building.
Comparison: https://spaw.co/compare/geoapify
## Google Address Validation API
Google's Address Validation API, part of Google Maps Platform, validates, standardises and geocodes an address, reports a confirmation level per component with inferred and replaced parts, and can run US and Puerto Rico addresses through USPS CASS on request.
Comparison: https://spaw.co/compare/google-address-validation
## Ideal Postcodes
Ideal Postcodes is a UK-first address lookup, autocomplete and cleansing API answered from Royal Mail's Postcode Address File, with Ordnance Survey AddressBase Core and Premium offered as additional datasets, sold as pay-as-you-go credit packs, with international address data for more than 240 countries and territories; its site also lists phone and email validation among its products.
Comparison: https://spaw.co/compare/ideal-postcodes
## Lob
Lob is a print-and-mail platform whose Address Verification API validates, standardises and enriches US and international addresses, CASS certified, with USPS delivery-point data and a deliverability likelihood drawn from Lob's own mail delivery records.
Comparison: https://spaw.co/compare/lob
## Loqate
Loqate sells address capture and address verification alongside email, phone and bank validation, priced per lookup from a pay-as-you-go credit balance, with international data for over 245 countries and territories.
Comparison: https://spaw.co/compare/loqate
## Melissa
Melissa sells address verification for the United States and "250+ Countries & Territories" against postal data, CASS and DPV certified, with a credit model for pay-as-you-go use and annual subscriptions sized by records, alongside name, phone, email and identity verification.
Comparison: https://spaw.co/compare/melissa
## Postcode.eu
Postcode.eu, from Postcode.nl B.V., sells address autocomplete and validation APIs for fourteen European countries, each answered from the official sources its pages name (government address registers for most, the postal operator's file for others), priced per validation inside an annual tier.
Comparison: https://spaw.co/compare/postcode-eu
## PostGrid
PostGrid sells an address verification API for the United States and Canada, CASS and SERP certified against USPS and Canada Post data, with an international service for "245+ Countries and Territories", autocomplete, parsing, batch verification and a Salesforce integration, priced by quotation.
Comparison: https://spaw.co/compare/postgrid
## Smarty
Smarty sells US address verification against postal data: a lookup validates to the secondary level and returns delivery-point metadata that open registers cannot answer, including a DPV match code, the residential indicator and vacancy status.
Comparison: https://spaw.co/compare/smarty
Postcoder and the other product names are trademarks of their owners. Spaw is not affiliated with them.
Reference: https://spaw.co/alternatives/postcoder
---
# Spaw vs PostGrid: address verification API comparison
How Spaw and PostGrid compare on pricing, billing rules, the registers behind each answer and limits, from PostGrid's public pages checked on 2026-09-16. Corrections: support@spaw.co.
| | Spaw | PostGrid |
| --- | --- | --- |
| Pricing model | Pay-as-you-go credit packs whose credits never expire, or optional plans, billed monthly or yearly, that deposit the same credits every month for less and roll unused plan credits over up to three months' allowance; both through Stripe Checkout | Quoted: the pricing page asks for annual mail-piece and address-verification volumes in bands from "Less than 50,000" to "More than 1,000,000" and publishes no rate. |
| Free tier | 100 free credits every month, no card required | "Try for Free. Sign Up for Address Verification API" on the product page; no standing free volume is stated. |
| Price per 1,000 at about 10k | $7.80 (Growth pack: 5,000 credits for $39), or $5.80 on the Growth monthly plan (5,000 credits for $29 a month) | not on the pages read |
| Price per 1,000 at about 100k | $4.49 (Volume pack: 100,000 credits for $449), or $3.49 on the Volume monthly plan (100,000 credits for $349 a month) | not on the pages read |
| Addresses no register covered billed | An address that stands as written costs 1 credit whether or not a register carried the building; where no register covers the country, exists answers null rather than a verdict | not on the pages read |
| Addresses that cannot stand billed | No: an address that cannot stand as written is free whatever the reason, and so are repeats within 7 days and rows repeating an address already in the same job | not on the pages read |
| Registers held | Open address registers for 23 countries and territories, each named in the answer with its declared coverage, complete or partial. Complete, building by building: Australia, Austria, Belgium, Czechia, Latvia, Luxembourg, Netherlands, Norway and Switzerland. Partial, building by building: Brazil, Denmark, France (with French Guiana, Guadeloupe, Martinique, Mayotte, Réunion, St. Barthélemy, St. Martin and St. Pierre & Miquelon), Germany and United States. Partial, streets only: Great Britain. House-number ranges as well: United States. Absence from a partial register is never read as evidence that an address does not exist. | Postal: "CASS & SERP Certified", matching "customer addresses with official postal records from USPS and Canada Post"; also "CASS/NCOA Certified". |
| Country coverage | Postal formats for 207 countries and territories: the line layout for 199 of them, and the required fields and postcode pattern wherever the table records them; 52 postal operators' rules for naming a delivery point that is not a street; open address registers for 23 countries and territories; postcode directories for Great Britain from the ONS Postcode Directory, with Northern Ireland excluded because reusing BT postcodes needs a separate licence, and for 108 more countries and territories from GeoNames' postal codes. | "245+ Countries and Territories" for the international service; the standard API covers the United States and Canada. |
| Claims post is delivered | No, and every answer says so: is_deliverable, is_cmra, is_vacant and is_residential answer null because that check is licensed and not enabled here. exists is a match in an open address register, never a statement that post arrives. | Yes for the United States and Canada: an address comes back "Verified", "Corrected" or "Failed" against USPS and Canada Post data, and the CASS description names ZIP+4, carrier route and delivery point lookup. |
| Bulk limit | 100,000 addresses per bulk job, with an HMAC-signed completion webhook | not on the pages read |
| Batch API | 100 addresses per synchronous request | Yes: "Process large datasets at once via the /verifications/batch endpoint"; no limit is stated on the page. |
| Rate limit | 50 requests per second per key on the address endpoints | not on the pages read |
| Official SDKs | Python, Node.js, PHP | not on the pages read |
| Data retention | No history row and no log line carrying the address. What a lookup leaves is the 7-day marker that makes a repeat free and the count behind seen_by_you, both kept under a digest, never the address. The address itself is kept only on the account's own suppression list and in its monitors. Bulk input lists and result files are deleted with the job after 30 days. | not on the pages read |
## When PostGrid fits better
Businesses mailing in the United States and Canada that want postal-certified verification and autocomplete under one contract, priced on their annual volume.
- CASS and SERP certified, so US and Canadian addresses are matched to the postal services' own records
- Autocomplete, freeform parsing, city and state from a postal code, and ZIP codes from a city
- A batch endpoint and a Salesforce integration for existing records
- Pricing by quotation against annual volume
## When Spaw fits better
Developers who want a written address parsed against its own country's rules and checked against open registers, where a field nobody could evaluate answers null instead of a guess.
- PostGrid answers deliverability from USPS and Canada Post records; Spaw's licensed delivery-point fields answer null, and its Canadian directory carries only the forward sortation area, which the answer says plainly
- PostGrid prices by quotation; Spaw publishes one price per credit, and an address that cannot stand as written costs nothing
- Spaw's confirmation map and changes list say which dataset stood behind each part of the address and what the parser altered, rather than a single Verified, Corrected or Failed status
- Spaw names the register behind every answer and whether its coverage is complete or partial, and a partial register never denies an address
## Questions
**Does Spaw charge for addresses no register covers?** An address that stands as written costs 1 credit, whether or not a register carried the building. An address that cannot stand costs nothing, whatever the reason, and a repeat within seven days is free. Where no register covers the country, or where the register that answered is one whose coverage is partial and does not carry the address, exists answers null — not evaluated — rather than a verdict, and the not_in_register weight cannot fire. PostGrid does not publish how it bills them.
**Can I move an address list from PostGrid to Spaw?** Yes. Upload the CSV in the dashboard or POST it to the bulk endpoint, up to 100,000 addresses a job; Spaw keeps your columns and adds 64 result columns after them (the address as submitted, the answer, the datasets behind it, what the row cost and whether your suppression list answered it), and a country column gives each row its own country. Rows repeating an address already in the list are answered once, cost nothing, and are counted as duplicates.
**Is there a free tier?** Every Spaw account receives 100 free credits at the start of each month with no card on file, and unused free credits accumulate. PostGrid offers: "Try for Free. Sign Up for Address Verification API" on the product page; no standing free volume is stated.
## Sources (checked 2026-09-16)
- [PostGrid pricing](https://www.postgrid.com/pricing)
- [PostGrid address verification API](https://www.postgrid.com/address-verification-api/)
PostGrid is a trademark of its owner. Spaw is not affiliated with it.
Reference: https://spaw.co/compare/postgrid
---
# PostGrid alternatives for address verification
Address verification APIs to consider instead of PostGrid, with the pricing model, free tier and price per 1,000 lookups each vendor publishes. Figures come from public pricing pages checked on 2026-09-16.
| Service | Pricing model | Free tier | Price per 1,000 at about 10k |
| --- | --- | --- | --- |
| [Spaw](https://spaw.co) | Pay-as-you-go credit packs whose credits never expire, or optional plans, billed monthly or yearly, that deposit the same credits every month for less and roll unused plan credits over up to three months' allowance; both through Stripe Checkout | 100 free credits every month, no card required | $7.80 (Growth pack: 5,000 credits for $39), or $5.80 on the Growth monthly plan (5,000 credits for $29 a month) |
| [Geoapify](https://www.geoapify.com) | Daily credit quotas on monthly subscriptions: Free (3,000 credits a day), API 10 (10,000 a day, $59 billed monthly), API 25 (25,000, $109), API 50 (50,000, $179), API 100 (100,000, $299), API 250 (250,000, $609) and Custom ("Unmetered credits / day", from $860). "1 Geocoding API request costs 1 credit"; in a batch each address counts 0.5 credits, plus 1 credit to create the job and 1 for each result request. A yearly term is offered, but its prices are not in the page text read. | A standing free plan: "3,000 credits / day", "No credit card required", up to 5 requests a second, usable for "commercial websites and apps" with the required Geoapify and data-source attribution | not on the pages read |
| [Google Address Validation API](https://mapsplatform.google.com) | "Pay-as-you-go", "charging per request according to the pricing table", under two SKUs named on the billing page, Address Validation Pro and Address Validation Enterprise; the per-request rates render inside the pricing table and are not stated in the page text as fetched, so no figure is quoted here. | not on the pages read | not on the pages read |
| [Ideal Postcodes](https://ideal-postcodes.co.uk) | Pay-as-you-go credit packs, "No Subscriptions. No Monthly Fees.": 200 credits for £9 ("£0.045 per lookup"), 1,100 for £42 (£0.038), 4,300 for £155 (£0.036), 12,800 for £420 (£0.033) and 32,100 for £900 (£0.028). "Credits are valid for 12 months"; the lookup guide adds that the timer "only starts when you begin using that batch of credits". Larger volumes and UK registered charities are priced on request. | A trial rather than a standing tier: "1-Month Trial", "50 free credits, full support and no credit card needed"; the trial searches worldwide except Ireland, "which requires separate licensing" | £33 per 1,000 lookups on the 12,800-credit pack (£420, "£0.033 per lookup"), the smallest pack that covers 10,000. |
| [Lob](https://www.lob.com) | Verifications are priced per call on top of a print-and-mail plan: on the Developer plan ($0 a month) "US Address Verifications $0.05/additional" and international $0.07; on Startup ($260 a month) $0.025 with "1,000 included for $25/mo" and international $0.06 with 500 included for $30 a month; on Growth ($550 a month) $0.009 with "50,000 included for $450/mo" and international $0.055 with 10,000 included for $550 a month; Enterprise is "Contact us for details". | The Developer plan is "Starting at $0 / month" with verifications billed per call; the site offers "Get started for free". | $50 per 1,000 US verifications on the Developer plan ($0.05 each) or $25 per 1,000 on Startup ($0.025 each beyond the 1,000 included for $25 a month); international $70 or $60. |
| [Loqate](https://www.loqate.com) | Pay-as-you-go credit purchases of £50, £100, £250, £500 and £1,000 (or 75€, 150€ and 300€), each buying a published per-lookup rate for every service, plus bespoke plans; "Credit is valid for 12 months after the purchase date" and there are no setup costs. The pricing page shows two rate tables headed "New Pricing" and "Old Pricing" without saying which accounts each applies to; the figures here are from "New Pricing". Its usage-example selector also lists a £2,500 plan that neither table prices. | A trial rather than a standing tier: "Start your 45-day free trial"; the FAQ adds "You get free credits to test the service" | £82 to £86 per 1,000 Address Verify lookups on the "New Pricing" table: 8.2p a lookup on the £100, £250 and £500 tiers and 8.6p on the £50 and £1,000 tiers. Address Capture is 8.6p to 4.9p in the UK and 7.1p to 4.1p for the rest of the world. |
| [Melissa](https://www.melissa.com) | US Address Verification pay-as-you-go "Starting at $40" for 10,000 credits at "10 Credits Per Address" (the same page's FAQ says a US address equals 3 credits and a global address 10); an API subscription "Starting at $5,145" for 1,000,000 records a year; an unlimited-records plan "Starting at $16,000". Global Address Verification subscriptions from $40 a month for 1,000 records and $95 a month for 2,500. | Global Address Verification "GAV Free" at $0 for 250 records a month; 1,000 lookup credits a month after a one-time $4.97 KYC/AML verification fee, described on the pricing page. | $40 per 1,000 US addresses on the smallest pay-as-you-go pack, taking the pack's own "10 Credits Per Address" ($40 buys 10,000 credits). |
| [Postcode.eu](https://www.postcode.eu) | Annual tiers that include validations: Tier 1 €50, Tier 2 €100, Tier 3 €250, Tier 4 €500 and Tier 5 €1,000 a year, each with a per-validation rate by country group. "Up to the Tier amount you can perform a mix of validations", subscriptions "are automatically upgraded when the maximum of the Tier is reached", and past the highest Tier usage "will be invoiced in blocks of 10% and the rates of this Tier apply". A validation is "a bundle of related requests that were used to validate an address". An SLA (€1,000 + 2% a year) and a DORA addendum (€2,500 a year) are extra. | A trial rather than a standing tier: "After registering an account it is possible to try the APIs for free", and "Testing the API is without obligations" | €17.64 per 1,000 validations on Tier 3 (€250 a year, "€0.01764 each") for Austria, Belgium, Denmark, Finland, France, Luxembourg, Norway, Spain, Sweden, Switzerland and the Netherlands. Germany, Italy and the United Kingdom cost more a validation, so 10,000 of them fall in Tier 4 (€500 a year): €34.17 per 1,000 for Germany (€0.03417 each) and €43.30 for Italy and the United Kingdom (€0.0433 each). "Tier prices are including validations" and "Prices and quantities are per year", so the tier amount is paid in full: 10,000 validations cost €250 a year for the eleven countries (€25 per 1,000) and €500 for Germany, Italy or the United Kingdom (€50 per 1,000). |
| [Postcoder](https://postcoder.com) | Credit packs (500 to 200,000 credits, "valid for 12 months from purchase") or monthly plans (1,000 to 100,000 credits, with unused credits not carried over). "Your credits can be used for any Postcoder request." The pack prices themselves render as placeholders ("£--") in the pack list; the credit calculator's recommendation box shows "Credit pack £25 500 Credits" and "Monthly plan £25 1,000 Credits". Larger packs and plans with volume discounts are quoted. | A free trial is offered; its duration and allowance are not stated on the pricing page | £100 per 1,000 UK address lookups pay-as-you-go (10.00p a request, 2.0 credits) or £45 per 1,000 on a monthly plan (4.50p). This is the address comparison, so only the address rate is quoted: the same account buys email and mobile validation at their own per-request rates, which are read off the same page. |
| [Smarty](https://www.smarty.com) | Annual lookup bundles on the Professional plan from $552 a year, sized 12K, 60K, 120K, 300K, 600K, 1M or 2M lookups; a monthly option and bespoke Custom pricing are offered. Only the $552 entry price is shown on the page. A subscription "expires when the address lookups or the time runs out, whichever comes first"; there are no overage costs, and an exhausted plan answers 402 "payment required" unless auto-renewal buys a new block. | A free trial rather than a standing tier: "42 days" and "1,000 address lookups", "No credit card required" | not on the pages read |
## Spaw
Verification API that bills per answered lookup across six signals on one key and one balance: Email verification, Phone intelligence, IP intelligence, Address verification, Business verification against open registers, and Cross-signal consistency, which runs any subset of the first four in one call and names where they agree and where they do not. The address side reads a written address against its own country's format, names the kind of delivery point behind it, checks the postcode against the national directory and follows the building into that country's own address register where one is installed.
## Geoapify
Geoapify is a location platform (maps, routing, places and geocoding) whose Geocoding API is also sold for address validation: an address is matched against open datasets such as OpenStreetMap and OpenAddresses and returned with coordinates and confidence scores for the city, the street and the building.
Comparison: https://spaw.co/compare/geoapify
## Google Address Validation API
Google's Address Validation API, part of Google Maps Platform, validates, standardises and geocodes an address, reports a confirmation level per component with inferred and replaced parts, and can run US and Puerto Rico addresses through USPS CASS on request.
Comparison: https://spaw.co/compare/google-address-validation
## Ideal Postcodes
Ideal Postcodes is a UK-first address lookup, autocomplete and cleansing API answered from Royal Mail's Postcode Address File, with Ordnance Survey AddressBase Core and Premium offered as additional datasets, sold as pay-as-you-go credit packs, with international address data for more than 240 countries and territories; its site also lists phone and email validation among its products.
Comparison: https://spaw.co/compare/ideal-postcodes
## Lob
Lob is a print-and-mail platform whose Address Verification API validates, standardises and enriches US and international addresses, CASS certified, with USPS delivery-point data and a deliverability likelihood drawn from Lob's own mail delivery records.
Comparison: https://spaw.co/compare/lob
## Loqate
Loqate sells address capture and address verification alongside email, phone and bank validation, priced per lookup from a pay-as-you-go credit balance, with international data for over 245 countries and territories.
Comparison: https://spaw.co/compare/loqate
## Melissa
Melissa sells address verification for the United States and "250+ Countries & Territories" against postal data, CASS and DPV certified, with a credit model for pay-as-you-go use and annual subscriptions sized by records, alongside name, phone, email and identity verification.
Comparison: https://spaw.co/compare/melissa
## Postcode.eu
Postcode.eu, from Postcode.nl B.V., sells address autocomplete and validation APIs for fourteen European countries, each answered from the official sources its pages name (government address registers for most, the postal operator's file for others), priced per validation inside an annual tier.
Comparison: https://spaw.co/compare/postcode-eu
## Postcoder
Postcoder is a UK-first address lookup API sold as credits that any of its services can spend: address lookup, bank validation, email validation, mobile validation and OTP verification, each costing a published number of credits per request.
Comparison: https://spaw.co/compare/postcoder
## Smarty
Smarty sells US address verification against postal data: a lookup validates to the secondary level and returns delivery-point metadata that open registers cannot answer, including a DPV match code, the residential indicator and vacancy status.
Comparison: https://spaw.co/compare/smarty
PostGrid and the other product names are trademarks of their owners. Spaw is not affiliated with them.
Reference: https://spaw.co/alternatives/postgrid
---
# Spaw vs proxycheck.io: IP intelligence API comparison
How Spaw and proxycheck.io compare on pricing, billing rules, the feeds behind each flag and limits, from proxycheck.io's public pages checked on 2026-09-12. Corrections: support@spaw.co.
| | Spaw | proxycheck.io |
| --- | --- | --- |
| Pricing model | Pay-as-you-go credit packs whose credits never expire, or optional plans, billed monthly or yearly, that deposit the same credits every month for less and roll unused plan credits over up to three months' allowance; both through Stripe Checkout. One credit buys five answered IP lookups | not on the pages read |
| Free tier | 100 free credits every month, no card required | not on the pages read |
| Price per 1,000 at about 10k | $1.80 per 1,000 IP lookups at 5 to the credit (Starter pack: 1,000 credits for $9), or $1.40 on the Starter monthly plan (1,000 credits for $7 a month) | not on the pages read |
| Price per 1,000 at about 100k | $1.56 per 1,000 IP lookups at 5 to the credit (Growth pack: 5,000 credits for $39), or $1.16 on the Growth monthly plan (5,000 credits for $29 a month) | not on the pages read |
| Addresses no feed covered billed | One credit buys five answered addresses whatever their flags say; a flag no feed could evaluate answers null rather than false, and the answer costs the same | not on the pages read |
| Reserved and private ranges billed | No: reserved, private, loopback, CGNAT, documentation and multicast ranges answer reason reserved_range and are free, unallocated space is free, and so are repeats within 7 days | not on the pages read |
| Feeds behind the flags | Public feeds, each named in the answer with the date it was synced: DB-IP Lite and RFC 8805 geofeeds for location, the five registries' delegated statistics for allocation, Spamhaus DROP and ASN-DROP and abuse.ch Feodo Tracker for blocklists, the Tor Project's exit list and Onionoo, the Mullvad, NordVPN and Private Internet Access server lists plus the open X4BNet lists, Apple's iCloud Private Relay egress list, the cloud providers' own published ranges, and the verification files Google, Microsoft, OpenAI, Perplexity, DuckDuckGo and Ahrefs publish for their crawlers. | not on the pages read |
| Refresh cadence | Every feed re-syncs daily, the botnet command-and-control list hourly, and the geofeed crawl weekly. Each response names every dataset it consulted with the date it was last synced, and /status lists them all. | not on the pages read |
| Geolocation source | The DB-IP Lite database, overridden wherever the operator publishes the location itself — an RFC 8805 geofeed, Apple's relay list, a VPN provider's server list. location_source says which answered, and location_confidence is graded from a weekly accuracy report rather than asserted. | not on the pages read |
| Bulk limit | 100,000 addresses per bulk job, with an HMAC-signed completion webhook | not on the pages read |
| Batch API | 1,000 addresses per synchronous request; repeats inside the list are looked up once | not on the pages read |
| Rate limit | 50 requests per second per key on the IP endpoints | not on the pages read |
| Official SDKs | Python, Node.js, PHP | not on the pages read |
| Data retention | No history row and no log line carrying the address. What a lookup leaves is keyed and never holds the address: the 7-day marker that makes a repeat free and, under the same keyed hash, the count behind seen_by_you; privacy: true skips both. The account's own outcome reports are kept as salted hashes for 90 days. Bulk input lists and result files are deleted with the job after 30 days. | not on the pages read |
## When proxycheck.io fits better
Not assessable from the vendor's pages on the checked date.
- proxycheck.io, its pricing page and its API documentation all answered 403 Forbidden to automated reading on 2026-09-12
- Nothing on this page is from memory or from third-party reviews: the vendor's own figures are the only source this catalog accepts
## When Spaw fits better
Developers who want every public IP feed compiled, dated and named for them, with a risk score whose weights are published so it can be recomputed or ignored.
- proxycheck.io's website blocked automated reading when we checked, so its prices and fields are not reproduced here; Spaw's packs, plans and free grant are listed at spaw.co/pricing
- Every dataset behind a Spaw IP answer is a public feed named in the response with the date it was synced, and the risk score is a published sum of weights anyone can recompute
- Spaw sells pack credits that never expire at five IP lookups to the credit, and reserved, private, documentation and unallocated ranges are free
- Spaw applies your own allow and deny lists inside every answer (an allow-list hit scores 0, a deny-list hit 100) and counts your own lookups of an address; neither is a field the other vendor's pages describe
## Questions
**Does Spaw charge for addresses its feeds do not cover?** Spaw bills per answered address, five to the credit: the answered lookup that opens a block costs 1 credit and the next four cost nothing. Reserved, private, loopback, CGNAT, documentation and multicast ranges answer reason reserved_range and are free, unallocated space is free, and so is a repeat of the same address within seven days. A flag no feed could evaluate answers null rather than false: the address is still billed, and the null tells you the signal was not checked instead of pretending it was. proxycheck.io does not publish how it bills them.
**Can I run a log file or an export through Spaw?** Yes. A bulk run takes up to 100,000 addresses from the dashboard or the API and returns one row per input address, in input order, with every flag, the risk score and the signals that made it. Five answered addresses cost a credit, repeats inside the list are looked up once, reserved ranges cost nothing, and the finish is announced on an HMAC-signed webhook.
**Is there a free tier?** Every Spaw account receives 100 free credits at the start of each month with no card on file, and unused free credits accumulate. proxycheck.io does not publish a free tier on the pages checked.
## Sources (checked 2026-09-12)
- [proxycheck.io pricing (answered 403 on 2026-09-12)](https://proxycheck.io/pricing)
- [proxycheck.io API documentation (answered 403 on 2026-09-12)](https://proxycheck.io/api/)
proxycheck.io is a trademark of its owner. Spaw is not affiliated with it.
Reference: https://spaw.co/compare/proxycheck
---
# proxycheck.io alternatives for IP intelligence
IP intelligence APIs to consider instead of proxycheck.io, with the pricing model, free tier and price per 1,000 lookups each vendor publishes. Figures come from public pricing pages checked on 2026-09-12.
| Service | Pricing model | Free tier | Price per 1,000 at about 10k |
| --- | --- | --- | --- |
| [Spaw](https://spaw.co) | Pay-as-you-go credit packs whose credits never expire, or optional plans, billed monthly or yearly, that deposit the same credits every month for less and roll unused plan credits over up to three months' allowance; both through Stripe Checkout. One credit buys five answered IP lookups | 100 free credits every month, no card required | $1.80 per 1,000 IP lookups at 5 to the credit (Starter pack: 1,000 credits for $9), or $1.40 on the Starter monthly plan (1,000 credits for $7 a month) |
| [AbuseIPDB](https://www.abuseipdb.com) | Plans with daily request allowances: Individual free "Forever!", Basic $25/mo or $228/year, Premium $99/mo or $1,068/year, Enterprise custom | Individual: free forever with no credit card, 1,000 check and report requests, 100 block checks and 5 bulk reports a day | not on the pages read |
| [ip-api](https://ip-api.com) | Two tiers. The free endpoint: "Our endpoints are limited to 45 HTTP requests per minute from an IP address" and "We do not allow commercial use of the free endpoint". The Pro service: "€13.3 per month, when paying for 12 months", with "Unlimited requests", "SSL (HTTPS)", "Commercial use allowed" and a "99.9% SLA" ("If the general API uptime falls below 99.9% in a month, we will automatically extend your subscription for 10 days"). | The free endpoint, for non-commercial use only: 45 requests per minute per address over HTTP ("256-bit SSL encryption is not available for this free API") | not on the pages read |
| [IP2Location](https://www.ip2location.com) | IP2Location.io API, monthly query plans: Free $0 ("50K IP Geolocation API queries/month", "11 IP Attributes", "Attribution Required"), Starter "$49/month" ("150K IP Geolocation API queries/month", "20 IP Attributes"), Plus "$249/month" ("300K IP Geolocation API queries/month", "59 IP Attributes"), Security "$499/month" ("600K IP Geolocation API queries/month", "79 IP Attributes"); overage per increment Starter $10, Plus $50, Security $100. Databases, standard licence per year: DB1 "$99", DB2 "$399", DB8 "$1,299", DB18 "$2,799", with a site licence at ten times those figures. | "50K IP Geolocation API queries/month" with 11 attributes, basic geolocation and open-proxy detection only, "Attribution Required"; without a key "up to 1,000 queries per day" | not on the pages read |
| [ipapi](https://ipapi.co) | Monthly plans by lookup quota: Starter "$15 billed monthly" for 60,000 lookups, Professional "$25 billed monthly" for 150,000, Business "$49 billed monthly" for 500,000, Enterprise "$99 billed monthly" for 2,000,000, Premium "$199 billed monthly" for 6,000,000, Ultimate "$399 billed monthly" for 15,000,000; annual billing is 20% less. | A free trial of "up-to 1000 IP lookups in a day" (about 30,000 a month), "for testing & development purposes" and "Not for production use" | not on the pages read |
| [ipdata](https://ipdata.co) | Monthly plans by daily request quota: Personal "$10 per month" with "2,500 requests daily", Lite "$30 per month" with "10,000 requests daily", Startup "$50 per month" with "50,000 requests daily", Business "$120 per month" with "100,000 requests daily"; "20% off all annual plans" at the time of reading. | "1500 free requests daily", "restricted to non-commercial use"; on the free tier, over the quota "all further requests are blocked and you receive an email notification" | not on the pages read |
| [ipgeolocation.io](https://ipgeolocation.io) | Monthly credit plans, "A standard unified IP lookup uses 1 credit": Starter "$19/month" for "150000 API Credits Per month", Plus "$49/month" for 500,000, Pro "$79/month" for 1,000,000, Business "$129/month" for 2,000,000, Premium "$249/month" for 5,000,000, Enterprise "Contact Sales"; annual billing gives "2 months free". "Paid monthly plans reset every month. Paid annual plans reset every year". | "1000 API Credits Per day for life" with no credit card, "a hard limit of 1000 requests per day" and "No overages on Free"; the free plan returns "core location without the radius and confidence fields" | not on the pages read |
| [IPinfo](https://ipinfo.io) | Monthly plans by API request quota: Lite $0/mo, Core $26/mo for 150,000 requests, Max $163/mo for 250,000, Enterprise custom for "1M+". Database downloads are listed separately, "Starting at $1,000/month". | IPinfo Lite at "$0/mo", free forever with no credit card, giving unlimited API requests at country level only | not on the pages read |
| [IPQualityScore](https://www.ipqualityscore.com) | Monthly plans by lookup quota: Free $0 (1,000 lookups a month), Startup "As low as: $99 / Month" (5,000), SMB Basic "$499" (10,000), SMB+ "$999" (75,000), Enterprise custom. "Any valid request sent to our Proxy Detection, Email Verification, or Device Fingerprinting services including lookups performed through a batch CSV file check will consume a lookup credit." | 1,000 lookups a month, capped at 35 a day | not on the pages read |
| [Ipregistry](https://ipregistry.co) | Credits, "1 lookup = 1 request = 1 credit" (batch endpoints use "one credit per lookup in the batch request, plus 1"). Subscriptions: Starter "100,000 credits / mo" at "$29 $24 / month", Professional "300,000 credits / mo" at "$79 $66 / month", Business "1,150,000 credits / mo" at "$199 $166 / month", Scale "2,450,000 credits / mo" at "$399 $333 / month" (the lower figure is the annual rate). Pay-as-you-go packs: 15,000 credits for $10, 100,000 for $50, 250,000 for $100, 1,750,000 for $500. | "20,000 free credits on sign-up" with "No credit card, no time limit" | $0.67 (the $10 pack of 15,000 credits, $0.000667 a lookup) |
| [ipstack](https://ipstack.com) | Monthly plans by request quota: Free "100 Request/mo." at $0, Basic "50,000 Request/mo." at "$12.99/mo.", Professional "500,000 Request/mo." at "$59.99/mo.", Professional + "2,000,000 Request/mo." at "$99.99/mo.", Custom on request; yearly billing "Save up to 15%". Overage fees apply past the quota, with notifications at 75%, 90% and 100%. | "100 Request/mo." at $0, without SSL encryption, support or the paid modules (SSL, commercial use and the Location, Time Zone and Connection modules are listed for paid plans) | not on the pages read |
| [MaxMind](https://www.maxmind.com) | Per query, by service, with no minimum published: "If you estimate your volume of queries to be over $2,500 a month, contact us to discuss rates." | A no-cost trial is offered ("Experience the functionality and benefits of our GeoIP web services at no cost"); no free monthly allowance is stated on the page checked | $0.10 per 1,000 for Country ($0.0001 a query), $0.30 for City Plus ($0.0003) and $2.00 for Insights ($0.002) |
| [Spur](https://spur.us) | not on the pages read | not on the pages read | not on the pages read |
## Spaw
Verification API that bills per answered lookup across six signals on one key and one balance: Email verification, Phone intelligence, IP intelligence, Address verification, Business verification against open registers, and Cross-signal consistency, which runs any subset of the first four in one call and names where they agree and where they do not. The IP side answers from local databases and compiled public feeds: where an address is, who runs it, and whether it is a visitor at all.
## AbuseIPDB
AbuseIPDB is a reputation service built on abuse reports its own users submit: a check returns an abuse confidence score with the report count, country, usage type, ISP and a Tor flag, and plans are sold as daily request allowances rather than credits.
Comparison: https://spaw.co/compare/abuseipdb
## ip-api
ip-api is a geolocation JSON API with a free, non-commercial, HTTP-only endpoint limited by request rate, and a Pro subscription that adds HTTPS, unlimited queries and commercial use; its answer carries one proxy flag, one hosting flag and one mobile flag beside the location and network fields.
Comparison: https://spaw.co/compare/ip-api
## IP2Location
IP2Location sells geolocation as downloadable databases (DB1 to DB26, licensed yearly) and, through IP2Location.io, as an API on monthly query plans whose higher tiers add proxy, VPN, Tor, datacenter, crawler and residential-proxy detection and a fraud score; the classic web service no longer takes new registrations.
Comparison: https://spaw.co/compare/ip2location
## ipapi
ipapi.co is a geolocation API sold as monthly lookup quotas, whose pricing page credits its data to DB-IP and the IP2Location LITE database; the answer carries location, country and network fields and no proxy, VPN or Tor flag.
Comparison: https://spaw.co/compare/ipapi
## ipdata
ipdata is a geolocation and threat API sold as daily request quotas, from a free non-commercial tier to a Business plan, whose pricing page says it does not keep records of the addresses looked up beyond a day; its documentation site refused automated reading, so the fields are not reproduced here.
Comparison: https://spaw.co/compare/ipdata
## ipgeolocation.io
ipgeolocation.io sells geolocation and a separate security API on monthly credit plans, with location built from its own measurements and a security answer that flags VPN, proxy, residential proxy and Tor with provider names, confidence scores and a 0 to 100 threat score.
Comparison: https://spaw.co/compare/ipgeolocation
## IPinfo
IPinfo is an IP data company that sells geolocation, ASN, privacy detection and company data as monthly API plans and as database downloads, collected through its own measurement network rather than assembled from third-party feeds.
Comparison: https://spaw.co/compare/ipinfo
## IPQualityScore
IPQualityScore sells fraud detection across several signals on one lookup credit: proxy and VPN detection, email verification and device fingerprinting all draw from the same monthly quota, and the IP answer carries a fraud score alongside the connection facts.
Comparison: https://spaw.co/compare/ipqualityscore
## Ipregistry
Ipregistry is a geolocation and threat API sold as credits, either monthly subscriptions or pay-as-you-go packs, whose security object answers abuser, attacker, bogon, cloud provider, proxy, relay, Tor, VPN, anonymous and threat flags and whose data is described as built from BGP tables, registry databases, geofeeds and latency measurements.
Comparison: https://spaw.co/compare/ipregistry
## ipstack
ipstack is a geolocation API on monthly request quotas from a 100-request free tier to a two-million-request plan, with a bulk endpoint and a security module reserved for its top plan; its documentation pages rendered only a header to our fetcher, so the field list is not reproduced here.
Comparison: https://spaw.co/compare/ipstack
## MaxMind
MaxMind sells GeoIP as three per-query web services — Country, City Plus and Insights — and as downloadable databases, with proxy and anonymiser detection reserved for the Insights tier.
Comparison: https://spaw.co/compare/maxmind
## Spur
Spur is an anonymity-infrastructure vendor: its Context API answers whether an address is a VPN, proxy or other tunnel with the operator named, and its feeds list anonymous, residential-proxy and datacenter address space; its pricing page rate-limited our fetcher, so prices are not reproduced here.
Comparison: https://spaw.co/compare/spur
proxycheck.io and the other product names are trademarks of their owners. Spaw is not affiliated with them.
Reference: https://spaw.co/alternatives/proxycheck
---
# Spaw vs Smarty: address verification API comparison
How Spaw and Smarty compare on pricing, billing rules, the registers behind each answer and limits, from Smarty's public pages checked on 2026-09-22. Corrections: support@spaw.co.
| | Spaw | Smarty |
| --- | --- | --- |
| Pricing model | Pay-as-you-go credit packs whose credits never expire, or optional plans, billed monthly or yearly, that deposit the same credits every month for less and roll unused plan credits over up to three months' allowance; both through Stripe Checkout | Annual lookup bundles on the Professional plan from $552 a year, sized 12K, 60K, 120K, 300K, 600K, 1M or 2M lookups; a monthly option and bespoke Custom pricing are offered. Only the $552 entry price is shown on the page. A subscription "expires when the address lookups or the time runs out, whichever comes first"; there are no overage costs, and an exhausted plan answers 402 "payment required" unless auto-renewal buys a new block. |
| Free tier | 100 free credits every month, no card required | A free trial rather than a standing tier: "42 days" and "1,000 address lookups", "No credit card required" |
| Price per 1,000 at about 10k | $7.80 (Growth pack: 5,000 credits for $39), or $5.80 on the Growth monthly plan (5,000 credits for $29 a month) | not on the pages read |
| Price per 1,000 at about 100k | $4.49 (Volume pack: 100,000 credits for $449), or $3.49 on the Volume monthly plan (100,000 credits for $349 a month) | not on the pages read |
| Addresses no register covered billed | An address that stands as written costs 1 credit whether or not a register carried the building; where no register covers the country, exists answers null rather than a verdict | not on the pages read |
| Addresses that cannot stand billed | No: an address that cannot stand as written is free whatever the reason, and so are repeats within 7 days and rows repeating an address already in the same job | not on the pages read |
| Registers held | Open address registers for 23 countries and territories, each named in the answer with its declared coverage, complete or partial. Complete, building by building: Australia, Austria, Belgium, Czechia, Latvia, Luxembourg, Netherlands, Norway and Switzerland. Partial, building by building: Brazil, Denmark, France (with French Guiana, Guadeloupe, Martinique, Mayotte, Réunion, St. Barthélemy, St. Martin and St. Pierre & Miquelon), Germany and United States. Partial, streets only: Great Britain. House-number ranges as well: United States. Absence from a partial register is never read as evidence that an address does not exist. | Postal rather than cadastral: the page claims "210M addresses (incl 20M non-USPS)" and "over 20 million non-postal addresses", with "Up to 55 points of metadata" per lookup. It names no national address register. |
| Country coverage | Postal formats for 207 countries and territories: the line layout for 199 of them, and the required fields and postcode pattern wherever the table records them; 52 postal operators' rules for naming a delivery point that is not a street; open address registers for 23 countries and territories; postcode directories for Great Britain from the ONS Postcode Directory, with Northern Ireland excluded because reusing BT postcodes needs a separate licence, and for 108 more countries and territories from GeoNames' postal codes. | The United States on the page checked; international address verification is priced on a separate page. |
| Claims post is delivered | No, and every answer says so: is_deliverable, is_cmra, is_vacant and is_residential answer null because that check is licensed and not enabled here. exists is a match in an open address register, never a statement that post arrives. | Yes. The metadata named on the page includes a DPV match code, the residential/commercial indicator (RDI), vacancy status, carrier route and ZIP type, and validation runs "down to the secondary level, including apartment and suite numbers". |
| Bulk limit | 100,000 addresses per bulk job, with an HMAC-signed completion webhook | not on the pages read |
| Batch API | 100 addresses per synchronous request | "API and batch processing (including no-code options)"; "Each address submitted for validation, whether in bulk or individually, is counted as an address lookup". No batch size is stated on the page. |
| Rate limit | 50 requests per second per key on the address endpoints | Professional: "Starting at 25,000 address lookups / second"; Custom: "Custom address lookups / second". |
| Official SDKs | Python, Node.js, PHP | not on the pages read |
| Data retention | No history row and no log line carrying the address. What a lookup leaves is the 7-day marker that makes a repeat free and the count behind seen_by_you, both kept under a digest, never the address. The address itself is kept only on the account's own suppression list and in its monitors. Bulk input lists and result files are deleted with the job after 30 days. | not on the pages read |
## When Smarty fits better
US mailers who need the postal service's own delivery-point answers — is it a real delivery point, is it occupied, is it residential — which open data cannot give.
- Answers the delivery-point questions licensed postal data can answer: DPV, RDI and vacancy
- Validates to the secondary level, so an apartment or suite number is checked rather than ignored
- ZIP9 and rooftop precise geocodes are listed on the plan
- A 42-day, 1,000-lookup trial with no card
## When Spaw fits better
Developers who want a written address parsed against its own country's rules and checked against open registers, where a field nobody could evaluate answers null instead of a guess.
- Smarty answers whether post is delivered to an address; Spaw does not and says so: is_deliverable, is_cmra, is_vacant and is_residential answer null because that check is licensed and not enabled
- Spaw's exists is a match in a country's own address register; where that register's coverage is partial, as the US National Address Database's is, an address it does not hold answers null rather than false
- Smarty's page prices the United States; Spaw reads an address against its own country's postal format and follows the building into that country's open address register where one is installed, naming the register in the answer and whether its coverage is complete or partial
- Smarty sells annual lookup bundles; Spaw sells pack credits that never expire, and an address that cannot stand as written costs nothing
## Questions
**Does Spaw charge for addresses no register covers?** An address that stands as written costs 1 credit, whether or not a register carried the building. An address that cannot stand costs nothing, whatever the reason, and a repeat within seven days is free. Where no register covers the country, or where the register that answered is one whose coverage is partial and does not carry the address, exists answers null — not evaluated — rather than a verdict, and the not_in_register weight cannot fire. Smarty does not publish how it bills them.
**Can I move an address list from Smarty to Spaw?** Yes. Upload the CSV in the dashboard or POST it to the bulk endpoint, up to 100,000 addresses a job; Spaw keeps your columns and adds 64 result columns after them (the address as submitted, the answer, the datasets behind it, what the row cost and whether your suppression list answered it), and a country column gives each row its own country. Rows repeating an address already in the list are answered once, cost nothing, and are counted as duplicates.
**Is there a free tier?** Every Spaw account receives 100 free credits at the start of each month with no card on file, and unused free credits accumulate. Smarty offers: A free trial rather than a standing tier: "42 days" and "1,000 address lookups", "No credit card required".
## Sources (checked 2026-09-22)
- [Smarty US address verification pricing](https://www.smarty.com/pricing)
Smarty is a trademark of its owner. Spaw is not affiliated with it.
Reference: https://spaw.co/compare/smarty
---
# Smarty alternatives for address verification
Address verification APIs to consider instead of Smarty, with the pricing model, free tier and price per 1,000 lookups each vendor publishes. Figures come from public pricing pages checked on 2026-09-22.
| Service | Pricing model | Free tier | Price per 1,000 at about 10k |
| --- | --- | --- | --- |
| [Spaw](https://spaw.co) | Pay-as-you-go credit packs whose credits never expire, or optional plans, billed monthly or yearly, that deposit the same credits every month for less and roll unused plan credits over up to three months' allowance; both through Stripe Checkout | 100 free credits every month, no card required | $7.80 (Growth pack: 5,000 credits for $39), or $5.80 on the Growth monthly plan (5,000 credits for $29 a month) |
| [Geoapify](https://www.geoapify.com) | Daily credit quotas on monthly subscriptions: Free (3,000 credits a day), API 10 (10,000 a day, $59 billed monthly), API 25 (25,000, $109), API 50 (50,000, $179), API 100 (100,000, $299), API 250 (250,000, $609) and Custom ("Unmetered credits / day", from $860). "1 Geocoding API request costs 1 credit"; in a batch each address counts 0.5 credits, plus 1 credit to create the job and 1 for each result request. A yearly term is offered, but its prices are not in the page text read. | A standing free plan: "3,000 credits / day", "No credit card required", up to 5 requests a second, usable for "commercial websites and apps" with the required Geoapify and data-source attribution | not on the pages read |
| [Google Address Validation API](https://mapsplatform.google.com) | "Pay-as-you-go", "charging per request according to the pricing table", under two SKUs named on the billing page, Address Validation Pro and Address Validation Enterprise; the per-request rates render inside the pricing table and are not stated in the page text as fetched, so no figure is quoted here. | not on the pages read | not on the pages read |
| [Ideal Postcodes](https://ideal-postcodes.co.uk) | Pay-as-you-go credit packs, "No Subscriptions. No Monthly Fees.": 200 credits for £9 ("£0.045 per lookup"), 1,100 for £42 (£0.038), 4,300 for £155 (£0.036), 12,800 for £420 (£0.033) and 32,100 for £900 (£0.028). "Credits are valid for 12 months"; the lookup guide adds that the timer "only starts when you begin using that batch of credits". Larger volumes and UK registered charities are priced on request. | A trial rather than a standing tier: "1-Month Trial", "50 free credits, full support and no credit card needed"; the trial searches worldwide except Ireland, "which requires separate licensing" | £33 per 1,000 lookups on the 12,800-credit pack (£420, "£0.033 per lookup"), the smallest pack that covers 10,000. |
| [Lob](https://www.lob.com) | Verifications are priced per call on top of a print-and-mail plan: on the Developer plan ($0 a month) "US Address Verifications $0.05/additional" and international $0.07; on Startup ($260 a month) $0.025 with "1,000 included for $25/mo" and international $0.06 with 500 included for $30 a month; on Growth ($550 a month) $0.009 with "50,000 included for $450/mo" and international $0.055 with 10,000 included for $550 a month; Enterprise is "Contact us for details". | The Developer plan is "Starting at $0 / month" with verifications billed per call; the site offers "Get started for free". | $50 per 1,000 US verifications on the Developer plan ($0.05 each) or $25 per 1,000 on Startup ($0.025 each beyond the 1,000 included for $25 a month); international $70 or $60. |
| [Loqate](https://www.loqate.com) | Pay-as-you-go credit purchases of £50, £100, £250, £500 and £1,000 (or 75€, 150€ and 300€), each buying a published per-lookup rate for every service, plus bespoke plans; "Credit is valid for 12 months after the purchase date" and there are no setup costs. The pricing page shows two rate tables headed "New Pricing" and "Old Pricing" without saying which accounts each applies to; the figures here are from "New Pricing". Its usage-example selector also lists a £2,500 plan that neither table prices. | A trial rather than a standing tier: "Start your 45-day free trial"; the FAQ adds "You get free credits to test the service" | £82 to £86 per 1,000 Address Verify lookups on the "New Pricing" table: 8.2p a lookup on the £100, £250 and £500 tiers and 8.6p on the £50 and £1,000 tiers. Address Capture is 8.6p to 4.9p in the UK and 7.1p to 4.1p for the rest of the world. |
| [Melissa](https://www.melissa.com) | US Address Verification pay-as-you-go "Starting at $40" for 10,000 credits at "10 Credits Per Address" (the same page's FAQ says a US address equals 3 credits and a global address 10); an API subscription "Starting at $5,145" for 1,000,000 records a year; an unlimited-records plan "Starting at $16,000". Global Address Verification subscriptions from $40 a month for 1,000 records and $95 a month for 2,500. | Global Address Verification "GAV Free" at $0 for 250 records a month; 1,000 lookup credits a month after a one-time $4.97 KYC/AML verification fee, described on the pricing page. | $40 per 1,000 US addresses on the smallest pay-as-you-go pack, taking the pack's own "10 Credits Per Address" ($40 buys 10,000 credits). |
| [Postcode.eu](https://www.postcode.eu) | Annual tiers that include validations: Tier 1 €50, Tier 2 €100, Tier 3 €250, Tier 4 €500 and Tier 5 €1,000 a year, each with a per-validation rate by country group. "Up to the Tier amount you can perform a mix of validations", subscriptions "are automatically upgraded when the maximum of the Tier is reached", and past the highest Tier usage "will be invoiced in blocks of 10% and the rates of this Tier apply". A validation is "a bundle of related requests that were used to validate an address". An SLA (€1,000 + 2% a year) and a DORA addendum (€2,500 a year) are extra. | A trial rather than a standing tier: "After registering an account it is possible to try the APIs for free", and "Testing the API is without obligations" | €17.64 per 1,000 validations on Tier 3 (€250 a year, "€0.01764 each") for Austria, Belgium, Denmark, Finland, France, Luxembourg, Norway, Spain, Sweden, Switzerland and the Netherlands. Germany, Italy and the United Kingdom cost more a validation, so 10,000 of them fall in Tier 4 (€500 a year): €34.17 per 1,000 for Germany (€0.03417 each) and €43.30 for Italy and the United Kingdom (€0.0433 each). "Tier prices are including validations" and "Prices and quantities are per year", so the tier amount is paid in full: 10,000 validations cost €250 a year for the eleven countries (€25 per 1,000) and €500 for Germany, Italy or the United Kingdom (€50 per 1,000). |
| [Postcoder](https://postcoder.com) | Credit packs (500 to 200,000 credits, "valid for 12 months from purchase") or monthly plans (1,000 to 100,000 credits, with unused credits not carried over). "Your credits can be used for any Postcoder request." The pack prices themselves render as placeholders ("£--") in the pack list; the credit calculator's recommendation box shows "Credit pack £25 500 Credits" and "Monthly plan £25 1,000 Credits". Larger packs and plans with volume discounts are quoted. | A free trial is offered; its duration and allowance are not stated on the pricing page | £100 per 1,000 UK address lookups pay-as-you-go (10.00p a request, 2.0 credits) or £45 per 1,000 on a monthly plan (4.50p). This is the address comparison, so only the address rate is quoted: the same account buys email and mobile validation at their own per-request rates, which are read off the same page. |
| [PostGrid](https://www.postgrid.com) | Quoted: the pricing page asks for annual mail-piece and address-verification volumes in bands from "Less than 50,000" to "More than 1,000,000" and publishes no rate. | "Try for Free. Sign Up for Address Verification API" on the product page; no standing free volume is stated. | not on the pages read |
## Spaw
Verification API that bills per answered lookup across six signals on one key and one balance: Email verification, Phone intelligence, IP intelligence, Address verification, Business verification against open registers, and Cross-signal consistency, which runs any subset of the first four in one call and names where they agree and where they do not. The address side reads a written address against its own country's format, names the kind of delivery point behind it, checks the postcode against the national directory and follows the building into that country's own address register where one is installed.
## Geoapify
Geoapify is a location platform (maps, routing, places and geocoding) whose Geocoding API is also sold for address validation: an address is matched against open datasets such as OpenStreetMap and OpenAddresses and returned with coordinates and confidence scores for the city, the street and the building.
Comparison: https://spaw.co/compare/geoapify
## Google Address Validation API
Google's Address Validation API, part of Google Maps Platform, validates, standardises and geocodes an address, reports a confirmation level per component with inferred and replaced parts, and can run US and Puerto Rico addresses through USPS CASS on request.
Comparison: https://spaw.co/compare/google-address-validation
## Ideal Postcodes
Ideal Postcodes is a UK-first address lookup, autocomplete and cleansing API answered from Royal Mail's Postcode Address File, with Ordnance Survey AddressBase Core and Premium offered as additional datasets, sold as pay-as-you-go credit packs, with international address data for more than 240 countries and territories; its site also lists phone and email validation among its products.
Comparison: https://spaw.co/compare/ideal-postcodes
## Lob
Lob is a print-and-mail platform whose Address Verification API validates, standardises and enriches US and international addresses, CASS certified, with USPS delivery-point data and a deliverability likelihood drawn from Lob's own mail delivery records.
Comparison: https://spaw.co/compare/lob
## Loqate
Loqate sells address capture and address verification alongside email, phone and bank validation, priced per lookup from a pay-as-you-go credit balance, with international data for over 245 countries and territories.
Comparison: https://spaw.co/compare/loqate
## Melissa
Melissa sells address verification for the United States and "250+ Countries & Territories" against postal data, CASS and DPV certified, with a credit model for pay-as-you-go use and annual subscriptions sized by records, alongside name, phone, email and identity verification.
Comparison: https://spaw.co/compare/melissa
## Postcode.eu
Postcode.eu, from Postcode.nl B.V., sells address autocomplete and validation APIs for fourteen European countries, each answered from the official sources its pages name (government address registers for most, the postal operator's file for others), priced per validation inside an annual tier.
Comparison: https://spaw.co/compare/postcode-eu
## Postcoder
Postcoder is a UK-first address lookup API sold as credits that any of its services can spend: address lookup, bank validation, email validation, mobile validation and OTP verification, each costing a published number of credits per request.
Comparison: https://spaw.co/compare/postcoder
## PostGrid
PostGrid sells an address verification API for the United States and Canada, CASS and SERP certified against USPS and Canada Post data, with an international service for "245+ Countries and Territories", autocomplete, parsing, batch verification and a Salesforce integration, priced by quotation.
Comparison: https://spaw.co/compare/postgrid
Smarty and the other product names are trademarks of their owners. Spaw is not affiliated with them.
Reference: https://spaw.co/alternatives/smarty
---
# Spaw vs Spur: IP intelligence API comparison
How Spaw and Spur compare on pricing, billing rules, the feeds behind each flag and limits, from Spur's public pages checked on 2026-09-12. Corrections: support@spaw.co.
| | Spaw | Spur |
| --- | --- | --- |
| Pricing model | Pay-as-you-go credit packs whose credits never expire, or optional plans, billed monthly or yearly, that deposit the same credits every month for less and roll unused plan credits over up to three months' allowance; both through Stripe Checkout. One credit buys five answered IP lookups | not on the pages read |
| Free tier | 100 free credits every month, no card required | not on the pages read |
| Price per 1,000 at about 10k | $1.80 per 1,000 IP lookups at 5 to the credit (Starter pack: 1,000 credits for $9), or $1.40 on the Starter monthly plan (1,000 credits for $7 a month) | not on the pages read |
| Price per 1,000 at about 100k | $1.56 per 1,000 IP lookups at 5 to the credit (Growth pack: 5,000 credits for $39), or $1.16 on the Growth monthly plan (5,000 credits for $29 a month) | not on the pages read |
| Addresses no feed covered billed | One credit buys five answered addresses whatever their flags say; a flag no feed could evaluate answers null rather than false, and the answer costs the same | not on the pages read |
| Reserved and private ranges billed | No: reserved, private, loopback, CGNAT, documentation and multicast ranges answer reason reserved_range and are free, unallocated space is free, and so are repeats within 7 days | not on the pages read |
| Feeds behind the flags | Public feeds, each named in the answer with the date it was synced: DB-IP Lite and RFC 8805 geofeeds for location, the five registries' delegated statistics for allocation, Spamhaus DROP and ASN-DROP and abuse.ch Feodo Tracker for blocklists, the Tor Project's exit list and Onionoo, the Mullvad, NordVPN and Private Internet Access server lists plus the open X4BNet lists, Apple's iCloud Private Relay egress list, the cloud providers' own published ranges, and the verification files Google, Microsoft, OpenAI, Perplexity, DuckDuckGo and Ahrefs publish for their crawlers. | Its own collection, sold as feeds and as an API. Feeds: Anonymous ("all IPs relating to anonymous VPNs, datacenter proxies, ISP proxies, virtual desktops, and other datacenter-centric anonymizing infrastructure"), Anonymous Residential ("The Anonymous feed, plus all IPs relating to residential proxies, residential VPNs, malware proxies, blockchain proxies, and other callback technologies"), Service Metrics and Datacenter/Hosting ("all netblocks belonging to datacenter and hosting providers"). The Context API answers infrastructure, organization, the autonomous system, client behaviours, proxies, device types, risks such as CALLBACK_PROXY, TUNNEL and GEO_MISMATCH, services such as OPENVPN, and tunnels with operator (for example PROTON_VPN), type and an anonymous flag. |
| Refresh cadence | Every feed re-syncs daily, the botnet command-and-control list hourly, and the geofeed crawl weekly. Each response names every dataset it consulted with the date it was last synced, and /status lists them all. | "Spur updates these feeds at least daily (typically by 0500 UTC). Some feeds and tiers may be more frequent" |
| Geolocation source | The DB-IP Lite database, overridden wherever the operator publishes the location itself — an RFC 8805 geofeed, Apple's relay list, a VPN provider's server list. location_source says which answered, and location_confidence is graded from a weekly accuracy report rather than asserted. | The Context API carries a location object with city, country and state; the pages read say nothing about its source |
| Bulk limit | 100,000 addresses per bulk job, with an HMAC-signed completion webhook | not on the pages read |
| Batch API | 1,000 addresses per synchronous request; repeats inside the list are looked up once | not on the pages read |
| Rate limit | 50 requests per second per key on the IP endpoints | not on the pages read |
| Official SDKs | Python, Node.js, PHP | not on the pages read |
| Data retention | No history row and no log line carrying the address. What a lookup leaves is keyed and never holds the address: the 7-day marker that makes a repeat free and, under the same keyed hash, the count behind seen_by_you; privacy: true skips both. The account's own outcome reports are kept as salted hashes for 90 days. Bulk input lists and result files are deleted with the job after 30 days. | not on the pages read |
## When Spur fits better
Security teams that want anonymity infrastructure, residential proxies included, as a feed to run inside their own systems, with the operator behind each tunnel named.
- spur.us/pricing answered 429 Too Many Requests to automated reading on 2026-09-12, so plans and prices are not reproduced here
- Residential proxies are a product line: the Anonymous Residential feed lists them explicitly
- Tunnels are answered with the operator named, PROTON_VPN for instance, and an anonymous flag
- Feeds are newline-delimited JSON refreshed at least daily
## When Spaw fits better
Developers who want every public IP feed compiled, dated and named for them, with a risk score whose weights are published so it can be recomputed or ignored.
- Spur lists residential proxies from its own collection; Spaw says plainly that residential proxies are not detectable from open data and does not claim them
- Spur's pricing could not be read on the checked date; Spaw sells pack credits that never expire at five IP lookups to the credit, and reserved, private, documentation and unallocated ranges are free
- Every dataset behind a Spaw IP answer is a public feed named in the response with the date it was synced, and the risk score is a published sum of weights anyone can recompute
- Both name the VPN operator; Spaw answers the anonymity flags as tri-state with the feed dated, and adds registries, crawlers, compliance facts and the country and time-zone facts a fraud check needs beside them
## Questions
**Does Spaw charge for addresses its feeds do not cover?** Spaw bills per answered address, five to the credit: the answered lookup that opens a block costs 1 credit and the next four cost nothing. Reserved, private, loopback, CGNAT, documentation and multicast ranges answer reason reserved_range and are free, unallocated space is free, and so is a repeat of the same address within seven days. A flag no feed could evaluate answers null rather than false: the address is still billed, and the null tells you the signal was not checked instead of pretending it was. Spur does not publish how it bills them.
**Can I run a log file or an export through Spaw?** Yes. A bulk run takes up to 100,000 addresses from the dashboard or the API and returns one row per input address, in input order, with every flag, the risk score and the signals that made it. Five answered addresses cost a credit, repeats inside the list are looked up once, reserved ranges cost nothing, and the finish is announced on an HMAC-signed webhook.
**Is there a free tier?** Every Spaw account receives 100 free credits at the start of each month with no card on file, and unused free credits accumulate. Spur does not publish a free tier on the pages checked.
## Sources (checked 2026-09-12)
- [Spur Context API documentation](https://docs.spur.us/context-api)
- [Spur data feeds documentation](https://docs.spur.us/feeds)
- [Spur pricing (answered 429 on 2026-09-12)](https://spur.us/pricing)
Spur is a trademark of its owner. Spaw is not affiliated with it.
Reference: https://spaw.co/compare/spur
---
# Spur alternatives for IP intelligence
IP intelligence APIs to consider instead of Spur, with the pricing model, free tier and price per 1,000 lookups each vendor publishes. Figures come from public pricing pages checked on 2026-09-12.
| Service | Pricing model | Free tier | Price per 1,000 at about 10k |
| --- | --- | --- | --- |
| [Spaw](https://spaw.co) | Pay-as-you-go credit packs whose credits never expire, or optional plans, billed monthly or yearly, that deposit the same credits every month for less and roll unused plan credits over up to three months' allowance; both through Stripe Checkout. One credit buys five answered IP lookups | 100 free credits every month, no card required | $1.80 per 1,000 IP lookups at 5 to the credit (Starter pack: 1,000 credits for $9), or $1.40 on the Starter monthly plan (1,000 credits for $7 a month) |
| [AbuseIPDB](https://www.abuseipdb.com) | Plans with daily request allowances: Individual free "Forever!", Basic $25/mo or $228/year, Premium $99/mo or $1,068/year, Enterprise custom | Individual: free forever with no credit card, 1,000 check and report requests, 100 block checks and 5 bulk reports a day | not on the pages read |
| [ip-api](https://ip-api.com) | Two tiers. The free endpoint: "Our endpoints are limited to 45 HTTP requests per minute from an IP address" and "We do not allow commercial use of the free endpoint". The Pro service: "€13.3 per month, when paying for 12 months", with "Unlimited requests", "SSL (HTTPS)", "Commercial use allowed" and a "99.9% SLA" ("If the general API uptime falls below 99.9% in a month, we will automatically extend your subscription for 10 days"). | The free endpoint, for non-commercial use only: 45 requests per minute per address over HTTP ("256-bit SSL encryption is not available for this free API") | not on the pages read |
| [IP2Location](https://www.ip2location.com) | IP2Location.io API, monthly query plans: Free $0 ("50K IP Geolocation API queries/month", "11 IP Attributes", "Attribution Required"), Starter "$49/month" ("150K IP Geolocation API queries/month", "20 IP Attributes"), Plus "$249/month" ("300K IP Geolocation API queries/month", "59 IP Attributes"), Security "$499/month" ("600K IP Geolocation API queries/month", "79 IP Attributes"); overage per increment Starter $10, Plus $50, Security $100. Databases, standard licence per year: DB1 "$99", DB2 "$399", DB8 "$1,299", DB18 "$2,799", with a site licence at ten times those figures. | "50K IP Geolocation API queries/month" with 11 attributes, basic geolocation and open-proxy detection only, "Attribution Required"; without a key "up to 1,000 queries per day" | not on the pages read |
| [ipapi](https://ipapi.co) | Monthly plans by lookup quota: Starter "$15 billed monthly" for 60,000 lookups, Professional "$25 billed monthly" for 150,000, Business "$49 billed monthly" for 500,000, Enterprise "$99 billed monthly" for 2,000,000, Premium "$199 billed monthly" for 6,000,000, Ultimate "$399 billed monthly" for 15,000,000; annual billing is 20% less. | A free trial of "up-to 1000 IP lookups in a day" (about 30,000 a month), "for testing & development purposes" and "Not for production use" | not on the pages read |
| [ipdata](https://ipdata.co) | Monthly plans by daily request quota: Personal "$10 per month" with "2,500 requests daily", Lite "$30 per month" with "10,000 requests daily", Startup "$50 per month" with "50,000 requests daily", Business "$120 per month" with "100,000 requests daily"; "20% off all annual plans" at the time of reading. | "1500 free requests daily", "restricted to non-commercial use"; on the free tier, over the quota "all further requests are blocked and you receive an email notification" | not on the pages read |
| [ipgeolocation.io](https://ipgeolocation.io) | Monthly credit plans, "A standard unified IP lookup uses 1 credit": Starter "$19/month" for "150000 API Credits Per month", Plus "$49/month" for 500,000, Pro "$79/month" for 1,000,000, Business "$129/month" for 2,000,000, Premium "$249/month" for 5,000,000, Enterprise "Contact Sales"; annual billing gives "2 months free". "Paid monthly plans reset every month. Paid annual plans reset every year". | "1000 API Credits Per day for life" with no credit card, "a hard limit of 1000 requests per day" and "No overages on Free"; the free plan returns "core location without the radius and confidence fields" | not on the pages read |
| [IPinfo](https://ipinfo.io) | Monthly plans by API request quota: Lite $0/mo, Core $26/mo for 150,000 requests, Max $163/mo for 250,000, Enterprise custom for "1M+". Database downloads are listed separately, "Starting at $1,000/month". | IPinfo Lite at "$0/mo", free forever with no credit card, giving unlimited API requests at country level only | not on the pages read |
| [IPQualityScore](https://www.ipqualityscore.com) | Monthly plans by lookup quota: Free $0 (1,000 lookups a month), Startup "As low as: $99 / Month" (5,000), SMB Basic "$499" (10,000), SMB+ "$999" (75,000), Enterprise custom. "Any valid request sent to our Proxy Detection, Email Verification, or Device Fingerprinting services including lookups performed through a batch CSV file check will consume a lookup credit." | 1,000 lookups a month, capped at 35 a day | not on the pages read |
| [Ipregistry](https://ipregistry.co) | Credits, "1 lookup = 1 request = 1 credit" (batch endpoints use "one credit per lookup in the batch request, plus 1"). Subscriptions: Starter "100,000 credits / mo" at "$29 $24 / month", Professional "300,000 credits / mo" at "$79 $66 / month", Business "1,150,000 credits / mo" at "$199 $166 / month", Scale "2,450,000 credits / mo" at "$399 $333 / month" (the lower figure is the annual rate). Pay-as-you-go packs: 15,000 credits for $10, 100,000 for $50, 250,000 for $100, 1,750,000 for $500. | "20,000 free credits on sign-up" with "No credit card, no time limit" | $0.67 (the $10 pack of 15,000 credits, $0.000667 a lookup) |
| [ipstack](https://ipstack.com) | Monthly plans by request quota: Free "100 Request/mo." at $0, Basic "50,000 Request/mo." at "$12.99/mo.", Professional "500,000 Request/mo." at "$59.99/mo.", Professional + "2,000,000 Request/mo." at "$99.99/mo.", Custom on request; yearly billing "Save up to 15%". Overage fees apply past the quota, with notifications at 75%, 90% and 100%. | "100 Request/mo." at $0, without SSL encryption, support or the paid modules (SSL, commercial use and the Location, Time Zone and Connection modules are listed for paid plans) | not on the pages read |
| [MaxMind](https://www.maxmind.com) | Per query, by service, with no minimum published: "If you estimate your volume of queries to be over $2,500 a month, contact us to discuss rates." | A no-cost trial is offered ("Experience the functionality and benefits of our GeoIP web services at no cost"); no free monthly allowance is stated on the page checked | $0.10 per 1,000 for Country ($0.0001 a query), $0.30 for City Plus ($0.0003) and $2.00 for Insights ($0.002) |
| [proxycheck.io](https://proxycheck.io) | not on the pages read | not on the pages read | not on the pages read |
## Spaw
Verification API that bills per answered lookup across six signals on one key and one balance: Email verification, Phone intelligence, IP intelligence, Address verification, Business verification against open registers, and Cross-signal consistency, which runs any subset of the first four in one call and names where they agree and where they do not. The IP side answers from local databases and compiled public feeds: where an address is, who runs it, and whether it is a visitor at all.
## AbuseIPDB
AbuseIPDB is a reputation service built on abuse reports its own users submit: a check returns an abuse confidence score with the report count, country, usage type, ISP and a Tor flag, and plans are sold as daily request allowances rather than credits.
Comparison: https://spaw.co/compare/abuseipdb
## ip-api
ip-api is a geolocation JSON API with a free, non-commercial, HTTP-only endpoint limited by request rate, and a Pro subscription that adds HTTPS, unlimited queries and commercial use; its answer carries one proxy flag, one hosting flag and one mobile flag beside the location and network fields.
Comparison: https://spaw.co/compare/ip-api
## IP2Location
IP2Location sells geolocation as downloadable databases (DB1 to DB26, licensed yearly) and, through IP2Location.io, as an API on monthly query plans whose higher tiers add proxy, VPN, Tor, datacenter, crawler and residential-proxy detection and a fraud score; the classic web service no longer takes new registrations.
Comparison: https://spaw.co/compare/ip2location
## ipapi
ipapi.co is a geolocation API sold as monthly lookup quotas, whose pricing page credits its data to DB-IP and the IP2Location LITE database; the answer carries location, country and network fields and no proxy, VPN or Tor flag.
Comparison: https://spaw.co/compare/ipapi
## ipdata
ipdata is a geolocation and threat API sold as daily request quotas, from a free non-commercial tier to a Business plan, whose pricing page says it does not keep records of the addresses looked up beyond a day; its documentation site refused automated reading, so the fields are not reproduced here.
Comparison: https://spaw.co/compare/ipdata
## ipgeolocation.io
ipgeolocation.io sells geolocation and a separate security API on monthly credit plans, with location built from its own measurements and a security answer that flags VPN, proxy, residential proxy and Tor with provider names, confidence scores and a 0 to 100 threat score.
Comparison: https://spaw.co/compare/ipgeolocation
## IPinfo
IPinfo is an IP data company that sells geolocation, ASN, privacy detection and company data as monthly API plans and as database downloads, collected through its own measurement network rather than assembled from third-party feeds.
Comparison: https://spaw.co/compare/ipinfo
## IPQualityScore
IPQualityScore sells fraud detection across several signals on one lookup credit: proxy and VPN detection, email verification and device fingerprinting all draw from the same monthly quota, and the IP answer carries a fraud score alongside the connection facts.
Comparison: https://spaw.co/compare/ipqualityscore
## Ipregistry
Ipregistry is a geolocation and threat API sold as credits, either monthly subscriptions or pay-as-you-go packs, whose security object answers abuser, attacker, bogon, cloud provider, proxy, relay, Tor, VPN, anonymous and threat flags and whose data is described as built from BGP tables, registry databases, geofeeds and latency measurements.
Comparison: https://spaw.co/compare/ipregistry
## ipstack
ipstack is a geolocation API on monthly request quotas from a 100-request free tier to a two-million-request plan, with a bulk endpoint and a security module reserved for its top plan; its documentation pages rendered only a header to our fetcher, so the field list is not reproduced here.
Comparison: https://spaw.co/compare/ipstack
## MaxMind
MaxMind sells GeoIP as three per-query web services — Country, City Plus and Insights — and as downloadable databases, with proxy and anonymiser detection reserved for the Insights tier.
Comparison: https://spaw.co/compare/maxmind
## proxycheck.io
proxycheck.io sells an API for checking whether an address is a proxy; every page we tried on 2026-09-12 answered 403 Forbidden to automated reading, so nothing about its pricing, limits, fields or data is reproduced here.
Comparison: https://spaw.co/compare/proxycheck
Spur and the other product names are trademarks of their owners. Spaw is not affiliated with them.
Reference: https://spaw.co/alternatives/spur
---
# Spaw vs Telnyx Number Lookup: phone intelligence API comparison
How Spaw and Telnyx Number Lookup compare on pricing, billing rules, where the line type comes from and limits, from Telnyx Number Lookup's public pages checked on 2026-09-27. Corrections: support@spaw.co.
| | Spaw | Telnyx Number Lookup |
| --- | --- | --- |
| Pricing model | Pay-as-you-go credit packs whose credits never expire, or optional plans, billed monthly or yearly, that deposit the same credits every month for less and roll unused plan credits over up to three months' allowance; both through Stripe Checkout | Per query, priced per lookup type, with "$0 platform fee"; pay as you go gets "Automatic tier discounts as usage crosses thresholds", committed plans start at $500 a month, and inbound CNAM is $0.40 per number per month |
| Free tier | 100 free credits every month, no card required | An outbound CNAM listing at no charge; the pages read list no free query allowance |
| Price per 1,000 at about 10k | $7.80 (Growth pack: 5,000 credits for $39), or $5.80 on the Growth monthly plan (5,000 credits for $29 a month) | $1.50 per 1,000 for a local routing number query ($0.0015 a query), $2.50 for MCC/MNC ($0.0025) and $3.00 for CNAM ($0.003) |
| Price per 1,000 at about 100k | $4.49 (Volume pack: 100,000 credits for $449), or $3.49 on the Volume monthly plan (100,000 credits for $349 a month) | No tier rate is published: the page mentions "Automatic tier discounts as usage crosses thresholds", and its worked example prices 1,000,000 queries at $1,500 on the base rate, "before volume tiers" |
| Numbers the vendor could not settle billed | A valid number costs 1 credit whatever its flags say. The live carrier-network check (hlr: true) is not enabled on the service: hlr_checked answers false and nothing extra is charged. | not on the pages read |
| Invalid numbers billed | No: invalid numbers are free, including a number in a block the regulator lists as unassigned, and so are repeats within 7 days | not on the pages read |
| Line type source | The numbering plan metadata everywhere except the North American plan, where mobiles and landlines share blocks and the plan can only say fixed_line_or_mobile; there the regulator's block holder settles it — a wireless carrier's block is mobile, a virtual-number wholesaler's is voip. line_type_source names which of the two answered. | Not named. The product page lists line type ("Mobile, landline, or VoIP") among the fields a lookup returns from "Live telecom databases, including HLR lookup"; the pricing page says portability and carrier come from "the network's own data" and CNAM from the registry. |
| Numbering plan coverage | libphonenumber metadata for every region it carries, plus the regulators' own allocation tables, each named in the answer with the date of the copy it read. Complete tables, where a geographic number in a block the table leaves out answers unassigned_block: NANPA for the United States (with Puerto Rico, the U.S. Virgin Islands, Guam, American Samoa and the Northern Mariana Islands), the CNAC for Canada, ARCEP for France (with Guadeloupe, St. Barthélemy, St. Martin, Martinique, French Guiana, Réunion, Mayotte and St. Pierre & Miquelon), ACM for the Netherlands and the CRT (formerly the IFT) for Mexico. Partial tables, answered only from the ranges they list (a range listed as free, spare or held in reserve answers unassigned_block), so block_status answers null for a number outside them: Ofcom for the United Kingdom (with Guernsey, Jersey and the Isle of Man), ACMA for Australia, the NAD for New Zealand, UKE for Poland and MIMIT for Italy. Re-synced weekly, NANPA daily; /status lists each table's age. Outside those regions, block_status and is_virtual answer null rather than a guess. | The docs say "any phone number worldwide", with caller name (CNAM) for US numbers; the pages read name no numbering plan or regulator table. |
| Live network check | Reserved behind hlr: true: 4 credits on top of the base credit, only where the network answers, reported in its own reachable, ported, current_carrier and roaming fields. The live carrier-network check (hlr: true) is not enabled on the service: hlr_checked answers false and nothing extra is charged. | Described but not priced on its own: the product page says "HLR lookup confirms the live carrier for mobile numbers" and whether a mobile number is active on the network; the pricing page lists LRN, MCC/MNC and CNAM rows and no HLR row. |
| Ported numbers | None from the tables, and the docs say so: carrier and line_type describe the block the number was allocated in, never a later port. Only a live carrier-network check can say who serves a number today. | Sold as the product: the local routing number identifies "the serving switch", and ported status "reflects the actual serving carrier today, not the original carrier", which a numbering plan or an allocation table cannot answer |
| SMS pumping controls | Flags in the risk score, and an SMS policy that turns them into ok_to_send and blocked_by: a fraud and deliverability decision, not consent and not a do-not-call scrub. allowed_countries limits sending to the regions you serve; it is empty (any country) until you set it. range_burst (+40) flags a number once your account has looked up 5 distinct numbers in its 1,000-number range within 60 minutes, and numbers_per_client (+30) once one client address (the client_ip you send, or a browser visitor's own) has tried 5 within 60 minutes, unless Spaw's own IP data places it on a mobile carrier or a corporate proxy, or it is private or reserved. Both count only your own single and browser lookups, so a new account has no history. The range is a heuristic, and in North America a burst more likely means someone testing numbers against a form; the client count does not stop an attack rotated through residential proxies. disposable (+70) flags a number SMS-receiving websites publish and disposable_stale (+40) an old listing; disposable_range (+30) flags an unlisted number in a 1,000-number range holding 10 or more fresh listings. The lists are public and synced daily. The default policy blocks the fictional, disposable, reported_abuse, reported_abuse_widely and premium_rate signals and a risk_score above 60, so neither traffic signal blocks on its own. | No pumping score or price row on the pages read. The product page's FAQ says a lookup helps against SMS pumping by "identifying line type and carrier in real time", so a VoIP, recently ported or high-risk-carrier number can be blocked or challenged before sending; the pricing page mentions "fraud signals from the identity layer" without a row for them. |
| Bulk limit | 100,000 numbers per bulk job, with an HMAC-signed completion webhook | No limit stated; the FAQ says the API "supports bulk lookups for high-throughput use cases" across "databases of millions of numbers" |
| Batch API | 1,000 numbers per synchronous request, answered in input order | No batch size or endpoint named; the product page says the API "handles batch phone number validation" |
| Rate limit | 50 requests per second per key on the phone endpoints | Not stated for pay as you go; committed plans (from $500 a month) list "higher limits" and enterprise plans (from $5,000 a month) "unlimited rate limits" |
| Official SDKs | Python, Node.js, PHP | TypeScript / Node.js, Python, Go, Java, Ruby, PHP |
| Data retention | No history row and no log line carrying the number. What a lookup leaves is keyed and never holds the number: the 7-day marker that makes a repeat free (a second one when the account paid for a live check); for single and browser lookups, a truncated keyed hash of the number filed under keyed hashes of its range and client address for 60 minutes, and the account's lookups per country and hour as counts for 30 days; and a live check's answer, held for a day under a hash of the number. The number itself is kept only in the account's own outcome reports, phone suppression list and monitors. Bulk input lists and result files are deleted with the job after 30 days. | not on the pages read |
## When Telnyx Number Lookup fits better
Teams that need to know who serves a number today, on numbers that have been ported, priced per dip from $0.0015 a query.
- A local routing number query is $0.0015 on the published rate card, before the volume tier discounts the page mentions
- Portability is the product rather than a side effect: an LRN query is priced and sold on its own
- No platform fee is charged for access
- The rate card is also published machine-readable, at GET /v2/public/pricing
## When Spaw fits better
Developers who want the numbering plan and the regulators' own allocation tables read for them, the dataset behind every field named in the answer, and an SMS policy that turns the evidence into ok_to_send and blocked_by — a fraud and deliverability decision, not consent and not a do-not-call scrub. Every answer is made on the server: the opt-in live carrier-network check, the one call that would send a number to a partner, is not enabled on the service.
- Telnyx answers portability from carrier data; Spaw's carrier and line_type describe the original allocation and never a later port, and the docs say so
- Telnyx prices each dip separately; Spaw returns 49 fields, the risk score and its signals in one 1-credit answer
- Telnyx sells CNAM caller name for US numbers ($0.003 a query); Spaw returns no owner, subscriber or caller name by design and does no reverse lookup
- Spaw's answers come from local tables, and a lookup keeps no history, only keyed hashes and counts of your own traffic. The one call that would send a number off the server is the opt-in live check. The live carrier-network check (hlr: true) is not enabled on the service: hlr_checked answers false and nothing extra is charged.
- The Telnyx pages read list no free query allowance; every Spaw account gets 100 free credits a month with no card
## Questions
**Does Spaw charge for numbers it cannot settle?** Spaw bills per answer. A number the numbering plan and, where one is installed, the regulator's own table agree is assignable costs 1 credit whatever its flags say. An invalid number is free — including one in a block the regulator lists as unassigned — and so is a repeat of the same number within seven days. The live carrier-network check (hlr: true) is not enabled on the service: hlr_checked answers false and nothing extra is charged. Telnyx Number Lookup does not publish how it bills them.
**Can I move a list of numbers from Telnyx Number Lookup to Spaw?** Yes. Upload the CSV in the dashboard or POST it to the bulk endpoint, up to 100,000 numbers a job; Spaw keeps your columns, adds the answer columns, and lets you download the whole file or only the valid, invalid, low, medium or high-risk rows. A country column gives each row its own region, so a mixed international list needs no splitting.
**Is there a free tier?** Every Spaw account receives 100 free credits at the start of each month with no card on file, and unused free credits accumulate. Telnyx Number Lookup offers: An outbound CNAM listing at no charge; the pages read list no free query allowance.
## Sources (checked 2026-09-27)
- [Telnyx Number Lookup pricing](https://telnyx.com/pricing/number-lookup)
- [Telnyx Number Lookup product page and FAQ](https://telnyx.com/products/number-lookup)
- [Telnyx Number Lookup API docs](https://developers.telnyx.com/docs/identity/number-lookup)
- [Telnyx server-side SDKs](https://developers.telnyx.com/docs/development/sdk)
Telnyx Number Lookup is a trademark of its owner. Spaw is not affiliated with it.
Reference: https://spaw.co/compare/telnyx-lookup
---
# Telnyx Number Lookup alternatives for phone intelligence
Phone intelligence APIs to consider instead of Telnyx Number Lookup, with the pricing model, free tier and price per 1,000 lookups each vendor publishes. Figures come from public pricing pages checked on 2026-09-27.
| Service | Pricing model | Free tier | Price per 1,000 at about 10k |
| --- | --- | --- | --- |
| [Spaw](https://spaw.co) | Pay-as-you-go credit packs whose credits never expire, or optional plans, billed monthly or yearly, that deposit the same credits every month for less and roll unused plan credits over up to three months' allowance; both through Stripe Checkout | 100 free credits every month, no card required | $7.80 (Growth pack: 5,000 credits for $39), or $5.80 on the Growth monthly plan (5,000 credits for $29 a month) |
| [Abstract Phone Validation](https://www.abstractapi.com/api/phone-validation-api) | Monthly request quotas on three plans that differ by the data returned: Starter (validation, location, carrier) from $19 a month for 5,000 requests to $199 for 150,000; Standard (adds messaging) from $19 for 3,000 to $199 for 100,000; Professional (adds registration and risk) from $19 for 2,000 to $199 for 50,000. Annual billing saves 10%; the pricing page quotes $3.40 per 1,000 requests at Starter's entry volume, billed annually | 100 requests a month with no card, on the full response including registration and risk | not on the pages read |
| [ClearoutPhone](https://clearoutphone.io) | Credits that never expire, bought one time or through monthly or annual subscriptions that add them each period and carry them forward. One Time, each shown as a starting price: 5,000 credits from $35, 10,000 from $58, 100,000 from $400 and 1,000,000 from $1,200. Standard validation is 1 credit; Smart validation is 1 credit for most numbers and up to 5 when it runs a deeper real-time check | 100 free credits on sign-up, one validation each | $5.80 per 1,000 one time (10,000 credits for $58), $5.20 on the monthly plan (10,000 credits for $52 a month) or $3.20 on the annual plan as its tab rendered ("Buy 120,000 credits for $384 billed yearly", 10,000 a month), although the page labels annual billing "Save 20%" against one-time prices; a Smart validation that runs the deeper check costs up to 5 credits |
| [IPQualityScore Phone Validation](https://www.ipqualityscore.com/solutions/phone-validation) | Monthly plans shared with IPQualityScore's other checks: Free $0 (1,000 lookups a month), Startup "As low as: $99 / Month" (5,000), SMB Basic "As low as: $499 / Month" (10,000), SMB+ "As low as: $999 / Month" (75,000), Enterprise custom; "Each plan requires a minimum monthly commitment". The phone validation page says "Score phone numbers on average from $0.0005 per user with no monthly contracts", on "tiered flat rate pricing across all countries that reduces as your monthly volume increases", without a tier table. Add-ons are enabled through the account manager or Support at no stated price. | 1,000 lookups a month on the Free plan, capped at 35 a day; the plan also covers IP, email and URL checks | not on the pages read |
| [Loqate Phone Validation](https://www.loqate.com/en-gb/phone-validation/) | Pay-as-you-go credit purchases of £50, £100, £250, £500 and £1,000 (or 75€, 150€ and 300€), each buying a published per-lookup rate for every service, plus bespoke plans; "Credit is valid for 12 months after the purchase date". Phone Validation on the "New Pricing" table is 7.1p, 6.7p, 5.6p, 4.7p and 4.1p a lookup across the pound tiers and 8.6c, 7.8c and 6.6c across the euro ones. The page also shows an "Old Pricing" table without saying which accounts each applies to; the figures here are from "New Pricing". Its usage-example selector also lists a £2,500 plan that neither table prices, and all prices exclude VAT. | A trial rather than a standing tier: "Start your 45-day free trial"; the FAQ adds "You get free credits to test the service" | £47 per 1,000 (4.7p a lookup) on the £500 credit, which buys about 10,600 lookups; the smaller credits run from 7.1p (£50) to 5.6p (£250) |
| [numlookupapi](https://numlookupapi.com) | Monthly subscription tiers by request quota: Free $0 for 100 requests, Basic $9.99 for 5,000, Pro $49.99 for 50,000, Scale $109.99 for 250,000, and Custom by contact; the page offers yearly billing at 20% off. Scale and Custom list "Overages" with no overage price shown, and the quota documentation says a request over the monthly quota answers 429 until the plan is upgraded or the month ends. | 100 requests a month on one API key, at 10 requests a minute | not on the pages read |
| [numverify](https://numverify.com) | Monthly subscription tiers by request quota: Free 100 requests, Basic $14.99 for 5,000, Professional $59.99 for 50,000, Enterprise $129.99 for 250,000, each with a published overage price per call ($0.011992, $0.0047992 and $0.00207984 respectively); annual billing saves up to 15%, and custom pricing is offered above the Enterprise quota | 100 API requests a month, for non-commercial use | not on the pages read |
| [Twilio Lookup](https://www.twilio.com/en-us/user-authentication-identity/lookup) | Per request, priced per data package on a Twilio account ("Pricing current as of August 2026"); line status and reassigned number risk are tiered by query volume, and SIM swap and call forwarding are contact sales | Formatting and validation are listed as Free, and so is the SMS Pumping Risk Score for North American numbers; no free allowance is published for the other packages | $8.00 per 1,000 for line type intelligence ($0.008 per request), $10.00 for inbound caller identification ($0.01, US only), $100.00 for identity match ($0.1, which the page says varies by country) |
| [Veriphone](https://veriphone.io) | One-time credit packs that never expire, from 10,000 credits for $24 to 5,000,000 for $999, or monthly plans whose credits reset each month: Starter $12.99 for 10,000, Professional $39 for 50,000, Business $99 for 250,000 and Enterprise $249 for 1,000,000. A standard validation is 1 credit and a Current Carrier Lookup 10 | 1,000 free credits a month with no card, not carried over; on the free plan a Current Carrier Lookup draws on purchased credits only | $2.40 per 1,000 on the 10,000-credit pack ($24, credits never expire), or $1.30 on the Starter plan (10,000 credits a month for $12.99, reset each month). A Current Carrier Lookup takes 10 credits, so ten times those figures |
## Spaw
Verification API that bills per answered lookup across six signals on one key and one balance: Email verification, Phone intelligence, IP intelligence, Address verification, Business verification against open registers, and Cross-signal consistency, which runs any subset of the first four in one call and names where they agree and where they do not. The phone side reads the numbering plan and the regulators' own allocation tables to name the line type, the block holder and the blocks nobody holds, and flags the numbers that are not a person's handset.
## Abstract Phone Validation
Abstract Phone Validation is a phone API from Abstract sold as monthly request quotas on three data tiers, returning carrier, line type and location, with messaging from Standard up and registration and risk on Professional and the Free plan; the Professional example response also carries breach history.
Comparison: https://spaw.co/compare/abstract-phone
## ClearoutPhone
ClearoutPhone is a phone validation service from Clearout sold as credits that never expire, with a 1-credit Standard mode and a Smart mode that runs a real-time telecom query, at up to 5 credits, where static data is uncertain.
Comparison: https://spaw.co/compare/clearoutphone
## IPQualityScore Phone Validation
IPQualityScore Phone Validation is the phone side of a fraud-scoring service: one API returns validity, carrier, line type and a 0 to 100 fraud score, with live-line, identity, do-not-call and SMS-pumping data as add-ons, on the same monthly plans as its IP, email and URL checks.
Comparison: https://spaw.co/compare/ipqualityscore-phone
## Loqate Phone Validation
Loqate Phone Validation checks a number's format, carrier and, for mobile numbers, its live network status through an HLR lookup, priced per lookup from the same pay-as-you-go credit balance as Loqate's address, email and bank validation.
Comparison: https://spaw.co/compare/loqate-phone
## numlookupapi
numlookupapi is a phone number validation API sold as a monthly request quota: one validate endpoint returns validity, formats, country, location, carrier and line type.
Comparison: https://spaw.co/compare/numlookupapi
## numverify
numverify is a phone number validation API sold as a monthly request quota, returning validity, country and location, carrier and line type.
Comparison: https://spaw.co/compare/numverify
## Twilio Lookup
Twilio Lookup is a phone number API sold as separately priced data packages on top of a Twilio account: formatting and validation are free, and line type, caller identity, line status and the risk packages are each priced on their own, per request (the SMS Pumping Risk Score is free for North American numbers) or through sales for SIM swap and call forwarding.
Comparison: https://spaw.co/compare/twilio-lookup
## Veriphone
Veriphone is a phone validation API sold as credit packs or monthly plans, returning validity, carrier, line type and region from national numbering plans, with a 10-credit Current Carrier Lookup read from national portability registries.
Comparison: https://spaw.co/compare/veriphone
Telnyx Number Lookup and the other product names are trademarks of their owners. Spaw is not affiliated with them.
Reference: https://spaw.co/alternatives/telnyx-lookup
---
# Spaw vs Twilio Lookup: phone intelligence API comparison
How Spaw and Twilio Lookup compare on pricing, billing rules, where the line type comes from and limits, from Twilio Lookup's public pages checked on 2026-09-27. Corrections: support@spaw.co.
| | Spaw | Twilio Lookup |
| --- | --- | --- |
| Pricing model | Pay-as-you-go credit packs whose credits never expire, or optional plans, billed monthly or yearly, that deposit the same credits every month for less and roll unused plan credits over up to three months' allowance; both through Stripe Checkout | Per request, priced per data package on a Twilio account ("Pricing current as of August 2026"); line status and reassigned number risk are tiered by query volume, and SIM swap and call forwarding are contact sales |
| Free tier | 100 free credits every month, no card required | Formatting and validation are listed as Free, and so is the SMS Pumping Risk Score for North American numbers; no free allowance is published for the other packages |
| Price per 1,000 at about 10k | $7.80 (Growth pack: 5,000 credits for $39), or $5.80 on the Growth monthly plan (5,000 credits for $29 a month) | $8.00 per 1,000 for line type intelligence ($0.008 per request), $10.00 for inbound caller identification ($0.01, US only), $100.00 for identity match ($0.1, which the page says varies by country) |
| Price per 1,000 at about 100k | $4.49 (Volume pack: 100,000 credits for $449), or $3.49 on the Volume monthly plan (100,000 credits for $349 a month) | The same rates: line type intelligence carries no published volume tier. Line status is tiered from $0.007 per query (0 to 100k) down to $0.00385 (over 10M), and reassigned number risk from $0.02 (0 to 1,000) down to $0.0015 (6M+). |
| Numbers the vendor could not settle billed | A valid number costs 1 credit whatever its flags say. The live carrier-network check (hlr: true) is not enabled on the service: hlr_checked answers false and nothing extra is charged. | not on the pages read |
| Invalid numbers billed | No: invalid numbers are free, including a number in a block the regulator lists as unassigned, and so are repeats within 7 days | not on the pages read |
| Line type source | The numbering plan metadata everywhere except the North American plan, where mobiles and landlines share blocks and the plan can only say fixed_line_or_mobile; there the regulator's block holder settles it — a wireless carrier's block is mobile, a virtual-number wholesaler's is voip. line_type_source names which of the two answered. | Not stated beyond Canada, where the docs require approved access to Canadian Number Portability Administration Center (NPAC) data. The line type intelligence documentation lists twelve types (landline, mobile, fixedVoip, nonFixedVoip, personal, tollFree, premium, sharedCost, uan, voicemail, pager, unknown) and returns carrier_name with mobile_country_code and mobile_network_code, but does not say which data settles them elsewhere. |
| Numbering plan coverage | libphonenumber metadata for every region it carries, plus the regulators' own allocation tables, each named in the answer with the date of the copy it read. Complete tables, where a geographic number in a block the table leaves out answers unassigned_block: NANPA for the United States (with Puerto Rico, the U.S. Virgin Islands, Guam, American Samoa and the Northern Mariana Islands), the CNAC for Canada, ARCEP for France (with Guadeloupe, St. Barthélemy, St. Martin, Martinique, French Guiana, Réunion, Mayotte and St. Pierre & Miquelon), ACM for the Netherlands and the CRT (formerly the IFT) for Mexico. Partial tables, answered only from the ranges they list (a range listed as free, spare or held in reserve answers unassigned_block), so block_status answers null for a number outside them: Ofcom for the United Kingdom (with Guernsey, Jersey and the Isle of Man), ACMA for Australia, the NAD for New Zealand, UKE for Poland and MIMIT for Italy. Re-synced weekly, NANPA daily; /status lists each table's age. Outside those regions, block_status and is_virtual answer null rather than a guess. | "Line Type Intelligence is available for phone numbers worldwide"; Canadian numbers need special approval for Canadian Number Portability Administration Center (NPAC) data and answer error 60601 without it. Carrier data is documented as unavailable for the personal, tollFree, premium, sharedCost, uan, voicemail, pager and unknown types. |
| Live network check | Reserved behind hlr: true: 4 credits on top of the base credit, only where the network answers, reported in its own reachable, ported, current_carrier and roaming fields. The live carrier-network check (hlr: true) is not enabled on the service: hlr_checked answers false and nothing extra is charged. | Sold as its own package: line status, which checks whether a number is "active or reachable on its carrier network", tiered from $0.007 per query. Call forwarding (UK only) and SIM swap are listed as contact sales rather than priced. |
| Ported numbers | None from the tables, and the docs say so: carrier and line_type describe the block the number was allocated in, never a later port. Only a live carrier-network check can say who serves a number today. | not on the pages read |
| SMS pumping controls | Flags in the risk score, and an SMS policy that turns them into ok_to_send and blocked_by: a fraud and deliverability decision, not consent and not a do-not-call scrub. allowed_countries limits sending to the regions you serve; it is empty (any country) until you set it. range_burst (+40) flags a number once your account has looked up 5 distinct numbers in its 1,000-number range within 60 minutes, and numbers_per_client (+30) once one client address (the client_ip you send, or a browser visitor's own) has tried 5 within 60 minutes, unless Spaw's own IP data places it on a mobile carrier or a corporate proxy, or it is private or reserved. Both count only your own single and browser lookups, so a new account has no history. The range is a heuristic, and in North America a burst more likely means someone testing numbers against a form; the client count does not stop an attack rotated through residential proxies. disposable (+70) flags a number SMS-receiving websites publish and disposable_stale (+40) an old listing; disposable_range (+30) flags an unlisted number in a 1,000-number range holding 10 or more fresh listings. The lists are public and synced daily. The default policy blocks the fictional, disposable, reported_abuse, reported_abuse_widely and premium_rate signals and a risk_score above 60, so neither traffic signal blocks on its own. | Sold as its own package, the SMS Pumping Risk Score: $0.025 per request outside North America and free within it. The docs describe a 0 to 100 score from "a proprietary risk model built from Twilio's network data", specific to the Twilio account, returned with a carrier risk category and whether Verify Fraud Guard, part of Twilio's Verify product, has blocked the number. The docs do not recommend it for US or Canadian numbers. |
| Bulk limit | 100,000 numbers per bulk job, with an HMAC-signed completion webhook | not on the pages read |
| Batch API | 1,000 numbers per synchronous request, answered in input order | not on the pages read |
| Rate limit | 50 requests per second per key on the phone endpoints | not on the pages read |
| Official SDKs | Python, Node.js, PHP | C# / .NET, Java, Node.js, PHP, Python, Ruby, Go |
| Data retention | No history row and no log line carrying the number. What a lookup leaves is keyed and never holds the number: the 7-day marker that makes a repeat free (a second one when the account paid for a live check); for single and browser lookups, a truncated keyed hash of the number filed under keyed hashes of its range and client address for 60 minutes, and the account's lookups per country and hour as counts for 30 days; and a live check's answer, held for a day under a hash of the number. The number itself is kept only in the account's own outcome reports, phone suppression list and monitors. Bulk input lists and result files are deleted with the job after 30 days. | not on the pages read |
## When Twilio Lookup fits better
Teams already sending messages through Twilio that want line type, caller identity and SMS-pumping risk billed on the same account as their traffic.
- Formatting and validation are free; every other signal is a package priced on its own, per request or through sales
- Line type intelligence returns carrier_name, mobile_country_code and mobile_network_code alongside twelve line types
- SMS pumping risk score is $0.025 per request outside North America and free within it
- Seven official server-side SDKs
## When Spaw fits better
Developers who want the numbering plan and the regulators' own allocation tables read for them, the dataset behind every field named in the answer, and an SMS policy that turns the evidence into ok_to_send and blocked_by — a fraud and deliverability decision, not consent and not a do-not-call scrub. Every answer is made on the server: the opt-in live carrier-network check, the one call that would send a number to a partner, is not enabled on the service.
- Twilio prices each signal as its own package; Spaw answers line type, carrier, block status and the fictional, virtual and disposable flags in one 1-credit lookup
- Twilio's pages name no source for its line type data beyond NPAC access for Canadian numbers; on every valid answer Spaw's line_type_source says whether the numbering plan or, for a North American number, the block holder in the regulator's table settled it, and every answer's sources block names each dataset it consulted with its version or sync date
- The Twilio pages read give no bulk or batch limit for Lookup; Spaw takes 1,000 numbers per batch request and 100,000 per bulk job
- Twilio ships seven server-side SDKs to Spaw's three plus an MCP server
- Twilio sells inbound caller identification ($0.01 a request, US only); Spaw returns no owner, subscriber or caller name by design and does no reverse lookup
- Twilio sells SMS pumping risk as its own package; Spaw's range_burst, numbers_per_client and disposable_range signals and the account's SMS policy (ok_to_send, blocked_by and an allowed_countries allow-list), a fraud and deliverability decision rather than consent or a do-not-call scrub, come in the same 1-credit lookup, counted from your own account's lookups and public lists of SMS-receiving numbers rather than from a network's traffic model
## Questions
**Does Spaw charge for numbers it cannot settle?** Spaw bills per answer. A number the numbering plan and, where one is installed, the regulator's own table agree is assignable costs 1 credit whatever its flags say. An invalid number is free — including one in a block the regulator lists as unassigned — and so is a repeat of the same number within seven days. The live carrier-network check (hlr: true) is not enabled on the service: hlr_checked answers false and nothing extra is charged. Twilio Lookup does not publish how it bills them.
**Can I move a list of numbers from Twilio Lookup to Spaw?** Yes. Upload the CSV in the dashboard or POST it to the bulk endpoint, up to 100,000 numbers a job; Spaw keeps your columns, adds the answer columns, and lets you download the whole file or only the valid, invalid, low, medium or high-risk rows. A country column gives each row its own region, so a mixed international list needs no splitting.
**Is there a free tier?** Every Spaw account receives 100 free credits at the start of each month with no card on file, and unused free credits accumulate. Twilio Lookup offers: Formatting and validation are listed as Free, and so is the SMS Pumping Risk Score for North American numbers; no free allowance is published for the other packages.
## Sources (checked 2026-09-27)
- [Twilio Lookup pricing](https://www.twilio.com/en-us/user-authentication-identity/pricing/lookup)
- [Twilio Lookup product page](https://www.twilio.com/en-us/user-authentication-identity/lookup)
- [Twilio Lookup line type intelligence docs](https://www.twilio.com/docs/lookup/v2-api/line-type-intelligence)
- [Twilio Lookup SMS Pumping Risk Score docs](https://www.twilio.com/docs/lookup/v2-api/sms-pumping-risk)
- [Twilio server-side SDKs](https://www.twilio.com/docs/libraries)
Twilio Lookup is a trademark of its owner. Spaw is not affiliated with it.
Reference: https://spaw.co/compare/twilio-lookup
---
# Twilio Lookup alternatives for phone intelligence
Phone intelligence APIs to consider instead of Twilio Lookup, with the pricing model, free tier and price per 1,000 lookups each vendor publishes. Figures come from public pricing pages checked on 2026-09-27.
| Service | Pricing model | Free tier | Price per 1,000 at about 10k |
| --- | --- | --- | --- |
| [Spaw](https://spaw.co) | Pay-as-you-go credit packs whose credits never expire, or optional plans, billed monthly or yearly, that deposit the same credits every month for less and roll unused plan credits over up to three months' allowance; both through Stripe Checkout | 100 free credits every month, no card required | $7.80 (Growth pack: 5,000 credits for $39), or $5.80 on the Growth monthly plan (5,000 credits for $29 a month) |
| [Abstract Phone Validation](https://www.abstractapi.com/api/phone-validation-api) | Monthly request quotas on three plans that differ by the data returned: Starter (validation, location, carrier) from $19 a month for 5,000 requests to $199 for 150,000; Standard (adds messaging) from $19 for 3,000 to $199 for 100,000; Professional (adds registration and risk) from $19 for 2,000 to $199 for 50,000. Annual billing saves 10%; the pricing page quotes $3.40 per 1,000 requests at Starter's entry volume, billed annually | 100 requests a month with no card, on the full response including registration and risk | not on the pages read |
| [ClearoutPhone](https://clearoutphone.io) | Credits that never expire, bought one time or through monthly or annual subscriptions that add them each period and carry them forward. One Time, each shown as a starting price: 5,000 credits from $35, 10,000 from $58, 100,000 from $400 and 1,000,000 from $1,200. Standard validation is 1 credit; Smart validation is 1 credit for most numbers and up to 5 when it runs a deeper real-time check | 100 free credits on sign-up, one validation each | $5.80 per 1,000 one time (10,000 credits for $58), $5.20 on the monthly plan (10,000 credits for $52 a month) or $3.20 on the annual plan as its tab rendered ("Buy 120,000 credits for $384 billed yearly", 10,000 a month), although the page labels annual billing "Save 20%" against one-time prices; a Smart validation that runs the deeper check costs up to 5 credits |
| [IPQualityScore Phone Validation](https://www.ipqualityscore.com/solutions/phone-validation) | Monthly plans shared with IPQualityScore's other checks: Free $0 (1,000 lookups a month), Startup "As low as: $99 / Month" (5,000), SMB Basic "As low as: $499 / Month" (10,000), SMB+ "As low as: $999 / Month" (75,000), Enterprise custom; "Each plan requires a minimum monthly commitment". The phone validation page says "Score phone numbers on average from $0.0005 per user with no monthly contracts", on "tiered flat rate pricing across all countries that reduces as your monthly volume increases", without a tier table. Add-ons are enabled through the account manager or Support at no stated price. | 1,000 lookups a month on the Free plan, capped at 35 a day; the plan also covers IP, email and URL checks | not on the pages read |
| [Loqate Phone Validation](https://www.loqate.com/en-gb/phone-validation/) | Pay-as-you-go credit purchases of £50, £100, £250, £500 and £1,000 (or 75€, 150€ and 300€), each buying a published per-lookup rate for every service, plus bespoke plans; "Credit is valid for 12 months after the purchase date". Phone Validation on the "New Pricing" table is 7.1p, 6.7p, 5.6p, 4.7p and 4.1p a lookup across the pound tiers and 8.6c, 7.8c and 6.6c across the euro ones. The page also shows an "Old Pricing" table without saying which accounts each applies to; the figures here are from "New Pricing". Its usage-example selector also lists a £2,500 plan that neither table prices, and all prices exclude VAT. | A trial rather than a standing tier: "Start your 45-day free trial"; the FAQ adds "You get free credits to test the service" | £47 per 1,000 (4.7p a lookup) on the £500 credit, which buys about 10,600 lookups; the smaller credits run from 7.1p (£50) to 5.6p (£250) |
| [numlookupapi](https://numlookupapi.com) | Monthly subscription tiers by request quota: Free $0 for 100 requests, Basic $9.99 for 5,000, Pro $49.99 for 50,000, Scale $109.99 for 250,000, and Custom by contact; the page offers yearly billing at 20% off. Scale and Custom list "Overages" with no overage price shown, and the quota documentation says a request over the monthly quota answers 429 until the plan is upgraded or the month ends. | 100 requests a month on one API key, at 10 requests a minute | not on the pages read |
| [numverify](https://numverify.com) | Monthly subscription tiers by request quota: Free 100 requests, Basic $14.99 for 5,000, Professional $59.99 for 50,000, Enterprise $129.99 for 250,000, each with a published overage price per call ($0.011992, $0.0047992 and $0.00207984 respectively); annual billing saves up to 15%, and custom pricing is offered above the Enterprise quota | 100 API requests a month, for non-commercial use | not on the pages read |
| [Telnyx Number Lookup](https://telnyx.com/products/number-lookup) | Per query, priced per lookup type, with "$0 platform fee"; pay as you go gets "Automatic tier discounts as usage crosses thresholds", committed plans start at $500 a month, and inbound CNAM is $0.40 per number per month | An outbound CNAM listing at no charge; the pages read list no free query allowance | $1.50 per 1,000 for a local routing number query ($0.0015 a query), $2.50 for MCC/MNC ($0.0025) and $3.00 for CNAM ($0.003) |
| [Veriphone](https://veriphone.io) | One-time credit packs that never expire, from 10,000 credits for $24 to 5,000,000 for $999, or monthly plans whose credits reset each month: Starter $12.99 for 10,000, Professional $39 for 50,000, Business $99 for 250,000 and Enterprise $249 for 1,000,000. A standard validation is 1 credit and a Current Carrier Lookup 10 | 1,000 free credits a month with no card, not carried over; on the free plan a Current Carrier Lookup draws on purchased credits only | $2.40 per 1,000 on the 10,000-credit pack ($24, credits never expire), or $1.30 on the Starter plan (10,000 credits a month for $12.99, reset each month). A Current Carrier Lookup takes 10 credits, so ten times those figures |
## Spaw
Verification API that bills per answered lookup across six signals on one key and one balance: Email verification, Phone intelligence, IP intelligence, Address verification, Business verification against open registers, and Cross-signal consistency, which runs any subset of the first four in one call and names where they agree and where they do not. The phone side reads the numbering plan and the regulators' own allocation tables to name the line type, the block holder and the blocks nobody holds, and flags the numbers that are not a person's handset.
## Abstract Phone Validation
Abstract Phone Validation is a phone API from Abstract sold as monthly request quotas on three data tiers, returning carrier, line type and location, with messaging from Standard up and registration and risk on Professional and the Free plan; the Professional example response also carries breach history.
Comparison: https://spaw.co/compare/abstract-phone
## ClearoutPhone
ClearoutPhone is a phone validation service from Clearout sold as credits that never expire, with a 1-credit Standard mode and a Smart mode that runs a real-time telecom query, at up to 5 credits, where static data is uncertain.
Comparison: https://spaw.co/compare/clearoutphone
## IPQualityScore Phone Validation
IPQualityScore Phone Validation is the phone side of a fraud-scoring service: one API returns validity, carrier, line type and a 0 to 100 fraud score, with live-line, identity, do-not-call and SMS-pumping data as add-ons, on the same monthly plans as its IP, email and URL checks.
Comparison: https://spaw.co/compare/ipqualityscore-phone
## Loqate Phone Validation
Loqate Phone Validation checks a number's format, carrier and, for mobile numbers, its live network status through an HLR lookup, priced per lookup from the same pay-as-you-go credit balance as Loqate's address, email and bank validation.
Comparison: https://spaw.co/compare/loqate-phone
## numlookupapi
numlookupapi is a phone number validation API sold as a monthly request quota: one validate endpoint returns validity, formats, country, location, carrier and line type.
Comparison: https://spaw.co/compare/numlookupapi
## numverify
numverify is a phone number validation API sold as a monthly request quota, returning validity, country and location, carrier and line type.
Comparison: https://spaw.co/compare/numverify
## Telnyx Number Lookup
Telnyx Number Lookup is a per-query number API sold beside Telnyx's carrier services, priced separately for local routing number (portability and carrier), MCC/MNC, and CNAM caller name.
Comparison: https://spaw.co/compare/telnyx-lookup
## Veriphone
Veriphone is a phone validation API sold as credit packs or monthly plans, returning validity, carrier, line type and region from national numbering plans, with a 10-credit Current Carrier Lookup read from national portability registries.
Comparison: https://spaw.co/compare/veriphone
Twilio Lookup and the other product names are trademarks of their owners. Spaw is not affiliated with them.
Reference: https://spaw.co/alternatives/twilio-lookup
---
# Spaw vs Verifalia: email verification API comparison
How Spaw and Verifalia compare on pricing, billing rules, verification depth and limits, from Verifalia's public pages checked on 2026-09-13. Corrections: support@spaw.co.
| | Spaw | Verifalia |
| --- | --- | --- |
| Pricing model | Pay-as-you-go credit packs whose credits never expire, or optional plans, billed monthly or yearly, that deposit the same credits every month for less and roll unused plan credits over up to three months' allowance; both through Stripe Checkout | Pay-as-you-go credit packs that never expire, or monthly, quarterly and yearly subscriptions; verification depth is priced as quality levels; pack and plan prices are shown in a calculator rather than listed |
| Free tier | 100 free credits every month, no card required | 25 free credits a day on the Free plan; unused daily credits expire each day |
| Price per 1,000 at about 10k | $7.80 (Growth pack: 5,000 credits for $39), or $5.80 on the Growth monthly plan (5,000 credits for $29 a month) | not on the pages read |
| Price per 1,000 at about 100k | $4.49 (Volume pack: 100,000 credits for $449), or $3.49 on the Volume monthly plan (100,000 credits for $349 a month) | not on the pages read |
| Unknown or catch-all results billed | No: an unconfirmed mailbox (reason unverified) and an answer Spaw could not obtain because its handshake partner was unavailable are free and leave no 7-day repeat marker, so a later lookup is a fresh one; a catch-all answer costs 1 credit like any risky answer, and an automatic re-check bills 1 credit only when it confirms the mailbox | not on the pages read |
| Undeliverable results billed | No, within a fair use: undeliverable answers that needed a mailbox probe are free up to max(500, your billed email answers) in a rolling 30 days, then 1 credit each; the rest are always free | not on the pages read |
| SMTP mailbox check | Yes | not on the pages read |
| Catch-all handling | A catch-all answer is remembered per domain for 7 days, and every unconfirmed mailbox carries a published 0 to 100 mailbox_confidence estimate | not on the pages read |
| Bulk limit | 100,000 addresses per bulk job, with an HMAC-signed completion webhook | Lists up to 100 MB, about 72 million addresses per list |
| Batch API | 50 addresses per synchronous request, probes sent concurrently | not on the pages read |
| Rate limit | 5 requests per second per key on the email and consistency endpoints | Configurable throttling; fixed limits not published on the pages checked |
| Official SDKs | Python, Node.js, PHP | .NET, Java, Node.js, JavaScript (browser), Ruby, PHP, Go |
| Data retention | Only email verification keeps a history row, for 30 days, then pruned; test addresses are never logged | Configurable from 5 minutes to 30 days, deletable any time; since 27 October 2025 deleted jobs keep an anonymized reference for up to 24 months |
## When Verifalia fits better
Developers who want to tune verification depth per request, official SDKs in six languages, and configurable retention.
- Standard, High and Extreme quality levels cost 1, 2 and 4 credits per verification
- Subscriptions include daily free credits, from 250 a day on Starter to 25,000 a day on Ultimate
- States that data is processed in memory rather than stored persistently
## When Spaw fits better
Developers who want one flat price per answered lookup across every signal a signup form produces, a transparent risk score, and an API that documents every field.
- Verifalia prices depth as 1, 2 or 4 credits per verification; Spaw runs its full pipeline including the SMTP handshake for 1 credit
- Spaw bills nothing for unverified mailboxes, for undeliverable verdicts within a fair-use allowance, or for repeats within 7 days
- Verifalia's daily free credits expire each day; Spaw's monthly free credits accumulate
- Verifalia lists SDKs for seven platforms; Spaw ships Python, Node.js and PHP clients and an OpenAPI document
## Questions
**Does Spaw charge for catch-all or unknown results?** Spaw bills per answer, not per probe outcome. A catch-all domain comes back risky with reason catch_all and costs 1 credit like any risky answer. A mailbox the handshake could not confirm (greylisting, a timeout, no definite answer) comes back risky with reason unverified and is free, and so is an answer Spaw could not obtain at all because its handshake partner was unavailable; neither leaves a 7-day repeat marker, so a later lookup is a fresh one (an uncertain result is reused for up to 15 minutes, then the mailbox is asked again) and bills 1 credit only once the mailbox is confirmed, and the automatic re-checks behind callback_url bill 1 credit only when they confirm it. Undeliverable verdicts are free within a fair use of max(500, your billed email answers) in a rolling 30 days, and invalid input and repeats within 7 days are free too. Verifalia does not publish how it bills them.
**Can I move a list from Verifalia to Spaw?** Yes. Upload the CSV to the dashboard or POST it to the bulk endpoint; Spaw keeps your columns, adds the verdict columns, and lets you download only the deliverable, risky or undeliverable rows. Addresses that come back undeliverable join your suppression list so later runs skip them for free.
**Is there a free tier?** Every Spaw account receives 100 free credits at the start of each month with no card on file, and unused free credits accumulate. Verifalia offers: 25 free credits a day on the Free plan; unused daily credits expire each day.
## Sources (checked 2026-09-13)
- [Verifalia pricing](https://verifalia.com/pricing)
- [Verifalia developers](https://verifalia.com/developers)
Verifalia is a trademark of its owner. Spaw is not affiliated with it.
Reference: https://spaw.co/compare/verifalia
---
# Verifalia alternatives for email verification
Email verification APIs to consider instead of Verifalia, with the pricing model, free tier and price per 1,000 lookups each vendor publishes. Figures come from public pricing pages checked on 2026-09-13.
| Service | Pricing model | Free tier | Price per 1,000 at about 10k |
| --- | --- | --- | --- |
| [Spaw](https://spaw.co) | Pay-as-you-go credit packs whose credits never expire, or optional plans, billed monthly or yearly, that deposit the same credits every month for less and roll unused plan credits over up to three months' allowance; both through Stripe Checkout | 100 free credits every month, no card required | $7.80 (Growth pack: 5,000 credits for $39), or $5.80 on the Growth monthly plan (5,000 credits for $29 a month) |
| [Abstract API](https://www.abstractapi.com) | Monthly subscription tiers with a request slider from 5,000 to 150,000 requests a month; Starter from $17, Standard from $37, Professional from $39 a month | 100 requests on the Free plan | not on the pages read |
| [Bouncer](https://www.usebouncer.com) | Pay-as-you-go credits that never expire (from 1,000 credits for $8 up to 1,000,000 for $2,000), auto-refill, plus a Deliverability Kit subscription at $25 to $250 a month | 100 free credits, no card required | $6.00 (10,000 credits for $60) |
| [Emailable](https://emailable.com) | Pay-as-you-go credits that never expire, or a monthly subscription at a 15% discount; minimum purchase 5,000 credits | 250 free credits at signup | $7.60 (the pay-as-you-go rate printed for the 5,000-credit package, $0.0076 a credit; larger packages are priced in a calculator, and the 5,000-credit subscription is $32.30 a month after the 15% discount) |
| [Hunter](https://hunter.io) | Monthly or yearly subscriptions with unified credits shared across search, verification and enrichment; credit packs on paid plans, usable for up to 12 months from purchase according to the pricing page | Free plan with 50 credits a month; one verification costs 0.5 credit, so 100 verifications | $7.45 (Growth: $149 a month for 10,000 credits, 20,000 verifications at 0.5 credit each; $5.20 billed yearly) |
| [Kickbox](https://kickbox.com) | not on the pages read | not on the pages read | not on the pages read |
| [Mailboxlayer](https://mailboxlayer.com) | Monthly or yearly subscription (10 to 15% off yearly) with a request quota per plan and per-request overage | 100 requests a month | $3.00 for the Basic quota (5,000 requests for $14.99 a month; overage $0.012 a request, so 10,000 requests on Basic cost about $74.95, $7.50 per 1,000); Professional Plus: 50,000 for $74.99, $1.50 per 1,000 |
| [NeverBounce](https://www.neverbounce.com) | not on the pages read | not on the pages read | not on the pages read |
| [ZeroBounce](https://www.zerobounce.net) | Pay-as-you-go credits from $39 for 2,000 addresses, 15% off with autopay, or the ZeroBounce ONE subscription at $99 a month with unused credits rolling over; volume pricing shown through a calculator rather than fixed packs | 100 validation credits a month on the free account | not on the pages read |
## Spaw
Verification API that bills per answered lookup across six signals on one key and one balance: Email verification, Phone intelligence, IP intelligence, Address verification, Business verification against open registers, and Cross-signal consistency, which runs any subset of the first four in one call and names where they agree and where they do not. The email side checks syntax, live MX, SPF and DMARC, disposable and role lists, and runs an SMTP mailbox handshake that never sends a message.
## Abstract API
Abstract API's email validation endpoint is one of a family of utility APIs sold as monthly request quotas with a free 100-request plan.
Comparison: https://spaw.co/compare/abstract
## Bouncer
Bouncer is an email verification service with pay-as-you-go credits that never expire, high-throughput batch APIs, a real-time form product (Bouncer Shield) and a Deliverability Kit subscription.
Comparison: https://spaw.co/compare/bouncer
## Emailable
Emailable is an email verification service with pay-as-you-go credits, a bulk verifier, a widget and an API, with Node.js, Ruby and Python libraries.
Comparison: https://spaw.co/compare/emailable
## Hunter
Hunter is an outreach platform whose Email Verifier shares unified plan credits with its email finder, enrichment and campaign tools.
Comparison: https://spaw.co/compare/hunter
## Kickbox
Kickbox is an email verification API that returns deliverable, undeliverable, risky or unknown plus a 0 to 1 Sendex quality score, with official libraries for PHP, Node.js, Ruby and Python.
Comparison: https://spaw.co/compare/kickbox
## Mailboxlayer
Mailboxlayer is apilayer's email validation API, sold as monthly request quotas from a free 100-request plan up to 250,000 requests a month.
Comparison: https://spaw.co/compare/mailboxlayer
## NeverBounce
NeverBounce is a ZoomInfo-owned email verification service with a bulk list-cleaning workflow, a real-time API and official client libraries in six languages.
Comparison: https://spaw.co/compare/neverbounce
## ZeroBounce
ZeroBounce is an email validation service with a pay-as-you-go credit model and a subscription (ZeroBounce ONE) that bundles deliverability tools such as activity data, inbox placement tests and blacklist monitoring.
Comparison: https://spaw.co/compare/zerobounce
Verifalia and the other product names are trademarks of their owners. Spaw is not affiliated with them.
Reference: https://spaw.co/alternatives/verifalia
---
# Spaw vs Veriphone: phone intelligence API comparison
How Spaw and Veriphone compare on pricing, billing rules, where the line type comes from and limits, from Veriphone's public pages checked on 2026-09-27. Corrections: support@spaw.co.
| | Spaw | Veriphone |
| --- | --- | --- |
| Pricing model | Pay-as-you-go credit packs whose credits never expire, or optional plans, billed monthly or yearly, that deposit the same credits every month for less and roll unused plan credits over up to three months' allowance; both through Stripe Checkout | One-time credit packs that never expire, from 10,000 credits for $24 to 5,000,000 for $999, or monthly plans whose credits reset each month: Starter $12.99 for 10,000, Professional $39 for 50,000, Business $99 for 250,000 and Enterprise $249 for 1,000,000. A standard validation is 1 credit and a Current Carrier Lookup 10 |
| Free tier | 100 free credits every month, no card required | 1,000 free credits a month with no card, not carried over; on the free plan a Current Carrier Lookup draws on purchased credits only |
| Price per 1,000 at about 10k | $7.80 (Growth pack: 5,000 credits for $39), or $5.80 on the Growth monthly plan (5,000 credits for $29 a month) | $2.40 per 1,000 on the 10,000-credit pack ($24, credits never expire), or $1.30 on the Starter plan (10,000 credits a month for $12.99, reset each month). A Current Carrier Lookup takes 10 credits, so ten times those figures |
| Price per 1,000 at about 100k | $4.49 (Volume pack: 100,000 credits for $449), or $3.49 on the Volume monthly plan (100,000 credits for $349 a month) | $1.19 per 1,000 on the 100,000-credit pack ($119), or $0.78 on the Professional plan (50,000 credits a month for $39) and $0.40 on Business (250,000 for $99); packs fall to $0.20 per 1,000 at 5,000,000 credits for $999 |
| Numbers the vendor could not settle billed | A valid number costs 1 credit whatever its flags say. The live carrier-network check (hlr: true) is not enabled on the service: hlr_checked answers false and nothing extra is charged. | A Current Carrier Lookup that cannot resolve the serving carrier is charged as a standard 1-credit validation ("no result, no surcharge"), and input that is not a phone number answers status "error" and is not charged |
| Invalid numbers billed | No: invalid numbers are free, including a number in a block the regulator lists as unassigned, and so are repeats within 7 days | Yes through the API: a number "verified and found not valid" is "a definitive, charged result". The bulk documentation bills a static-mode file 1 credit per number, with rows that have syntax errors not billed, and prices a current-mode file at "nothing for invalid rows" |
| Line type source | The numbering plan metadata everywhere except the North American plan, where mobiles and landlines share blocks and the plan can only say fixed_line_or_mobile; there the regulator's block holder settles it — a wireless carrier's block is mobile, a virtual-number wholesaler's is voip. line_type_source names which of the two answered. | The numbering plan: the default Static mode "reports its original carrier and line type from national numbering-plan data", checked "against the latest numbering plans from the international telecom regulator and national regulators". Current mode adds the carrier and line type serving the number today, from national portability registries. |
| Numbering plan coverage | libphonenumber metadata for every region it carries, plus the regulators' own allocation tables, each named in the answer with the date of the copy it read. Complete tables, where a geographic number in a block the table leaves out answers unassigned_block: NANPA for the United States (with Puerto Rico, the U.S. Virgin Islands, Guam, American Samoa and the Northern Mariana Islands), the CNAC for Canada, ARCEP for France (with Guadeloupe, St. Barthélemy, St. Martin, Martinique, French Guiana, Réunion, Mayotte and St. Pierre & Miquelon), ACM for the Netherlands and the CRT (formerly the IFT) for Mexico. Partial tables, answered only from the ranges they list (a range listed as free, spare or held in reserve answers unassigned_block), so block_status answers null for a number outside them: Ofcom for the United Kingdom (with Guernsey, Jersey and the Isle of Man), ACMA for Australia, the NAD for New Zealand, UKE for Poland and MIMIT for Italy. Re-synced weekly, NANPA daily; /status lists each table's age. Outside those regions, block_status and is_virtual answer null rather than a guess. | "All 243 countries and territories" for validation, carrier and line type, "using the official national numbering plans"; Current Carrier Lookup in 239 countries as of 2026-09-24, listed on its coverage page and by a public /v3/coverage/current endpoint |
| Live network check | Reserved behind hlr: true: 4 credits on top of the base credit, only where the network answers, reported in its own reachable, ported, current_carrier and roaming fields. The live carrier-network check (hlr: true) is not enabled on the service: hlr_checked answers false and nothing extra is charged. | No device or reachability check described. Current mode, which the API reference calls Veriphone's "live-registry product", "resolves the number's serving carrier from national portability registries" and "does not indicate whether the device is switched on or reachable". |
| Ported numbers | None from the tables, and the docs say so: carrier and line_type describe the block the number was allocated in, never a later port. Only a live carrier-network check can say who serves a number today. | Current Carrier Lookup (mode=current, 10 credits, 1 when unresolved) returns original_carrier, current_carrier, current_line_type and a ported flag from national portability registries, which "update within hours to days of a porting event". For US numbers it "also resolves pooled thousands-blocks that prefix-level data cannot see". |
| SMS pumping controls | Flags in the risk score, and an SMS policy that turns them into ok_to_send and blocked_by: a fraud and deliverability decision, not consent and not a do-not-call scrub. allowed_countries limits sending to the regions you serve; it is empty (any country) until you set it. range_burst (+40) flags a number once your account has looked up 5 distinct numbers in its 1,000-number range within 60 minutes, and numbers_per_client (+30) once one client address (the client_ip you send, or a browser visitor's own) has tried 5 within 60 minutes, unless Spaw's own IP data places it on a mobile carrier or a corporate proxy, or it is private or reserved. Both count only your own single and browser lookups, so a new account has no history. The range is a heuristic, and in North America a burst more likely means someone testing numbers against a form; the client count does not stop an attack rotated through residential proxies. disposable (+70) flags a number SMS-receiving websites publish and disposable_stale (+40) an old listing; disposable_range (+30) flags an unlisted number in a 1,000-number range holding 10 or more fresh listings. The lists are public and synced daily. The default policy blocks the fictional, disposable, reported_abuse, reported_abuse_widely and premium_rate signals and a risk_score above 60, so neither traffic signal blocks on its own. | not on the pages read |
| Bulk limit | 100,000 numbers per bulk job, with an HMAC-signed completion webhook | CSV upload through the dashboard or the /v3/file endpoints: 100 MB and 1 million rows a file, and a file starts only when the balance covers every row at the mode's full rate |
| Batch API | 1,000 numbers per synchronous request, answered in input order | No batch endpoint in the API reference; the MCP server's verify_phones tool takes up to 50 numbers in one call |
| Rate limit | 50 requests per second per key on the phone endpoints | No per-key figure published; the error table lists 429 for a rate limit exceeded, and the home page says the API "scales to thousands of requests per second" |
| Official SDKs | Python, Node.js, PHP | not on the pages read |
| Data retention | No history row and no log line carrying the number. What a lookup leaves is keyed and never holds the number: the 7-day marker that makes a repeat free (a second one when the account paid for a live check); for single and browser lookups, a truncated keyed hash of the number filed under keyed hashes of its range and client address for 60 minutes, and the account's lookups per country and hour as counts for 30 days; and a live check's answer, held for a day under a hash of the number. The number itself is kept only in the account's own outcome reports, phone suppression list and monitors. Bulk input lists and result files are deleted with the job after 30 days. | API lookups are "processed in memory and are not written to a database, application logs or analytics"; infrastructure request logs keep a GET request's URL, which carries the number, for 30 days (a POST body avoids this). Sending record=true saves a result to the account's verification history, and uploaded CSVs and their results are kept until you delete them, though the page adds that "files may be deleted more than 90 days after upload". Processing is on Google Cloud in the EU (europe-west1). |
## When Veriphone fits better
Teams that want numbering-plan validation with carrier and line type at a low per-number price, and the carrier serving a number today from portability registries when they pay for it.
- Current Carrier Lookup returns the original and the current carrier with a ported flag, and an unresolved one is charged as a standard 1-credit validation
- Credit packs never expire; monthly plan credits reset each month and are spent before pack credits
- A hosted MCP server at mcp.veriphone.io, and Zapier and Make connectors
- The documentation says no SDK is needed and gives code examples in cURL and seven languages
## When Spaw fits better
Developers who want the numbering plan and the regulators' own allocation tables read for them, the dataset behind every field named in the answer, and an SMS policy that turns the evidence into ok_to_send and blocked_by — a fraud and deliverability decision, not consent and not a do-not-call scrub. Every answer is made on the server: the opt-in live carrier-network check, the one call that would send a number to a partner, is not enabled on the service.
- Veriphone's Current Carrier Lookup reads national portability registries for the carrier serving a number today; Spaw's carrier and line_type describe the original allocation and never a later port, and the docs say so
- Veriphone charges for a number it verifies as not valid; Spaw answers an invalid number for free, including one in a block the regulator lists as unassigned, and a repeat within 7 days costs nothing
- Spaw returns 49 fields in one 1-credit answer, adding the fictional, virtual and disposable flags, a risk score with the signals behind it, and ok_to_send from your account's SMS policy, a fraud and deliverability decision rather than consent or a do-not-call scrub
- Veriphone's API validates one number per call and takes lists as CSV files; Spaw also answers 1,000 numbers in one synchronous batch request and takes 100,000 per bulk job
## Questions
**Does Spaw charge for numbers it cannot settle?** Spaw bills per answer. A number the numbering plan and, where one is installed, the regulator's own table agree is assignable costs 1 credit whatever its flags say. An invalid number is free — including one in a block the regulator lists as unassigned — and so is a repeat of the same number within seven days. The live carrier-network check (hlr: true) is not enabled on the service: hlr_checked answers false and nothing extra is charged. Veriphone's pages say: A Current Carrier Lookup that cannot resolve the serving carrier is charged as a standard 1-credit validation ("no result, no surcharge"), and input that is not a phone number answers status "error" and is not charged.
**Can I move a list of numbers from Veriphone to Spaw?** Yes. Upload the CSV in the dashboard or POST it to the bulk endpoint, up to 100,000 numbers a job; Spaw keeps your columns, adds the answer columns, and lets you download the whole file or only the valid, invalid, low, medium or high-risk rows. A country column gives each row its own region, so a mixed international list needs no splitting.
**Is there a free tier?** Every Spaw account receives 100 free credits at the start of each month with no card on file, and unused free credits accumulate. Veriphone offers: 1,000 free credits a month with no card, not carried over; on the free plan a Current Carrier Lookup draws on purchased credits only.
## Sources (checked 2026-09-27)
- [Veriphone pricing](https://veriphone.io/pricing)
- [Veriphone home page and FAQ](https://veriphone.io/)
- [Veriphone API documentation (getting started and errors)](https://veriphone.io/docs)
- [Veriphone API reference (v3)](https://veriphone.io/docs/v3)
- [Veriphone bulk verification](https://veriphone.io/docs/bulk)
- [Veriphone supported countries](https://veriphone.io/docs/answers/supported-countries)
- [Veriphone free plan](https://veriphone.io/docs/answers/free-tier)
- [Veriphone data retention](https://veriphone.io/docs/answers/data-retention)
- [Veriphone code examples](https://veriphone.io/docs/sdks)
- [Veriphone MCP server](https://veriphone.io/docs/mcp)
Veriphone is a trademark of its owner. Spaw is not affiliated with it.
Reference: https://spaw.co/compare/veriphone
---
# Veriphone alternatives for phone intelligence
Phone intelligence APIs to consider instead of Veriphone, with the pricing model, free tier and price per 1,000 lookups each vendor publishes. Figures come from public pricing pages checked on 2026-09-27.
| Service | Pricing model | Free tier | Price per 1,000 at about 10k |
| --- | --- | --- | --- |
| [Spaw](https://spaw.co) | Pay-as-you-go credit packs whose credits never expire, or optional plans, billed monthly or yearly, that deposit the same credits every month for less and roll unused plan credits over up to three months' allowance; both through Stripe Checkout | 100 free credits every month, no card required | $7.80 (Growth pack: 5,000 credits for $39), or $5.80 on the Growth monthly plan (5,000 credits for $29 a month) |
| [Abstract Phone Validation](https://www.abstractapi.com/api/phone-validation-api) | Monthly request quotas on three plans that differ by the data returned: Starter (validation, location, carrier) from $19 a month for 5,000 requests to $199 for 150,000; Standard (adds messaging) from $19 for 3,000 to $199 for 100,000; Professional (adds registration and risk) from $19 for 2,000 to $199 for 50,000. Annual billing saves 10%; the pricing page quotes $3.40 per 1,000 requests at Starter's entry volume, billed annually | 100 requests a month with no card, on the full response including registration and risk | not on the pages read |
| [ClearoutPhone](https://clearoutphone.io) | Credits that never expire, bought one time or through monthly or annual subscriptions that add them each period and carry them forward. One Time, each shown as a starting price: 5,000 credits from $35, 10,000 from $58, 100,000 from $400 and 1,000,000 from $1,200. Standard validation is 1 credit; Smart validation is 1 credit for most numbers and up to 5 when it runs a deeper real-time check | 100 free credits on sign-up, one validation each | $5.80 per 1,000 one time (10,000 credits for $58), $5.20 on the monthly plan (10,000 credits for $52 a month) or $3.20 on the annual plan as its tab rendered ("Buy 120,000 credits for $384 billed yearly", 10,000 a month), although the page labels annual billing "Save 20%" against one-time prices; a Smart validation that runs the deeper check costs up to 5 credits |
| [IPQualityScore Phone Validation](https://www.ipqualityscore.com/solutions/phone-validation) | Monthly plans shared with IPQualityScore's other checks: Free $0 (1,000 lookups a month), Startup "As low as: $99 / Month" (5,000), SMB Basic "As low as: $499 / Month" (10,000), SMB+ "As low as: $999 / Month" (75,000), Enterprise custom; "Each plan requires a minimum monthly commitment". The phone validation page says "Score phone numbers on average from $0.0005 per user with no monthly contracts", on "tiered flat rate pricing across all countries that reduces as your monthly volume increases", without a tier table. Add-ons are enabled through the account manager or Support at no stated price. | 1,000 lookups a month on the Free plan, capped at 35 a day; the plan also covers IP, email and URL checks | not on the pages read |
| [Loqate Phone Validation](https://www.loqate.com/en-gb/phone-validation/) | Pay-as-you-go credit purchases of £50, £100, £250, £500 and £1,000 (or 75€, 150€ and 300€), each buying a published per-lookup rate for every service, plus bespoke plans; "Credit is valid for 12 months after the purchase date". Phone Validation on the "New Pricing" table is 7.1p, 6.7p, 5.6p, 4.7p and 4.1p a lookup across the pound tiers and 8.6c, 7.8c and 6.6c across the euro ones. The page also shows an "Old Pricing" table without saying which accounts each applies to; the figures here are from "New Pricing". Its usage-example selector also lists a £2,500 plan that neither table prices, and all prices exclude VAT. | A trial rather than a standing tier: "Start your 45-day free trial"; the FAQ adds "You get free credits to test the service" | £47 per 1,000 (4.7p a lookup) on the £500 credit, which buys about 10,600 lookups; the smaller credits run from 7.1p (£50) to 5.6p (£250) |
| [numlookupapi](https://numlookupapi.com) | Monthly subscription tiers by request quota: Free $0 for 100 requests, Basic $9.99 for 5,000, Pro $49.99 for 50,000, Scale $109.99 for 250,000, and Custom by contact; the page offers yearly billing at 20% off. Scale and Custom list "Overages" with no overage price shown, and the quota documentation says a request over the monthly quota answers 429 until the plan is upgraded or the month ends. | 100 requests a month on one API key, at 10 requests a minute | not on the pages read |
| [numverify](https://numverify.com) | Monthly subscription tiers by request quota: Free 100 requests, Basic $14.99 for 5,000, Professional $59.99 for 50,000, Enterprise $129.99 for 250,000, each with a published overage price per call ($0.011992, $0.0047992 and $0.00207984 respectively); annual billing saves up to 15%, and custom pricing is offered above the Enterprise quota | 100 API requests a month, for non-commercial use | not on the pages read |
| [Telnyx Number Lookup](https://telnyx.com/products/number-lookup) | Per query, priced per lookup type, with "$0 platform fee"; pay as you go gets "Automatic tier discounts as usage crosses thresholds", committed plans start at $500 a month, and inbound CNAM is $0.40 per number per month | An outbound CNAM listing at no charge; the pages read list no free query allowance | $1.50 per 1,000 for a local routing number query ($0.0015 a query), $2.50 for MCC/MNC ($0.0025) and $3.00 for CNAM ($0.003) |
| [Twilio Lookup](https://www.twilio.com/en-us/user-authentication-identity/lookup) | Per request, priced per data package on a Twilio account ("Pricing current as of August 2026"); line status and reassigned number risk are tiered by query volume, and SIM swap and call forwarding are contact sales | Formatting and validation are listed as Free, and so is the SMS Pumping Risk Score for North American numbers; no free allowance is published for the other packages | $8.00 per 1,000 for line type intelligence ($0.008 per request), $10.00 for inbound caller identification ($0.01, US only), $100.00 for identity match ($0.1, which the page says varies by country) |
## Spaw
Verification API that bills per answered lookup across six signals on one key and one balance: Email verification, Phone intelligence, IP intelligence, Address verification, Business verification against open registers, and Cross-signal consistency, which runs any subset of the first four in one call and names where they agree and where they do not. The phone side reads the numbering plan and the regulators' own allocation tables to name the line type, the block holder and the blocks nobody holds, and flags the numbers that are not a person's handset.
## Abstract Phone Validation
Abstract Phone Validation is a phone API from Abstract sold as monthly request quotas on three data tiers, returning carrier, line type and location, with messaging from Standard up and registration and risk on Professional and the Free plan; the Professional example response also carries breach history.
Comparison: https://spaw.co/compare/abstract-phone
## ClearoutPhone
ClearoutPhone is a phone validation service from Clearout sold as credits that never expire, with a 1-credit Standard mode and a Smart mode that runs a real-time telecom query, at up to 5 credits, where static data is uncertain.
Comparison: https://spaw.co/compare/clearoutphone
## IPQualityScore Phone Validation
IPQualityScore Phone Validation is the phone side of a fraud-scoring service: one API returns validity, carrier, line type and a 0 to 100 fraud score, with live-line, identity, do-not-call and SMS-pumping data as add-ons, on the same monthly plans as its IP, email and URL checks.
Comparison: https://spaw.co/compare/ipqualityscore-phone
## Loqate Phone Validation
Loqate Phone Validation checks a number's format, carrier and, for mobile numbers, its live network status through an HLR lookup, priced per lookup from the same pay-as-you-go credit balance as Loqate's address, email and bank validation.
Comparison: https://spaw.co/compare/loqate-phone
## numlookupapi
numlookupapi is a phone number validation API sold as a monthly request quota: one validate endpoint returns validity, formats, country, location, carrier and line type.
Comparison: https://spaw.co/compare/numlookupapi
## numverify
numverify is a phone number validation API sold as a monthly request quota, returning validity, country and location, carrier and line type.
Comparison: https://spaw.co/compare/numverify
## Telnyx Number Lookup
Telnyx Number Lookup is a per-query number API sold beside Telnyx's carrier services, priced separately for local routing number (portability and carrier), MCC/MNC, and CNAM caller name.
Comparison: https://spaw.co/compare/telnyx-lookup
## Twilio Lookup
Twilio Lookup is a phone number API sold as separately priced data packages on top of a Twilio account: formatting and validation are free, and line type, caller identity, line status and the risk packages are each priced on their own, per request (the SMS Pumping Risk Score is free for North American numbers) or through sales for SIM swap and call forwarding.
Comparison: https://spaw.co/compare/twilio-lookup
Veriphone and the other product names are trademarks of their owners. Spaw is not affiliated with them.
Reference: https://spaw.co/alternatives/veriphone
---
# Spaw vs ZeroBounce: email verification API comparison
How Spaw and ZeroBounce compare on pricing, billing rules, verification depth and limits, from ZeroBounce's public pages checked on 2026-09-13. Corrections: support@spaw.co.
| | Spaw | ZeroBounce |
| --- | --- | --- |
| Pricing model | Pay-as-you-go credit packs whose credits never expire, or optional plans, billed monthly or yearly, that deposit the same credits every month for less and roll unused plan credits over up to three months' allowance; both through Stripe Checkout | Pay-as-you-go credits from $39 for 2,000 addresses, 15% off with autopay, or the ZeroBounce ONE subscription at $99 a month with unused credits rolling over; volume pricing shown through a calculator rather than fixed packs |
| Free tier | 100 free credits every month, no card required | 100 validation credits a month on the free account |
| Price per 1,000 at about 10k | $7.80 (Growth pack: 5,000 credits for $39), or $5.80 on the Growth monthly plan (5,000 credits for $29 a month) | not on the pages read |
| Price per 1,000 at about 100k | $4.49 (Volume pack: 100,000 credits for $449), or $3.49 on the Volume monthly plan (100,000 credits for $349 a month) | not on the pages read |
| Unknown or catch-all results billed | No: an unconfirmed mailbox (reason unverified) and an answer Spaw could not obtain because its handshake partner was unavailable are free and leave no 7-day repeat marker, so a later lookup is a fresh one; a catch-all answer costs 1 credit like any risky answer, and an automatic re-check bills 1 credit only when it confirms the mailbox | No |
| Undeliverable results billed | No, within a fair use: undeliverable answers that needed a mailbox probe are free up to max(500, your billed email answers) in a rolling 30 days, then 1 credit each; the rest are always free | not on the pages read |
| SMTP mailbox check | Yes | not on the pages read |
| Catch-all handling | A catch-all answer is remembered per domain for 7 days, and every unconfirmed mailbox carries a published 0 to 100 mailbox_confidence estimate | catch-all is one of seven statuses (valid, invalid, catch-all, unknown, spamtrap, abuse, do_not_mail) with 21 sub-statuses; only unknown results are stated to be free |
| Bulk limit | 100,000 addresses per bulk job, with an HMAC-signed completion webhook | Bulk file endpoints for larger lists; size limits not published on the pages checked |
| Batch API | 50 addresses per synchronous request, probes sent concurrently | Up to 100 emails per batch request, at most 30 requests a minute (40 on ZeroBounce ONE); the bulk file endpoints carry no rate limit |
| Rate limit | 5 requests per second per key on the email and consistency endpoints | Single validation: 80,000 requests in 10 seconds per the docs (100,000 on ZeroBounce ONE); batch: 30 requests a minute; exceeding a limit triggers a temporary block |
| Official SDKs | Python, Node.js, PHP | PHP, Python, Java, JavaScript, Node.js, C#, Go, Ruby, Rust, Kotlin, Swift |
| Data retention | Only email verification keeps a history row, for 30 days, then pruned; test addresses are never logged | not on the pages read |
## When ZeroBounce fits better
Marketing teams that want a deliverability suite around verification and prefer a subscription with bundled tools.
- Credits never expire for ZeroBounce ONE subscribers and on the free account; pay-as-you-go expiry depends on the account's terms
- Unknown results never consume a credit
- ZeroBounce ONE bundles activity data, AI scoring, an email finder, inbox placement tests, blacklist and DMARC monitoring
- 24 sub-statuses explain a result, from greylisted to possible_typo and mailbox_quota_exceeded
## When Spaw fits better
Developers who want one flat price per answered lookup across every signal a signup form produces, a transparent risk score, and an API that documents every field.
- Spaw lists fixed pack prices and a monthly free grant on one page; ZeroBounce prices volume through a calculator and its ONE subscription
- Both leave unknown results unbilled; Spaw also leaves undeliverable verdicts free within a fair-use allowance
- Spaw's risk score is a sum of published weights on every lookup; ZeroBounce offers AI scoring as part of its ONE subscription
- Spaw batches take 50 addresses at 5 requests a second; ZeroBounce batches take 100 addresses at 30 requests a minute
## Questions
**Does Spaw charge for catch-all or unknown results?** Spaw bills per answer, not per probe outcome. A catch-all domain comes back risky with reason catch_all and costs 1 credit like any risky answer. A mailbox the handshake could not confirm (greylisting, a timeout, no definite answer) comes back risky with reason unverified and is free, and so is an answer Spaw could not obtain at all because its handshake partner was unavailable; neither leaves a 7-day repeat marker, so a later lookup is a fresh one (an uncertain result is reused for up to 15 minutes, then the mailbox is asked again) and bills 1 credit only once the mailbox is confirmed, and the automatic re-checks behind callback_url bill 1 credit only when they confirm it. Undeliverable verdicts are free within a fair use of max(500, your billed email answers) in a rolling 30 days, and invalid input and repeats within 7 days are free too. ZeroBounce states that it does not bill unknown results.
**Can I move a list from ZeroBounce to Spaw?** Yes. Upload the CSV to the dashboard or POST it to the bulk endpoint; Spaw keeps your columns, adds the verdict columns, and lets you download only the deliverable, risky or undeliverable rows. Addresses that come back undeliverable join your suppression list so later runs skip them for free.
**Is there a free tier?** Every Spaw account receives 100 free credits at the start of each month with no card on file, and unused free credits accumulate. ZeroBounce offers: 100 validation credits a month on the free account.
## Sources (checked 2026-09-13)
- [ZeroBounce pricing](https://www.zerobounce.net/email-validation-pricing/)
- [ZeroBounce API rate limits](https://www.zerobounce.net/docs/api-dashboard/api-rate-limits)
- [ZeroBounce batch validation docs](https://www.zerobounce.net/docs/email-validation-api-quickstart/v2-batch-validate-emails)
ZeroBounce is a trademark of its owner. Spaw is not affiliated with it.
Reference: https://spaw.co/compare/zerobounce
---
# ZeroBounce alternatives for email verification
Email verification APIs to consider instead of ZeroBounce, with the pricing model, free tier and price per 1,000 lookups each vendor publishes. Figures come from public pricing pages checked on 2026-09-13.
| Service | Pricing model | Free tier | Price per 1,000 at about 10k |
| --- | --- | --- | --- |
| [Spaw](https://spaw.co) | Pay-as-you-go credit packs whose credits never expire, or optional plans, billed monthly or yearly, that deposit the same credits every month for less and roll unused plan credits over up to three months' allowance; both through Stripe Checkout | 100 free credits every month, no card required | $7.80 (Growth pack: 5,000 credits for $39), or $5.80 on the Growth monthly plan (5,000 credits for $29 a month) |
| [Abstract API](https://www.abstractapi.com) | Monthly subscription tiers with a request slider from 5,000 to 150,000 requests a month; Starter from $17, Standard from $37, Professional from $39 a month | 100 requests on the Free plan | not on the pages read |
| [Bouncer](https://www.usebouncer.com) | Pay-as-you-go credits that never expire (from 1,000 credits for $8 up to 1,000,000 for $2,000), auto-refill, plus a Deliverability Kit subscription at $25 to $250 a month | 100 free credits, no card required | $6.00 (10,000 credits for $60) |
| [Emailable](https://emailable.com) | Pay-as-you-go credits that never expire, or a monthly subscription at a 15% discount; minimum purchase 5,000 credits | 250 free credits at signup | $7.60 (the pay-as-you-go rate printed for the 5,000-credit package, $0.0076 a credit; larger packages are priced in a calculator, and the 5,000-credit subscription is $32.30 a month after the 15% discount) |
| [Hunter](https://hunter.io) | Monthly or yearly subscriptions with unified credits shared across search, verification and enrichment; credit packs on paid plans, usable for up to 12 months from purchase according to the pricing page | Free plan with 50 credits a month; one verification costs 0.5 credit, so 100 verifications | $7.45 (Growth: $149 a month for 10,000 credits, 20,000 verifications at 0.5 credit each; $5.20 billed yearly) |
| [Kickbox](https://kickbox.com) | not on the pages read | not on the pages read | not on the pages read |
| [Mailboxlayer](https://mailboxlayer.com) | Monthly or yearly subscription (10 to 15% off yearly) with a request quota per plan and per-request overage | 100 requests a month | $3.00 for the Basic quota (5,000 requests for $14.99 a month; overage $0.012 a request, so 10,000 requests on Basic cost about $74.95, $7.50 per 1,000); Professional Plus: 50,000 for $74.99, $1.50 per 1,000 |
| [NeverBounce](https://www.neverbounce.com) | not on the pages read | not on the pages read | not on the pages read |
| [Verifalia](https://verifalia.com) | Pay-as-you-go credit packs that never expire, or monthly, quarterly and yearly subscriptions; verification depth is priced as quality levels; pack and plan prices are shown in a calculator rather than listed | 25 free credits a day on the Free plan; unused daily credits expire each day | not on the pages read |
## Spaw
Verification API that bills per answered lookup across six signals on one key and one balance: Email verification, Phone intelligence, IP intelligence, Address verification, Business verification against open registers, and Cross-signal consistency, which runs any subset of the first four in one call and names where they agree and where they do not. The email side checks syntax, live MX, SPF and DMARC, disposable and role lists, and runs an SMTP mailbox handshake that never sends a message.
## Abstract API
Abstract API's email validation endpoint is one of a family of utility APIs sold as monthly request quotas with a free 100-request plan.
Comparison: https://spaw.co/compare/abstract
## Bouncer
Bouncer is an email verification service with pay-as-you-go credits that never expire, high-throughput batch APIs, a real-time form product (Bouncer Shield) and a Deliverability Kit subscription.
Comparison: https://spaw.co/compare/bouncer
## Emailable
Emailable is an email verification service with pay-as-you-go credits, a bulk verifier, a widget and an API, with Node.js, Ruby and Python libraries.
Comparison: https://spaw.co/compare/emailable
## Hunter
Hunter is an outreach platform whose Email Verifier shares unified plan credits with its email finder, enrichment and campaign tools.
Comparison: https://spaw.co/compare/hunter
## Kickbox
Kickbox is an email verification API that returns deliverable, undeliverable, risky or unknown plus a 0 to 1 Sendex quality score, with official libraries for PHP, Node.js, Ruby and Python.
Comparison: https://spaw.co/compare/kickbox
## Mailboxlayer
Mailboxlayer is apilayer's email validation API, sold as monthly request quotas from a free 100-request plan up to 250,000 requests a month.
Comparison: https://spaw.co/compare/mailboxlayer
## NeverBounce
NeverBounce is a ZoomInfo-owned email verification service with a bulk list-cleaning workflow, a real-time API and official client libraries in six languages.
Comparison: https://spaw.co/compare/neverbounce
## Verifalia
Verifalia is an email verification service with selectable quality levels, credit packs that never expire, subscriptions with daily free credits, and official SDKs in six languages.
Comparison: https://spaw.co/compare/verifalia
ZeroBounce and the other product names are trademarks of their owners. Spaw is not affiliated with them.
Reference: https://spaw.co/alternatives/zerobounce
---
# Changelog
What changed in Spaw, newest first. Reference: https://spaw.co/changelog · Atom feed: https://spaw.co/changelog.atom
## 2026-10-08: Relay aliases scored on what they show, 23 more alias domains, and Amazon SES told apart from the rest of AWS
Reference: https://spaw.co/changelog/2026-10-08-relay-confidence-23-more-alias-domains-and-amazon-ses-told-apart-from-the-rest-of-aws
- **`mailbox_confidence` on a relay alias stays at 50.** It used to add the relay operator's DMARC, SPF, mail provider and domain age, and to score the alias's name, which says nothing about whether one alias exists, so a made-up alias at bravealias.com scored 75. Now only evidence against the alias moves it: a bounce rate of 50% or more among the sends reported for the domain takes 20 off, and a plus-tag takes 10. A sending policy with a `min_mailbox_confidence` above 50 may now hold catch-all relay aliases it used to let through; the default policy sets no floor.
- **23 more alias domains answer as relays.** They are:
- Surfshark Alternative ID: ten domains, such as flounderfantasy.com and dontsp.am.
- IronVest Masked Email: opayq.com, and Abine's older abinemail.com, blurmail.net, blurme.net and maskmemail.com.
- Erine.email: erine.email and erine.eu.
- Alias Email: bumpmail.io.
- Guard Email: m.guardemail.com and m1.guardemail.com.
- Addy.io: 4wrd.cc and addy.to.
- 33Mail: 33m.co.
Several sat on the open burner list, so an address there could answer `disposable` with the 80-point weight and no mailbox check. They now answer `is_relay` true with their `relay_service`. Cloaked's identity domains are not listed, because Cloaked keeps identity mail in its own inbox unless the owner turns forwarding on. Lookups already logged were relabelled for the email statistics.
- **Only Amazon's inbound mail hosts read as `amazon-ses`.** Any mail host named under amazonaws.com used to answer `mx_provider` amazon-ses and `mx_provider_type` transactional, including an EC2 instance, a load balancer or an SES Mail Manager gateway. Now only `inbound-smtp.{region}.amazonaws.com`, which SES receiving and WorkMail use, does; the rest answer `other`. Domain monitors start a fresh provider baseline for the domains they last saw as amazon-ses, so the relabelling sends no alert. The mx-provider-patterns version is 2026-10-08.
- **A delivery report no longer says a domain is not catch-all.** An address your account reported delivered answers as a confirmed mailbox. Its `catch_all` used to read false, even at a domain known to accept every address. It is now null, because the report says nothing about the rest of the domain. The verdict, score and billing are unchanged.
- **Domain pages describe relay domains as alias services.** The domain checker pages called a relay domain an organisation's own domain, and called Amazon SES "a transactional or application setup". They now name the relay service and say it forwards each alias to its owner's inbox.
- The forwarding-services dataset version is 2026-10-08.
## 2026-10-07: Brave Email Aliases and eight more alias domains answer as relays
Reference: https://spaw.co/changelog/2026-10-07-brave-email-aliases-and-eight-more-alias-domains-answer-as-relays
- **Brave Email Aliases are relays.** An address at bravealias.com, where the Email Aliases built into the Brave browser are issued, answers `is_relay` true with `relay_service` "Brave Email Aliases", `domain_category` `relay` and the 5-point relay weight. It used to answer `is_relay` false and `domain_category` `other`. The servers that receive Brave's alias mail accept every address at the domain, so a Brave alias still typically answers `risky` with reason `catch_all`: the mailbox handshake cannot tell a live alias from a made-up one.
- **Sign in with Apple's new relay domain is flagged before the switch.** Apple said in August 2026 that new Sign in with Apple addresses will be issued on private.icloud.com from later this year. Addresses there answer like those at privaterelay.appleid.com, with `relay_service` "Sign in with Apple private relay". The flag belongs to that exact name, so ordinary icloud.com addresses are unchanged.
- **ExpressMailGuard is a relay.** clivaro.io, a shared alias domain of ExpressVPN's ExpressMailGuard, answers with `relay_service` "ExpressMailGuard", and so do the dedicated subdomains the service hands out under it.
- **More alias domains of services Spaw already knew.** mailer.me and addymail.com answer as Addy.io, silomails.com and slmails.com as SimpleLogin, and passmail.com and pass2mail.com as Proton Pass.
- **mailer.me addresses are no longer disposable.** mailer.me is on the open burner list Spaw syncs every week, so an Addy.io alias there could answer `disposable`, with the 80-point weight and no mailbox check. A relay is never disposable, so these addresses are now checked and scored like any other relay.
- **A sending policy that blocks relays now blocks these addresses too.** The default policy does not block the `relay` signal.
- **Lookups already logged were relabelled**, so the email statistics count these addresses as relays. The verdict and score each lookup was answered and billed with are unchanged. The forwarding-services dataset version is 2026-10-07.
## 2026-10-02: iCloud and Proton mailboxes are not relays, LEIs meet their Companies House record, and honest location and freshness grades
Reference: https://spaw.co/changelog/2026-10-02-icloud-and-proton-mailboxes-lei-cross-checks-location-confidence-and-dataset-freshness
- **iCloud Mail and Proton Mail addresses are mailboxes, not relays.** Every address at icloud.com, me.com, mac.com, proton.me, protonmail.com, protonmail.ch and pm.me used to answer `is_relay` true with a `relay_service`, `domain_category` `relay` and the 5-point relay weight. A sending policy that blocks relays blocked them too. They now answer like any free mailbox provider: `is_relay` false, `relay_service` null, `domain_category` `webmail`. iCloud+ Hide My Email aliases do live on icloud.com, but nothing in the domain separates them from a real iCloud mailbox, so none is flagged. Proton Pass aliases have their own domains and still answer as relays. Lookups already logged were relabelled, so the email statistics do not count these addresses as relays.
- **Sign in with Apple private relay addresses are flagged.** An address at privaterelay.appleid.com answers `is_relay` true with `relay_service` "Sign in with Apple private relay". Such an address only accepts mail from the sender domains the app registered with Apple.
- **A LEI is now compared with its Companies House record.** The register key that links a LEI to its UK company number was stored cut short, so no LEI was ever matched with its Companies House record. LEI answers had no `agreements`, named only the GLEIF source, and could never raise `name_differs_across_registers` or `status_differs_across_registers`. A LEI with a Companies House cross-reference now answers with both records, the agreements between them and either flag where they differ. The same fault had cut long Companies House company categories and very long GLEIF legal names. The categories that can be rebuilt were restored at once; the rest are stored whole from the next weekly register update.
- **`location_confidence` answers low for anycast, mobile-carrier and satellite addresses.** An anycast address is served from many places at once, and a mobile or satellite network declares the location of its gateway or ground station, not the user's. Such an address used to be graded high when its operator declared the location, or by its country's measured agreement. It now answers `low` whenever `is_anycast`, `is_mobile` or `is_satellite` is true, whatever the location source. Operator-declared locations for other networks are still graded high. The consistency check withholds the IP-to-address distance for these addresses, as it does for any location not graded high.
- **`/status` calls a dataset current only when its latest scheduled update landed.** Each dataset's freshness window now follows its own schedule plus two days' grace, so one missed run shows as stale within days instead of weeks. The lists curated by hand get a review window and are no longer shown as unchecked: the sanctions and FATF lists, the satellite and mobile network lists, and the address formats and rules. The FATF window counts from the plenary the lists reflect. The North Rhine-Westphalia address register is now dated by its publisher's release, not by the day it was synced.
- **An address longer than mail can be sent to answers `invalid_syntax`.** The limit is 254 octets with the domain converted to punycode, the longest path RFC 5321 allows. An internationalized domain grows when it is converted, so a 129-character address could be 315 octets long; such an address used to be answered and billed, and its history row could then fail with an error. It is now `invalid_syntax` and free, and an address of exactly 254 octets is still checked.
- **The email suppression list compares addresses exactly.** josé@ and jose@ are different mailboxes, but the list used to treat them as one entry: importing the second spelling was skipped, and removing one could remove the other. They are now two entries. A batch of delivery reports is also recorded all or nothing, so a provider's webhook retry no longer stores part of a batch twice.
- **Flemish place lists are no longer cut.** Some Flemish postcodes name a list of sub-municipalities up to 162 characters long, which the addresses-at-a-postcode answer cut at 120. They are now returned whole, and the stored rows were repaired.
- **A long API key name no longer breaks lookups made with it.** The usage label `api · ` is now cut to 100 characters, which every column that records it can hold.
## 2026-10-02: New pricing: a longer ladder, yearly plans, five IP lookups to the credit and a bigger free tier
Reference: https://spaw.co/changelog/2026-10-02-new-pricing-yearly-plans-ip-lookups-at-five-to-the-credit-and-a-bigger-free-tier
- **A bigger free tier.** Every account now gets 100 free credits at the start of each month (it was 10), and confirming the email address adds a one-off bonus of 500 (it was 25). Unused free credits still accumulate, and balances already on accounts are untouched. The low-balance warning now fires at 50 credits.
- **The ladder goes to a million credits a month.** Plans are Starter 1,000 credits for $7 a month, Growth 5,000 for $29, Scale 25,000 for $119, Volume 100,000 for $349 (was $399), and three new tiers: Pro 250,000 for $749, Business 500,000 for $1,249 and Enterprise 1,000,000 for $1,999, which is $2.00 per 1,000. One-time packs are 1,000 for $9, 5,000 for $39, 25,000 for $149, 100,000 for $449 (was $499), and the new 250,000 for $899 and 1,000,000 for $2,499. Above a million credits a month, ask us at /support?topic=volume.
- **Priority support covers Scale and every tier above it.** Pro, Business and Enterprise carry it from the start, as Scale and Volume already did; the free tier, Starter and Growth have email support. Priority puts a request at the top of the support queue and is not a response-time promise.
- **Every plan can be billed yearly**, at ten times the monthly price: Enterprise is $19,990 a year, $1.67 per 1,000. A yearly plan still deposits its credits once a month, on each monthly anniversary of the year's start, not all at once. A monthly plan can move to yearly at any time from the Billing page and the year starts that day; a bigger yearly plan starts a new year that day and credits the unused part of the old one. A yearly plan cannot be switched to monthly from the dashboard.
- **Pack credits never expire; plan credits roll over up to three months' allowance and stay usable for 60 days after you cancel.** Credits a plan deposits are tracked apart on the balance and spent first. While a plan is active, a deposit that would take them above three months of the plan's allowance expires the excess at once. After a plan ends, the plan credits left stay usable for 60 days past the end of the last paid period, then expire. Each expiry is its own ledger entry with the reason `plan_credits_expired`, and the Billing page shows the cap or the date the plan credits stay usable until. Pack, free grant, signup bonus and automatic top-up credits never expire.
- **Automatic top-ups on a plan buy a block of the plan.** An account with an active plan tops up with one block the size of its plan at the plan's own rate, and those credits never expire. Accounts without a plan still buy the pack they chose. The top-up line has to sit below the block and the monthly ceiling has to cover it.
- **IP lookups cost a fifth of a credit.** One credit now buys five answered IP lookups. The lookup that opens a block reports `credits_used` 1 and leaves four prepaid on the account; the next four report `credits_used` 0. Every IP answer carries `meta.ip_lookups_prepaid`, the prepaid lookups left after it, on single lookups, each batch item and the batch, the consistency IP signal and the MCP tools. A ledger row with the reason `ip_lookup` now stands for five IP lookups, and the Usage page counts it that way. Bulk IP runs, IP monitors and the IP leg of a consistency check bill the same way, and a bulk preview's `credits_upper_bound` for an IP list is a fifth of its distinct rows, rounded up. ASN and range lookups still cost 1 credit each.
- **Unallocated IP space is free**, as the pricing page already said. It used to be billed. Reserved and private ranges, unknown addresses and repeats within seven days stay free, and none of them uses a prepaid lookup.
- **An email answer whose mailbox check was uncertain is free.** A `risky` answer with the reason `unverified` used to cost 1 credit unless the handshake partner was down. It now costs nothing whatever the cause: greylisting, a timeout, or a server that would not say. It leaves no seven-day repeat marker, so a later lookup is a fresh one: the uncertain result is reused for up to 15 minutes (`refresh` and `max_age` do not set it aside), then the mailbox is asked again, and the lookup bills only once the mailbox is confirmed. A re-check behind `callback_url` bills 1 credit when it confirms the mailbox and nothing while it still cannot say; a re-check the balance cannot pay for ends with a final callback that is not settled, carries no data and says `meta.stopped_reason: "insufficient_credits"`. Catch-all, role and other definite risky answers still cost 1 credit.
- **Undeliverable email verdicts are free within a fair-use allowance.** An undeliverable answer that needed a mailbox check is free up to 500 in any 30 days, or the number of deliverable and risky email answers billed in those 30 days when that is more. Past the allowance each one costs 1 credit and answers `meta.fair_use_billed: true`. Verdicts settled without a mailbox check (bad syntax, no working mail server, a username the provider never allows), suppressed addresses, test addresses and repeats are always free and never counted. The rule is the same on single lookups, batches, bulk runs, monitors and the browser endpoint, and the Billing page shows how much of the allowance has been used.
- **A mailbox the handshake reports invalid without a finer reason now answers `mailbox_not_found`.** These answers used to come back `unverified`. They are now `undeliverable` with no `smtp_reason`, and count toward fair use like any undeliverable answer that needed a check.
- **The licensed add-ons are repriced, and neither is enabled.** The live carrier-network check would add 4 credits to the base credit, 5 in all (it was 3 extra), and the list of addresses at a postcode from a licensed partner would cost 25 credits per answered list (it was 2). The address deliverability check stays at 3 extra credits. The live carrier-network check (`hlr: true`) is not enabled on the service: `hlr_checked` answers false and nothing extra is charged. The street listings from open registers still cost 1 credit per answered postcode.
- **Every customer-facing statement follows.** The pricing page and its Markdown twin, the rate card in the API reference and the OpenAPI document, the getting-started guide, the product and docs pages, the comparison pages, the press kit, the glossary, the MCP tool descriptions, the welcome and low-balance emails, the terms, llms.txt and the three client libraries' notes all state the new rules.
## 2026-09-27: Phone line types from carriers' filings, a number formatter, and pages for every phone source, signal, carrier and state
Reference: https://spaw.co/changelog/2026-09-27-phone-line-types-from-carrier-filings-a-number-formatter-and-pages-for-every-phone-source
- **Line types from what carriers file.** Phone lookups now type the numbers of 28 more carriers from what each files with the FCC. In North America a number's line type cannot be read from the number itself, so these numbers now answer from the carrier holding the block. Wireless carriers' numbers answer `line_type` `mobile`: Boost Mobile, T-Mobile's former Powertel companies, Bell Atlantic Mobile (now Verizon Wireless), Liberty Mobile in Puerto Rico and the US Virgin Islands, Southern Linc, STPCS Joint Venture, and Fido in Canada. Numbers of VoIP providers that sell numbers, SIP trunks or programmable voice now answer `line_type` `voip` and `is_virtual: true`. That adds the `virtual` signal (+40) to `risk_score`, so such a number that also carries a signal worth more than 20 now scores above 60 and is refused by the default SMS policy. Cable companies' phone lines and wireline carriers still answer `fixed_line_or_mobile`. On NANPA's file of 25 September 2026, the share of allocated US thousands-blocks with a settled line type rises from 63.8% to 67.0%.
- **Time zones on `GET /api/v1/phone/block`.** `timezones` now lists only zones in the range's own country: the +1 709 report no longer names America/Puerto_Rico. For North American codes it reads every exchange rather than twelve sampled numbers, so +1 867 names all five of its zones.
- **Fictional ranges have a length.** A number a digit short or long of one of Ofcom's drama ranges is no longer taken for a fictional number. It now answers `valid: false` with the numbering plan's own reason and is free, where it answered valid and fictional and was charged: `invalid_number` for 020 7946 001, a digit short, and `too_long` for 020 7946 00181.
- **A free phone number formatter.** [/tools/phone-number-formatter](/tools/phone-number-formatter) takes up to 200 numbers and gives each in E.164, national and international form, with its country, the numbering plan's line type, time zones and local time, and whether it fits the plan. Numbers that don't fit get the lookup's reason and did-you-mean suggestion. Results can be copied as CSV, and the numbers are not stored. The page also publishes two tables derived from libphonenumber 9.0.37: every region whose trunk prefix is not 0 or whose numbers keep a leading 0 after the calling code (the North American 1, the 8 of Russia and Kazakhstan, Hungary's 06, Italy's kept 0), and every international call prefix other than 00. It explains why +44 (0)20 is wrong, citing ITU-T E.123.
- **A page for every phone data source.** [/tools/phone-number-lookup/sources](/tools/phone-number-lookup/sources) gives every publisher a phone answer reads its own page: the ten regulators whose allocation tables are synced (NANPA, CNAC, Ofcom, ARCEP, ACMA, ACM, NAD, the CRT, UKE and MIMIT), libphonenumber, the two SMS-receiving lists and the FTC's reported-calls files. Each page names the files and their format, how often they are published and refreshed here, the terms as the publisher states them, which numbers a table is complete for, and what the installed copy holds as counts by status and distinct holders, never rows or names. It also says what the table cannot tell you. The index lists the regulators whose tables are not synced (Germany, Spain, Brazil, Ireland, Denmark and Switzerland), each with a dated reason, and has a Markdown twin at `/tools/phone-number-lookup/sources.md`, also carried in `llms-full.txt`. The phone product page's data-sources table links each dataset to its publisher's page, and its Licence column is now Terms and states what each publisher actually publishes: no terms of use from NANPA, CC0 for ACM's register, Ofcom copyright with acknowledgement, terms not confirmed for ACMA and the CRT. The status page lists NANPA's NPA report, the table of every North American area code, synced daily and dated by the date NANPA puts on the file.
- **A page for every phone risk signal.** Each of the 18 phone signals, from `fictional` (+80) to `reported_unwanted_calls` (+20), has its own reference page at `/docs/signals/phone/{signal}` with a Markdown twin: the weight, the dataset it is read from, the field it reads, whether the default SMS policy blocks on it, what it cannot tell you, what to do, and an example answer. [/docs/signals](/docs/signals) now has an email section and a phone section, so a phone reader no longer lands on the email weight: `disposable` is +80 on an email answer and +70 on a phone one. The IP and address weights stay in their guides until they get pages of their own. The risk-score table in the phone intelligence guide and its Markdown twin show each signal's slug, linked to its page and anchored as `#signal-{slug}`.
- **North American area codes.** Area-code pages now show NANPA's facts about the whole code: the state or province, the codes sharing its area, when it went into service, how local calls are dialled and whether relief planning is under way. They also show the rate centres its codes are assigned for, the codes and thousands-blocks assigned in the last 12 months and those not yet in service, and every assigned central office code with its rate centre and the code holder's network brand. Every table is original allocation, and there is no page per code. A code no number can ring in yet says so in NANPA's terms, with NANPA's dates, instead of "line types unknown": codes whose every assigned code waits for its effective date (357, 483, 837, 861), overlays in service with no code assigned (465, 471, 748 and seven more, now indexed) and planned codes (273), each with whether libphonenumber accepts its numbers yet. [New and upcoming North American area codes](/tools/phone-number-lookup/new-area-codes), with a Markdown twin, lists from NANPA's NPA report the codes put in service in the last 24 months, those assigned but not yet in service and those in relief planning, each with its overlay, its central office code counts, its first dated code and whether libphonenumber accepts its numbers yet.
- **States and provinces.** Each US state and Canadian province with more than one area code, about 50, has a page at `/tools/phone-number-lookup/countries/{us|ca}/{state}`: its area codes with overlays and in-service dates from NANPA's NPA report, the allocated central office codes counted by rate centre or exchange area, the pooled share (US), the codes and thousands-blocks assigned in the last twelve months, and the codes by network brand, as original allocation and as shares of the regulator's table.
- **Network carriers.** [/tools/phone-number-lookup/carriers](/tools/phone-number-lookup/carriers) has a page for each of about 45 brands in the US, Puerto Rico, Canada, the UK and France: the legal entities and codes the regulator lists, the numbers they were allocated (counted in numbers, as a share of the regulator's table), how a lookup reads those numbers, thousands-blocks held inside other brands' pooled US codes, assignments by year and the area codes with most of the brand's numbers. The carrier on a lookup is the block's original holder, not the caller or the network serving the number today. The pages are counted weekly from the NANPA, CNAC, Ofcom and ARCEP tables, on Mondays after the phone feed sync, and listed in `sitemap-phone-carriers.xml`.
- **Phone statistics from public data.** [/statistics/phone](/statistics/phone), with a Markdown twin and a CSV of Spaw's derived counts under CC BY 4.0, counts where the caller IDs consumers report to the FTC sit in the US numbering plan. On 26 September 2026, 60.8% of the 489,940 US-area-code numbers reported between 28 June and 23 September sat in blocks NANPA lists under VoIP providers, against 19.7% of all allocated US numbers; both shares are lower bounds, because they count only holders on Spaw's classification list. Toll-free numbers are 12.3% of reported caller numbers but 49.3% of those reported at least three times on two days. The page also gives the FTC counts by month, the codes and thousands-blocks NANPA, CNAC and ARCEP assigned in the last 7, 30 and 365 days, and SMS-receiving listings by calling code. Every FTC figure carries three statements: this is not the Do Not Call Registry, it says nothing about whether you may call or text a number, and the reports are unverified and caller ID is often spoofed.
- **Country pages.** Each region's page now says how its numbers are dialled and written, read from libphonenumber's numbering metadata: the trunk prefix, the international prefix (and, where the metadata allows several and prefers none, every prefix it allows), the number lengths per line type, the digits every mobile number starts with, whether mobile and fixed-line numbers share ranges, and one number written nationally, internationally and as dialled from the US and the UK. SMS-receiving listings are counted for the region's own numbers, never worldwide or per calling code, with the listings that still score `disposable` first. The countries index lists each region's trunk and international prefix, and a page for a region with no synced table says what its carrier answer is: the range's original network in libphonenumber's carrier data, which a port never updates.
- **France, the Netherlands and the UK by the plan's own codes.** France has one page per zone, 01 Île-de-France to 05 South-west France, named from ARCEP's plan, which since 2023 allows a 01 to 05 number anywhere in metropolitan France, and pages for its 08x and 09 ranges. The Netherlands has a page per area code (010, 0111, 0229 Hoorn …) and one 06 page. Old two-digit pages redirect where all their blocks moved to one page, and France's 06 and 07 mobile ranges no longer have indexed pages, because ARCEP's table misses part of them. The UK's non-geographic ranges have pages named by type, such as "074 mobile numbers" and "070 personal numbers": 03x, 055, 056, 070, 071 to 079, 080, 084, 087, 090, 091 and 098. The United Kingdom's country page lists its 07 numbers by original range holder, counted in numbers from Ofcom's table: VodafoneThree 38.0%, Virgin Media O2 28.9% and EE 27.5% of the allocated mobile numbers, with notes on 070 personal numbers, 076 pagers and the Crown Dependencies' 07 prefixes. Where a regulator's blocks differ in size, area-code pages rank holders and brands by the numbers their blocks cover rather than by rows, with a numbers column: on France's 01 that puts Orange at about 59% of the numbers, where the rows showed 4.5%. The `GET /api/v1/phone/block` report still counts rows. Outside North America and the UK's area codes, a range page with fewer than 100,000 allocated numbers still renders but is no longer indexed.
## 2026-09-27: Fictional phone ranges in France, Australia and Ireland, and new phone guides, terms and comparisons
Reference: https://spaw.co/changelog/2026-09-27-fictional-phone-ranges-in-france-australia-and-ireland-and-phone-guides
- **Fictional ranges in France, Australia and Ireland.** Phone lookups now flag the numbers ARCEP, ACMA and ComReg reserve for films, television and other creative works, as they already did for the North American 555-01XX block and Ofcom's drama ranges. The ranges are ARCEP's audiovisual blocks 01 99 00, 02 61 91, 03 53 01, 04 65 71 and 05 36 49, plus 06 39 98, which sits in Mayotte's mobile range and is dialled +262 639 98 xx xx; ACMA's (02), (03), (07) and (08) 5550 xxxx and 7010 xxxx, its thirty listed 0491 57x mobiles, 1800 160 401, 1800 975 707 to 711 and 1300 975 707 to 711; and ComReg's 089 011 0000 to 089 011 0999. A number in one of them answers `valid: true` with `is_fictional: true` and the `fictional` signal (+80). The default SMS policy refuses it with `blocked_by: "fictional"`, and it costs one credit like any valid number. Before this change the five French geographic blocks answered `unassigned_block` and +262 639 98 answered `invalid_number`, both free, while the Australian and Irish numbers answered valid, unflagged and `ok_to_send: true`. Ireland's 020 code is recorded but still answers `invalid_number`, because ComReg gives it no line type. The fictional-ranges dataset version is 2026-09-27, and each range now names its publisher's document and the day it was read.
- **Country pages count reserved single numbers apart.** The fictional-ranges line on a country page counts ranges and single numbers separately (Australia: 8 ranges and 41 single numbers), and ARCEP's +262 639 98 is counted on Mayotte's page, not Réunion's.
- **Phone guides, terms, comparisons and integrations.** Eight new guides: [an area code is not a time zone](/guides/an-area-code-is-not-a-time-zone), [fictional and test phone numbers](/guides/fictional-and-test-phone-numbers), [how to store phone numbers in E.164](/guides/how-to-store-phone-numbers-in-e164), [number pooling](/guides/number-pooling-why-a-us-codes-holder-is-often-not-your-blocks-holder), [SMS delivery error codes](/guides/sms-delivery-error-codes-explained), [SMS pumping and the default SMS policy](/guides/sms-pumping-what-the-default-sms-policy-lets-through), [UK number ranges from 01 to 09](/guides/uk-number-ranges-in-a-phone-lookup) and [what FTC unwanted-call complaints can and cannot say about a number](/guides/what-ftc-unwanted-call-complaints-can-and-cannot-say-about-a-number). The [glossary](/glossary) gains fifteen phone terms, from [number pooling](/glossary/number-pooling) and [rate centre](/glossary/rate-centre) to [SMS pumping](/glossary/sms-pumping) and the [live carrier check](/glossary/live-carrier-check), and the phone docs' field table, area-code pages and country pages link them. [/compare/phone](/compare/phone) adds Veriphone, Abstract Phone Validation, ClearoutPhone, numlookupapi, IPQualityScore Phone Validation and Loqate Phone Validation, each read from the vendor's own pages on 27 September 2026, and every phone comparison gains an SMS pumping controls row. The HubSpot, Klaviyo, Typeform, Pipedrive, Salesforce, ActiveCampaign and Airtable [integration guides](/integrations) now cover phone numbers, and a new Sign-in and one-time codes category has recipes for [Firebase Authentication](/integrations/firebase-auth), [Supabase Auth](/integrations/supabase-auth), [Amazon Cognito](/integrations/amazon-cognito) and [Auth0](/integrations/auth0) that check a number before a code is sent. The guide on why US numbers cannot be split into mobile and landline no longer says every UK 07 number is a mobile: only 071 to 075 and 077 to 079 are.
## 2026-09-26: Phone places named from the regulators' tables, prefixes as dialled, and Northern Mariana Islands numbers answered again
Reference: https://spaw.co/changelog/2026-09-26-phone-places-dialled-prefixes-and-the-northern-mariana-islands
- **Northern Mariana Islands numbers stop answering `unassigned_block`.** NANPA files the islands' +1 670 codes under the state code NN, which the sync read as the United States. A lookup of a Northern Mariana Islands number then found no block of its own in NANPA's table, and because that table lists every assigned code, the number answered `valid: false` with `unassigned_block`. The codes are now filed under MP, so from the first NANPA sync after this release those numbers answer from their own blocks, with the holder as `carrier`, and the islands' country page lists +1 670.
- **Cleaner `region` names.** Where `region` on `POST /api/v1/phone` falls back to the number's NANPA rate centre, the name no longer carries NANPA's billing zones and district areas: "New York City Zone 01, NY" reads "New York City, NY" and "Los Angeles DA 06, CA" reads "Los Angeles, CA", and names are cased properly (Fremont-Newark, McKinney, La Grange). A number in one of Washington, DC's zones in Maryland or Virginia now answers what the numbering metadata says, such as "Maryland", not "Washington Zone 03, MD". Three misspellings in the numbering metadata are corrected wherever a place is shown: "Chtamstths, VI" reads "Charlotte Amalie (St. Thomas), VI", "British Colombia" reads "British Columbia" and "Lódź" reads "Łódź".
- **A North American `place` the data agrees on, and a new `places` list.** `GET /api/v1/phone/block` names a North American area code after a city only when most of its sample numbers are geocoded to that city and a rate centre agrees, or when one rate centre holds half or more of the allocated blocks. Otherwise `place` is the state, province or territory ("Montana"; "Nova Scotia and Prince Edward Island"), where it used to be whichever town the samples happened to name. The new `places` array lists up to three rate centres (NANPA) or exchange areas (CNAC) holding the most allocated blocks, filled only when `place` names a state, province or territory and empty everywhere else. Both describe where the regulator recorded the codes, not where subscribers are.
- **Prefixes as people dial them.** `GET /api/v1/phone/block` accepts a prefix written with its trunk prefix, the 0 of the UK, France, the Netherlands, Australia and New Zealand or the North American 1: `gb/020`, `gb/0161` and `us/1212` answer the report of `gb/20`, `gb/161` and `us/212`, with that stored `prefix`, its charge and its repeat marker. Italian, Polish and Mexican prefixes keep their leading digits. A miss on such a form names both spellings.
- **The area-code and country pages.** Area-code pages lead with the number as dialled inside the country, name mobile, freephone and premium-rate ranges by type instead of "area code", and say "area code" only where the prefix is the plan's own area code: Sydney's 029 sits inside the 02 area, so its page reads "029 numbers". They sit under their country in a breadcrumb and are dated by the tables they cite, never by the day they were rendered, and their structured date is the day what the page shows last changed, the same date the sitemap gives. They no longer name Polish and Dutch holders listed without a legal form, such as sole traders, Polish partnerships whose names carry their partners' names, or the holders of single Dutch numbers, which the API's block report still returns; the holder table's last row adds up the rest of the regulator's table, the smaller holders included, and no network is credited with blocks the page does not attribute to it. `/gb/020`, `/us/787` (Puerto Rico's page), the Saint Martin and Saint Barthélemy pages and a bare `/tools/phone-number-lookup/us` redirect to their canonical pages. Country pages list every area code with a page instead of the first 24, grouped by state, province or code length, and Guernsey, Jersey and the Isle of Man say their numbers sit in the UK +44 plan and are answered from Ofcom's table.
- **Also.** The phone product page, the phone guide and API reference, the OpenAPI document, the MCP tools, the SDKs, the integrations, the phone comparison pages and the phone guides now state the live carrier-network check's status in one sentence: it is not enabled on the service, `hlr_checked` answers false and nothing extra is charged. The status page calls the UK numbering table stale after ten days without a new copy instead of thirty, and addresses left over from the domain's previous owner answer 410 Gone.
## 2026-09-25: Suppressions you import, spam complaints and abuse reports stay until you remove them
Reference: https://spaw.co/changelog/2026-09-25-email-suppressions-stay-yours
- **A lookup no longer clears what you put on the list.** A recovered verdict used to remove an email suppression entry whoever added it, so an address you had imported, or one a complaint had put there, came off the list the next time it was verified afresh: a single lookup, the MCP tool, a visitor typing it into the form on your site, or a batch, bulk or monitor run once the entry was more than 90 days old. A verdict that is not undeliverable now clears only an entry an earlier undeliverable verdict added. Imports stay until you remove them, and an entry a bounce report added stays until you remove it or report a delivery to that address.
- **An undeliverable verdict no longer relabels your entries.** It used to rewrite an imported or reported entry as an automatic one, which the next recovered verdict would then clear. It now only renews the entry, which keeps a bounce report's entry past the 90-day staleness check, and leaves its source and reason as they were.
- **Importing an address already on the list makes it yours.** It keeps the verdict stored with it and is counted as `already_suppressed`, as before, but becomes an import, so no later lookup or delivery report takes it off. Importing an address again is harmless and never needed to keep it listed.
- **Stale entries are still re-checked, imports never.** Batch, bulk and monitor runs still verify an entry a verdict or a bounce report added afresh once it is untouched for 90 days, instead of trusting it. An import is never stale, on any of the three lists, and is served for as long as it is listed. A verdict or lookup answer an import kept from before you imported it is a measurement, though, and nothing measures an import again: it is served for 90 days after the entry was last renewed, and after that the import answers `suppressed` like any other, so an answer months old is never repeated as today's.
- **Phone and postal imports work the same way.** Importing a number or an address that a lookup or your own reports already put on the list keeps its reason, and any stored answer, but makes it an import, so a later valid lookup, delivery or verified report no longer takes it off. And a moved, refused or returned report about a postal address you imported no longer takes it over: it used to relabel the entry as a report, which the next delivery report then cleared.
- **A spam complaint stays until you remove it.** A complaint is the recipient's objection, not a fact about the mailbox, so it is now treated like an import. A later delivery report no longer clears it, a later bounce report no longer relabels it as a bounce (which a delivery then cleared), a complaint reported beside a bounce in one batch wins whichever comes first, and it never goes stale, so batch, bulk and monitor runs keep serving it after 90 days instead of re-verifying the address. An erasure request from the address itself keeps it as well, as it keeps a phone number's opt-out, because deleting it would put the address back in front of your next campaign; the privacy policy and the trust page say so.
- **A phone abuse report stays until you remove it.** An `abused` report puts the number on your phone suppression list with reason `abuse`, and that entry used to leave once the report was more than 90 days old and a delivery was reported after it — or it was relabelled `undelivered` by later failures, which a delivery then cleared. Now no delivery, verification, failure or lookup clears or relabels it, and it never goes stale, so batch, bulk and monitor runs keep serving it. The `reported_abuse` risk signal on a single lookup still counts reports from the last 90 days, as before.
- **Removing an abuse entry sticks.** Only an `abused` report in the feedback call that carries it now adds a number to the list with reason `abuse`, as only an opt-out in the call adds an opt-out. A removed abuse entry used to come back with the number's next report of any kind — delivered or undelivered — while the old abuse report was still inside its 90 days. A later delivered, verified or undelivered report no longer brings it back as an abuse entry; only a new abuse report does (an opt-out, or failures that would suppress any number, still add it with their own reason).
- **An opt-out, abuse report or complaint about an entry you imported is kept.** An email complaint used to leave an imported entry exactly as it was, so it still went stale 90 days after the import, and a phone abuse report turned an imported number into an entry of your own reports, which a later delivery could clear once the report was 90 days old. The report's reason — `opted_out`, `abuse` or `complained` — is now written onto the import, which stays an import: it never goes stale, `meta.opted_out` answers true, and an erasure keeps it. An opt-out still replaces an abuse reason, never the other way round. A report that justifies the entry without standing for good — a permanent failure, repeated failed deliveries, an email bounce, post reported moved, refused or returned — leaves an import's reason alone, and a delivery or verification still never clears an import. A report that says nothing against the number or the address — a failure your SMS provider puts on the sender's side, or a late delivery receipt dated before the failures — renews nothing and never puts back an entry you removed. An import never goes stale, on any of the three lists.
- **Drama numbers stop answering unassigned in batch, bulk and monitor runs.** Before Ofcom's drama ranges answered valid where Ofcom's own table lists the block as protected, a lookup of such a number — 020 7946 0018 was the documented example — put it on your phone suppression list as `unassigned_block`, and batch, bulk and monitor runs kept serving that stored answer. Such an entry is now removed the next time a run meets it, and the number is looked up afresh: valid, with `is_fictional` true.
- **Time zones, precisely.** Round four's note that `timezones` names the zones of the number's own country holds wherever the time-zone database files a zone under that country: a Palestinian number answers Asia/Gaza and Asia/Hebron rather than the Europe/Bucharest the numbering data gives, whose daylight-saving dates differ. The database files none under Kosovo, Ascension Island or Tristan da Cunha, so their numbers answer the zone the numbering data gives, Europe/Belgrade or Atlantic/St_Helena, which keeps the same clock.
## 2026-09-24: A business park named after its road is found under either spelling
Reference: https://spaw.co/changelog/2026-09-24-business-park-addresses
- **Both spellings read the same companies.** A business park that takes its road's name — Ferry Road Office Park, Stone Road Business Park, Green Lane Business Park — is filed under the park and under the road, so `companies_registered` answers the same whichever way the address is written. Measured on the Companies House copy of 1 September 2026: 7 Ferry Road, Preston PR2 2YH read 10 companies and the park spelling 80; both now read 90. 55 Stone Road, Stoke-on-Trent ST4 6SR read 15 and now reads 361, the same as 55 Stone Road Business Park; 6 Green Lane, London SE9 3TL read 2 and now reads 182. Where the count crosses the published threshold of 25, `is_mail_drop` answers `true` with `mail_drop_source: company_cluster` for the road spelling too.
- **Only inside a full postcode.** The road spelling is filed only when the address carries a full British postcode, so a park is never merged with a road of the same name elsewhere in a town or a postcode district. The designators read are OFFICE PARK, BUSINESS PARK, BUSINESS CENTRE and BUSINESS VILLAGE, after a name ending in a road word; a park whose name is not a road (Viewpoint Office Village) is still found by its own name.
- **One filing, one count.** A filing is counted once, never once per spelling: the district pages' company tally and the cluster threshold see the same number as the lookup.
- **Mailbox operators are not widened.** A mail-drop branch listed under a park still flags only that listing; it is not copied onto the road, because an operator's listing answers `is_mail_drop` on its own, with no threshold between it and the answer.
- **Known limit.** A park that numbers its units rather than its buildings can share a count with the same number on its road: at Stamford PE9 2PR, 1 West Street reads 6 companies, 5 of them filed at unit 1 of West Street Business Park. That is below the threshold, so it raises no flag.
## 2026-09-24: A building before its number, the street behind a care-of line, and a town under another of its names
Reference: https://spaw.co/changelog/2026-09-24-address-parser-and-town-matching
- **A building named in front of its number and street.** `Richmond House 50 Tontine Street, Blackburn BB1 7ED` now reads Richmond House as the `building` and 50 Tontine Street as the number and street, as the comma form already did; the whole line used to be the street, with no house number. The split needs a name of two words or more that ends in a building word — House, Building, Court, Point, Quay, Wharf, Park and the like — followed by a number and a street that ends in a street type, in a country that writes the number first, so a street such as Mill House Close is never cut. A unit written in front reads the same way: `Unit 3 Morris Park 37 Rosyth Road` is Unit 3 of Morris Park, at 37 Rosyth Road.
- **The street on a care-of line.** In separated fields, a care-of line that carries the street keeps the street and drops the name: `C/O CSC CLS (UK) LIMITED 5 CHURCHILL PLACE` above `10TH FLOOR` reads 5 Churchill Place with the unit 10TH FLOOR, where the whole line used to be set aside and the address was left with no street. An ordinal street (`c/o ACME Corp 623 26th Ave`) and one whose type comes first (`c/o Smith LLP 1345 Avenue of the Americas`) are read too, and so, in any country, is a street parted from the name by a comma (`c/o Ola Hansen, Karl Johans gate 22` reads Karl Johans gate 22). Wherever the name can be told from the street it never becomes the street, and a care-of line with no street on it is set aside once a town or postcode is given, as it already was in a written address.
- **A town after a floor.** `167-169 Great Portland Street 5th Floor London, W1W 5PF` reads London as the `locality`, where it used to take London for the `dependent_locality` and leave the town empty. That happens only when no other town line is written, and a room or building named after the floor — Lion Suite, Lion House — is never the town.
- **States in Mexico and India.** A Mexican state written by its INEGI code, abbreviation or name — `Jal.`, `N.L.`, `Edo. Méx.` — and an Indian state written by its name are read as the `administrative_area`, before or after the postcode, a full name when it stands on a line of its own: `Avenida Juárez 100, Guadalajara, Jal. 44100` reads Guadalajara in Jal. and matches the postcode on both, where Jal. used to be taken for the town, and `1 MG Road, Bengaluru, Karnataka 560001` reads Bengaluru in Karnataka. A state whose full name is also a large city's — Puebla, Durango, Oaxaca, Delhi — and Mexico City are left to be read as the town.
- **An Irish county is not an Eircode.** A postcode with no digit in it is no longer read as one, so `12 Main Street, Naas, Co. Kildare` reads Naas in Co. Kildare with no Eircode, where it used to take KILDARE for the postcode, and a town written alone after the street — Athlone, New Ross — stays the town. A real Eircode (`D02 X285`) still reads, and Bermuda's letter-only codes (`HM BX`) are the one exception.
- **Unit lines as each country writes them.** A German `3. OG` or `EG`, a Danish `2. tv`, a Spanish `3º B`, an Italian `Scala B` after the town and a French `Bât. B` are the `unit`, where they used to be read as a district or as the region, and a French Résidence is a building: `Calle Mayor 5, 3º B, 28013 Madrid` answers `unit` 3º B. An Austrian `Stiege 3, Top 7` is the unit too, but only in Austria: in Germany and Switzerland Stiege is a street name, so `Stiege 12, 48739 Legden` reads the street Stiege at number 12. Studio is a unit word when a number or a single letter follows it (`Studio 16, 5 Marine Crescent`), while Studio Rd stays a street; a box line keeps the street written below it (`CS 70001` above `12 rue Bichat`); and a building number written before a unit carries to the street on the next line.
- **Unit and number, the way they are written.** In Australia and New Zealand a slash is unit over number: `3 / 1234 Pacific Highway, Pymble NSW 2073` answers `unit` 3 and `house_number` 1234, where the number used to be `3 / 1234`, and `Level 3/123 Collins Street, Melbourne VIC 3000` reads unit Level 3 at 123 Collins Street. An Austrian `Hauptstraße 3/2/12` reads number 3 with unit 2/12, and a Russian `ул. Тверская, д. 1` reads the street and house number 1. A company name above a numbered street is the organisation, and a numbered street is never taken for the town: `Knoll Buchler Phillips Limited, 84 Grosvenor Street, W1K 3LN` reads 84 Grosvenor Street.
13 of the parser corpus's 18 known limits are closed. It now holds 238 written addresses, 6 of them known limits with today's answer and a note.
- **Company counts, re-keyed.** Re-keying the whole Companies House copy of 1 September 2026 with the new parser, the way the company index files it, 4,221,495 companies key to a building, up from 4,109,723. Of the 110,663 that had no key before and have one now, 46,438 gained it through a building named in front of its number, 41,041 through a unit written before a building and 10,153 through a care-of line. Richmond House, 50 Tontine Street BB1 7ED goes from 89 companies to 307, 5 Churchill Place E14 5HU from 1,921 to 2,640, Carpenter Court, 1 Maple Road SK7 2DH from 11 to 865, Lake House, 2 Port Way PO6 4TY from 7 to 772 and Lytchett House, 13 Freeland Park BH16 6FA from 142 to 1,711, and the filings keyed to a street that is really a care-of line fall from 4,356 to 1,601. Brønnøysund's file and the five US business registers go through the same separated fields and lose 1,114 Norwegian entries and 632 American ones: every Norwegian one a lone care-of line, and each of the 107 American ones sampled a lone care-of or attention line.
- **Other names of a town.** A town written under another name of the same place now matches its postcode: Munich for `80331` (München), Cologne, Vienna, Prague, Lisbon, Rome, Milan, Brussels, Bucharest, Moscow and Kiev each scored `locality_mismatch` and now read `locality_matches_postcode: true`. The names come from GeoNames' list of places of 15,000 people or more (CC BY 4.0), compared inside one country only; a name that another listed place of that country also carries is left out, and so are airport codes. Another name is matched against the postcode's own places, or the name of a one-city region, never against its neighbours', and a town's only where the postcode's centroid lies within 30 km of the point GeoNames gives the place, so Bristol written for Briston's `NR24 2AD` and Berlin for Perlin's `19209` are refused. A name the directory also lists as a town of its own more than 30 km away counts only when it is the place's own name cut short — Biberach for Biberach an der Riß, Yarmouth for Great Yarmouth — so Perlin, a village near Schwerin that GeoNames lists among Berlin's names, is refused for a Berlin code too. An answer confirmed through another name names `geonames-cities` in its `sources`, dated by the day the cities file was installed; one written in the directory's own spelling, München, does not. Measured on 24 September 2026 across every installed directory, Great Britain's included, as it stood before the GeoNames re-read below, writing 23,683 of the list's towns in those countries under their GeoNames names: the postcode nearest the town accepts it 77.6 percent of the time, up from 72.5; a wrong postcode 15 to 40 km away 56.0 percent, up from 55.6, every gain within 30 km of the town at a code that lists it under another of its names or in its one-city region; and one 40 to 150 km away 36.2 percent, unchanged but for a single Bangkok code, which Bangkok's one-city region accepts. Of 32,700 real pairs of a postcode and its own first place none changed, and of 25,867 wrong pairs none was newly accepted. A wrong name — Pressburg, Bratislava's German name, for a Budapest code — is still refused.
- **A town written as its one-city region.** Where the region a postcode is filed under is a single city, the city's name written as the town reads `confirmation.locality: postcode_area`, reported and never scored. Mexico City was the only one; Delhi, DKI Jakarta, Bangkok, Bogotá, Moscow, Saint Petersburg, Bucharest, Seoul and Korea's other metropolitan and special self-governing cities — Busan, Incheon, Daegu, Gwangju, Daejeon, Ulsan and Sejong — now join it, and so does Tokyo for a code filed under one of its special wards, 7,510 of the directory's 8,747 Tokyo codes. Tokyo for `100-0005`, Jakarta for `10110`, Seoul, Busan, Sejong, Bangkok and Bogotá scored `locality_mismatch` and no longer do, while Tokyo written for a Hachiōji code (`192-0001`) still is: the Tama cities and the islands are not the city. Bucharest is the municipality alone, not Ilfov around it, and Bucharest written as the region for `010011` no longer scores `region_mismatch`. A region that merely holds a city of its name — Bremen, São Paulo, New York — is never one city.
- **Region spellings for Indonesia, Korea and Colombia.** The directory files Indonesian postcodes under the statistics office's province numbers alone, so any province written by name scored `region_mismatch`. All 34 provinces now answer to their number, ISO code, Indonesian and English names and the usual short forms (Jawa Barat, West Java, Jabar), and the four Papuan provinces created in 2022 read as inside the one they were split from. Korea's 17 regions, which the directory stores in Hangul, answer to their romanised, English and Hangul names, and Bogotá to its capital-district spellings. Across every installed code, the 8,266 Indonesian codes written with their province's Indonesian name went from `region_mismatch` to a match, and so did the 34,249 Korean codes written with the short names (Seoul, Gyeonggi), and the 22,273 of them the romanised ISO names had failed.
- **Danish towns without their district letter, and postcodes in two regions.** GeoNames writes a Danish postal town with the letter or name of its district — København K, Aarhus C, Viby J, Nykøbing F, Brenderup Fyn — so København, Copenhagen or Aarhus written on its own scored `locality_mismatch`. The directory now lists the bare town beside it, and does the same for eleven Dutch codes and one in Liechtenstein whose town carries letters that tell it from a namesake (Bergen L, Mauren FL). A postcode GeoNames files under two regions now keeps both, where it kept only the first: 3644 lies in New South Wales and Victoria, so `Cobram East VIC 3644` no longer scores `region_mismatch`, and nor does Queanbeyan's 2620 written with New South Wales. Measured on 24 September 2026, before and after the directory was re-read from GeoNames with the new reader: the 605 Danish codes whose town carries such a suffix, written with the bare town, go from 599 `locality_mismatch` to none, and Copenhagen written for the 446 codes of København from 446 to 2 — 0900 and 0999, which GeoNames places in North Jutland, beyond the 30 km another name reaches; 314 codes in 25 countries are filed under two regions or more, and written with a region after the first they go from 269 `region_mismatch` to none.
- **Eircode routing keys that cross a county line.** GeoNames files each routing key under a single province, so an address in a county across the line from the key's town scored `region_mismatch`. Which counties a key reaches now comes from the Property Services Regulatory Authority's Residential Property Price Register, as last updated on 23 September 2026 and read on the 24th: 250,671 of its 807,724 sales carry an Eircode, and a county counts for a key once it holds at least ten of that key's sales. Fourteen keys reach a county of another province, or are filed under the wrong one: A82, A91, E32, E41, E53, F91, H12, H91, N37, N39, N41, P14, R42 and X91. For those, every county the key reaches, and that county's province, answers `administrative_area_matches_postcode: null`, neither confirmed nor refused; the province GeoNames files the key under answers null rather than true; and a county the key does not reach is still refused unless it lies in that province — and for P14, which GeoNames files under Leinster though none of its sales is there, even then: Athlone's N37 written with Roscommon no longer scores, written with Kerry it still does, and Crookstown's P14 written with Kildare now does. D11 and D15 reach into Meath, 17 of D11's 3,132 sales and 149 of D15's 7,075, so neither confirms County Dublin any longer; Dublin 11 and Dublin 15 still do. Written with each sale's own county, the register's addresses now score `region_mismatch` 330 times, down from 3,644, and 10,035 D11 and D15 sales written with Dublin (3,115 and 6,920) read null where they read true, measured on the same sales that set the ten-sale line. Only the facts derived from it ship — the counties of those fourteen keys, and D11's and D15's — and the register is credited in the footer and in the [address guide's](/docs/address-verification) table of datasets.
- **Known limits.**
- `1-1 Marunouchi, Chiyoda-ku, Tokyo, 100-0005` keeps `1-1` on the street line: Japan's format writes the block number after the name, as the Japanese script does, and the number is split off only once the formatter writes it first for an address in Latin letters.
- `10 downing street london sw1a2aa`, written on one line with no separators, keeps the town on the street line: only a register can say where the street ends.
- A building, court or park named in front of a number, above a street address the parser does not take for one (`Innovation Centre 1 Liverpool Science Park` over `131 Mount Pleasant`, `Aftermarket House 5 Marlin Office Village` over `1250 Chester Road Castle Bromwich`), is keyed at its own number rather than the street's. Of the 185 filings the whole file re-keys, 138 move onto a road OS Open Names lists in the postcode's district and 8 move off one.
- A care-of line in a country that writes the number after the street, with no comma between the name and the street, cannot be cut, because where the name ends and the street begins cannot be read: written as an address, `c/o Firma AS Storgata 5, 0155 Oslo` is set aside and answers `missing_street`, and in separated fields the whole line stays the street, `c/o Firma AS Storgata` at number 5.
- A Mexican state's full name run on after the town on the same line (`Guadalajara Jalisco 44100`) stays with the town, because towns end in state names too (Miguel Hidalgo, Dolores Hidalgo); written on a line of its own (`Guadalajara, Jalisco 44100`) it is the state.
- The directory carries no names for Indonesia's regency codes, so `Jakarta Pusat` written for `10110` still scores `locality_mismatch`, even though DKI Jakarta now matches the region.
- `Cundinamarca` for a Bogotá code, and a Russian region written in English (`Moscow Oblast`), still score `region_mismatch`.
- Another name that the directory also lists as a town of its own more than 30 km away does not count unless it is the place's own name cut short: Calicut is not taken for Kozhikode at `673001`, because a Calicut in the Andamans has a code of its own, and reads `postcode_area`, unscored; Llanelly for Llanelli's `SA15 1AB` scores `locality_mismatch`, because the directory also lists a Llanelly in Monmouthshire. Nor does another name at a postcode GeoNames places more than 30 km from its town: Helsingfors for Helsinki's `00002`, which it places at Hattula.
- A code GeoNames files under a region by mistake answers to that region too: Bulgaria's `8543`, a Burgas code, is also filed under Montana, so Монтана written for it is accepted.
- A county that holds fewer than ten of a key's sales is still refused: Mullingar's N91 written with Cavan, which holds 4, scores `region_mismatch`. The register lists residential sales only, so a cross-border area with no recorded sale stays unseen.
## 2026-09-24: Phone intelligence round four, from an SMS policy to Poland's and Italy's number blocks
Reference: https://spaw.co/changelog/2026-09-24-phone-intelligence-round-four
- **49 fields.** `POST /api/v1/phone`, the batch, the browser endpoint, monitors and the MCP `verify-phone` tool now answer sixteen more fields after `roaming`: `carrier_code`, `carrier_brand`, `block_pooled`, `block_assigned_at`, `region_level`, `local_time`, `utc_offsets`, `in_call_window`, `did_you_mean`, `range_seen_by_you`, `client_velocity`, `destination_new_for_you`, `complaints_90d`, `complaints_last_at`, `ok_to_send` and `blocked_by`. The published weights gain five signals: `range_burst` 40, `numbers_per_client` 30, `disposable_range` 30, `international_network` 30 and `reported_unwanted_calls` 20. The full guide is at `/docs/phone-intelligence`.
- **An SMS policy decides `ok_to_send`.** Every phone answer ends with `ok_to_send` and `blocked_by` from your account's SMS policy: the countries you send to or never send to, the line types you refuse, the signals that block, a ceiling on the score, whether a number your suppression list holds as opted out is blocked (`block_opted_out`) and, if you want one, a live check that reached the handset. Set it with `GET`, `PUT` and `DELETE /api/v1/phone/policy` or the SMS policy tab on the phone page. The defaults block `fictional`, `disposable`, `reported_abuse`, `reported_abuse_widely` and `premium_rate`, any score over 60 and a number your suppression list holds as opted out, and send to every country and line type; `blocked_by` names the rule that decided, in a fixed order that starts with `invalid`. It is a fraud and deliverability decision, not consent and not a do-not-call scrub, and a country list is a business choice about where you send, not a verdict on a country. The guide [Gate SMS verification codes with the SMS policy](/guides/gate-sms-verification-codes-with-the-sms-policy) walks through a signup flow.
- **Signals from your own traffic.** `range_seen_by_you` counts the distinct numbers your account looked up in the same 1,000-number range in the last hour, and five or more add `range_burst` (+40). `client_velocity` does the same for one client address: send `client_ip` on `POST /api/v1/phone`, which is counted and never looked up or billed, and a browser lookup counts the visitor's address on its own; five or more add `numbers_per_client` (+30), unless Spaw's own IP data places the address on a mobile carrier or a corporate proxy, where many people share one. `destination_new_for_you` says whether the country is new for your account in the last 30 days. Only single and browser lookups are counted, a browser answer carries the three fields null, and no other account's lookups are ever read. The account owner is emailed about range bursts, destination spikes and enumeration runs, and the Usage page shows the last seven days of phone lookups by country. `destination_new_for_you` and the spike alert start once a week of the account's own lookups has been counted, and until 14 days have been counted a spike is measured against the days counted so far; the enumeration alert watches from the first counted hour.
- **Who holds the block.** `carrier_code` is the regulator's code for the block holder: the NANPA or CNAC operating company number, ARCEP's operator code, UKE's operator ID. `carrier_brand` names the network behind a legal name — AT&T for New Cingular Wireless PCS — from a bundled, versioned map cited as `carrier-brands` in `sources`. `block_pooled` marks North American codes split into thousands-blocks that other carriers may hold, 71.8% of the assigned codes in NANPA's file of 24 September 2026. In the United States the lookup reads NANPA's daily thousands-block report too, so in a pooled code `carrier`, `carrier_code`, `carrier_brand`, a settled `line_type` and `is_virtual` describe the holder of the number's own block of a thousand, and a block the pool holds free answers `unassigned_block`; in Canada, and for a block the report does not settle, they describe the code holder. `block_assigned_at` is the date the regulator assigned the code or range, from NANPA, CNAC and ARCEP, or in a pooled US code the date NANPA assigned the number's own block; it is never the code's date for a pooled code. `GET /api/v1/phone/block` adds each holder's brand, a brand breakdown, the pooled count and the span of assignment dates, and now answers Guernsey, Jersey and the Isle of Man from the UK table.
- **Poland and Italy.** Phone lookups read UKE's numbering tables for Poland and MIMIT's assigned-numbering workbook for Italy, synced weekly: the operator behind Polish and Italian fixed and mobile blocks, a Polish number in the space the regulator holds in reserve answered `unassigned_block` from that row itself, and mobile ranges assigned to messaging platforms flagged `is_virtual`. Neither table lists every block, so a number missing from one is never called unassigned. The footer credits both, UKE with its retrieval date on the status page.
- **SMS-receiving numbers, kept as they age.** Two open lists of the numbers SMS-receiving websites publish are merged and synced daily now. A listed number scores `disposable` (70) for a year after it was last seen; one that leaves them is kept at its last-seen date for two years and scores `disposable` for 90 days after it was last seen, then `disposable_stale` (40), instead of vanishing. `disposable_range` (+30) flags a number that is not listed itself in a 1,000-number range holding ten or more listings that still score disposable (still listed and seen within the year, or dropped and seen within 90 days); `GET /api/v1/phone/block` reports the same count under `disposable`, and +1 202 555 0113 is its test number, the eighth.
- **Complaints about callers.** For North American numbers, `complaints_90d` and `complaints_last_at` count the consumer complaints in the Federal Trade Commission's published reported-calls data that name the number as the caller over 90 days, and `reported_unwanted_calls` (+20) fires at three or more complaints on at least two days. The reports are unverified and caller ID is often spoofed: this is context about a caller, not proof of wrongdoing, and not a do-not-call scrub.
- **The clock and a typo check.** `region_level` says how specific `region` is: `locality`, `area` or `country`. `local_time` gives the time at the number now where its time zones agree, `utc_offsets` their current offsets, and `in_call_window` answers when you send `call_window` (`"09:00-20:00"` in the number's local time) on `POST /api/v1/phone` or `/phone/batch` — clock arithmetic, not compliance advice. On an invalid answer to a number sent with a country, `did_you_mean` suggests the one valid reading of what was typed, such as a missing `+` or a foreign international prefix left in; it is never applied, and the answer stays free. A reading in a range reserved for fiction is offered even where the regulator's table lists its block as protected, as the lookup answers such a number valid. `international_network` (+30) scores the international-network and satellite codes +870, +881, +882 and +883, on top of `voip` for the last two.
- **SMS outcomes that say what failed.** Delivery receipts from the SMS providers whose webhooks Spaw reads are classified by the provider's error code, and `POST /api/v1/phone/feedback` takes an optional `failure_class`: `permanent`, `transient`, `not_sms_capable` or `sender_side`. Only failures about the number count against it: one permanent failure suppresses it at once, a transient or unclassified one after two, and a carrier filter, an unregistered campaign or a sender limit is recorded without scoring your recipient. `GET /api/v1/phone/feedback/summary` splits failures by class. Two more outcomes: `verified` (the recipient entered the code) counts as a delivery, and the summary's `verified_share` reads as your code conversion, overall and per block; `opted_out` never scores, keeps the number on your suppression list with reason `opted_out` until you remove it, and a single lookup reports it as `meta.opted_out: true` and, under the default SMS policy (`block_opted_out`, on by default; an OTP sender may switch it off), answers `ok_to_send: false` with `blocked_by: "opted_out"`. Honouring an opt-out stays your obligation. Every feedback endpoint, for all four products, refuses an `occurred_at` more than five minutes in the future.
- **Suppression entries stay until you say.** A valid lookup now clears only an entry an unassigned block added. An import, an opt-out, an abuse report, or an entry your own failure reports added stays — including when somebody types the number into your public form — until you remove it or, for a failure report, a later delivery or verification clears it.
- **Bulk runs.** Bulk phone runs mark every repeated number (`is_duplicate`, `duplicate_of_row`), add up a finished run in a `summary` (verdicts, reasons, line types, the top countries and carriers, signals, suppressed rows, what the live check found, and the rows your SMS policy held back), and download as `deduped`, `mobile`, `ok` or `blocked` as well as by validity and risk. The result file carries every new field a run fills, which leaves out the three own-traffic fields and `in_call_window`.
- **Monitors.** Phone monitors save each number once, in E.164, read against the monitor's country where it has no calling code: two spellings of one number are one entry, counted and billed once, the 500-number limit counts distinct numbers, and an entry that does not parse is kept as typed and answers invalid. Existing monitors were converted in place and keep their baselines, and `results` on `GET /api/v1/phone/monitors/{id}` is keyed by E.164. A monitor now flags numbers that still answer but changed — listed by an SMS-receiving website, reported for abuse by three or more accounts, risen to high risk, a new line type, and with the live check ported or served by another network — in their own email, in the webhook's `flags` and on the monitor itself. A line type is compared only with one read from the same basis, so a type now read from another block holder, or from the number's own thousands-block, sets a new baseline rather than a flag. A number on your suppression list answers `suppressed`, which never counts as decay. Phone monitors stay weekly or monthly: 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 with the live check a daily run would pay for the network check seven times a week.
- **Fixes.** Phone inputs accept a full-width plus, a `00` international prefix and up to 64 characters everywhere. Ofcom's drama ranges answer valid and fictional, including 07700 900xxx, 01632 960xxx and 0115 496 0xxx, and including where Ofcom's own table lists the drama block as protected. The Northern Ireland drama range is 028 9649 6xxx: real Belfast numbers on 028 9018 0xxx no longer read as fictional. `timezones` names the zones of the number's own country: a Jamaican mobile answers America/Jamaica instead of every zone of +1, and a Lithuanian or Moroccan number its own zone instead of a neighbour's. A fictional number is never sent to the live check. A repeated live check is free while the network's day-long answer is held; after that the repeat asks the network again and bills the premium, reserved first and refused with `402` when the balance cannot cover it, and resuming a live-check bulk run more than a day after it stopped does the same for the rows it had answered. The base credit is charged once per number every seven days whether or not a lookup asked for the live check: a check after a plain lookup of the same number costs only the check, and a plain lookup after a check costs nothing. The `hlr-live-status` source is dated when the network answered. UK 084 and 087 numbers answer `shared_cost` (+20) instead of `premium_rate` (+60), as Ofcom's numbering plan classes them as service numbers with a capped charge, so the default SMS policy, which blocks `premium_rate`, no longer blocks them; 0871 to 0873 still fall under Ofcom's premium-rate-services rules, so add `shared_cost` to `blocked_line_types` to keep refusing them. Erasing a phone number also drops the shared live-check answer, its FTC complaints and any listing the SMS-receiving sites no longer publish.
- **Clients.** The Python, Node and PHP SDKs (0.5.0, unreleased) carry the new fields and the SMS policy calls, and their phone lookup takes `client_ip` and `call_window`. The Zapier, Make, n8n and Google Sheets connectors return the new fields, with call window and client IP inputs where a step can use them. The footer credits UKE, MIMIT, the FTC and the second SMS-receiving list.
## 2026-09-23: Address verification fixes: postcodes no directory lists, ZIP+4, company counts without the dissolved, and a parser that finds the town
Reference: https://spaw.co/changelog/2026-09-23-address-verification-fixes
- **A postcode the directory never lists is not evaluated, not refused.** Great Britain's directory leaves out Northern Ireland's BT postcodes, because reusing them needs a Land & Property Services licence, and the release carries no British Forces Post Office numbers, no BF1 codes and no non-geographic BX codes. The directory is complete, so until today every one of those answered `valid: false` with `postcode_not_found`. They now answer `postcode_status: null`: valid, with no town or county comparison, no centroid, `confirmation.postal_code: "pattern"`, and no register match, because Ordnance Survey's Open Names covers Great Britain only as well. `1 High Street, Belfast, BT1 5GS` is a valid street address with a risk score of 0, and an organisation above a BX code needs no street at all: `HM Revenue and Customs, BX9 1AS` is valid with the organisation separated. Boxes get the same treatment from the other side. A directory built from places — GeoNames, and every directory a register builds — does not list Postfach PLZs or APO ZIPs, so `Postfach 10 01 01, 50441 Köln` and `PSC 1234 Box 5678, APO AE 09204` are no longer refused by one. The British directory, which does list box and large-user codes, and the French copy, which carries La Poste's business codes, still refuse a box code they lack.
- **A ZIP+4 is matched on its ZIP everywhere.** The United States directory is complete, and it used to be asked for all nine digits, so `1600 Amphitheatre Parkway, Mountain View, CA 94043-1351` answered `postcode_not_found`. The directory now looks a ZIP+4 up by its ZIP, and so do the building registers, the Census Bureau's number ranges, the company counts, the mail-drop lists and the registered-agent list, on both sides of every match: a filing or a branch published with a ZIP+4 is found by an address written with the plain ZIP, and the reverse. `postal_code_formatted` keeps all nine digits, and `premise_key` is the same for either spelling. It matters most in Connecticut, whose business file writes a ZIP+4 on 150,652 of its 439,769 filings, 34 percent; 43 Anvil Drive in Avon now reads the same company count written as `06001` or as `06001-3218`. The street listings follow: `GET /v1/address/streets` and the open half of `GET /v1/address/postcode` answer a ZIP+4 with every street in its ZIP and label the streets with the ZIP rather than the nine digits, every spelling of one ZIP is one charge for the week, and a test address written `99999-0001` is still a test address.
- **companies_registered counts only companies that still exist.** Every register was audited against its own file:
- Colorado's file lists every entity the state has ever registered. Of its 3,105,488 rows, 2,219,686 belong to entities that still exist — in good standing, behind on a periodic report (which the Secretary of State says does not mean a business has stopped operating), of a type that files no report, or whose agent has resigned — and the other 885,802, among them the dissolved, withdrawn, revoked, merged and converted, are no longer counted. 1500 North Grant Street in Denver read 136,102 companies in the 22 September compile and reads 122,145.
- New York's file held one row per address per document filed, so a company was counted once per filing, under agents it had since replaced, and after it dissolved: its 869,311 registered-agent rows with a street belonged to 542,338 entities. The register now reads the Department of State's active-corporations file, 4,284,739 active entities on 23 September, each counted once, 873,062 of them with a registered agent's street. 28 Liberty Street reads 53,904 rather than 89,029, and 111 Eighth Avenue, where agents that have since moved once filed, 128 rather than 81,625.
- Oregon's file lists active registrations only, but 1,819 of them named their agent more than once, spelled two ways at one address; each now counts once.
- France already counted active establishments only. The 2,429,502 active ones whose owner has opposed publication read "[ND]" in every address cell, and they were filed under placeholder keys no real address matches; they are now left out altogether, so no count at a real French address moves — 60 rue François Ier in Paris still reads 31,270.
- Connecticut and Hawaii (active entities only), Norway (a deleted entity leaves the file) and Companies House (a copy of the live register, with no dissolved company in it; a company in liquidation or under a strike-off notice still holds its registered office and still counts) were checked and are unchanged.
- An agent who writes a building name, a suite or "C/O" on the first line and the street on the second is now read at the street, when only the second line carries a house number: 15,065 New York rows, 6,261 Colorado and 4,995 Oregon. 99 Washington Avenue in Albany, which many filings write as "One Commerce Plaza", now reads 4,794.
Across the eight registers the local compile went from 23,946,643 company records over 9,652,828 addresses on 22 September to 20,812,823 over 9,478,132 today.
- **The parser finds the town, the organisation and the county.**
- A lone line before the state and ZIP, or before the postcode, is the town: `Springfield, IL 62701`, `Southampton, SO14 2AQ` and `Paris, 75001` answer `missing_street`, as `75001 Paris` already did, where they used to take the town for the street and pass. A line that carries a number, a road or building word, a unit or a box stays the street (`Rose Cottage, SO14 2AQ` is still a street address), and nothing changes in a country whose format has no town line, such as Singapore.
- Where the line's shape leaves it open, the installed data decides: a place the postcode directory lists for the code or around it makes the line the town, and a street a register's listing files there makes it the street. `Strand, WC2R 0RL` is a valid street address confirmed by Open Names, and so are Piccadilly, Whitehall and Deansgate written the same way, while `Mountain View, CA 94043`, whose name ends in a street type, answers `missing_street` with Mountain View as the town, as Beverly Hills and Oak Park do. Where the data says both or neither, the shape still decides. Nothing new is reported in `changes`: the line was read, not moved.
- A floor is the unit, not the street: `5th Floor, 167-169 Great Portland Street, London W1W 5PF` reads 167-169 Great Portland Street with `unit` 5th Floor, and so does the floor written after the street or between the number and the street. Ground Floor Flat, 3rd Floor North, Level 3 and The Basement are units too, every unit an address carries is kept in the order written (`5th Floor, Suite 500`), and a building named with its number above a numbered street is the building: `Tower 42, 25 Old Broad Street` reads 25 Old Broad Street. In today's local compile the Companies House snapshot places 29,852 companies at 167-169 Great Portland Street and 27,998 at 86-90 Paul Street, and each of those spellings finds the same count. Across the whole snapshot no filing is keyed to a street that is only a floor any more, and 688 are keyed to a street with the word floor somewhere in it.
- An organisation line needs evidence — a word such as Ltd, PLC, Bank or HM, or an acronym above a town written in mixed case — so `HMRC, BX9 1AS` is valid with `organization` HMRC while `asdf, BX1 1AA` answers `missing_street`. `DVLA, Swansea, SA99 1AR` separates the organisation too, and still answers `missing_street`: only a BX code stands in for the street.
- A numbered box written after the street on the same line is the delivery point: `123 Main St P.O. Box 5, Springfield, IL 62701` answers `po_box: "PO BOX 5"`, no street and `box_line_moved`, as `PO Box 5` alone does.
- An Irish `Co. Mayo` or `Co. Kerry` line after the town is the county, and the town stays the town: `Main Street, Castlebar, Co. Mayo` reads Castlebar in Co. Mayo.
- Where a country writes the area after the town — Mexico, Italy, Spain, Turkey, Argentina, Chile and others — a line after the postcode's line is the area: `Avenida Madero 1, Centro, 06000 Ciudad de México, CDMX` reads Ciudad de México and CDMX, and `İstiklal Caddesi 1, 34433 Beyoğlu, İstanbul` reads Beyoğlu and İstanbul.
- A ` / ` splits a line only where the postcode leads the town and never after a bare number, so `CS 70001 / 01001 Bourg-en-Bresse CEDEX` is a box above its town while `3 / 1234 Pacific Highway, Pymble NSW 2073` keeps its unit and number.
- A care-of line that carries the street keeps it: `C/O 39 Castle Street, Leicester LE1 5WN` reads 39 Castle Street, where it used to read Leicester as the street.
- Unit words match whole words only, so Stevenage, Flagstaff, Levelland and Unity are towns again rather than an `STE`, `FL`, `LEVEL` or `UNIT` unit.
The parser corpus now holds 212 written addresses, 18 of them known limits with today's answer and a note.
- **Real streets are not placeholders.** The shapes that mark a placeholder — no vowel, one letter repeated, a keyboard run — are now judged on the street without its type, never on the folded key that turned `MG Road` into MGRD and `Bridge Street` into BRST, which made the shape rules flag 1,013 of about 380,000 street names in Ordnance Survey's Open Names, and `MLK Blvd` besides. W counts as a vowel in Welsh (Bwlch, Cwmffrwd), R and L in Czech and Slovak (Tvrz). A part a dataset has confirmed is never judged at all: the comune of None under 10060, Jbphh for Joint Base Pearl Harbor–Hickam, CDMX for Mexico City. Measured over about 925,000 street names from the installed registers, 10 still flag.
- **Regions written the way people write them.** Mexico's 32 states answer to their INEGI number, ISO code, abbreviation or name — CDMX, CMX, DF and Distrito Federal all mean Mexico City — and Ireland's provinces to their name, letter or Irish name, where any spelling but the directory's own used to score `region_mismatch`; Indian states answer to their ISO codes old and new, and a Spanish province with two official names to either. An Eircode routing key is filed under its province, so a county inside that province answers `administrative_area_matches_postcode: null` rather than a mismatch, a Dublin district key (D01–D24, D6W) confirms County Dublin (`Dublin 2, Co. Dublin, D02 X285` matches), and a county in another province is still refused. Mexico City is a single city, so `Ciudad de México` written as the town of a Mexico City postcode is accepted, while any other state's name is still a wrong town.
- **The town check knows the municipality.** GeoNames lists a colonia, a post office or a street under many postcodes instead of the town. For eighteen countries whose file carries it, the directory now also lists the municipality or city from the dump's own administrative columns: Guadalajara 44100, Mumbai 400001, Makati 1200, Sapporo 060-0001, Chiyoda-ku 100-0005 and București 030031 match their postcodes. Bulgarian cells holding a Cyrillic and a Latin name are split (Plovdiv 4000, Sofia 1000), as are Spanish ones holding two official names; every Singapore code lists Singapore; and Metro Manila's capital district lists Manila. Measured on the 9,903 towns of 15,000 people or more in those countries, the postcode nearest each town now names it 55.6 percent of the time, up from 32.8. The cost is leniency: a code 15 to 40 km away now names the town 10.7 percent of the time, up from 1.2, so a wrong town nearby is accepted more often than before.
- **CEDEX, box lines and Canada Post's stations.** A CEDEX after the town is business-mail routing: `10 rue de Rivoli, 75384 Paris CEDEX 08` keeps its street and answers `address_type: bulk_mail` with `po_box` null, where it used to be read as a box and lose the street; a CS or TSA line is still the box. A street line that opens with a box written the short way — `GD`, `P O Box`, `Apdo` — becomes the box under the operator's name (GENERAL DELIVERY, PO BOX, APARTADO) and no longer stays the street as well, and a box on its own line beside a street keeps both and is written once in `formatted`, under the street, never again as a district. Canada Post's station stays on the box: `GD STN A Calgary AB T2P 2M2` reads GENERAL DELIVERY STN A in Calgary, and `PO Box 45 Stn LCD 1, Toronto ON M5W 1A1` keeps STN LCD 1 whole.
- **The sandbox takes an empty optional field.** The published sandbox key answered `403 SANDBOX_VALUE_NOT_ALLOWED` to the test order the Shopify page prints, because the empty `address_line2` a checkout sends reaches the API as null. An empty or null optional field now counts as absent; a value of any other type is still refused, and an empty field never makes an address that is not a test address answerable.
- **Two data pages, and fuller district pages.** [Address data sources](/tools/address-checker/sources) lists every address register and postcode directory installed — publisher, licence, declared coverage and what partial means there, the levels it answers at, its size and version, how often it is refreshed — with the company registers and mailbox operators counted but not named, and a Markdown twin. [Great Britain postcodes in numbers](/tools/address-checker/gb/postcodes) counts the live and terminated postcodes in the ONS directory, with the Channel Islands and the Isle of Man counted apart, the codes introduced and terminated in each of the last 24 complete months, and a row per postcode area. Each [British district page](/tools/address-checker/gb/districts) now also lists its postcode sectors, the twelve nearest districts, every road Open Names files under it (named roads first, then numbered ones) and how many companies Companies House lists with a registered office there, with, from a hundred up, how many sit at addresses 25 or more companies share: WC2H has 91,283, of which 89,741. A page headlines the place Open Names most often lists first rather than the region, and the 26 Channel Islands and Isle of Man districts have pages of their own, apart from every Great Britain figure.
- **Measured on real business addresses.** The address product page now publishes a recall table: every Sunday a seeded sample of up to 2,000 addresses from each installed company register goes through the ordinary lookup, uncharged and unlogged, and the page prints how far each got — the building, the street, the postcode only, nowhere, no register to ask, or refused as written. In the local run of 23 September 2026, 1,441 of 2,000 Companies House filings matched a street. It measures the matcher on addresses other people wrote, skewed urban and towards formation agents, and it is not coverage.
- **Street listings read the register that files the postcode.** Belgium's three regional registers share its postcodes, and the streets and postcode endpoints used to ask only the first, Flanders, so every Brussels and Walloon postcode listed nothing: the streets endpoint answered 404 and the postcode endpoint an empty list. They now try each register in turn and name the one that answered: 1000 Brussels lists 1,394 streets and 4000 Liège 857, and 1,143 Belgian postcodes can be listed, up from 517.
- **Known limits.** Where neither the directory nor a street listing settles a lone line, its shape still decides: `London Wall, EC2M 5QD` is read as the town and answers `missing_street`, because Open Names files London Wall under EC2Y, and `Canary Wharf, E14 5AB` still passes as a street called Canary Wharf. And only a BX code stands in for a street; other large-user codes do not.
## 2026-09-20: The sign-up form now runs the email lookup on your address
Reference: https://spaw.co/changelog/2026-09-20-the-sign-up-form-runs-the-email-lookup
- **The address is checked while you are still looking at the field.** Leave the email box on /register and the form runs the same lookup `POST /api/v1/email` answers, and says what came back: a likely misspelling is offered as one click that rewrites the field (`gmial.com` for `gmail.com`, and `acme.co` for a domain you would otherwise have to spot yourself), a mailbox the handshake confirms gets a quiet line saying so, and an address that cannot receive mail gets the reason before the form is submitted rather than after. It is advisory — nothing is disabled and the button always works, because the address is verified for real when the account is created.
- **Only an undeliverable address is refused.** A domain publishing no mail servers or a null MX record, mail servers that do not resolve, a username the provider does not issue, a mailbox the provider reports as missing or closed: in each case the confirmation link cannot arrive, and that link is the account — the API refuses every request from an unconfirmed address, and the starting credits land on the click. Everything the product calls risky still opens an account, exactly as before: a role address, a free provider, a catch-all domain, a domain registered last week. Those are answers this product sells, not grounds to turn a customer away at its own door. A disposable address is refused by the rule that has always refused it, in its own words.
- **The refusal you read is the refusal you get.** The line under the field and the error on submit are one sentence from one answer, so fixing what the hint told you cannot lead to a second refusal worded differently — and the answer is reused across both, so signing up costs one mailbox check rather than two.
- **A check that cannot run opens the door.** If DNS, the registration data or the mailbox handshake cannot be reached, the form stops objecting and the sign-up goes through. Refusing a paying customer because something upstream is down is worse than the unconfirmable address the check exists to catch, so it is never the outcome.
- **Changing the address on your profile is checked the same way** — with one exception that has always applied to the disposable check and now applies here too: the address your account already holds always passes. A mailbox that goes quiet for an afternoon must not lock you out of the one screen you would correct it from.
## 2026-09-17: Address intelligence round three: the decision, the placeholders, the units, and what the parser did to your input
Reference: https://spaw.co/changelog/2026-09-17-address-intelligence-round-three
- **One boolean per address, decided once.** Every address answer now ends with `ok_to_ship` and `blocked_by`, decided by a shipping policy the account keeps: the signals that block whenever they fire, a ceiling on the risk score, and whether a national register must have confirmed the building. The order is fixed — an address that cannot stand as written first (`blocked_by: invalid`), then the first blocked signal in the published weights' order, then the score ceiling (`risk_score`), then the register requirement (`unconfirmed`) — and the defaults block `undeliverable`, `placeholder` and `vacant` and any score over 60, require no register, and leave a mail drop at 50 to pass, because whether a parcel may go to a mailbox shop is a merchant's call. `GET`, `PUT` and `DELETE /api/v1/address/policy` read, change and reset it, free and unlogged; the Shipping policy tab on the verification page edits the same three rules with checkboxes beside the weights; batch, bulk, monitored lists, the browser endpoint and the five test addresses all apply the account's policy, the guest demo the defaults. The guide at /guides/gate-a-checkout-with-the-shipping-policy suggests three policies and says why `not_in_register` deserves thought before it is blocked.
- **Placeholders are named for what they are.** "asdf", "test", "n/a", "no address", "same as above", "123 Fake Street", a row of x's, a keyboard run, four letters with no vowel: `looks_placeholder` is true when the street or the town of a valid address is one of those, by WHOLE name only — "Test Valley Road" in Hampshire and "Sample Road" fold to keys the list does not carry and are never flagged, and a short real town like Rye or Ely is never judged. A true answer carries the `placeholder` signal at 60, cites the bundled `address-placeholders` list in `sources`, and is blocked by the default shipping policy. It is not a typo detector: a misspelt real street still answers `street_suggestion`.
- **A suite at a single-family home.** `unit_check` reads the flat or suite you wrote against the units a register lists at the building it found: `ok`, `missing` (several units listed, none written, weight 20 as `unit_missing`) or `unexpected` (a flat or suite written at a building the register lists as a single residence, weight 30 as `unit_unexpected`), and null when the register did not find the building, does not say how many units it has, or lists one unit at a non-residential building, where a suite is ordinary. A private-mailbox number never counts as a unit, so a PMB is scored once. The Austrian register counts dwellings, the Belgian one carries box numbers and the Australian G-NAF lists flats as rows; the United States National Address Database fills its unit column on about fourteen percent of rows and therefore never calls a building single.
- **How often you have seen this address.** `seen_by_you` counts your own account's lookups of the same address inside a seven-day window, with the first sighting — kept under the premise digest, never the address, and never anybody else's lookups. A free repeat still counts, the window closes from its first sighting, the guest demo and the test addresses answer null, and it carries no weight; what three lookups in a day should mean is a rule for your policy, not for the score. There is still no cross-account address reputation, and there will not be one.
- **The address as its operator prints it, as lines, and in its own script.** `postal_code_formatted` writes the postcode the way its operator does — the British space, the Dutch space, the Canadian and Irish spaces, the Polish, Brazilian, Japanese and Portuguese hyphens, the ZIP+4 hyphen, the Luxembourg, Latvian, Lithuanian and Moldovan prefixes, plus Malta, Azerbaijan, Bermuda, Brunei, Somalia, Iran, the Cayman Islands, Lebanon and the American territories — and null when the postcode is missing or of the wrong shape. `formatted_lines` is the same rendering as `formatted`, one line per entry, for a label printer or a form with a field per line; a named building now stands on its own line above the street. `formatted_local` hands a Japanese, Chinese, Taiwanese, Hong Kong, Macao or Korean address written in Han, kana or Hangul back in that script and in the country's own largest-first order, 〒 kept, and stays null for a Latin-written one, because transliterating would be inventing characters the customer never typed.
- **Three new weights, seventeen in all.** `placeholder` 60 joins `private_mailbox` at the top below `undeliverable`; `unit_unexpected` 30 sits with the 30s and `unit_missing` 20 with the 20s. The docs page, its twin and the product page carry the same seventeen rows in the same order, test-enforced.
- **A parser corpus, and what it found.** 128 written addresses across 28 countries now run through the parser on every test run, each with the parts it must produce: the shapes fixed last round (country lines, care-of lines, building names, number ranges) and the ordinary ones. The first run disagreed on nine; five were parser gaps and are fixed — a spaced letter after a trailing number ("Musterstr. 12 a"), Singapore's "#20-01" unit written without a space, Korea's trailing house number, an Emirati office line in a country with no postcode and no town line, and a lone postcode-and-town line — beside the Italian province code after the town ("00100 Roma RM"). Twelve cases are recorded as known limits with today's answer and a note, capped at a fifth of the corpus, so the file documents rather than hides them: Irish counties, Turkish provinces, German, Danish and Spanish floor lines, French résidence and bâtiment lines, the Austrian door triple, Japanese block numbers, fractional American numbers, addresses written on one line with no separator, Indian states before the PIN, Russian house lines.
- **Directionals and repetition indices fold.** "1201 W Peachtree St NE" now finds a register row filed as "West Peachtree Street", and the reverse, because every street is keyed under its variants — as written, without a leading directional, without a trailing one, without both — at query time and, from today, at compile time; "9 North Elm Street" finds "Elm Street". French "6 bis rue de la Paix" reads as number 6 bis, the way the Base Adresse Nationale files it, with ter, quater and quinquies alongside. One caveat: a register compiled before today answers the typed-side variants only until it is recompiled, which its next weekly sync does.
- **Postcode directories learn from the registers, and from the Census.** Ten complete registers — Switzerland, Austria, Czechia, Norway, the three Belgian regions, Luxembourg, Australia and Latvia — now add, as they compile, every town they write under each postcode and a centroid to the country's directory, beside the directory's own rows and never in their place: a register of buildings never lists the box-only and large-recipient codes, so replacing the directory would have turned real box addresses into `postcode_not_found`. Coverage moves to complete only when an operator's published total is configured and the union reaches it, and none is configured yet. Measured on the Czech register: 38 codes added to the dump's 2,694 and 2,639 given the register's places. The United States, with no complete register, gets its extra place names from the Census Bureau's ZCTA-to-place file, public domain: 28,668 of 41,488 ZIPs gained places in 3.5 seconds, 94043 now lists Mountain View, Palo Alto and Sunnyvale, and "1 Main Street, Palo Alto, CA 94043" answers a matching town. It does not list Moffett Field or Hollywood — the file describes the places whose land a ZIP touches, not the names the postal service accepts — so an American town mismatch is still reported and still never scored.
- **A summary on every finished bulk run.** The address bulk payload now carries `summary`, built from the same cells the CSV holds: rows and duplicates, valid and invalid with their reasons, the risk split, the top ten countries and mail-drop operators, the address types, how far each row was followed into a register, how many rows carried each of the seventeen signals, the placeholders, and `blocked` — the rows the shipping policy refused. The dashboard's bulk panel shows it under each run; the webhook body is unchanged.
- **A page for every British postcode district.** Each of the 2,872 outward codes with data behind it — 2,752 of them indexable — has a page under the address checker at /tools/address-checker/gb/districts: the postcode area and its name, the places Open Names lists for the district's postcodes, the live and terminated postcode counts, the streets filed under the district with the first twenty, a centroid, the datasets and their versions with the Open Government Licence attribution, and a hand-off into the checker with the district prefilled. SW1A lists 145 live postcodes and 33 streets; RG5, 504 and 285. A page says whether codes are in use, never who is there; a road is filed under one district; and there is no open register of buildings in Britain, so `exists` stays null there. Northern Ireland has no pages, because BT postcodes need a licence the open directory does not carry.
- **The round-trip report stopped failing on what it cannot measure.** The weekly self-check that sends each register's own rows back through the pipeline now lists a register it cannot run under `unmeasured` with the reason — the British register of streets is keyed by district, whose rows the lookup refuses as a postcode before any match; the TIGER range table is managed by its own command and has no raw file to sample — instead of scoring them nought and failing every Saturday.
- **More registers.** Germany gets its first open registers, a Land at a time, from the three Länder that publish house coordinates a fetcher can read: Berlin (Datenlizenz Deutschland Zero 2.0, 402,523 buildings), North Rhine-Westphalia (Datenlizenz Deutschland Zero 2.0, 4,503,290 buildings, keyed by municipality because the file carries no postcode) and Rhineland-Palatinate (Datenlizenz Deutschland Namensnennung 2.0, 1,404,472 buildings), all compiled on 17 September and round-tripping at 100 percent; each is `partial`, because a Land is not the country, so a German address the installed Länder do not carry answers `exists: null`, never false, and the other Länder either sell their data or publish nothing a fetcher can read. Brazil gets the IBGE's 2022 census address list, CNEFE, as twenty-seven state registers under one dataset — free public data with attribution to IBGE, `partial` because it is a 2022 snapshot — with a position, a building use and a unit from the complement fields for every point; verified on Roraima, 135,417 buildings at 100 percent, and the national compile is about 226 million rows and two and three-quarter hours. To read it, Portuguese street types and titles now fold onto one spelling, so RUA and R, AVENIDA and AV, SANTA and STA agree on both sides of a match. And the Dutch BAG, CC0 and complete, is in the register list: the parser reads the 3.6-gigabyte archive of archives through a new nested-archive reader, verified on a fixture built to the extract's shape, with the compile of the real file under way as this entry is written. Each register names its licence in the answer's `sources` and its coverage claim in `register_coverage`.
- **Deliberately not built.** An American town mismatch still carries no weight, for the reason above. A register compiled before today answers only the typed-side directional variants until it is recompiled. The unit check counts a register's units and never matches them, because "Apt 7", "7", "Wohnung 7" and "bus 7" rarely agree across a register and a customer. And there is still no cross-account address reputation: `seen_by_you` is your own account's traffic and nobody else's.
## 2026-09-16: Address intelligence round two: what a register knows about a building, and what the parser did to your input
Reference: https://spaw.co/changelog/2026-09-16-address-intelligence-round-two
- **The parser stopped losing four kinds of address.** A last line that only repeats the country ("10 Downing Street, London, SW1A 2AA, United Kingdom", "…, CA 94043, USA", "…, 10115 Berlin, Germany") used to become the town, with the real town demoted to a dependent locality; it is now set aside, and when no country was sent and the line names exactly one, it becomes the country. A care-of or attention line ("c/o Jane Smith" above "221B Baker Street") used to become the street; it is dropped, because the product is about places. A building name above a numberless street ("Rose Cottage, High Street") used to become the street with the street demoted; it is now `building`, a new field that is also accepted as a separated input and as a list column headed building, building name, premise or house name. And a house-number range ("71-75 Shelton Street") used to fold to a number no register lists; it is now asked as written and then by either end, in the registers, the company register and the mailbox lists alike. Each of these moves is recorded in a new `changes` list — `country_line_removed`, `care_of_removed`, `building_separated`, `organization_separated`, `administrative_area_folded`, `box_line_moved`, `private_mailbox_moved_to_unit` — so the answer says what it did to your input.
- **53 fields, and a map of who confirmed what.** Beside `building`: `confirmation`, an object naming for each of the postcode, the street, the house number, the town and the region which dataset stood behind it (`postcode_directory`, `postcode_area`, `register`, `range`, `company_register`, `mail_drop_list`, `pattern`), `unconfirmed` where a dataset could have and did not, or null where nothing could check; `administrative_area_matches_postcode`; `premise_key`, the hex digest of the building that every spelling of one address shares and that feedback and suppressions are filed under; `building_use` and `is_multi_unit` as the register declares them; `street_suggestion`; and `changes`. The guide at /guides/how-to-read-the-address-confirmation-map reads the map field by field; `unconfirmed` is never `wrong`.
- **Postcode directories for a hundred countries.** The GeoNames postal-code dump, CC BY 4.0, installs one directory per country beside Britain's ONS directory: 105 countries and 1,097,486 codes from the 16 September copy, with the places each code is written with, the region and its code, and a centroid. Fifteen directories — the United States, Germany, France, Spain, Italy, Austria, Switzerland, Belgium, Denmark, Norway, Finland, Slovakia, Japan, Mexico and India — reach their operators' published totals and are recorded complete, so a code they lack answers `postcode_not_found`; the rest are partial and deny nothing. Canada, the Netherlands and Ireland publish only the forward sortation area, the four-digit district and the routing key, so those three match on their prefix and are partial whatever their size. Two consequences: `administrative_area_matches_postcode` compares the written state or province with the directory's, and a disagreement carries a new `region_mismatch` weight of 30; and `locality_matches_postcode` now checks the town against every place the directory lists for the code, then — before scoring anything — against every place in the postcode's area, so a post town written for a settlement reads `confirmation.locality: postcode_area` with no weight. The United States dump carries only each ZIP's default city, so an alternate city name there is reported as not matching and never scored.
- **Great Britain gets its streets and its towns.** Ordnance Survey's Open Names, Open Government Licence, is installed as a register of streets keyed by postcode district: 999,065 road rows compile to 876,830 street keys in a 7.3-megabyte index, Welsh names indexed beside English ones. A British street the file lists answers `match_level: street` with `confirmation.street: register`; `exists` stays null because a register of streets lists no buildings, and a miss costs nothing because a road that runs through several districts is filed under one of them (87.3 percent of 49,106 Companies House street lines matched). The same file's postcode rows gave 1,742,466 of 1,749,109 installed British postcodes their places, so `locality_matches_postcode` answers in Great Britain for the first time, lenient to the region and the borough. The country page for GB says all of this in its own words.
- **A register now says where a building stands.** The premise index keeps, beside every key, what the register publishes about the building: its position to about 1.2 metres, its use where the register categorises buildings, and its unit rows. A `premise` match answers `latitude` and `longitude` from the register with `location_source: register`, falling back to the postcode centroid as before; `building_use` reads residential, commercial, mixed, industrial, institutional, agricultural or other, and `is_multi_unit` is true at two or more listed units, false only where a register declares its units and lists none (the Austrian register counts dwellings), and null otherwise. National grids are converted in code — Swiss LV95, the Austrian Gauss-Krüger zones, Czech Křovák, Norwegian UTM 33 — and each conversion was checked against proj4js: the transverse Mercator and Helmert paths agree to between 0.02 and 0.06 metres, the Swiss approximate formula to between 0.4 and 2.3 metres. Recompiling the Swiss, Austrian, Czech and Norwegian registers grew each premise index from eight to fifteen bytes a key — the Swiss one from 25.8 to 48.1 megabytes — and the Federal Palace in Bern, the Storting and Oslo City Hall each answered within about twenty metres of their reference positions. A register compiled before this answers as it did until it is recompiled.
- **Seven more national registers, all open.** Belgium's BeST address register in its three regional files (CC BY 4.0: Brussels 451,111 buildings, Wallonia 1,663,329, Flanders 2,798,435, every language spelling of a street indexed), Luxembourg's national addresses (CC0, 357,776 keys, filed under both spellings of the postcode), Latvia's State Address Register (CC BY 4.0, 322,198 buildings), Australia's G-NAF (CC BY 4.0 with its end-user clause against mailing lists; verified on the real tables, full compile pending), Denmark's national addresses (the agency's free geographic data; parser verified on fixtures, full compile not yet run) and, when a LINZ key is configured, New Zealand's street addresses (CC BY 4.0). Each names its licence in the answer's sources and its coverage claim in `register_coverage`. The Dutch BAG is not among them: its extract is a 3.6-gigabyte archive of archives that the reader cannot yet open, and it will come when it can.
- **United States address ranges.** The Census Bureau's TIGER/Line address-range tables, public domain, compile into a range table consulted after the National Address Database has missed: a number inside a range published for its street answers a new `match_level: range` with `confirmation.house_number: range` and `exists` null, and a street the table knows whose number falls outside every range answers `street` with a new `number_out_of_range` weight of 20. A street the table does not know costs nothing, because the table is partial by measurement: not every edge carries a range and a street on a ZIP boundary can be filed under its neighbour. Delaware compiled in 23 seconds to 139,683 ranges on 24,424 streets; the national run is a few hours and about a gigabyte, started by hand.
- **Street suggestions, and a streets endpoint.** Every register sync now keeps the streets it lists under each postcode by name. An address followed no further than its postcode answers `street_suggestion`, the register's spelling of the closest street (Banhofstrasse becomes Bahnhofstrasse, Limatquai becomes Limmatquai), and `GET /api/v1/address/streets?country=CH&postal_code=8001` lists every street the register holds under a postcode — 200 in central Zürich — for one credit when it answers and nothing when it does not, with a seven-day repeat. `GET /api/v1/address/postcode/{postcode}` answers the same streets from the open registers wherever a listing is installed, at one credit, and keeps the licensed partner's full list for Great Britain and Ireland when one is configured.
- **Registered agents are named for what they are.** A bundled list of registered-agent and formation services' own published offices — seventeen operators on 16 September, from the Delaware and Wyoming agents to the London formation addresses — answers `mail_drop_source: registered_agent` with the agent's name in `mail_drop_provider`, taking precedence over the company-cluster heuristic. The weight is the mail-drop 50, because nobody lives there; the guide says why a company registered there has done nothing unusual.
- **Three new weights, fourteen in all.** `region_mismatch` 30, `street_not_found` 20 (a complete register of streets has no such street) and `number_out_of_range` 20 join the published table; `locality_mismatch` keeps its 30 but fires only when the town is found nowhere in the postcode's area and only where the directory lists every place a code is written with.
- **Five test addresses, a fields option, a ninth comparison.** The houses numbered 1 to 5 on Spaw Test Street, Springfield, IL 99999 answer a valid address, a mail drop, an address a complete register lacks, a terminated postcode and — written "5 Spaw Test Street PMB 5" — a private mailbox, free, unlogged and recognised by every path including the sandbox key. `fields` on `POST /api/v1/address` and the browser endpoint keeps only the keys you name. And the cross-signal consistency check emits a ninth comparison, `phone_address_region`, the state a North American number was issued in against the address's region — evidence only, never a flag, because people keep their numbers when they move.
- **Deliberately not built.** No cross-account address reputation, still: a postal address is where somebody lives, and a shared "post fails here" database would be a database about residences. No Dutch register yet, for the reason above. And no licensed delivery-point answers: `is_deliverable`, `is_cmra`, `is_vacant` and `is_residential` stay null until a partner is enabled, and `building_use` from a cadastre is never presented as the postal service's residential indicator.
## 2026-09-13: A sending policy, monitored domains, and the decision the answer now makes
Reference: https://spaw.co/changelog/2026-09-13-a-sending-policy-and-domain-monitors
- **The answer makes the decision now: `ok_to_send` and `blocked_by`, 46 fields in all.** The score was always arithmetic, and the decision was always yours to write as an if-chain. A sending policy writes it once: the signals that block whenever they fire, a ceiling on the score, and a floor for a catch-all or unverified mailbox's confidence, applied in that order after the verdict. An undeliverable verdict never passes; `blocked_by` names the rule that stopped anything else — `undeliverable`, the blocking signal's own name, `risk_score` or `mailbox_confidence`. Until an account sets one, the published defaults apply (disposable, likely_typo, parked_domain, mx_blocklisted and no_reply block, and so does any score over 60), and the guest demo, the sandbox key and the test addresses answer with those. The policy belongs to the account and reaches every channel it is billed for — single and batch lookups, bulk runs (two more result columns and a `blocked` count in the summary), monitors, the form widget and the dashboard. `GET`, `PUT` and `DELETE /api/v1/email/policy` read, change and reset it; the Sending policy tab on the verification page edits the same three rules.
- **Monitored domains.** A domain's mail setup drifts: a provider is switched, a DMARC policy loosened, a registration lapses into a parking page, a mail host lands on a blocklist. `POST /api/v1/email/domain-monitors` saves up to 500 domains and re-checks them daily, weekly or monthly through the domain lookup, billed the same way — one credit when a domain accepts mail, nothing when it is dead, repeats inside seven days free, so a daily cadence costs at most a credit per domain a week. Each domain answers `ok`, `flagged`, `dead` or `invalid` with every flag that fired: parked, blocklisted, expiring, no_spf, no_dmarc, and five compared with the previous run (provider_changed, spf_changed, dmarc_changed, dkim_lost, mta_sts_lost). The first run baselines; after that an alert goes out only for a domain that was ok and no longer is, or gained a flag it did not have, by email and, when set, by the same signed webhook the other monitors post. List, get, update, run and delete endpoints, a Domains tab on the verification page, and the SDKs carry it.
- **`fields` on the single and browser endpoints.** A comma-separated list of the keys you want, answered in the answer's own order; unknown names are ignored, `sources` stays only when named, `meta` is never narrowed. With 46 fields and a batch of fifty, an answer should be as wide as the caller needs.
- **The bulk summary on the dashboard.** A finished run's verdict and risk splits, its most frequent reasons, providers and domains, and how many rows carried each signal, drawn beside the run.
- **A second handshake path.** When the first handshake path is unavailable — the circuit breaker open, a transport failure — a second one now answers the same probe, so a partner outage costs latency rather than verdicts. Neither is named anywhere a reader can see, as before.
- **Two more free tools.** A DKIM checker at /tools/dkim-checker lists the common selectors that publish a key; an MTA-STS checker at /tools/mta-sts-checker reads MTA-STS, TLS-RPT and BIMI beside the SPF all-qualifier and the DMARC `pct` and reporting address. Both run on the domain report the domain tools already share.
- **Typo suggestions read more than the big providers.** `did_you_mean` now draws on the thousand most-linked domains and the domains an account verifies most, so acme.co is corrected to acme.com; a domain outside the provider list is suggested only when its own mail servers accept mail. The bundled first-name list gained the MIT male list, so a lone male first name reads as a first name rather than a surname.
- **Bounce reports feed the not-found memory.** A reported bounce whose provider text says the mailbox does not exist (a 5.1.1 code, "user unknown" and its kin) is recorded the way a probe's not-found answer is, so a later mailbox that exists again reads as recycled from day one; a one-off command backfills the last six months of stored reports.
- **Measured reasons and confidence.** The status page's measured block gains two tables: the delivered share per verdict reason, and per mailbox-confidence bucket (under 40, 40 to 69, 70 and over), each published only under the same floors as the verdict figures — a hundred outcomes from three accounts, none holding more than half — and never called accuracy.
## 2026-09-12: What the username and the domain look like, and two signals read from traffic
Reference: https://spaw.co/changelog/2026-09-12-email-intelligence-round-two
- **Seventeen more fields on every email answer, 44 in all.** About the username, with no network call: `local_part_pattern` names its shape (first.last, flast, a lone first name, digits, a plain word), `first_name` and `last_name` carry the name tokens the bundled name lists recognised — read from the address itself, never appended from outside, and never a promise of correctness — and `looks_generated` marks a hex string, a UUID, a placeholder word or a name glued to a run of digits. About the domain: `domain_category` sorts relay, disposable, webmail, academic, government and military domains from the rest; `mx_provider_type` says whether the provider hosts mailboxes, is a security gateway, a transactional relay, a web host or the domain's own servers; `mx_blocklisted` and `mx_network_type` run the mail hosts through the threat blocklists and network feeds the IP product already installs; `domain_expires_at` and `parked` read the registration's end and the parking nameservers. About the mailbox: `role_type` names the family a role address belongs to, `previously_invalid` and `previously_invalid_at` say whether a confirmed mailbox once answered "no such mailbox", and `widely_seen` says whether three or more other accounts looked the address up inside a day. `risk_signals` lists every weight that fired with the dataset it was read from, so the score is always recomputable from the answer; it is empty on an undeliverable verdict, where the verdict is the answer.
- **Relay addresses are real mailboxes.** Firefox Relay, SimpleLogin, Addy.io, DuckDuckGo Email Protection and their kind sit on the open burner list because they hand out addresses on demand, and Spaw used to report them as disposable. An address there is a real person's real mailbox reached through a name they control and often kept for years, so it now answers `is_relay: true` with the service in `relay_service`, `disposable` is forced false, and the score carries 5 for the withheld identity instead of 80.
- **Four new reasons, all risky and all billed like any risky answer:** `parked_domain` (the nameservers belong to a parking or for-sale service and the domain still accepts mail: whoever collects that mail is not your customer), `mx_blocklisted` (the mail hosts resolve into a Spamhaus DROP netblock or the abuse.ch botnet list — a burner signal no domain list has caught yet), `no_reply` (a noreply, donotreply or mailer-daemon shape, recognised by pattern even when the role list lacks the spelling; mail to it is discarded by design) and `recycled_mailbox` (the mailbox exists now and once answered that it did not — the shape of a recycled address, the spam-trap kind included). Every one has its own page under https://spaw.co/docs/reasons.
- **Twenty-one published weights.** New rows: parked domain 60, blocklisted mail network 60, recycled mailbox 30, generated username 20, no-reply 20 (on top of the role row), widely seen 20, catch-all behind a security gateway 15 (instead of the catch-all 30, because behind Proofpoint, Mimecast or Barracuda a catch-all is the gateway's design rather than a squat), registration ending within 30 days 10, relay 5. The twelve rows that were there are unchanged. `mailbox_confidence` also reads whether a username follows the naming convention seen in the domain's confirmed deliveries: +15 when it does, −10 when a name shape breaks it, from pooled outcome counts per domain and never from an address.
- **The domain endpoint answers 31 keys.** Beside what it answered before: `domain_category`, `mx_provider_type`, `mx_blocklisted` and `mx_network_type`; the SPF `all` qualifier and the DNS lookup count the policy needs (over ten is a permanent error at every receiver); the DMARC `pct` tag and whether aggregate reports are collected; the common DKIM selectors that publish a key; MTA-STS, TLS-RPT and BIMI; the registration's expiry, last change, registrar and whether the registrant is withheld; `parked`, `is_relay` and `relay_service`. All free DNS and the RDAP record already fetched, cached for a day so the twenty TXT queries behind the DKIM block are paid once per domain.
- **Bulk runs finish with a summary.** A finished run carries rows and duplicates, the verdict and risk splits, every reason and domain category seen with its row count, the ten most frequent providers and domains, and how many rows carried each of the disposable, relay, role, catch-all, parked, blocklisted, previously-invalid, widely-seen and generated signals — built from the same rows the result file holds, so the two can never disagree. The result file carries the new columns in the answer's order, with `risk_signals` flattened to `signal:weight` pairs; the webhook body is unchanged.
- **What is stored, and what is deliberately not built.** `widely_seen` is a keyed hash of the address with the account ids that saw it and when, kept for the day; the "no such mailbox" memory behind `previously_invalid` is a date under a keyed hash, kept for a year. Both are read cross-account as counts and dates only, neither says who asked, and both are erased with the address through the existing erasure route; the privacy page says so. Not built, on purpose: breach-exposure data, which would send every address to a third party; account enumeration through other sites' password-reset endpoints, which is exactly the enumeration this product tells its customers not to do; bought spam-trap lists, which no vendor can legitimately sell; and an opaque learned score in place of the published weights.
- **Every client follows.** The Node SDK types the 44-field result, the 31-key domain answer and the bulk summary; the Python and PHP clients pass the new fields through; the Zapier app (1.9.0), the Make modules, the Google Sheets functions (0.9.0) and the MCP tools describe and map them.
## 2026-09-12: IP intelligence round four, from corporate gateways to your own lists
Reference: https://spaw.co/changelog/2026-09-12-ip-intelligence-round-four
- `is_corporate_proxy` and `corporate_proxy` name the egress of a corporate secure web gateway from the cloud enforcement node ranges Zscaler publishes for its eight clouds. An employee behind one reaches the Internet from the gateway's address, so the answer now says workplace rather than datacenter: no weight, the datacenter weight is skipped for it like relay egress and verified crawlers, and the consistency endpoint withholds its country comparisons for it.
- Three more named VPN lists — IVPN, AirVPN and the VPN Gate volunteer relays — join Mullvad, NordVPN and Private Internet Access, and Cloudflare WARP is answered by inference as Cloudflare's own network outside the CDN ranges it publishes, with `privacy_service` reading `Cloudflare WARP (inferred)` so nobody mistakes an inference for a list. GitHub's hosted runner ranges join the datacenter feeds as `hosting_provider` GitHub Actions.
- The Tor feeds sync hourly now, and each sync rebuilds a thirty-day exit history: `tor_last_seen` dates the address's last sighting as an exit, and an address that left the list inside the last seven days carries a `recently_tor` weight of 40, because exits rotate daily and yesterday's exit is still the same machine.
- `sanctions` names the OFAC country programmes that name the country, comprehensive ones marked apart, and `fatf_list` says whether the FATF lists it under a call for action or increased monitoring as of its June 2026 plenary. Both come from a curated file that names its sources and dates, carry no weight, and are facts about a place rather than a verdict about a person.
- `timezone` now resolves for split countries and regions — Texas, Canada, Australia, Brazil, Russia, Indonesia — from the nearest place in the GeoNames list inside the located country, and `timezone_source` says whether the region, the country or the nearest city settled it. Send `browser_timezone` with a lookup, from the server endpoints or the browser endpoint, and `timezone_matches` says whether the visitor's clock agrees with the network's.
- `connection_type` names what positive evidence says the address is attached to — corporate_proxy, hosting, satellite, mobile, or residential from a dynamic reverse-DNS name — and `is_mobile` and `is_satellite` come from curated positive-only operator lists (Starlink, Viasat and HughesNet; T-Mobile, Verizon Wireless, AT&T Mobility, EE, Three, Jio, Airtel, China Mobile, Telcel and more). Null never means residential. With `hostname: true`, `hostname_kind` and `hostname_domain` say what the naming convention says and which registrable domain the name belongs to.
- Your own lists: `/api/v1/ip/lists` and the dashboard's new Lists tab keep up to 20 allow or deny lists of up to 5,000 addresses or prefixes each, applied to every lookup the account is billed for. An allow-list hit zeroes the score with a single `allowlisted` signal saying why, a deny-list hit tops it at 100, deny is checked first, and `account_list` names the list and the entry that matched.
- `seen_by_you` counts how many times your account looked the address up in the last seven days and when it first did — velocity, kept under the same keyed hash as the repeat marker, never the address — and `privacy: true` skips it along with the marker.
- A `fields` option on the single-address endpoints keeps only the keys you name, so a page that wants a country and a currency receives two keys rather than sixty; the two GET endpoints answer with `Cache-Control: private, max-age=300`. Monitored lists accept CIDRs of up to 256 addresses and expand them under the 500 cap.
- Bulk result files carry the new columns at their response positions, and a finished run's payload adds a `summary`: rows, reserved addresses, the top countries and networks, how many rows carried each signal, how many hit one of your lists, and the risk split, added up from the same cells the CSV holds.
## 2026-09-12: Hosts seen attacking, as their own signal
Reference: https://spaw.co/changelog/2026-09-12-attack-source-lists
- Every IP answer carries `is_attack_source` and `attack_source`: whether an attack-source list names the address, and which one. The lists are Emerging Threats' compromised-hosts list, the CINS Army list and blocklist.de, and they name single hosts seen attacking honeypots and sensors in the last days rather than netblocks or operators, which is why they sit beside `is_blocklisted` rather than inside it. The flag is tri-state like every other, and the three lists cover IPv4 today.
- A hit weighs 50, between your own reports at 60 and a datacenter range at 40, because an entry says a machine was misbehaving this week, not that a netblock belongs to a criminal operation. `risk_signals` carries it as `attack_source` with the dataset `attack-source-lists`, the bulk CSV gained the two columns after `blocklist`, and a finished bulk run's summary counts how many rows carried it.
- The lists are refreshed hourly, like the botnet list and the Tor exits, and a monitored address that newly appears on one is flagged `attack_source` and named in the alert as "seen attacking, listed by" the list.
- The publishers' terms, as published on 12 September 2026 and repeated on the product page: Emerging Threats' open ruleset, which its compromised-hosts rules are built from, is BSD-licensed and is credited as that licence asks; CINS says its Army list is "provided as a simple text file, with which you can parse and use in any way you see fit"; blocklist.de calls itself a free and voluntary service and offers its export files "as they are", with no licence text.
## 2026-09-10: One upload for every signal, a hero that answers, and a reference you can run
Reference: https://spaw.co/changelog/2026-09-10-one-upload-a-live-hero-and-a-reference-you-can-run
- **The cross-signal check takes a list now.** It was strictly one record at a time, so scoring a signups export meant four separate uploads to four pages and a spreadsheet join afterwards — and the comparison nobody else computes was the one thing you could not get for a list. There is a fifth bulk run type: one upload, one header row, and the parser works out which column is the email, which is the phone, which is the IP and which are the address fields by composing the four detectors that already existed. A row where only two of the four resolve is a perfectly good check of those two and is run as one.
- It bills exactly as the single call does, leg by leg, and the comparison stays free — so a row costs between nothing and four credits depending on what its columns reached and what answered. The preview prices it on the columns actually mapped, so a file that only reaches two signals is quoted at two a row rather than four. It goes through the same preview-then-start flow as the other four, with no one-shot upload and no paste box: a pasted line cannot say which signal it is, and a mis-read column costs four times more here than anywhere else.
- **The landing page stops playing a recording.** The hero has been auto-rotating four canned lookups with no input at all, on the busiest page on the site — while five real widgets and five throttled guest endpoints existed and were mounted on every marketing page except that one. Each tab now holds the live widget for its signal, seeded from the lookup it used to play, so the panel still shows a full answer the instant it loads and runs a real one the moment you ask.
- One of the recorded values it paints at rest was wrong, which only mattered once the live answer landed on top of it: mailinator.com is a free provider and the record said it was not. A test now runs each recorded value through the real pipeline, so the page cannot show a number and then correct itself in front of the visitor.
- **The endpoint reference runs.** A hundred-odd pages you could read and none you could try. Six operations — the ones that cost at most a credit, that a guest can already run free, and whose demo answers the same shape the page documents — now have a console on their own page. It says what the call will cost before you press anything, never runs on load, and every other operation says why it has none rather than silently missing one.
- A run does everything the endpoint does, and the page says so: the answer is logged where that endpoint logs it, and a verdict that suppresses an address or a number suppresses it here too. The first version claimed it "changes nothing", which was not true of three of the six and would have had somebody delete them.
- The consistency run also got what the other four have had for weeks and it did not: a completion email when it finishes or stops, a place on the dashboard's live strip, its row in the published list of webhook events, and its uploaded file deleted with the account. That last one is the third time this exact gap has appeared — a database cascade fires no application code, so a new run type's files outlive the account unless something deletes them by hand.
## 2026-09-10: See what we parsed before you pay for it
Reference: https://spaw.co/changelog/2026-09-10-see-what-you-bought-before-you-buy-it
- **Choosing a file used to be starting a paid run.** Upload, and the list was parsed, the columns guessed, the job created and the work queued in one request — the first time you saw what we made of your file was on the invoice. It is two steps now. The first parses the list and shows you the delimiter we read, which column filled which field and whether a heading named it or we guessed, the first rows as we parsed them, the repeats, your balance and the most the run can cost. The second starts it, and takes corrections.
- That matters most for addresses. The column headings we recognise are English, so a German or French export fell through to a heuristic that picks "the column with the most address-shaped values" — and a wrong guess on forty thousand rows was a wrong guess you paid for. Every column now has a select beside it with its first value, and correcting one re-reads the file through the corrected mapping.
- The cost is called an upper bound, not an estimate, everywhere it appears — rows come back free for reasons no preview can know in advance, so the real figure is always the one on the finished job. Two ways that promise was wrong when it landed, both caught in review: correcting a column changed which values repeat, and the ceiling was still quoting the guess; and a privacy IP run writes no repeat marker at all, so the repeats it was subtracting as free were going to be billed. A draft you never start is deleted after a day, and every read path enforces that whether or not the sweeper has reached it.
- **Automatic top-ups, with a ceiling you set.** Running out of credits means your own signup form starts rejecting people, and until now the only warning was the balance email. You can now have a pack bought automatically when the balance falls below a line you choose — strictly opt-in, off by default, and hard-capped by a monthly ceiling we never exceed. Reach the ceiling and the answer is the ordinary out-of-credits refusal, not one more charge: "a hard stop, never a surprise bill" is a promise this site makes in four places and it still holds.
- Every automatic charge is on the usage page as its own ledger entry and confirmed by email. It is charged straight to the card rather than invoiced, unlike a pack you buy yourself, and both the email and the page now say so — they had said the opposite.
- **A trust page.** The data processing addendum was a bullet under an unrelated heading, subprocessor disclosure was one vague sentence, and there was no questionnaire answer set, no security.txt and no statement about encryption, backups, access or breach notification. `/trust` carries all of it: the subprocessors by role and region, a standing DPA, the answers a security review actually asks for, and an RFC 9116 security.txt with a response window. It holds no certifications and says so plainly rather than implying an audit that has not happened.
- **A way to be forgotten for people who never signed up.** Abuse reports about a number or an address raise the risk other customers see, which means this service holds data about people who have no account here — and the only "forget" in the whole codebase was a cache invalidation. There is a form under `/privacy` now, verified by email so it cannot be used against somebody else's data, an admin action, and a command behind both that sweeps every table holding that identifier and leaves an audit row proving it happened.
- One thing it deliberately does not delete: a suppression entry a customer imported themselves. That is their own do-not-mail list, and erasing it would do the opposite of what the person asking wants — it would un-suppress them and the next campaign would reach them again. The first version deleted those; the rule against it has been written down since the feedback system shipped.
- The link the form emails is now one per mailbox rather than one per spelling. Keyed on what was typed, `mia+1@`, `mia+2@` and `m.i.a@` each opened their own window on the same inbox — so the only thing standing between a public form and a way to mail a stranger repeatedly was not standing anywhere.
- **A page per country for the address formats we hold.** Two hundred and seven countries of write order, local terminology, required fields and postcode shape had been sitting in a data file, published nowhere. Each country that has enough to say now has its own page, gated by a catalog so the ones that do not are left out rather than filled with prose. Every page says what this service can actually check there, which for most of them is the format and the postcode and not a register lookup.
## 2026-09-10: Your team shares one balance, one set of keys, and one way out
Reference: https://spaw.co/changelog/2026-09-10-accounts-your-team-shares-one-balance
- **An account can have more than one person on it.** Everything about the product assumed one login owned one balance, so a team either shared a password or bought a second account and reconciled two bills. An account now holds the credit balance and the API keys, and the people on it — an owner, admins and members — spend the one balance and use the one set of keys. The monthly free grant is per account, not per head, which is what it always said on the pricing page.
- **The team page says what is shared and what is not, because not everything is.** Per-resource ownership deliberately did not move: your bulk runs, your lookup history and your suppression lists stay yours and stay private from your colleagues. A customer who read "shared account" as "shared everything" would be wrong about three things they care about, so the page lists them rather than leaving it to be discovered. The ledger does answer "where did my credits go" account-wide, with a spend column per member.
- **Inviting is a signed link that consumes nothing when it is opened.** Corporate mail scanners fetch every link in an inbound message, and a one-step accept would be burned by a robot before the human ever clicked it. Only the token's hash is stored, so a database dump hands over no live invitation, and withdrawing one kills the link immediately. Nothing in the flow says whether an address already has an account here.
- Before you accept, the page prices it: the balance you are leaving behind, the keys that change hands, and how many scheduled monitors move with you. Accepting is a move, not a merge — the account you own stays exactly as it is, dormant, holding its balance and its whole history, and it is what you come back to if you leave the team.
- **Leaving settles what you leave behind, whichever of the three ways out you take.** Being removed, leaving, and accepting an invitation somewhere else are three different doors and only two of them settled anything. Your secret keys on a team you have left are deleted, because a secret key is the account's credential and somebody who has gone must not walk away with one that works. Your publishable keys are not: those are embedded in the customer's own site and revoking one takes their widget down, so they are repointed at the owner instead.
- Your scheduled monitors come with you. They were stamped with whoever was paying at the time they were made and nothing ever revisited that, so a list belonging to somebody who had left went on running hourly and billing the team they left — for good, while they could still add rows to it. They now move to whichever account you are on, so the bill follows the person who controls the spend. Nothing pauses and nothing is deleted.
- **A daily check that can actually be cleared.** There is a command that asserts the whole thing — every person on exactly one account, every row that spends money pointing at a live one, every monitor billing an account its owner is actually on — and it runs at 03:35 every night. It had one hole worth naming: it did not know about the dormant account somebody leaves behind when they join a team, so the first accepted invitation would have failed it permanently. An alarm that can never be cleared is worse than no alarm.
## 2026-09-10: Webhooks that cannot be replayed, and pages that only say what they know
Reference: https://spaw.co/changelog/2026-09-10-webhooks-that-cannot-be-replayed-and-pages-that-say-what-they-know
- **Webhook signatures carry a timestamp now, and the old header has an end date.** A signature over the body alone never goes stale: anyone who captures one delivery — a log aggregator, a proxy, an error report pasted into a ticket — can post those exact bytes at your endpoint next year and your check still passes. Every delivery now also carries `X-Spaw-Signature-V2`, written `t={unix seconds},v2={hex}`, where the hex is the HMAC-SHA256 of `v2:{t}:{raw body}` — so the timestamp is signed as well as sent, and a receiver that refuses anything more than five minutes from its own clock narrows a replay from forever to five minutes.
- Both headers go out **until 1 March 2027**, and only the timestamped one after that. Nothing about your secret, your URL or the payloads changes when the old one goes. Move your check any time before then: all three clients ship it as one call, and `POST /api/v1/webhooks/test` now hands back both signatures and the timestamp, so you can check new code against a real signed delivery before anything depends on it. It had been publishing only the header being retired — the one field it told you to trust was the one that stops arriving.
- **A new API key hands you a command that key can run.** The panel that shows a secret key once now picks the request from the key's own scopes — the free email test address, or the free phone test number — instead of always printing an email lookup, so a key narrowed to one product is no longer given a request that answers `403 KEY_SCOPE_DENIED` for the rest of its life; a key that can run neither is told so, and what it would get back if it tried. A key pinned to an address allowlist keeps the command but not the "send" button, because a request from your browser would be refused — the allowlist doing its job. The button really does use the key, so pressing it finishes the dashboard checklist rather than pretending to.
- **Every bulk run inside the retention window is reachable from the dashboard.** The panels showed ten and stopped, while the runs themselves are kept for thirty days — so an eleventh run was billed, finished, on disk, and unreachable except through the API. There is a pager now, with one-click jumps to the newest and oldest runs, and starting a run from an older page returns you to the page the new run is actually on, so its progress ticks over instead of sitting behind a page you had no reason to turn.
- **The comparison pages say what they actually know.** A row a vendor's own pages carry no figure for now reads "not on the pages read" rather than "not published" — a claim about our reading, not about the vendor, because two of the vendors compared here block automated readers and do publish the prices those rows were denying. Where that is the case the page says so beside that vendor. Each product also has its own comparison hub now, reachable from the footer, and a product added with no comparison written for it fails by name instead of quietly inheriting another product's page.
- **The measured-quality section on /status states only what it can prove.** The figure weighing customer-reported outcomes against our own verdicts counts the same population on both sides, so it can no longer print a share above 100% by dividing reports by lookups. A bounce share appears only once at least a hundred reported outcomes stand behind that verdict, from at least three accounts counted for that verdict alone, with no single account holding more than half of them — and the page says which floor is holding a figure back. A business register whose release file reads back as no identifiers at all is a failed check named on the page, not a silent clean sheet.
- **The privacy policy and the terms stop saying things the billing code does not do.** An automatic top-up is charged straight to the card and raises no invoice — the terms claimed every payment raised one, and sent you to support with an invoice number that cannot exist. Both now say what the billing page has always said, the refunds route works for a charge that has only a date and an amount, and the switch that stops automatic top-ups is named as being on the billing page, which is where it is.
- The terms no longer promise an email and a ledger entry for every top-up: when the payment processor's answer is lost to us, a charge can end with no credits, no entry and no mail, and the page says so rather than hoping. Two things that made that worse are fixed: the code that reconciles exactly this case was listening for a payment event the payment endpoint was never subscribed to, and so was the code that tells you a subscription payment failed. Both are live now.
- On the privacy side the retention table gained the three stores it was missing — your account, your API keys, and the pending erasure request the form on that page holds for an hour — and names the erasure route for a postal address and an IP as it already did for a phone number. Every window it publishes, in the table and in the prose around it, is now pinned to the configuration that does the deleting. Two of them were published with nothing behind them at all.
- **Smaller things.** The signed-in tab strip fades on whichever edge still has tabs behind it, measured rather than always on, and keyboard focus keeps clear of that fade instead of parking underneath it. And every code block on the site was copying its own "Copy" button label into the command — pasted into a shell, that ran the command and then reported `Copy: command not found`.
## 2026-09-10: A monitor that tries again, and a webhook you can ask for again
Reference: https://spaw.co/changelog/2026-09-10-a-monitor-that-retries-and-a-webhook-you-can-ask-for-again
- **A monitored list whose run dies is checked again within the hour, not next month.** A verification run that loses its worker — a deploy, a restart, a host that goes away — used to leave the schedule where it stood, because the schedule is pushed forward before the run starts so an hourly tick can never run a slow monitor twice and bill you twice. The cost was that a monthly list went unwatched for another month, while the dashboard showed the run *before* it as if nothing had happened.
- A run that dies is now recorded as one, and the list is re-checked about 45 minutes later — the sweep ticks hourly, so in practice between 45 minutes and about an hour and three quarters. **Exactly once:** a list that is broken rather than unlucky waits for its normal cadence instead of burning a run every 45 minutes, and pressing "Run now" while that re-check is pending no longer resets the count, or the row, which used to say "we have stopped trying" while the next run was forty minutes away. A run that stopped for lack of credits is untouched by any of this — it stopped for a reason a timer cannot fix.
- The re-check window is derived rather than picked: it is longer than the longest a run may take, so a re-check can never fall due while the attempt it replaces might still be going. And a monitor row whose run never finished no longer prints a green "all clean" it has no results to stand on — a failed run has no counts, and an absent count was reading as zero.
- **A bulk run's completion webhook can be asked for again.** Press **Send webhook again** beside the run on any Bulk runs tab, or call `POST /api/v1/{email,phone,ip,address,consistency}/bulk/{jobId}/webhook/redeliver`. Until now a 100,000-row run whose endpoint happened to be down for four minutes was finished, billed, on disk — and your pipeline never learned, with polling the only way back.
- The body is byte-for-byte the body the first delivery carried. The signature is stamped afresh, because a receiver refuses anything whose timestamp is more than five minutes old. The delivery is queued rather than made while you wait: one attempt against an endpoint that is down takes half a minute, and that is precisely the endpoint you are asking about — so the call answers `202`, the run reads `webhook_status: "pending"` until a worker has an answer, and then `delivered` or `failed` with what your endpoint said. Free, and capped at ten a minute.
- In the same release, cancelling a run that was still queued now fires its completion webhook with `status: "cancelled"` — which the documentation had always promised and the code had never done, so a redelivery for such a run would have been its *first* delivery while every page called it a repeat.
- **A monitor's `stopped_reason` is documented now**, on all five products: `insufficient_credits` and `key_spend_cap` where a run stopped part-way, `run_failed` where it died, and `run_failed_retrying` where it died and the schedule has been pulled in — with `retry_at` beside it saying when. A summary carrying either failure reason has no counts in it at all, so a missing count means unknown rather than zero.
## 2026-09-10: Invoices, ninety days of uptime, and a key you can use before you sign up
Reference: https://spaw.co/changelog/2026-09-10-invoices-uptime-a-sandbox-key-and-a-way-in
- **The first request in the documentation now runs.** Every generated sample printed `sk_live_…`, so evaluating this API meant creating an account, confirming an email and minting a key before you could see a single real response. There is a published sandbox key now. It answers a handful of read-only lookups for the sample values the documentation prints — the test addresses, the fiction-block numbers, the sample IP and postal address — and refuses everything else with a typed error that names what it would have answered for.
- It is a credential everybody has, so its limits are the point: no account behind it, no credits spent, nothing written that lasts, no arbitrary value looked up, and its own small budget counted per source address. Anything that would cost something — a callback URL, a forced refresh, the live carrier check, the deliverability check — is refused rather than ignored. `meta.sandbox` is true and `credits_remaining` is null, because there is no balance it could be about.
- **Businesses can get an invoice.** Checkout collected no tax number and no billing address, and a credit pack produced a receipt rather than an invoice — so an EU or UK company could not get the reverse-charge document its accountant needs. Both now ask for a tax number (optional: a private buyer has none) and a billing address, and a pack raises a real invoice. Past invoices are listed on the billing page with the number, the date and the total each one states.
- **A customer who only ever bought credit packs could not reach the billing portal at all.** The "Card and invoices" button sat inside a branch that only rendered for subscribers, so a pack customer had no way to update their card and no way to read past invoices. It is reachable by anyone with a card on file now.
- **Ninety days of measured availability, and a straight account of what it cannot see.** `/status` had no memory: no uptime figure, no incidents, no feed, and the raw data was deleted after a week before anything could roll it up. There is a daily rollup now, a ninety-day strip on the status page and the same figures on the status endpoint.
- Read it as a floor on the bad days, never as proof of a good one — and the page says exactly that, at some length. It only counts requests that reached this server and were recorded. A whole day of outage is blank, and blank is never counted as good; but an hour of outage leaves the other twenty-three, and the day can read as a clean 100%. A partial outage is the common case and this figure is close to blind to it. A fault that also stops the recording write — a database incident, the likeliest cause of a real burst of 5xx — takes those failures out of the number along with everything else. There is an incident log because that is where an outage is actually described, by a person, and a feed at `/status.rss` to follow it.
- **The widgets ask for something now.** All four ended at the verdict with the nearest sign-up button a page-height away. There is a footer under each: "Copy as cURL" writing the real request for the value just typed, a link to the full response fields, and the way to get a key. A test replays every command the footer offers against the real API, and another pins each widget's own default to the command being replayed — so a widget cannot quietly start copying something that fails.
- The landing page and the product pages read as one site again. They had drifted into two design languages, one with uppercase section eyebrows and one without; the rule already said which wins, and the eyebrow text is folded into the headings rather than dropped.
- **Three things found while checking the above, all in work from this same day.** A value that was not a string — `{"email": ["a"]}` — made the sandbox throw rather than refuse, so a credential printed in our own documentation could force a 500 on the most-read endpoint on the site. Sandbox calls were being kept out of the request timings entirely, which was right for the latency percentiles and quietly took sandbox failures out of the availability figure that exists to catch them; they are recorded and flagged now, so each number sees what it should. And the availability strip's own caveat was wrong in the direction that flatters us — it described a whole-day outage and left the reader to assume it covered a partial one.
- Smaller corrections: the invoice list said every row carries a tax number and a billing address, which is not true of anything bought before today; the billing page was making an outbound call to Stripe on every tab focus, which also made the local credit balance wait on it; the consistency product page still said "four signals" in six more places; and the rate-limit reference page did not know about the sandbox budget, so a reader meeting it had nowhere to look it up.
## 2026-09-10: A support page, a glossary, and the work nobody could reach
Reference: https://spaw.co/changelog/2026-09-10-support-glossary-and-the-things-that-were-not-linked
- **Support is a page now, not a mailto.** Writing to us meant an email address in the footer, with nothing said about what happens next. `/support` is a form that, when you are signed in, already knows your account, your plan, your balance and your five most recent request ids — the things support would otherwise have to ask you for, and the ones you would otherwise have to go and look up. It says plainly what happens next, including what it does not: no auto-reply, no ticket portal, and no response-time commitment we have not measured. What "priority support" on the paid plans actually buys is stated in one sentence rather than implied.
- Filed requests are kept for 180 days and then deleted whether or not anyone read them, and the privacy policy now says so — a form that stores a message, a reply-to address and the account it came from is a store of personal data, and the policy's collection and retention sections are exhaustive lists.
- **Thirty terms have a page each.** Every machine value already had one — 58 verdict reasons, 18 error codes — while the words around them lived only inside guide bodies, where nothing could link to them. Catch-all, greylisting, null MX, implicit MX, ASN, CIDR, E.164, NPA/NXX, LEI, PMB, mail drop, delivery point and the rest are now at `/glossary`, each opening with its definition, each true of this product specifically rather than of the dictionary, and each naming the field or reason it maps to.
- **Pricing goes above the top of the ladder, and shows its working.** The ladder stopped at 100,000 credits and offered nothing beyond it: somebody with a million rows a month read to the bottom and left. There is a way to ask now. Beside it, an estimator: put in a month's volume and the share of it you expect to be free — invalid rows, undeliverable verdicts and 7-day repeats all cost nothing — and it names the cheapest tier that covers it and what the two ways of buying work out at.
- It compares them honestly, which took two goes. A pack is bought once and a plan is charged monthly, so the first version subtracted the two prices and told you the plan saved you money in most of the ladder — while the pack it was comparing against covered three or four months. It now spreads the pack over the months its credits actually last, which is what makes the two comparable, and says which is cheaper only where that is true.
- **The comparison pages cover the whole product.** All nine were email vendors, and the table itself was email-shaped — every competitor compared on an SMTP check, whatever they sold. There are twenty-one now across five products, each compared on the axes that product is actually judged on: a phone vendor on where its line type comes from and how much of the numbering plan it carries, an address vendor on the registers it holds, a business vendor on the registers behind an identifier. `/compare` is grouped by product, and each page carries the date its own figures were read rather than one date for all of them.
- **Twenty-five guides, and every product page links its own.** Nineteen guides had been written and not one product or documentation page linked a single one, so the only way to find them was the index. Guides now declare the product they are about, the index groups by it, a guide's "related" reading prefers its own product instead of whatever was published most recently, and every product and docs page carries a rail of its own. Six are new: business verification and cross-signal consistency had none at all, and no other marketing surface either.
- **You can send yourself a webhook.** Proving your signature check works meant queueing a real, billed bulk run and waiting for it to finish. `POST /api/v1/webhooks/test` posts a sample payload for any documented event to a URL you name, signed the way a real delivery is, and hands back the secret, the signature and the exact bytes the two were computed over, so you can verify your own HMAC against ours before anything depends on it. Free, and in all three clients.
- **The email callback had no private-address guard.** Every webhook we send refuses a URL whose host resolves onto a private or reserved address — the check runs at delivery rather than when the URL is saved, because a name that resolved publicly can be repointed later. The `callback_url` on an email lookup was the exception: that path had its own copy of the delivery code, written before the guard existed, and never gained it. It goes through the same signer as everything else now.
- The guard also missed one address of its own: `::`, the IPv6 unspecified address, which the network stack routes to the local machine exactly as `127.0.0.1` does, and which the loopback entry beside it does not cover.
- **Every free tool and browser endpoint has its own rate budget.** A guest has no key for the throttle to hash, so all six tool demos shared one allowance and so did the four browser endpoints — while each page and each documentation section quoted its own number. A checkout form checking an address and a phone number was spending one budget twice. Each is separate now, which is what makes the published number true, and a test refuses the next unauthenticated route that joins a shared one.
- The Requests page says "every API call this account made" and now means it: the request log and the public latency figures shared one early return, so a call on any route outside the five lookup groups wrote no row at all. The latency table is still lookups only, deliberately — a free endpoint whose duration is mostly your own server's would make a published percentile mean something else.
- Smaller corrections, all of them a number that had quietly stopped being true: the consistency endpoint takes five signals and raises nine flags, not four and seven; `/pricing` names three answers that cost more than one credit, not two; the glossary's own first draft misstated the phone feed count, the heaviest address weight and the PO box rule, all three of which are published elsewhere on the site and now agree.
## 2026-09-09: Three fixes: partial bulk results, monitor baselines and key spend caps
Reference: https://spaw.co/changelog/2026-09-09-partial-results-monitor-baselines-and-spend-caps
- **A failed bulk run's results are downloadable.** A run that fails part-way keeps the rows it did write, and those rows were billed, so they now download exactly like the rows of a run you cancelled part-way. Until today they were unreachable: work you had paid for was thrown away with the run that failed.
- A run that failed before writing anything answers a new `409 JOB_FAILED` — see `/docs/errors/JOB_FAILED`. It used to answer `RESULTS_NOT_READY`, which asks you to poll the job until it reports `completed`, for a job that had already stopped and never would. Nothing was billed for a run like that.
- **A monitor that runs out of credits no longer loses its place.** Every monitor compares each entry against the previous run to decide whether to alert you. A run that stopped on credits used to discard the stored answers for everything it had not reached, so the next run read those entries as seen for the first time — and a first sighting only sets the baseline, it never emails. Whatever changed in between was never reported.
- Worst case, on the business monitor: a company struck off the register read as `unverifiable` rather than `left_register`, and the single most important event a monitor exists to catch passed silently. The stored answers are now carried forward for every entry a stopped run did not reach, so the next run compares against the last real answer.
- **A key's daily credit cap now holds inside a batch.** The cap was checked once, before the request ran, so a single batch could spend far past it — a cap of 10 did not hold. A batch now stops at the cap, keeps everything it has already paid for, and answers 200 with `meta.stopped_reason: "key_spend_cap"` beside the existing `"insufficient_credits"`; those two remain the only values.
- A call that starts with the cap already spent still answers `429 KEY_SPEND_CAP_REACHED` and runs nothing, as before. Bulk runs and monitor runs bill in the background, are never counted against a key, and are deliberately unaffected by the cap once they are under way.
- **Every API failure now answers in the documented shape.** The reference has always promised one envelope — `success: false` with a `code`, a `message` and a `request_id` — but only four failures kept that promise. A wrong job or monitor id answered a raw framework message that named an internal class, carried no code to branch on and no request id to quote at support; a wrong HTTP verb and an unhandled failure did the same. Those now answer `404 NOT_FOUND`, `405 METHOD_NOT_ALLOWED` (with an `Allow` header) and `500 SERVER_ERROR` in the same envelope as everything else, each with its own reference page.
- A record belonging to another account answers exactly as one that never existed, deliberately: telling the two apart would confirm that somebody else holds that id. And on a `500` alone we say plainly that a failure landing after the lookup ran leaves the credit spent — every other failure bills nothing.
- **The MCP server's nine tools are listed everywhere again.** `verify-entity` and `check-consistency` shipped yesterday and the pages that list the tools were not updated with them: the documentation said seven, the integrations page said four, and the registry entry named three of the five products. All four now agree, and a test pins them to what the server actually registers.
- **A dataset that stops refreshing now raises an alarm.** Only the phone feeds warned when a sync failed; the other eleven could fail every night and nobody would learn until an answer was wrong. A daily sweep now reads the same list the status page publishes and reports any dataset that is missing or older than its own refresh cadence allows, naming the command that fixes it. It checks the age of the data rather than the success of a job on purpose: if the scheduler itself stops, no job runs to report anything, and that is the failure that matters most.
- Failed completion webhooks now record what came back — the HTTP status your endpoint answered with, or the transport failure when it answered at all — so a wrong URL, an expired credential and a fault at your end are no longer the same word. The response body is never stored.
- **You get told before the balance runs out, and when a card fails.** Until now an empty balance was something you discovered as a 402 in your own traffic, and a failed subscription payment was not surfaced at all. There is now a warning email as the balance approaches the line, an amber banner in the dashboard while it is under it, and an email when a payment fails saying what happens next — your credits and keys are untouched while the card is retried. The warning is sent at most once a day, and again if the balance climbs back over the line and falls a second time.
- Uploading a file larger than the per-run limit used to report the truncated number as though it were the whole file. It now says how many rows were left out.
- **A monitor run that fails now says so.** The scheduler moves a monitor's next run forward before it starts, so a slow one can never be run twice and billed twice — but that meant a run which died had already moved the schedule on, and left the previous run's figures on screen. The monitor looked healthy while it had skipped a cycle. A failed run is now recorded and shown as such, the saved answers it never got to are kept, and the panels name the reason a run stopped rather than assuming it was credits.
- Entity monitors were rate-limited ten times more tightly than the other four products — their scope allowed 50 requests a second and the routes were getting 5, because one pattern was missing a wildcard the other three had.
- **Deleting your account now really removes your files.** Every table belonging to an account already went with it, which made the deletion look complete — but a database cascade fires none of the application's own cleanup, so each bulk run's uploaded list and result file stayed on disk, orphaned, with no record left pointing at them. Those files are the addresses and numbers you sent us; the table rows never were. They are now deleted with the account, and any live subscription is cancelled at the same time so a closed account stops being charged.
- **The request id now leads somewhere.** Every response has always carried an `X-Request-Id` header and repeated it in the error envelope, and until today that id led nowhere — you could quote it to support and nothing else. There is now a Requests page listing your own API calls, opening on the ones that were refused or failed, with the typed error code beside each and a box to paste an id into. `GET /api/v1/account/requests/{id}` reads the same row back, so a script that catches a failure can ask what happened without a human involved. Kept for 30 days.
- The log holds no value you looked up. It names the endpoint, never the address, number or identifier the call was about — the promise that a phone or IP lookup writes no history row still stands, and the privacy policy describes this store rather than leaving you to find it.
- **Search, on every page.** Press Cmd+K, or Ctrl+K, or the search button in the header. It covers everything published here — the documentation, every API endpoint, every reason and error code, the guides, the free tools, the integrations, the comparisons and each changelog entry — just under three hundred pages that until now could only be reached by knowing the URL. The whole index is fetched once and searched in the browser, so it answers as fast as you can type.
- **Business verification and cross-signal consistency have a dashboard.** Both shipped with an API, docs, an MCP tool and SDK methods, and no way to try either without writing code — the API-key form even offered scopes for products the dashboard could not exercise. `/verify/entity` reads an identifier against the registers held here and carries a Monitors tab; `/verify/consistency` runs any of the four signals together and shows where they agree and where they do not, quoting both sides of every comparison. Both are in the signed-in navigation.
- The consistency page states what it will not do: no score, no risk level, no verdict, no recommendation. A difference between two fields is a fact about the fields, not a judgement about a person — and where the IP is a VPN or datacenter exit the country comparisons are withheld and say why, because that country belongs to the network operator.
- **Business verification and cross-signal consistency have product pages.** Both were sold only through their API docs. The consistency page leads with what it refuses to do, because that is the product: no score, no risk level, no verdict and no recommendation, and there will not be one. The email product page also caught up with the other three — it now publishes the same measured latency and the same table of the datasets behind an answer.
- **The site now sells what shipped.** The landing page still led with email verification alone, its heading counted four endpoints, and a footnote said email, phone and IP were "live today" — directly beneath a card selling the fourth product, with a fifth and sixth already live. The hero, the heading, the search-engine description and the structured data now describe all six, business verification and cross-signal consistency have cards and footer links, and a test keeps the page from falling behind the next launch.
- **Pricing says what each signal costs.** `/pricing` described email verdicts only, for a product billing six signals off one balance. It now carries a per-signal rate card — what earns a credit and what is always free, for each endpoint — and states plainly that the two optional premium checks are off until a licensed partner is configured, so they currently add nothing.
- **A page shared or crawled while the renderer is down now carries its own title.** Every public page's title, description and canonical are written by the server-side renderer. When that renderer is not answering the page still works — the browser draws it — but the HTML that arrives carries only the site name, so a link posted to Slack, an AI assistant reading the page, and a crawler that does not run scripts all saw "SPAW.co" and one sentence, whatever page it was. The HTML now falls back to the page's own title and description, taken from the same list the sitemap and the search box are built from, so the two can never disagree. A rendered page is unaffected: its head still wins, and no page ever carries two.
- `/status` now reports whether the renderer is answering, because nothing else did: the site stayed up and every link preview quietly went generic. It is listed on the status page as its own component and is in the sitemap.
- **The address page publishes the weekly index check.** Every Saturday, rows are taken straight out of each installed national register's own file and sent back through the ordinary lookup. It is a check on our own plumbing — a parser that spells a street one way and an index that folds it another would answer nothing for a whole country, silently — and the result is now on `/products/address-verification` with the share of rows found again for each register. It is deliberately not called coverage: it says nothing about how many of a country's addresses a register carries, and we make no claim there.
- **The site navigates like an app again.** Every link in the header — the logo, Docs, Pricing, Guides, and the eleven signed-in tabs — was a plain anchor, so each click tore down the page and downloaded the whole front end again, while the footer had been navigating properly all along. They now navigate in place and fetch the next page while the pointer is still on the link, so most clicks land on a page already in hand. The docs sidebar does the same.
- **A first-run dashboard.** The dashboard rendered exactly the same thing to an account that had just signed up and one running in production. A new account now sees what is left to do — create a key, make the first call — and the checklist disappears for good once that call lands.
- **The footer is a way around the site.** It now carries the reason reference, the error codes, the MCP server, both OpenAPI formats and `llms-full.txt` alongside the docs, in place of a link to a scroll position on the front page. There is a live service-status pill beside the logo, and the twelve-line block of data licences — which we are obliged to publish and will keep publishing in full — is behind a "Data sources and licences" disclosure instead of outweighing the navigation above it. The signed-in pages, which had a copyright line and one email address, now carry the same way out.
- **Failures look like the rest of the site, and carry the id.** A 500, a 503, an expired form and a rate-limit refusal all rendered the framework's own grey page. Each now has a page of its own that says what happened, whether anything was billed, and what to do next — and the 500 prints the request id, so someone hitting it in a browser can quote the same number an API caller reads out of the error envelope.
- **Confirmations reach every page.** The component that draws a confirmation was mounted only inside the signed-in shell, so a message flashed to the front page, the docs, the pricing page or a tool was thrown away silently. Deleting your account was the worst case: it worked, signed you out, and said nothing at all. It now confirms.
- **Keyboard and screen-reader gaps.** Every page now opens with a skip link, so a keyboard user is not walked through the logo, five nav links and eleven tabs before reaching the page. The mobile menu closes on Escape and hands focus back, and closes when you navigate instead of covering the page you just opened. The signed-in tab strip scrolls the tab you are on into view. The theme switcher announces as one setting with three values rather than three unrelated buttons.
- **A theme control for everyone.** Light, dark and system have worked since day one, but the only switch was inside the account settings, behind sign-in and email verification — so a visitor reading the documentation could not override their operating system. The control is now in the header on every page.
- Every Markdown twin (`/docs/api.md` and several hundred like it) now names the HTML page it is a twin of, so the two stop competing with each other in search. The docs twins and the About and Security pages also end with a link home, which the guides and comparisons already did.
- **A bulk run that stopped on credits can be resumed.** Until today the only way out was to upload the file again: the run kept the list it was given, and nothing offered it back to you. Top up and press Resume, on any of the four products, and the run continues — it re-runs the list from the top, the rows it already verified come back as free repeats inside the seven-day window, and only the rows it never reached are charged. `POST /api/v1/{product}/bulk/{id}/resume` does the same from a script.
- A run whose uploaded list has passed its retention window cannot be resumed and says so, rather than starting again with nothing. Neither can an IP run made in privacy mode: privacy keeps no record of the addresses checked, not even the repeat marker, so a second pass would charge for every row again — the run says that instead of quietly billing twice.
- **Usage answers what you spent.** `/usage` listed credit movements and nothing else — there was no credits-spent figure anywhere in the product. It now filters by date, activity, channel and key, and summarises whatever you have narrowed to: spent, added, a per-activity breakdown and a bar per day. The figures cover the whole range you filtered, not the rows currently on screen, and the CSV download takes the same filters.
- **Spend is attributed to the exact key that made it.** Ledger rows recorded a label — "api · Production" — which two keys of the same name share and a rename does not move. Each row now also carries the key's own id, so `/usage` can break spend down by key, `GET /api/v1/account/ledger` can be filtered to one with `api_token_id`, and the CSV carries the column too. A revoked key keeps its history under the name it was spending with. Work that runs in the background — bulk runs, monitors — is deliberately attributed to the run rather than to the key that scheduled it, and the table says so instead of leaving you to wonder why the numbers do not add up.
- The API keys page showed "none" in the cap column for a key with no daily cap, throwing away the number of credits it had spent today — which the page below it was already showing for publishable keys. It now shows both.
- **The documentation says where you are again.** The sidebar decided which entry was current by page, not by section, so on a page with ten anchors all ten lit up at once — a highlight that was not merely unhelpful but wrong. It now follows the section actually on screen. And on a phone, where the sidebar is hidden, a long reference page had no in-page navigation at all: there is now a row of the current page's sections above it.
- **Analytics now asks first.** Google Analytics used to load on every page for everyone. It is not loaded at all until you say yes — nothing about a visit reaches Google before you answer — and the answer is remembered in your own browser, not on your account. The privacy policy says exactly that now, and carries a control to change your mind, because a policy that says you can and offers nowhere to do it is not a policy. Saying no changes nothing else about the site.
- **The privacy policy and the terms cover what we actually run.** Both still described the product as email and domains "and — as they launch — phone numbers and IP addresses", while five signals and a cross-signal comparison were live. They now name all of them, and the privacy policy says plainly what each one keeps: a postal address lookup writes no history, but your suppression list and the delivery outcomes you report store the address as you wrote it rather than a hash, because both exist to be matched against a later lookup. A company lookup writes nothing at all and its answer never carries a street address — a registered address is very often a home address. Cross-signal consistency keeps nothing of its own: it writes whatever the signals it ran would have written, and the comparison itself is returned and never stored.
- **A bulk run tells you when it is over.** Until now the only way to learn that a run had finished was to keep the tab open for its four-second poll: a 40,000-row list started before lunch reported to nobody. There is now an email — counts, spend and a link, never a row of your list. A run that stopped on credits or failed is always mailed, because those are the endings you have to act on; a clean run only when nobody could still plausibly be watching. Cancelling mails nobody: you were the one who cancelled it. And the dashboard now shows the runs still going, whoever on the account started them.
- A run started from the dashboard can post to your endpoint when it finishes, which until now only a run started through the API could do. Webhooks are also refused outright when the URL resolves onto our own network — a customer-supplied URL fetched by our servers is the shape of a server-side request forgery, and the check runs at delivery rather than only when the URL is saved, because a name can be repointed afterwards.
- **Suppression lists can be searched and cleared by value.** Removing one address from a 50,000-entry list meant paging through 500 requests to find its numeric id. All three lists now filter by value, source, reason and date, and `DELETE /api/v1/{signal}/suppressions` takes the values themselves and answers which were on the list. The search matches the way the entry was stored, so an address you wrote differently, a number in national form, or a different capitalisation all find the row they folded into.
- **A free IP lookup, and an index of every free tool.** The IP checker was the only one of the five signals with no tool page, though the widget for it already existed. It is at `/tools/ip-lookup`, and `/tools` now lists all eight — from any one of them you can reach the others, which until now you could only do through the footer.
- **A browser can finally read the headers we tell it to read.** There was no CORS policy, so the framework defaults applied: no exposed headers and no preflight cache. `X-Request-Id` — the id `/docs/errors` tells you to quote to support — was invisible to browser JavaScript, as were the rate-limit headers, `Retry-After` and `Idempotent-Replayed`. All six are exposed now, preflights are cacheable for a day (the form widget posts JSON, so it was paying a full extra round trip on every lookup a visitor typed), and the policy is written down on `/security` rather than left to be discovered.
- **Every release has its own link, and the changelog has a feed.** A release could not be linked to a customer and there was no way to follow the product without checking the page. Each entry is now its own page with its own Markdown twin, and `/changelog.atom` is a real Atom feed a reader can subscribe to — advertised from the pages themselves, so a reader finds it without being told.
- **Monitors can post to your endpoint, and can be changed without being rebuilt.** Mail was a monitor's only channel while bulk runs have posted webhooks for months; monitors now do too, signed the same way, and refused outright when the URL resolves onto our own network. And `PATCH /api/v1/{signal}/monitors/{id}` finally exists: adding one entry, renaming a list or changing its cadence meant deleting and recreating the monitor, which threw away the baseline every alert is judged against — a full cycle of silence at exactly the moment you were paying attention.
- **The Python, Node and PHP clients caught up with the API.** They had fallen a long way behind: resuming a bulk run, removing suppressions by value, filtering a suppression list, the request log, `api_token_id` on the ledger, and the monitor webhooks and updates above were all reachable over HTTP and from none of the three. They are now, with the same surface in each. Two older gaps closed with them: the ledger's rows were being thrown away by the Python and Node clients before you ever saw them, and narrowing bulk IP results by risk level was documented and implemented but unreachable from every client.
- **A monitor now tells you which entries changed, not just how many.** Three of the four panels stopped at the summary counts, so you could read that two addresses had decayed and never learn which two — while the API had been returning the per-entry map all along. Every monitor now expands to the last run's answer for each entry, worst first. The address monitor marks the entries that changed, because its run records that per entry; the email and phone monitors record only how many changed, so they show the status and do not guess.
- **Every answer names its sources on the page, not just in the raw response.** "A verdict that names its sources" is the whole position, and on four of the five signals the line was only visible to somebody who opened "Show raw response" — the pages filtered it out of the field list and printed nothing. It is now on all five, in one wording.
- **A leaked API key is no longer an outage.** The page told you to revoke the key and create another, which means every integration using it is down between the two. Rotate issues a replacement carrying the same scopes, cap, address allowlist and default country, and the old key keeps working for 24 hours so you can move each service across — or you can end it immediately with Revoke now. The pair shares one daily spend cap while both work, because a cap is a promise about your spend and not about a row.
- **New accounts start with more than the monthly grant, and get an email that says so.** Confirming your address now adds a one-off credit bonus and sends a welcome message with your balance, a link to make a key and a request you can paste and run. The bonus is once per mailbox: closing an account and opening another with the same address does not pay again, which we keep track of as a one-way hash and nothing else — the privacy policy says so plainly.
- **Business identifiers can be checked in batches.** Entity was the only signal with no batch endpoint. `POST /v1/entity/batch` takes up to 500 in one call and bills the way a single lookup does: only the ones that were found. An empty balance no longer abandons the identifiers that cost nothing.
- **Cross-signal consistency can compare a company.** The endpoint that exists to cross-check signals could not cross-check the newest one. It now puts the registered name beside the organisation on the address, and the country of registration beside the country the address stands in — kept out of the overall country agreement on purpose, because where a company is registered is not where a person is. The name comparison is a normalised exact match and says so: accents, ampersands, apostrophes, initials and company-form words are taken out first, so a difference is a difference in the name rather than in how it was typed.
- The request we print for your first call was missing a header, so pasting it answered "the email field is required". Fixed everywhere it appears.
- **What the API promises, written down.** There was nothing anywhere saying what counts as a breaking change, what may appear without notice, or what would happen if something were deprecated — so an integration had to guess. `/security` now says it: the OpenAPI document is the contract and a test fails the build if it and the routes disagree; new fields, new enum values and new error codes ship without notice, so match on the codes you handle and let the rest fall through; a breaking change gets a new path rather than a quiet edit; nothing is deprecated today, and when something is it will carry a `Sunset` header and be announced in the changelog. The same page states plainly that there is no numeric uptime commitment — and points at the live measurement there is, including why a refused request is kept out of the latency percentiles.
- **A copy button on every code block.** Around forty of them across the documentation, four generated samples per endpoint, and every block in the guides — all of them had to be selected by hand. The button leaves the shell prompt behind, so what you paste is a command that runs.
- **A privacy-forwarding address can open an account.** Public burner lists carry the permanent alias services — Firefox Relay, Addy.io, SimpleLogin, DuckDuckGo Email Protection and the rest — and we were reading those lists at our own signup form, so a customer whose only address was a Firefox Relay or Addy alias was turned away at 422. Those are real mailboxes people keep for years, and for a privacy-minded customer they are often the only address on offer. They open an account now. What the API answers about those domains is deliberately unchanged: opening our own front door is not the same as changing the verdict you buy.
- **Knowing someone's email address no longer blocks their password reset.** Asking for a reset link is limited by the address on purpose, so a botnet cannot fill one person's inbox from a thousand source addresses — but completing a reset shared that counter, so an attacker who knew nothing but a victim's address could spend the hourly allowance and leave the owner holding a valid link the form would no longer take. The two are separate now; completing a reset is limited by the source address, which is what the risk actually is.
- **Deleting your account deletes the devices it was signed in on.** The confirmation says "your account and everything in it", and the sessions table — one row per browser, carrying the IP address and user agent each was last seen on, the same record the security page had just shown you — outlived the account by whatever was left of the session lifetime. Nothing else ever deleted those rows.
- **"Sign out everywhere else" is always offered.** The button appeared only when the device list had rows in it. The list shows sessions that have been active recently, so a browser that chose to stay signed in and has not been used since drops off it and is still signed in — the button was being hidden in exactly the case somebody came to the page for. The dialog now says what it covers, including the devices no longer listed.
- **A malformed query parameter is a 422, not a 500.** `?format[]=json` on a bulk results download answered a server error — our fault, reported as something you could do nothing about — after the endpoint had already decided you were allowed to ask. It now answers the same validation error a misspelled value does.
- **`format=json` on a bulk download handles what CSV always could.** A quoted cell holding a newline — a two-line address, a note — was being read as two rows, so the JSON came back with extra rows and the value cut in half while the CSV download of the same file was perfect. And a single byte that is not UTF-8, which an uploaded list routinely carries, raised mid-stream after the 200 and half the rows had already gone out. Both fixed, and a test now downloads one file both ways and requires them to agree.
- **A report you type into the dashboard stops disturbing everyone else's lookups.** Dashboard reports are deliberately left out of the figures other accounts feel, and the code still threw away the shared cached count on every one of them — a count that could not have moved — making each click re-run the query behind everybody's lookups.
- The line under the phone report buttons said repeated failures suppress a number, while one report of abuse suppressed it immediately. The button with the most consequence was the one described least accurately.
- The privacy policy now covers the devices list and says what removes a row, and names the dashboard as the third way an outcome report reaches us alongside the API and your provider's bounce webhook — with the rule that separates it from the other two.
- **The three clients caught up again, with six operations.** Listing your bulk runs in each of the four products (how a caller that lost a job id finds the run again), `format=json` on all four results downloads, the business-identifier batch, the regulator's number-range report, and the status endpoint — the one call that works before a key exists. Every batch item's `index` and `input` are documented and typed too: a batch stopped early answers only a prefix of the list, and the answer carries the normalised value, so those two are what line a result up with the row it came from.
- **A key's controls now hold over the MCP server.** A secret key can carry a scope, a daily credit cap and an allowlist of addresses, and `/mcp` sat outside the middleware that enforces all three — so a key scoped to email alone, capped at ten credits and locked to one office address ran unrestricted lookups from anywhere as soon as an agent pointed at it. The documentation had been telling you the allowlist "makes a leaked key useless elsewhere". It does now. A refusal comes back as something the model can read and act on, not as a broken transport: an agent that asks for a product its key does not cover is told which scope it needs.
- **Agents can check a list.** The MCP server had one batch tool, for email, and its input was literally named `emails`, so an agent checking two hundred phone numbers made two hundred calls. There are now batch tools for phone numbers, IP addresses and postal addresses on the same shape — twelve tools in all — and they bill and stop exactly as the REST endpoints do, keeping the rows already paid for when a balance runs out.
- **The form widget checks postal addresses.** The browser endpoints for all four signals have been built and billed for weeks, and the widget on your own pages attached to email and phone inputs only — so the highest-value case, validating a shipping address at checkout, had no first-party widget at all. `data-spaw-address` now does it, taking its country from the field you name, and the `spaw:result` event says which of the three fired.
- The widget waits for a country select to settle before it looks anything up. A keyboard user arrowing down a country list fires a change on every option they pass, and each is a different country, so the first version of this would have spent ten credits on nine countries nobody chose. And an answer for a country that has since been changed is now dropped rather than rendered: it described an address nobody was submitting.
- **Business identifiers reached the connectors.** Zapier, n8n, Make and Google Sheets had every other signal — email, domain, phone, IP, address, suppressions and outcome reporting — and not this one, though all three SDK clients have covered it since it shipped. All four now verify a LEI or a company number, with the field lists, the caps and the caching each platform expects.
- Those pages carry the caveat that matters more than anything else on them: the UK company file is a snapshot of the live register, so a struck-off company is removed from it rather than marked dissolved. A real dissolution arrives as `found: false`, not as the flag whose name suggests it — and on a LEI lookup it is quieter still, showing up as an empty `agreements` block while the register is still named. Filtering on the flag alone keeps you dealing with exactly the companies the feature exists to catch.
- **Business verification has a free tool.** It was the only product with no way to try it without an account, though both registers are synced onto this server, so a public lookup is one database query with nobody to pay. `/tools/company-lookup` reads a company number or a LEI and answers the legal name, the register's own wording for its status, the jurisdiction and the incorporation date, each stamped with the date of the copy read. No registered address, deliberately — an identifier does not say whether a company or a sole trader stands behind it.
- Every free tool now has its own rate budget, and so does every browser endpoint. All six tool demos shared one bucket per visitor, and so did the four widget endpoints, while each page and each documentation section promised its own number: checking an address and a phone number on one page quietly spent one allowance twice. Each is separate now, which is what makes the published number true. A test refuses any new guest-facing route that joins a shared bucket.
- **The endpoint reference shows the official clients.** Every one of the API pages offered curl, fetch, requests and curl_setopt, and never `spaw.verifyEmail(...)`, though all three clients cover every endpoint. All three now appear beside the four, generated from the same example the curl uses so they cannot drift apart — and the OpenAPI document you import into Postman, Scalar or Redocly carries them as `x-codeSamples`, where it previously arrived with a hundred example-free operations.
- The samples are checked rather than trusted: a test fails if a sample names a method its client no longer declares, or passes a field the operation does not have, or quietly omits one the curl sample sends — which it was doing, dropping the address from the consistency call.
- Smaller things: Billing has a place in the navigation and in the account menu instead of one link on the dashboard, the credit balance on the usage page is now the way to top it up, and the key-controls copy on `/docs` names all seven scopes rather than the four that existed when it was written.
## 2026-09-08: Business verification, and cross-signal consistency
Reference: https://spaw.co/changelog/2026-09-08-business-verification
- `POST /api/v1/entity` is the fifth product: a Legal Entity Identifier read against the GLEIF Level 1 golden copy, or a UK company number read against the Companies House free company data product. Both are bulk files synced into a local table, so no third party sits in the request path. The guide is at `/docs/business-verification`.
- It answers the legal name, whether the entity is still live (`active`, `inactive`, `dissolved`, with the register's own wording in `status_detail`), the country and jurisdiction of formation, the entity category and legal form, and the incorporation date. For a LEI it adds `checksum_valid` — the ISO 7064 MOD 97-10 check digit from ISO 17442 — plus `registration_status` and `next_renewal_at`.
- A LEI can lapse while the company behind it is perfectly alive, so the two facts are separate: `status` is about the entity, `registration_status` is about the identifier, and `lei_registration_not_current` flags the difference.
- Where a LEI names a Companies House number and both registers are installed, the two records are compared field by field. `agreements` quotes both sides and `flags` names `name_differs_across_registers`, `status_differs_across_registers` and `dissolved_in_national_register`. Names are compared on their letters and digits with the company-form words removed, so "ACME HOLDINGS LTD." and "Acme Holdings Limited" agree.
- The checksum is reported beside the register answer rather than standing in for it. Measured against the published register: of 45,515 consecutive real LEIs, 45,512 satisfy the check digit, and all three that do not are annulled registrations.
- No registered address is stored or returned, and none will be. A registered address is very often somebody's home, and an identifier alone does not say whether a limited company or a sole trader stands behind it. The product carries country and jurisdiction, which answer whether this is a real, live entity without holding a register of where people live.
- Monitored lists: save up to 500 identifiers and have them re-checked weekly or monthly (no daily cadence — the UK company file is republished monthly). A run answers every identifier `ok`, one of `left_register`, `dissolved`, `inactive`, `registration_lapsed` and `registers_disagree`, or `unverifiable` where no register could answer at all.
- `left_register` exists because of how the UK register actually works: the Companies House free product is a snapshot of the live register, so a company that is struck off is removed from the file rather than marked dissolved. A dissolution therefore reaches you as the identifier disappearing between runs, and reporting that as `unverifiable` would have let the single most important event pass silently. An email goes out only for an identifier that was fine last run and is not now; the first run only sets the baseline.
- **Every monitor now prunes itself, across all five products.** A saved list that stops being re-checked is deleted along with the identifiers and the results it stored — an email monitor, a phone monitor, an IP monitor, an address monitor and a business monitor alike. A list in active use is never touched, because each run advances `last_run_at`, so the window only closes on one that has genuinely stopped. Retention is 365 days by default and configurable per product, `prunes_after_days` is on every monitor payload and `meta.retention_days` on every list endpoint, so the rule is visible in the API rather than only in the docs.
- The business monitor is where the reasoning started: an identifier is often a company number but can equally be a sole trader's, and nothing in it says which. The same argument holds for an email address, a phone number, an IP and a postal address, which is why the rule was extended to the other four rather than left as a special case.
- 1 credit when a register answers. A malformed identifier, one no register carries, an unsupported register and a register that is not yet installed are all free — and a register that is not installed says so, rather than reporting that the company does not exist.
- Data: the LEI reference data is published under CC0 1.0 and refreshed daily; the Companies House file is Open Government Licence v3.0 and republished monthly, so an answer can be up to five weeks behind the live register. Every answer names both registers with the date of the copy, and `/status` lists them. Spaw is not affiliated with or endorsed by the organisation that publishes the LEI data.
- `POST /api/v1/consistency` takes any subset of `email`, `phone`, `ip` and `address`, runs each through the product that owns it, and returns `data.signals` with all four keys always present — each holding that product's full answer, field for field, or `null` for a signal you did not ask for. The signals run in a fixed order (`ip`, `phone`, `address`, `email`) because a JSON object has no input order, and because email is the only leg that does DNS and a mailbox handshake. The guide is at `/docs/cross-signal-consistency`.
- On top of them, a free `consistency` block: `country_agreement` over the phone, address and IP countries, and eight comparisons that each quote both sides — the signal, the field and the value that decided it — so nothing is asserted you cannot see the working for.
- Seven flags, each with its own reference page: `phone_country_differs_from_address_country`, `ip_country_differs_from_address_country`, `ip_country_differs_from_phone_country`, `disposable_email_from_datacenter_ip`, `disposable_email_from_anonymous_network`, `mail_drop_address_with_free_email_provider` and `disposable_phone_with_disposable_email`. They sit in a `flag` field rather than a `reason`, because they describe two signals rather than one.
- A comparison is emitted only when both of its signals answered. Where one side is null it answers `not_compared` with `withheld: "missing_value"` — a null means not evaluated, never no. Where the IP is a VPN exit, a relay or a datacenter egress, the country comparisons are withheld as `low_confidence`: that country belongs to the network operator, and reporting a difference would flag everyone who uses a VPN.
- No score, no risk level, no verdict and no recommendation, and there will not be one. A difference between two fields is a fact about the fields, not a judgement about a person — people travel, move house, keep a foreign number and browse privately.
- No new dataset, licence or sync command: every comparison is arithmetic over fields the four pipelines already computed. Each signal bills exactly as its own single lookup, `meta.credits_used` is the sum, and the analysis is free. Running out part-way keeps what was paid for and reports `meta.stopped_reason`.
- The MCP server gained `verify-entity` and `check-consistency`, and the Python, Node and PHP SDKs gained `check_consistency` / `checkConsistency`. Keys carry a new `consistency` scope, and the endpoint has its own rate-limit bucket, since one call fans out to as many as four lookups.
- A free `/tools/card-number-checker` joins the tool pages: brand, Luhn check digit and permitted length, computed entirely in the browser — no request, no endpoint, no log line, and the number never appears in the URL. It makes no issuer, bank, country or debit/credit/prepaid claim, and says plainly why: there is no free, openly-licensed, authoritative source for those, so it answers nothing rather than guessing.
- Fixes: a mailbox probe lost to a provider outage is now free whatever names the verdict — a role address on an accepts-mail domain was still being charged during an outage, against the published rule. The French national address register (Base Adresse Nationale) is installed alongside the other five, and the Swiss register's coverage no longer claims Liechtenstein, which its file has never contained.
## 2026-09-05: Address verification: formats, boxes, registers and postcodes
Reference: https://spaw.co/changelog/2026-09-05-address-verification
- `POST /api/v1/address` answers 45 fields: the address split into the parts its country writes, the country's facts, the address rendered the way that operator writes it, `postal_code_valid` and `postal_code_type`, `postcode_status` and `locality_matches_postcode` from the national postcode directory, `address_type` with the source that settled it, `exists` and `match_level` from the national address register with the coverage its publisher claims, the postcode centroid, `companies_registered`, `is_mail_drop` with the source that decided it, and a recomputable `risk_score` with its `risk_signals`. The full guide is at `/docs/address-verification`, and the product page at `/products/address-verification` has a live demo.
- Send one written address or the separated fields, always with a `country` — the postal format is what the address is measured against, so a country the format table does not cover is refused rather than answered. Nine typed reasons say what is wrong, and every one of them is free.
- `address_type` reads the postal operators' own naming in 52 countries: post-office boxes, private mailboxes at mail-receiving agencies, general delivery, rural routes, military mail, parcel lockers, and the business-volume arrangements that replace a street address such as the French CEDEX and the Dutch Antwoordnummer. A box replaces the street line; a private mailbox keeps it and moves its number into `unit`.
- The data is free and licence-clean: postal formats for 206 countries (MIT), the national address registers of the United States (public domain), France (Licence Ouverte 2.0), Switzerland (© swisstopo), Austria, Czechia and Norway (CC BY 4.0), the ONS Postcode Directory for Great Britain (Open Government Licence v3.0 — Northern Ireland postcodes are excluded because their reuse needs a separate Land & Property Services licence), Companies House free company data and the Colorado, New York and Oregon business registries, and the branch lists mailbox operators publish themselves. The footer carries every attribution the licences ask for, and the status page lists each dataset with its sync date.
- `exists` is deliberately cautious. It is true only on a building match, and false only when the register that answered claims to list every address. Where coverage is partial — the United States and France, whose registers are fed by whoever chose to take part — an address that is not in it answers `null`, and the `not_in_register` weight never applies. Absence from a partial register is not evidence that an address does not exist.
- Two answers stay licensed and are not enabled: whether post is actually delivered to an address, with the postal service's own agency, vacant and residential flags, and which addresses sit at a UK postcode — every full UK address list derives from the Royal Mail Postcode Address File. The first answers `deliverability_checked: false`, the second answers `501 ADDRESS_LIST_UNAVAILABLE`, and neither is charged or guessed. Their fields and billing are fixed now so nothing changes for your client when a partner is signed.
- `POST /api/v1/address/batch` takes up to 100 addresses, `POST /api/v1/address/public` validates from a page with a publishable key, and the MCP server gained `verify-address`. The dashboard's new Address verification tab runs a single lookup written or as separated fields and shows every field, with "not evaluated" where a dataset is not installed.
- Billing follows the same rules as the rest: 1 credit for a fresh answer that stands as written, nothing for one that cannot, and nothing for a repeat within seven days — the repeat marker is keyed on the folded address, so two spellings of one address are one lookup and one charge.
- Nothing in the response says who lives or works at an address. The product is about places, and no field will ever be about people.
## 2026-09-05: Phone intelligence: block data, flags and a risk score
Reference: https://spaw.co/changelog/2026-09-05-phone-intelligence
- `POST /api/v1/phone` answers 33 fields: the E.164, national, international and `tel:` forms, the country's facts, `line_type` and `carrier` each with the source that settled it, the regulator's `block_status`, the `is_fictional`, `is_virtual` and `is_disposable` flags with the date a disposable listing was last seen, a recomputable `risk_score` with its `risk_signals`, and the fields of the live carrier-network check, which answers `hlr_checked: false` until a partner is configured. The full guide is at `/docs/phone-intelligence`.
- Block data comes from NANPA's central office code report for the United States and its territories, the Canadian Numbering Administrator's table, and Ofcom's National Numbering Scheme for the UK and the Crown dependencies, synced weekly: a geographic number in a block nobody holds answers `valid: false` with reason `unassigned_block`, free, and a wireless or wholesaler block holder settles the North American `fixed_line_or_mobile`.
- Three new reasons, each with its reference page: `unassigned_block`, `emergency_number` and `short_number`. Invalid numbers stay free.
- Seven test numbers, +1 202 555 0100 to 0106, answer fixed results at no cost and leave no repeat marker, so integration tests never spend credits.
- `POST /api/v1/phone/batch` takes up to 1,000 numbers, `POST /api/v1/phone/bulk` up to 100,000 with a signed webhook and CSV variants, and each item may carry its own country. `POST /api/v1/phone/public` and the `data-spaw-phone` attribute of the form helper validate numbers from a page with a publishable key.
- The MCP server gained `verify-phone`; the n8n, Zapier, Make and Google Sheets connectors gained a phone operation; the Python, Node and PHP SDKs are at 0.4.0 with phone batch and bulk methods.
- The footer credits Ofcom (Open Government Licence v3.0) and the open disposable-number list (MIT), as the licences ask, and the status page lists every phone dataset with its sync date.
## 2026-09-05: IP lookups check the Spamhaus and abuse.ch threat blocklists
Reference: https://spaw.co/changelog/2026-09-05-ip-threat-blocklists
- `POST /api/v1/ip` now consults three public threat blocklists: Spamhaus DROP (netblocks hijacked or leased by spam and cybercrime operations, IPv4 and IPv6), Spamhaus ASN-DROP (the network operators behind them, matched by the address's ASN) and abuse.ch's Feodo Tracker list of active botnet C2 servers. `is_blocklisted` is the tri-state flag and `blocklist` names the list.
- A blocklisted address adds 80 to `risk_score`, between unallocated space (100) and Tor (70). `risk_signals` carries it as `blocklist` with the dataset `threat-blocklists`, which also appears in `sources` with its sync date.
- The lists are compiled locally like every other feed, so nothing about the lookup leaves the server. ASN-DROP can only answer once the ASN database resolves the address, so `is_blocklisted` stays null rather than false when neither a range list nor the ASN could be checked.
- Bulk result files carry `is_blocklisted` and `blocklist` after `hosting_provider`; the connectors and the Node types know the two fields.
- The Spamhaus lists sync daily with the other feeds; the botnet C2 list refreshes hourly.
## 2026-09-05: IP intelligence round three, from verified crawlers to your own abuse reports
Reference: https://spaw.co/changelog/2026-09-05-ip-intelligence-round-three
- `is_crawler`, `crawler` and `crawler_kind` name the search, AI and SEO crawlers from the ranges their operators publish for verification: Googlebot and Google's special crawlers and fetchers, Bingbot, GPTBot, ChatGPT-User, OAI-SearchBot, PerplexityBot, Perplexity-User, DuckDuckBot and AhrefsBot, synced daily. A crawler carries no risk weight and, like relay egress, never the datacenter weight.
- `registry`, `registry_country` and `allocated_at` come from the RIR delegated statistics that were already synced: which registry allocated the block, the country it recorded and when. A block allocated weeks ago, or a registry country that disagrees with the geolocation, is worth a second look; neither is weighted.
- City answers improve from every RFC 8805 geofeed discovered across the five registries, refreshed weekly (`lookup:sync-geofeeds`), and `location_confidence` says how far to trust a city: high when the operator declared it, otherwise graded from the weekly accuracy report's agreement for that country.
- `is_eea` and `privacy_regime` settle consent decisions: the comprehensive privacy law where the address is, by region first (California `ccpa`, Quebec `law25`) and otherwise by country (`gdpr` across the EU and EEA, `uk_gdpr`, `lgpd`, `pipeda`, `appi`, `pipl`, `popia`, …); null when none is known.
- `POST /api/v1/ip/feedback` records what an address turned out to be (abuse, fraud, bot, chargeback), free, stored only as a salted hash for 90 days. Your own reports score your own later lookups of the address (`reported_abuse`, +60); three or more accounts reporting an address make `reported_abuse_widely` (+40) for everyone. `GET /api/v1/ip/feedback/summary` counts your reports.
- Monitored IP lists: register up to 500 of your own egress, mail-server or office addresses under `/api/v1/ip/monitors`, choose a daily, weekly or monthly cadence, and get an email the day one of them lands on Spamhaus DROP or a botnet list, becomes a Tor exit or scores high.
- Two opt-in network extras on the single-address endpoints: `network_details: true` adds the registry's RDAP record (name, organisation, range, dates) from the same cached lookup as the abuse contact, and `hostname: true` adds the reverse DNS name with `hostname_verified` saying whether it forward-confirms. A verified name under a crawler operator's domain proves the crawler too.
- Bulk result files carry the new columns at their response positions, and the Node, Python and PHP clients, the Zapier, n8n, Make and Google Sheets connectors and the MCP tool know the new fields and endpoints.
## 2026-09-04: IP lookups read the providers' own range feeds
Reference: https://spaw.co/changelog/2026-09-04-ip-network-feeds
- `POST /api/v1/ip` now consults the published ranges of AWS, Google Cloud, Microsoft Azure, Oracle Cloud, DigitalOcean, Linode, Vultr, Cloudflare and Fastly next to the open X4BNet lists. `hosting_provider` names the provider when the address is inside its own feed.
- Known VPN exits carry a name: `privacy_service` says Mullvad, NordVPN or Private Internet Access when that provider's server list contains the address.
- `is_relay` flags iCloud Private Relay egress addresses from Apple's own list, with `privacy_service` set to iCloud Private Relay. Relay adds 10 to `risk_score`, and the datacenter weight is skipped for relay addresses because their egress sits on CDN infrastructure by design. Relay egress also appears in the aggregate VPN list, so `is_vpn` answers false there unless a VPN provider's own list names the address.
- Where the network operator publishes the location itself (RFC 8805 geofeeds, Apple's relay list, VPN server lists), that declaration overrides the database answer and `location_source` says `operator`; when it disagrees with the database, `region`, `latitude` and `longitude` are null rather than a contradicting value.
- `asn_type` classifies the network as hosting, isp, education or government from the flags and the operator name, and stays null when it cannot tell.
- The datacenter, VPN and relay flags are evaluated for IPv6 wherever a feed covers it. Only the Tor exit list is IPv4-only, so `is_tor` stays null for IPv6.
- `sources` gains `relay-ranges`; each group's version is the oldest sync date among its feeds. Every feed syncs daily.
## 2026-09-04: IP lookups by path, for the caller, in batches, in bulk and from the browser
Reference: https://spaw.co/changelog/2026-09-04-ip-api-round-two
- `GET /api/v1/ip/{ip}` runs the same lookup as `POST /api/v1/ip` with the address in the path, and `GET /api/v1/ip/me` answers for the address the call came from.
- Privacy mode: `privacy: true` in the body or `?privacy=1` on a GET keeps nothing about the lookup, not even the seven-day repeat marker; every such call is billed as fresh. Nothing about an IP lookup was ever logged or written to history; the security page now says so.
- `POST /api/v1/ip/batch` looks up as many as 1,000 addresses in one call, in input order, with the paid partial results returned if the balance runs out mid-list.
- Bulk runs: `POST /api/v1/ip/bulk` queues up to 100,000 addresses, with polling, cancellation, a CSV of every response field per row, an `Idempotency-Key` header and a signed `bulk_ip_job.finished` webhook, exactly like bulk email runs.
- `POST /api/v1/ip/public` looks an address up from the browser with a domain-locked publishable key; leave `ip` out for the visitor's own address. Daily credit caps and Cloudflare Turnstile apply as they do for the email widget.
- New fields on every IP answer: `network` (the prefix the location covers), `continent`, `country_name`, `is_eu`, `timezone` (only when the country or a single-zone region settles it), `currency`, `calling_code`, `is_anonymous`, and `risk_signals`, which lists every contributing signal with the weight applied.
- IP endpoints have their own rate limit of 50 requests per second per key. The email endpoints keep 5.
- The dashboard gained an IP intelligence page for single lookups, batches and bulk runs, and the docs gained an IP intelligence guide with three companion guides.
- Three more signals from free data: `is_unallocated` (no regional registry has allocated or assigned the space, from the five RIRs' daily statistics; it scores 100), `is_anycast` (published CDN edge and public resolver ranges), and `is_tor` for IPv6 from the Tor Project's Onionoo relay data. `abuse_contact: true` adds the network's abuse mailbox from its RDAP record, looked up only on request.
## 2026-09-04: IP intelligence is live
Reference: https://spaw.co/changelog/2026-09-04-ip-intelligence-live
- `POST /api/v1/ip` and its GET, batch, bulk and browser forms are now a launched product: 1 credit per fresh answered lookup, reserved ranges and 7-day repeats free, 10 free credits a month on every account.
- The product page runs a guest demo against the live service and publishes a measured-accuracy report: every week we sample the ranges whose operators publish a location and report how often the database agrees, per feed, plus what the named VPN lists add over the aggregate open list.
- A public status page at `/status` (and `GET /api/v1/status/data` for monitors) reports the sync date of every dataset and measured API latency.
- The footer credits DB-IP Lite (CC BY 4.0) for IP geolocation, as the licence requires.
## 2026-09-03: Documentation and public pages
Reference: https://spaw.co/changelog/2026-09-03-documentation-and-public-pages
- The OpenAPI document is published at `/openapi.json` and `/openapi.yaml`.
- Every docs page has a Markdown twin at the same URL with `.md` appended, and `/llms-full.txt` gathers all of them.
- One reference page per endpoint, with request and response tables and code samples.
- Reference pages for every verdict reason, SMTP reason, and API error code.
- The free domain tools have permalinks: one page per domain for MX, SPF and DMARC, and disposable checks.
- New public pages: pricing, about, security, and this changelog.
## 2026-09-03: Email verification required
Reference: https://spaw.co/changelog/2026-09-03-email-verification-gate
- New accounts confirm their email address before the dashboard, the API keys page, and every lookup surface open up. The verification email is sent at signup and can be resent from the verification notice.
- API requests from an account that has not confirmed its address answer `403 EMAIL_NOT_VERIFIED` with the usual error envelope. Nothing is charged and nothing is logged, so the same call can be retried once the address is confirmed. The MCP server answers the same way.
- The browser form endpoint and the bounce webhook URLs keep working as before: their keys can only be created from a confirmed session.
## 2026-09-03: Unconfirmed mailboxes are now risky, and outages are free
Reference: https://spaw.co/changelog/2026-09-03-unverified-verdict
- A mailbox the SMTP handshake could not confirm (greylisting, a timeout, an unreachable server) now answers `risky` with `reason: "unverified"`. It used to read as deliverable with `smtp_checked: false`, which hid the doubt behind the headline verdict. `smtp_reason` still names the cause and `mailbox_confidence` still estimates the odds; the risk weights are unchanged.
- An answer Spaw could not obtain because its handshake partner was unavailable (`smtp_reason: "provider_unavailable"`) is free, never cached, and probed again on the next lookup. Other unverified answers cost 1 credit like any risky verdict, and repeats stay free for 7 days.
- The test address `unverified@spaw.test` now answers `risky` / `unverified`.
- List monitors no longer count an unverified answer as decay, so a partner outage cannot trigger a false alert or erase a confirmed baseline.
- The reason reference gained a page for `unverified`, and the field tables, guides and test-address table were updated to match.
## 2026-09-03: Monitors, history, and safe retries over the API
Reference: https://spaw.co/changelog/2026-09-03-monitors-history-api
- Monitors over the API: `GET`, `POST` and `DELETE /api/v1/email/monitors`, `GET /api/v1/email/monitors/{id}` with the last run's verdict per address, and `POST /api/v1/email/monitors/{id}/run` to re-verify now. The same 500-address lists, weekly or monthly cadences and decay alerts as the dashboard.
- History over the API: `GET /api/v1/email/history` lists every lookup the account ran through any channel, newest first, with filters for verdict, address, date range and channel.
- Bulk creation accepts an `Idempotency-Key` header. A retry with the same key and request replays the job it created, with `Idempotent-Replayed: true` and the same webhook secret, instead of queueing a second run; the same key with a different request answers `409 IDEMPOTENCY_KEY_REUSED`.
- Every new endpoint has a reference page under the API docs and a Markdown twin.
## 2026-09-03: Freshness controls and a per-call handshake timeout
Reference: https://spaw.co/changelog/2026-09-03-freshness-controls
- `POST /api/v1/email` and `POST /api/v1/email/batch` accept `refresh: true`, which discards the answer from the seven-day repeat window and the shared mailbox answer, runs a real handshake again, and bills the lookup as fresh when it is answered.
- `max_age` (seconds, 60 to 604800) does the same only when the previous answer is older than that, so a list can be re-checked at the cadence you choose without paying for answers that are still young.
- The response says what happened: `meta.refreshed` is true whenever an earlier answer was discarded for the call.
- `timeout` (seconds, 2 to 60) caps how long the mailbox handshake may wait for one call. A handshake that runs out of time answers `unverified` with `smtp_reason: timeout` instead of holding the request.
- The Python, Node.js and PHP clients, the Zapier app and the Google Sheets functions expose the same options.
## 2026-09-03: Monthly plans
Reference: https://spaw.co/changelog/2026-09-03-monthly-plans
- Monthly plans: Starter, Growth, Scale and Volume are now also sold as monthly subscriptions at $7, $29, $119 and $399 a month, 20 to 26 percent less per credit than the packs of the same size. The credits arrive on every paid invoice, land in the same balance as packs and the free grant, and never expire.
- Start, switch, cancel and resume a plan from the Billing page. Moving to a bigger plan is charged today and deposits its credits at once; moving to a smaller one takes effect at the next renewal; cancelling keeps the plan until the end of the paid month and keeps the credits.
- Stripe's customer portal is linked from the Billing page for card changes, invoices and receipts.
- `GET /api/v1/account` reports the plan key: `free`, or `starter`, `growth`, `scale`, `volume` while a plan is paid up.
- The pricing page and its Markdown twin show the pack and the plan side by side for every tier, and the usage page labels plan deposits as "Monthly plan".
## 2026-09-02: Verdict quality
Reference: https://spaw.co/changelog/2026-09-02-verdict-quality
- Lenient address extraction: display names, `mailto:` links, quotes, trailing punctuation, and invisible characters are stripped before validation.
- Provider username rules reject local parts that Gmail, Outlook, Yahoo, iCloud, AOL, and Proton would never issue.
- A curated typo-squat map turns `gmail.con` and similar domains into a `likely_typo` verdict with a suggestion.
- New MX sanity statuses: `implicit_mx` for domains that rely on the A-record fallback and `mx_unresolvable` for MX hosts that point nowhere usable.
- Disposable detection now also matches on the MX hosts, so a fresh burner domain hosted by a known operator is caught; local allow and deny overrides beat waiting for the upstream list.
- Role and free-provider lists sync weekly from their open sources.
- `smtp_reason` carries the provider detail: a full mailbox is risky, a disabled mailbox is undeliverable.
- The risk score adds a missing SPF record and the domain's registration age, read from RDAP.
- `mailbox_confidence` estimates the odds of an unverified mailbox.
- The response grew from 21 to 26 fields.
## 2026-09-02: Reliability, feedback loop and billing
Reference: https://spaw.co/changelog/2026-09-02-reliability-feedback-loop-and-billing
- A shared mailbox answer cache across customers, so the same address is probed once.
- Bulk runs process in chunks, and each chunk writes its own part file before results are stitched.
- Provider balance alerts warn before the mailbox partner runs dry.
- Six `@spaw.test` test addresses answer fixed verdicts at no cost and are never logged.
- Every response carries `X-Request-Id`, repeated in `meta.request_id` and `error.request_id`.
- Publishable keys accept a daily credit cap (`429 KEY_SPEND_CAP_REACHED` once it is spent).
- A circuit breaker keeps a struggling mailbox provider from slowing every lookup.
- Batch requests send their mailbox probes concurrently.
- A bounce feedback API (`POST /v1/email/feedback`) and webhook targets for Postmark, Amazon SES, Mailgun, and SendGrid; bounces join the suppression list, deliveries clear it, and the dashboard shows measured accuracy.
- `smtp_checked_at`, the 27th response field, records when the mailbox was last probed.
- Catch-all domains are remembered for a week, and an account's own delivered addresses count as verified for 90 days.
- Credit packs are sold through Stripe Checkout from the Billing page.
- `callback_url` and `callback_secret` on a lookup re-check an unverified answer after 5 and 20 minutes and POST the settled verdict, signed.
- Bulk uploads keep the file's own columns, add the verdict columns, and offer `?variant=deliverable|risky|undeliverable` downloads.
- Publishable keys can require a Cloudflare Turnstile token per lookup.
## 2026-08-31: Batch, bulk, domains, monitors, MCP and free tools
Reference: https://spaw.co/changelog/2026-08-31-batch-bulk-domains-monitors-mcp-and-free-tools
- `POST /v1/email/batch` verifies up to 50 addresses in one request.
- `POST /v1/email/bulk` queues up to 100,000 addresses, with an HMAC-signed completion webhook, cancellation, and CSV results.
- `GET /v1/email/domain/{domain}` answers MX, provider, SPF and DMARC, registration age, and list membership for a whole domain.
- A per-account suppression list: undeliverable verdicts are remembered, and existing lists can be imported.
- Scheduled list monitors re-verify a list on a cadence and alert when its deliverability decays.
- Publishable keys (`pk_…`) power a browser endpoint locked to allowed origins.
- An MCP server lets AI agents verify addresses with an existing API key.
- Four free tools without signup: email checker, MX lookup, SPF and DMARC checker, and disposable domain checker.
- The documentation became a multi-page area with a getting-started guide, an email verification guide, and the API reference.
- The product is now Spaw at spaw.co.
## 2026-08-30: Email verification API launch
Reference: https://spaw.co/changelog/2026-08-30-email-verification-api-launch
- `POST /v1/email` verifies one address: RFC syntax, live MX resolution, open disposable and role lists, typo suggestions, SPF and DMARC records, and a 0 to 100 risk score built from published weights.
- An SMTP mailbox handshake runs on every lookup whose domain accepts mail. No message is ever sent.
- MX provider classification names the infrastructure behind a domain (Google, Microsoft, Proofpoint, and others).
- Secret API keys (`sk_live_…`) are created from the dashboard, shown once, and stored hashed.
- Credits bill answers only: a deliverable or risky verdict costs 1 credit, undeliverable and invalid answers are free, and a repeat within 7 days is free.
- The dashboard playground runs the same lookups as the API.
- `spaw-form.js`, a form helper for the browser, checks addresses on blur, offers a did-you-mean correction, and never blocks a submit.