Daniel's been deep in the telephony migration weeds, and this week he's asking about something that sounds simple but unravels into one of the messiest parts of running your own phone stack. Here's what he wrote.
"In recent episodes we've talked about telephony, and I mentioned I'm moving my business and personal phone lines over to Twilio. My goal isn't just to change providers, it's to gain much more control and flexibility than a traditional phone company offers. We've already explored voicemail, local phone numbers, and emergency calling. Today I'd like to focus on one very specific topic: how phone numbers are interpreted when you dial them."
"Here in Israel, as in many other countries, there are special service numbers such as star three thousand. These short codes are designed to be easy to remember and are generally only meaningful within that country's telephone network. If they weren't, there would obviously be conflicts between different countries' services. The same is true for ordinary phone numbers. Most people dial them in whatever local format they're used to, even though the network ultimately has to translate those numbers into the international E dot one six four format before the call can be routed correctly."
"So I'm curious about how Twilio handles all of this. If you're using Twilio as the core of your phone service rather than a traditional carrier, does Twilio automatically understand local dialing conventions and country-specific short codes, or do you need to explicitly configure translation rules? In other words, how much of the quote it just works unquote behavior that people take for granted with a normal phone provider is built into Twilio, and how much becomes your responsibility once you're running your own telephony stack?"
There's a lot here. The short version of the answer is: almost none of it is built in. But the long version is where this gets interesting.
The long version is where you realize you've been outsourcing an entire category of thinking to your phone company your whole life. And the moment you become your own carrier, that thinking lands on your desk.
Daniel's asking, essentially, who's responsible for making sense of what you dial. And the answer shifts completely when you move from renting a phone company to owning the switch.
Right. Let's start with what Twilio actually does with a phone number you hand it. Because the answer is genuinely surprising if you haven't read the docs.
Twilio's core routing engine, the thing that handles calls and SMS, expects phone numbers in E dot one six four format. That's the international numbering plan standard. It's structured as a plus sign, then a country code, then the national significant number. Maximum fifteen digits total. So plus nine seven two, then the rest.
Plus nine seven two five four something something something.
That's the universal routing format. Every carrier on earth understands it. When you feed Twilio an E dot one six four number, it routes it correctly, end of story.
Now, here's where the surprise lives. The documentation for the Number noun in TwiML, which is Twilio's markup language for call flow, says this. I'm quoting directly: "Phone numbers should be formatted in E dot one six four format with a plus and country code." Then there's a period. And then: "Twilio will also accept unformatted US numbers, for example, four one five five five five one two one two or four one five dash five five five dash one two one two."
Wait. So they carved out an entire exception just for the United States?
They did. And it's not hidden in some footnote. It's right there in the primary documentation for the most basic call-routing element in the entire platform.
That's... almost aggressively American.
It's not even aggressive. It's just the gravitational pull of where Twilio started. They're a San Francisco company. Their earliest customers were American. The North American Numbering Plan has a uniform ten-digit structure. Every US number is exactly three digits of area code plus seven digits of subscriber number. Normalizing that is trivial. Strip the parentheses, strip the dashes, count to ten, slap a plus one on the front, done.
So the US gets a free pass because the US numbering system is boring.
Boring in a way that makes parsing deterministic. But the moment you cross a border, that determinism evaporates. Take Israel. A Tel Aviv number in local format looks like zero three dash one two three four five six seven. That's ten digits including the leading zero. If you pass that string to Twilio's Number noun without any preprocessing, what does Twilio see?
A ten-digit string that doesn't start with a plus.
And doesn't match any US area code pattern, probably. So it either rejects it outright or, if those ten digits happen to align with a valid US number format, it tries to route it as a US call.
Which would mean someone in Tel Aviv dials their neighbor and gets connected to... what, a dentist in Cleveland?
Worst case, yes. Best case, the call fails with an invalid number error. Neither outcome is what you'd call a working phone system.
And the leading zero is the real trap here. Because in Israel, you dial zero three for Tel Aviv, zero two for Jerusalem. That zero is the trunk prefix. It tells the local exchange "this is a domestic long-distance call within the country." But in E dot one six four, that zero gets stripped. The country code replaces it. So zero three one two three four five six seven becomes plus nine seven two three one two three four five six seven.
And Twilio does none of that stripping for you. If you feed it the zero-three version, it doesn't know what to do with it. This is the first big realization: outside the United States, Twilio assumes you've already done the normalization work before the number hits its API.
So the "it just works" layer that Daniel's asking about? For international numbers, it's not there.
It's not there. And this is by design, not by oversight. Twilio's philosophy is that it gives you primitives, not a finished phone company. The tradeoff is you get complete control over call routing, but you also get complete responsibility for making sure the inputs to that routing are valid.
Let's talk about the star codes. This is where it gets even weirder.
The star three thousand problem. So in Israel, if you dial star three thousand from a regular cell phone or landline, you reach... what exactly?
It's a general information and service line. Think of it like a municipal hotline or a government services portal. You dial those four characters and it connects you.
And here's the thing: star three thousand is not a phone number. Not in any sense that the international numbering plan recognizes. It has no E dot one six four representation. You can't write it as plus something something something. It's a service code. It exists only within the context of the Israeli telephone network.
When you dial it on a traditional carrier, your phone doesn't even send it as a number. It sends a signal that says "the user wants service code three thousand." The local exchange recognizes that code, looks it up in a table, and routes the call to whatever endpoint the carrier has mapped it to.
And Twilio has no concept of service codes.
None. Twilio's API understands phone numbers. That's it. If you pass star three thousand to the Number noun, Twilio looks at it and sees... a string that starts with an asterisk. It's not a valid E dot one six four number. It's not a valid US number. It's not anything Twilio knows how to route. The call fails.
So Daniel's question about whether Twilio "automatically understands" these short codes has a very clean answer: no, it does not understand them at all. They're not even in the same category of thing.
And this isn't a flaw in Twilio. It's a category difference. Traditional carriers operate a closed network. They control the entire chain from handset to exchange to destination. They can define whatever service codes they want because they own the namespace. Twilio operates at the API layer. It routes calls to and from the public telephone network using standard numbering. Service codes don't cross that boundary.
Unless you build the bridge yourself.
Which is exactly what you have to do. And we'll get to that.
First, let me make sure I understand the full scope of what a traditional carrier absorbs. We've got local number formatting, leading zero stripping, area code recognition, service code routing. What else?
Intra-country dialing variations. In Israel, if you're calling a number within the same area code, you can often drop the area code entirely and just dial the seven-digit subscriber number. The local exchange fills in the rest. Twilio has no idea what area code you're in, so it can't fill in anything.
There's also the emergency services routing we covered in a previous episode. Dialing one zero zero or one zero one from any phone in Israel connects you to police or ambulance, and the network knows your location. Twilio doesn't do that natively either.
So the list of things you're taking on when you become your own carrier is: number formatting, area code inference, trunk prefix handling, service code mapping, emergency routing. And that's just the dialing layer.
It's the invisible switchboard. The thing that's been quietly doing work for you since the day you got your first phone. And most people never think about it because it's invisible. You pick up the phone, you dial, it works. The fact that a small army of translation logic ran in the background is not something anyone appreciates until they try to replicate it.
Until they're Daniel, staring at a Twilio console, wondering why star three thousand doesn't connect.
So now that we know the gap, the real question is what you do about it. And this is where the Lookup API and some of your own code come in.
Let's talk about the Lookup API. What does it actually give you?
The Twilio Lookup API version two is a validation and normalization service. You send it a raw string, something a user typed into a dialer, and it returns structured data about that number. The key field for our purposes is the national format and the E dot one six four format. So if you send it zero three one two three four five six seven, it'll return plus nine seven two three one two three four five six seven.
It does the leading-zero stripping and country code prepending automatically?
For standard phone numbers, yes. It knows that Israel's country code is nine seven two. It knows that the zero-three prefix maps to area code three. It does the translation. But here's the catch: it's a separate API call.
Meaning it doesn't run automatically when a call comes in.
It does not. You have to explicitly integrate it into your call flow. When a call arrives at your Twilio number, the inbound webhook fires and sends you the To and From parameters. Those parameters contain whatever the caller dialed and whatever number they're calling from, in whatever format the carrier passed them. If the carrier passed them in local format, that's what you get. Twilio doesn't normalize them for you before hitting your webhook.
So your application receives the raw input, and it's your job to say "okay, before I route this, let me clean it up."
You'd write a function that intercepts every inbound call, takes the To and From numbers, runs them through the Lookup API, and replaces them with the normalized E dot one six four versions before proceeding with your routing logic.
And that works for standard phone numbers. But we already established that star three thousand is not a standard phone number.
Right. The Lookup API won't help you with star three thousand. If you send it a string that starts with an asterisk, it's going to return an error or an empty result. Because star three thousand is not a phone number. It's not in any numbering plan database. The Lookup API validates phone numbers. Service codes are outside its scope entirely.
So for service codes, you need something else entirely.
You need a mapping table. A simple key-value store in your application that says: if the user dialed star three thousand, route the call to this TwiML bin, or this function, or this external number. You're essentially recreating the lookup table that exists inside the traditional carrier's exchange.
And you have to populate that table yourself.
You do. Twilio doesn't publish a global directory of service codes. How would it? Every country defines its own. Some carriers within a country define additional ones. There's no central authority for "things that start with asterisks." So you research the codes that matter for your users, you map them to endpoints, and you maintain that mapping as services change.
This is the philosophical core of the whole migration, isn't it? When you run your own telephony stack, you're not just a customer anymore. You're the switch.
The dialing plan, which is the set of rules that determines how numbers are interpreted and routed, is now your code. It's not a configuration option hidden in some carrier's backend that you never see. It's a function you wrote, in a file you can open, that does exactly what you tell it to do.
And that's both the burden and the opportunity. The burden is obvious: you have to write it. The opportunity is that you can implement behavior no traditional carrier would ever offer.
Give me an example.
Say you want star three thousand to route to a different destination depending on the time of day. Or depending on which of your Twilio numbers the caller dialed. Or you want it to play a custom message first, then connect. A traditional carrier gives you whatever routing they've pre-configured. You get zero control. With your own stack, star three thousand can do literally anything you can express in code.
And that's the reframe. It's not that you're losing the "it just works" behavior. It's that you're dismantling it and rebuilding it with knobs you can actually turn.
Let's get concrete about the architecture. If Daniel's building this for Israel, what does the inbound call flow actually look like?
Call arrives at his Twilio number. The webhook fires. His application receives the HTTP request. First thing it does is check the To parameter. Does it start with an asterisk or a hash?
Hash being the pound sign, for American listeners.
Right. If it starts with an asterisk or hash, it's a service code. Skip the Lookup API entirely, it won't help. Instead, look it up in the service code mapping table. If there's a match, route to the configured endpoint. If there's no match, play an error message or connect to a fallback.
And if it doesn't start with a special character?
Then it's probably a phone number. Pass it through the Lookup API. Get back the normalized E dot one six four version. Use that for routing. If the Lookup API returns an error, the number might be malformed. Play a message saying the number couldn't be recognized, or try a best-guess normalization using a library.
Which brings us to libraries. You mentioned something earlier about Google's phone number library.
libphonenumber. This is a library Google maintains that handles phone number parsing, validation, and formatting for basically every country on earth. It knows about leading zeros and trunk prefixes. It knows that in Israel you dial zero three for Tel Aviv but plus nine seven two three for international. It knows which area codes are valid in which countries. It's the closest thing to a universal dialing plan in a box.
And it's free and open source.
It is. There are ports in Java, C plus plus, JavaScript, Python. You can run it in your own application without making an external API call. It's faster than the Lookup API for simple normalization, and it works offline.
So why use the Lookup API at all?
Because libphonenumber tells you whether a number is validly formatted. It doesn't tell you whether the number is currently assigned to a real phone. The Lookup API can return carrier information, caller name in some cases, and most importantly, it can tell you the current line type. Is this a mobile number? A landline? A VoIP number? That's data libphonenumber doesn't have.
So the two tools solve different problems. libphonenumber handles formatting. The Lookup API handles validation against live data.
And for most normalization use cases, libphonenumber is enough. You're not trying to verify that the number exists. You're just trying to turn zero three one two three four five six seven into plus nine seven two three one two three four five six seven. libphonenumber does that in a single function call, no network request required.
Let's talk about what happens on the outbound side. Daniel's building a system where he can make calls through Twilio. What does he need to normalize on the way out?
Same thing, but the stakes are different. On an inbound call, if normalization fails, the call doesn't connect and the caller hears an error. Annoying, but survivable. On an outbound call, if you pass a malformed number to Twilio's API, the call attempt fails and you get charged for it anyway if you're using certain pricing models.
You want to catch formatting problems before they hit the API.
Your dialer interface should normalize on input. As the user types, or when they hit dial, run the number through libphonenumber. Show them the normalized version. If it can't be parsed, show an error before the call is even attempted. Don't waste an API call on a number that's obviously broken.
For the service codes on outbound?
If you're building a dialer that lets users type star codes, you need the same mapping table. But honestly, most outbound dialing from a custom stack is going to be standard phone numbers. The service code problem is primarily an inbound one.
Because the people calling you are the ones who might dial star three thousand.
Right. Your users, your callers, they're operating with the mental model of a traditional phone. They dial what they've always dialed. Your system has to meet them where they are.
This is where the "it just works" expectation collides with reality. Daniel's callers aren't going to change their behavior just because he moved to Twilio. They're going to dial zero three whatever, or star three thousand, and expect it to connect.
If it doesn't, they're not going to think "ah, Daniel must have migrated to a programmable telephony platform without implementing a full dialing plan normalization layer." They're going to think his phone system is broken.
Which it is, from their perspective.
Fair.
The normalization layer isn't optional. It's not a nice-to-have. It's the thing that makes your custom phone stack behave like a phone.
This is where I think a lot of people who start down the Twilio migration path get surprised. They think they're just changing providers. They don't realize they're signing up to reimplement a chunk of the public switched telephone network in their application code.
Is that hyperbole?
A little. You're not reimplementing the physical switching infrastructure. You're not running fiber. But you are reimplementing the logic layer. The part of the phone company that knows what to do when someone dials a number that doesn't look like plus something.
Let's distill this into some rules. For Daniel, and for anyone else thinking about this migration.
Rule one: assume nothing is normalized outside the United States. If you're building for any country other than the US, plan for a normalization step on every inbound and outbound call. No exceptions.
If you're building for the US, still plan for it, because the moment you have one international caller or one user dialing an international number, the US exception stops helping you.
Good point. Rule two: use the Lookup API for validation against live data, but don't rely on it for short codes or service numbers. Those need a custom mapping table in your application logic. The Lookup API validates phone numbers. Service codes are not phone numbers.
Rule three: consider using libphonenumber as the foundation of your normalization layer. It handles country-specific dialing rules, leading-zero stripping, and formatting edge cases that you would otherwise have to research and implement yourself. And it runs locally, no API call needed.
Rule four, which is more of a mindset shift: "it just works" is a feature you have to build, not a default you inherit. But that's the whole point of moving to Twilio. You're trading convenience for control, and the dialing plan is one of the first places you feel that trade.
I want to push on that trade a little. Because "trading convenience for control" makes it sound like you're losing something. But the convenience you're losing is the convenience of not having to think about how your phone system works. And for a lot of people, that convenience came with a lot of constraints they didn't choose.
That's fair. The traditional carrier's dialing plan is convenient, but it's also opaque and inflexible. You can't modify it. You can't add your own service codes. You can't route calls differently based on context. You get what the carrier gives you.
Whereas with your own stack, star three thousand can do anything. It can trigger a workflow. It can query a database. It can forward to a different number depending on who's calling. The carrier's version of star three thousand is a single destination hard-coded in their exchange. Your version is software.
That's the opportunity. The dialing plan becomes a programmable surface. It's not just a lookup table anymore. It's logic.
Before we wrap, there's a bigger question lurking here. As Twilio and similar platforms expand globally, do we see more country-specific normalization built in, or is the "bring your own dialing plan" model here to stay?
My guess is it's here to stay, at least at the API level. Twilio's value proposition is that it gives you primitives, not a finished product. Building country-specific normalization into the core routing layer would add complexity that most of their global customers don't need, and it would create expectations they can't consistently meet across two hundred countries.
What I think we might see is better tooling around the normalization problem. More SDKs that bundle libphonenumber. Pre-built functions in the Twilio console that handle common normalization patterns. Maybe a managed service that does for dialing plans what Twilio did for voice and messaging.
A dialing-plan-as-a-service.
Someone's going to build that. The demand is clearly there.
The other implication worth carrying forward is what this means for regulatory compliance and number portability. If the dialing plan is becoming a software artifact, who's responsible when it breaks? The developer who wrote the normalization function? The platform that routed the call?
Right now, the answer is the developer. Twilio's terms are pretty clear that you're responsible for the legality and correctness of your application. If your normalization layer routes emergency calls incorrectly, that's on you.
Which is why the emergency services question we covered previously is so tightly coupled to this one. The dialing plan and emergency routing are the same problem from different angles.
They're both about taking responsibility for logic that used to be someone else's job.
The single biggest misconception people have about this topic is that Twilio handles phone numbers like a carrier does. The reality is it only auto-normalizes US numbers. Everything else has to be pre-formatted to E dot one six four or normalized by the developer. And short codes like star three thousand aren't phone numbers at all, they're service codes that require custom routing logic.
The Lookup API, for all its usefulness, is not a magic bullet. It validates standard phone numbers. It won't help with service codes, and it's an explicit API call you have to build into your flow. It doesn't run automatically.
This whole migration Daniel's doing is a fascinating case study in what you actually inherit when you become your own carrier. It's not just about cheaper calls or more flexible routing. It's about discovering all the invisible machinery that was quietly running inside your old phone company and realizing that machinery is now yours to build, maintain, and improve.
If you do it right, you end up with something better than what you left behind. Not because it's easier, but because it's yours.
Our thanks as always to producer Hilbert Flumingtop for making this show happen.
This has been My Weird Prompts. If you've got a question like Daniel's, something that seems simple on the surface but unravels into a whole world of hidden complexity, send it our way. Email the show at show at my weird prompts dot com.
We'll be back soon with more from the telephony migration trenches.