Skip to content

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 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.

$ 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.

validAssignable 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.
reasonWhy 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.
e164The canonical form to store and dedupe by, such as +14155550142. Glossary: E.164.
national / internationalDisplay formats: (415) 555-0142 and +1 415-555-0142.
rfc3966The tel: URI, tel:+1-415-555-0142, ready for a click-to-call link.
countryISO 3166-1 alpha-2. Null for non-geographic numbers such as international toll-free +800. Glossary: International network number.
country_name / calling_codeThe English name of the country and its calling code, such as 44.
continent / is_eu / currencyFacts about the number’s country, from the same data the IP lookup uses: continent code, EU membership, ISO 4217 currency.
line_typemobile, 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.
line_type_sourcenumbering_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.
carrierThe 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.
carrier_sourceWhere the carrier came from.
block_statusWhat 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.
regionA geographic description such as "San Francisco, CA".
is_geographicTied to a place (fixed lines and geographic mobiles) rather than a service such as toll-free.
timezonesEvery IANA zone the number can belong to: one for most numbers, several for country-wide ranges, none for non-geographic numbers.
extensionParsed from input like "ext 123" and kept out of e164.
is_fictionalThe 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.
is_virtualThe block is held by a virtual-number wholesaler or VoIP provider. Null when no regulator table covers the number. Glossary: Virtual number.
is_disposablePublished by an SMS-receiving website, so anyone can read a code sent to it. Null until the list is installed. Glossary: SMS-receiving number.
disposable_seen_atWhen 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_levelThe sum of the weights below, capped at 100; high from 60, medium from 30. Null for invalid numbers.
risk_signalsEach contributing signal with the weight applied and the dataset behind it; the weights sum to risk_score.
hlr_checkedWhether 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.
reachable / ported / current_carrier / roamingWhat the live check learned about the handset. Null until it runs.
carrier_codeThe 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).
carrier_brandThe 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_pooledNorth 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, NPA/NXX.
block_assigned_atWhen 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_levelHow 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, Area-code overlay.
local_timeThe 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_offsetsThe 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_windowOnly 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.
did_you_meanInvalid 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.
range_seen_by_you{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.
client_velocity{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_youTrue 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_atNorth 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.
ok_to_sendYour 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.
blocked_byThe 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.
sourcesThe 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 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 gives each publisher a page: its files and their format, the terms as it states them, and what the copy installed here holds.

libphonenumberValidity, formats, line type, the carrier prefixes it knows, geographic descriptions, time zones, emergency numbers and short codes.
nanpa-co-codesFor 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.
cnac-co-codesThe same for Canada, from the Canadian Numbering Administrator’s code status table. See phone facts for Canada.
ofcom-numberingFor the United Kingdom and the Crown dependencies: allocated, free, protected and quarantined blocks and their communications providers. See phone facts for the United Kingdom.
arcep-numberingFor 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.
acma-numberingFor Australia: allocated and spare ten-digit ranges with their current holder, from the ACMA numbering register. See phone facts for Australia.
acm-nummerregisterFor the Netherlands: assigned, cooling-off and blocked ranges with their holder, from the ACM number register. See phone facts for the Netherlands.
nad-number-registerFor New Zealand: assigned, spare and reserved codes with their holder, from the Number Administration Deed register. See phone facts for New Zealand.
ift-numeracionFor 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.
uke-numeracjaFor 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.
mimit-numerazioneFor 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.
fictional-rangesThe 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.
carrier-brandsThe 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.
disposable-phone-numbersNumbers 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.
us-complaint-reportsComplaints 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.
test-numbersThe eight 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. 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.

+1 202 555 0100valid, mobile from the block holder, carrier Spaw Test Wireless, risk 0
+1 202 555 0101valid, fixed_line, carrier Spaw Test Telecom, risk 0
+1 202 555 0102valid, voip, is_virtual true, risk 40
+1 202 555 0103valid, is_disposable true, seen today, risk 70
+1 202 555 0104invalid, reason unassigned_block, the parsed number kept
+1 202 555 0105valid, is_fictional true, risk 80
+1 202 555 0106invalid, reason invalid_number, every other field null
+1 202 555 0113valid, 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.

fictionalRange reserved for fiction (555-01XX; Ofcom, ARCEP, ACMA and ComReg drama ranges)+80
disposablePublished 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_ratePremium-rate number (in the UK, the 09 range)+60
reported_abuseReported as abused by your own account within 90 days+60
reported_undeliveredReported as undelivered by your own account within 90 days, with no later delivery or verification+50
reported_abuse_widelyReported as abused by three or more accounts within 30 days+40
reserved_blockBlock reserved for special use (555, test codes, designated ranges)+40
virtualBlock held by a virtual-number wholesaler+40
disposable_stalePublished 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_burstYour account looked up several numbers in this 1,000-number range within the window+40
voipVoIP number by the numbering plan+30
pagerPager number+30
digit_patternRepeated or sequential subscriber digits (555-5555, 123-4567)+30
numbers_per_clientOne client IP tried several different numbers within the window+30
disposable_rangeNot listed itself, but in a 1,000-number range holding ten or more listings that still score disposable+30
international_networkInternational network or satellite number (+870, +881, +882, +883)+30
shared_costShared-cost number, including the UK 084 and 087 ranges+20
reported_unwanted_callsConsumers 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:

A UK mobile in a block Ofcom lists as allocated to its carriernone0, low
A US number in a block held by a virtual-number wholesalervirtual 4040, medium
A number an SMS-receiving site still lists but last showed fourteen months agodisposable_stale 4040, medium
A number every list dropped, last seen four months agodisposable_stale 4040, medium
An unlisted mobile beside a run of recently listed SMS-receiving numbersdisposable_range 3030, medium
+1 415 555 5555: the reserved 555 code, one digit repeatedreserved_block 40, digit_pattern 3070, high
The docs example +1 415 555 0142: fictional, in the reserved 555 codefictional 80, reserved_block 40100, high (capped)
A +882 international-network number, which the plan also types as VoIPvoip 30, international_network 3060, high
A geographic US number whose block nobody holdsunassigned_blocknull, 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"

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.

allowed_countriesnull (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_countriesnull (none)Never send to numbers of these regions.
blocked_line_typesnoneBlock 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_signalsfictional, disposable, reported_abuse, reported_abuse_widely, premium_rateBlock whenever one of these signals fires, whatever the score. Any weight name may be listed.
max_risk_score60Block when risk_score is above this; null ignores the score.
require_reachablefalseBlock unless a live check reached the handset: unreachable when the network could not reach it, unconfirmed when no live check answered.
block_opted_outtrueBlock 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 in one page, and the guide to gating SMS verification codes with it 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 (the API, 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.

Three emails go to the account owner, all worked out from these counts:

Range burstA range reaches five distinct numbers inside the window.Once per range per window, at most five a day.
Destination spikeAn 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.
EnumerationAn 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 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. Every one of them is free.

missing_countryA national-format number came without a country to parse it in.
invalid_country_codeThe calling code after the plus belongs to no plan.
too_shortFewer digits than the region ever assigns.
too_longMore digits than the region ever assigns.
invalid_lengthA length the region uses for no type of number.
not_a_numberNothing in the input parses as a number.
invalid_numberThe right length, but no range in the plan admits it.
unassigned_blockThe plan allows it, but the regulator’s table shows nobody holds the block.
emergency_numberAn emergency number for the given country, such as 911 or 999.
short_numberA 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 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.

$ 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, 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 your credits ran out is resumed rather than uploaded again: top up, then press Resume on the run or call POST /api/v1/phone/bulk/{jobId}/resume. 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. 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. 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 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, 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 X-Spaw-Signature-V2 carries 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, is sent beside it until 1 March 2027 and still verifies. 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} 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. 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.

<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. 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, 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 states the same facts alongside the rest of the data handling.

Next: the endpoint reference for every field, example, and error code.