DEV Community

A A
A A

Posted on

What a phone number tells you before you send an SMS

If you send an SMS to a landline it disappears. No bounce, no error you can act on, and you are still billed for it. The same goes for a disconnected number and for plenty of VoIP numbers. The fix is to find out what a number actually is before you send to it.

Here is what a phone number can tell you, and what it cannot.

Start by normalizing to E.164
Before anything else, get the number into E.164: a plus sign, the country code, then the national number, with no spaces, dashes or brackets.

07400 123456 -> +447400123456
(202) 555-0143 -> +12025550143
Two things go wrong at this step.

First, a national number with no country code is ambiguous. 07400123456 is a UK mobile, but a bare 7400123456 could belong to several countries. You need a default country to parse against, usually the country of the account that submitted the number.

Second, people paste extensions, letters and stray leading zeros. Anything that fails to parse should stop here rather than being sent anywhere downstream.

Line type is the decision you actually care about
Once you have a valid E.164 number, the useful question is what kind of line it is.

mobile: can receive SMS

fixed_line: cannot receive SMS in most countries

voip: sometimes can, often cannot, and a meaningful fraud signal at signup

toll_free, premium_rate, pager: do not send

A validation library on its own will not tell you this reliably. Number ranges are allocated by type, so a library that knows only the allocation table tells you what the range was issued as, not what the number is today.

Portability is why the allocated range is not enough
Most countries let a subscriber keep their number when they switch network. That is mobile number portability, and it means the carrier that was allocated a range is often no longer the carrier serving a given number in it.

If you route SMS by allocated range in a country with portability, you pay the wrong carrier's rate and your delivery rate quietly drops. Getting the current network needs a live lookup, not a static table.

What a lookup gives you back
A useful response is more than valid: true. A real one looks like this:

{
"input_phone": "+447400123456",
"normalized_e164": "+447400123456",
"valid": true,
"country": "GB",
"country_name": "United Kingdom",
"region": "Europe",
"line_type": "mobile",
"carrier": "EE",
"sms_ready": true,
"call_ready": true,
"reason": "valid_mobile",
"cached": false,
"checked_at": "2026-06-30T10:04:11.204Z",
"credits_debited": 1
}
The fields worth building logic on:

valid together with reason, so you know why a number failed and not just that it did

line_type, the routing decision

carrier, the current network after portability

sms_ready and call_ready, if you would rather not write that logic yourself

cached, so you can tell a fresh lookup from a cached one

Three places the check pays for itself
At signup. Reject a number that cannot receive your OTP before the user presses send, instead of after they wait for a code that was never going to arrive.

Before a campaign. Split the list on sms_ready and suppress the rest. You pay per message either way.

Before a dialler run. call_ready and line_type keep agents off numbers that will not connect.

Cache, and deduplicate first
Lookups cost money per number, so cache them. The network a number sits on does not change often, and a 24 hour cache absorbs most repeat traffic in practice.

Deduplicate inside a batch before you send it, too. The same number turns up several times in one CSV more often than you would expect, and each copy would otherwise be a separate paid lookup.

Trying it
Disclosure: this is our own product. We built Carrier Reveal around exactly this, so treat the example below as one implementation rather than a recommendation. The API is one POST per number, or up to 100 in a batch:

curl -X POST https://api.phonelistcleaner.com/v1/lookup \
-H "Authorization: Bearer plc_" \
-H "Content-Type: application/json" \
-d '{"phone":"+447400123456","defaultCountry":"GB"}'
The docs are at Carrier Lookup API | Carrier Reveal. Twilio Lookup and Numverify solve the same problem if you would rather use something you already have.

The general point holds whatever you use: normalize first, check the line type, and never trust the allocated range in a country with number portability.

Top comments (0)