- 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.
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 | description |
|---|---|---|
| phonerequired | string | The number, in any common notation. Include the country calling code or pass `country`. At most 64 characters. |
| country | string | null | 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 | 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 | 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 | 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
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"
}'Run it
free demo · no key needed
Running a request from this page needs JavaScript. The command above makes the same call from a terminal.
Responses
200The parsed number; `valid: false` with a `reason` for numbers that do not parse, are not assignable, or sit in a block nobody holds.
{
"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
}
}401The key is missing, malformed, or revoked.
{
"success": false,
"error": {
"code": "UNAUTHENTICATED",
"message": "Provide a valid API key as a bearer token.",
"request_id": "req_01m1kgdm4xngzmbmff68g94w0c"
}
}402The balance is empty. The lookup did not run.
{
"success": false,
"error": {
"code": "INSUFFICIENT_CREDITS",
"message": "Your credit balance is empty.",
"request_id": "req_01m1kgdm4xngzmbmff68g94w0c"
}
}422The request body could not be validated; `error.errors` lists the fields.
{
"success": false,
"error": {
"code": "VALIDATION_FAILED",
"message": "The email field is required.",
"errors": {
"email": [
"The email field is required."
]
},
"request_id": "req_01m1kgdm4xngzmbmff68g94w0c"
}
}429Over 5 requests per second for the key. Retry after the limit resets.
{
"success": false,
"error": {
"code": "RATE_LIMITED",
"message": "Too many requests. Retry after the limit resets.",
"request_id": "req_01m1kgdm4xngzmbmff68g94w0c"
}
}Error codes
Failures answer { success: false, error: { code, message, request_id } }. Each code has its own page.