Phone intelligence
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. The API and its product page are live; this guide covers every field.
$ 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 33 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.
| valid | Assignable under the numbering plan and, where a regulator table is installed, inside an allocated block. |
| reason | 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. |
| e164 | The canonical form to store and dedupe by, such as +14155550142. |
| national / international | Display formats: (415) 555-0142 and +1 415-555-0142. |
| rfc3966 | The tel: URI, tel:+1-415-555-0142, ready for a click-to-call link. |
| country | ISO 3166-1 alpha-2. Null for non-geographic numbers such as international toll-free +800. |
| country_name / calling_code | The English name of the country and its calling code, such as 44. |
| continent / is_eu / currency | Facts about the number’s country, from the same data the IP lookup uses: continent code, EU membership, ISO 4217 currency. |
| line_type | mobile, fixed_line, fixed_line_or_mobile, voip, toll_free, premium_rate, shared_cost, personal_number, pager, uan, voicemail or unknown. |
| line_type_source | numbering_plan when the metadata decided the type. 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 | The block holder as the regulator lists it, else the carrier the metadata knows for the prefix. The original allocation, never a later port. |
| carrier_source | Where the carrier came from. |
| block_status | 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. |
| region | A geographic description such as "San Francisco, CA". |
| is_geographic | Tied to a place (fixed lines and geographic mobiles) rather than a service such as toll-free. |
| timezones | Every IANA zone the number can belong to: one for most numbers, several for country-wide ranges, none for non-geographic numbers. |
| extension | Parsed from input like "ext 123" and kept out of e164. |
| is_fictional | The number sits in a range reserved for fiction and drama: the NANP 555-01XX block, Ofcom’s drama ranges. Still valid, never in service. |
| is_virtual | The block is held by a virtual-number wholesaler or VoIP provider. Null when no regulator table covers the number. |
| is_disposable | Published by an SMS-receiving website, so anyone can read a code sent to it. Null until the list is installed. |
| disposable_seen_at | When that list last saw the number (YYYY-MM-DD), where the list says so. A listing older than a year scores as disposable_stale instead of disposable. |
| risk_score / risk_level | The sum of the weights below, capped at 100; high from 60, medium from 30. Null for invalid numbers. |
| risk_signals | Each contributing signal with the weight applied and the dataset behind it; the weights sum to risk_score. |
| hlr_checked | Whether a live carrier-network check answered. False whenever the lookup did not ask for one with hlr: true, and until a carrier-network partner is configured. |
| reachable / ported / current_carrier / roaming | What the live check learned about the handset. Null until it runs. |
| sources | The datasets consulted, each with its version or sync date: the numbering metadata always, the regulator table when it answered, the fictional ranges, the listed numbers, the live check when it ran. |
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.
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.
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.
| libphonenumber | Validity, formats, line type, the carrier prefixes it knows, geographic descriptions, time zones, emergency numbers and short codes. |
| 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. |
| cnac-co-codes | The same for Canada, from the Canadian Numbering Administrator’s code status table. |
| ofcom-numbering | For the United Kingdom and the Crown dependencies: allocated, free, protected and quarantined blocks and their communications providers. |
| 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. |
| acma-numbering | For Australia: allocated and spare ten-digit ranges with their current holder, from the ACMA numbering register. |
| acm-nummerregister | For the Netherlands: assigned, cooling-off and blocked ranges with their holder, from the ACM number register. |
| nad-number-register | For New Zealand: assigned, spare and reserved codes with their holder, from the Number Administration Deed register. |
| ift-numeracion | For Mexico: every assigned range with its operator and modality from the IFT national numbering plan, so a number in a range nobody holds is unassigned. |
| fictional-ranges | The NANP 555-01XX block and Ofcom’s drama ranges. |
| disposable-phone-numbers | Numbers published by SMS-receiving websites, with the date each was last seen. |
| test-numbers | The seven documented test numbers below. |
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.
Test numbers
Seven 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.
| +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 |
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.
| Range reserved for fiction (555-01XX, Ofcom drama ranges) | +80 |
| Published by an SMS-receiving website within the last year | +70 |
| Premium-rate number | +60 |
| Reported as abused by your own account within 90 days | +60 |
| Reported as undelivered by your own account within 90 days, with no later delivery | +50 |
| Reported as abused by three or more accounts within 30 days | +40 |
| Block reserved for special use (555, test codes, designated ranges) | +40 |
| Block held by a virtual-number wholesaler | +40 |
| Published by an SMS-receiving website more than a year ago | +40 |
| VoIP number by the numbering plan | +30 |
| Pager number | +30 |
| Repeated or sequential subscriber digits (555-5555, 123-4567) | +30 |
| Shared-cost number | +20 |
Worked examples, as the API answers them:
| 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 last showed fourteen months ago | disposable_stale 40 | 40, 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 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 above it reads:
"risk_signals": [ { "signal": "virtual", "weight": 40, "dataset": "nanpa-co-codes" } ], "risk_score": 40, "risk_level": "medium"
Reasons
An invalid number always says why, in one of ten values, each with its own reference page. Every one of them is free.
| 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 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 batch counts as one request against the rate limit.
$ 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 or from the phone validation page in the dashboard, 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): 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, sources, credits_used, suppressed
Cancelling stops a queued job on the spot and a running one at its next checkpoint; rows already answered stay billed and downloadable. With a webhook_url, the finish is POSTed there as bulk_phone_job.finished, signed in X-Spaw-Signature with the HMAC-SHA256 of the raw body keyed with the secret creation returns once. 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.
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 or abused; such numbers answer free, marked meta.suppressed: true per item and in the CSV's trailing suppressed column, while a single lookup always re-checks. 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.
A monitored list ( POST /api/v1/phone/monitors, 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.
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.
<script src="https://spaw.co/spaw-form.js" data-key="pk_live_…"></script> <select name="country">…</select> <input type="tel" name="phone" data-spaw-phone data-spaw-country-field="country">
Or call POST /api/v1/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.
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. The live carrier-network check adds three credits only when it actually answers; while no partner is configured it answers hlr_checked: false at no extra cost, and asking for it reserves the three credits up front so a request that cannot afford them is refused cleanly. 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 trace is the keyed seven-day marker that makes a repeat free. Every signal is a local table or a bundled file, so no third party sees the number, and 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 states the same facts alongside the rest of the data handling.
Next: the endpoint reference for every field, example, and error code.