# Phone intelligence

How Spaw phone intelligence works: every field, where each fact comes from, the recomputable risk score, the reasons, batch and bulk runs, browser lookups, test numbers, and what is stored (nothing).

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.

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

| Field | Type | Meaning |
| --- | --- | --- |
| valid | boolean | Assignable under the numbering plan and, where a regulator table is installed, inside an allocated block. |
| 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. |
| e164 | string or null | The canonical form to store and dedupe by, such as +14155550142. |
| 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. |
| 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`. |
| line_type_source | numbering_plan, block_allocation or null | `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 | 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. |
| 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. |
| 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, Ofcom's drama ranges. Still valid, never in service. |
| 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. |
| 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. |
| disposable_seen_at | string or null | 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 | 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 with `hlr: true`, and until a carrier-network partner is configured. |
| reachable, ported, current_carrier, roaming | boolean, boolean, string, boolean or null | What the live check learned about the handset. Null until it runs. |
| sources | array | 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.

| 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. | weekly, from NANPA's daily report |
| cnac-co-codes | The same for Canada, from the Canadian Numbering Administrator's code status table. | weekly |
| ofcom-numbering | For the United Kingdom and the Crown dependencies: allocated, free, protected and quarantined blocks and their communications providers. | 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. | weekly |
| acma-numbering | For Australia: allocated and spare ten-digit ranges with their current holder, from the ACMA numbering register. | weekly |
| acm-nummerregister | For the Netherlands: assigned, cooling-off and blocked ranges with their holder, from the ACM number register. | weekly |
| nad-number-register | For New Zealand: assigned, spare and reserved codes with their holder, from the Number Administration Deed register. | weekly |
| 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. | weekly |
| fictional-ranges | The NANP 555-01XX block and Ofcom's drama ranges. | bundled and versioned |
| disposable-phone-numbers | Numbers published by SMS-receiving websites, with the date each was last seen. | weekly |
| test-numbers | The seven 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.

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

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

## 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 | Points |
| --- | --- |
| 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:

| 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 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 it reads:

```json
"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 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 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): 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`. Manage the list with `GET`, `POST` and `DELETE /api/v1/phone/suppressions` (https://spaw.co/docs/api/list-phone-suppressions).

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.

## 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
<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` (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. 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 (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.
