Supporting International Phone Numbers at Toast

Mar 17, 2025

Supporting international phone numbers at Toast

Toast initially developed its platform with a focus on the U.S. market, capturing  phone numbers either as national numbers or with the country code +1 from user interfaces or external apis. As we expanded internationally, this approach presented significant challenges. International phone number formats are complex and vary significantly between countries. Our approach, which worked well for NANP(North American Numbering Plan) numbers, became difficult to adapt to more complex global formats. 

In this post, we’ll discuss how we are currently addressing these issues across our products. We are normalizing phone numbers, adopting international standards like E.164, and effectively handling legacy U.S. numbers leveraging tools like Google’s libphonenumber. 

The Problem

Given how ubiquitous phone numbers are and how long they've been around, some people use spaces, others use dashes or parentheses, and the way digits are grouped varies from person to person. Consider the following example

U.S. Numbers: A phone number in the U.S. can be written in several different ways:

  • (341) 678-XXXX

  • 341-678-XXXX

  • +1-341-678-XXXX

These different representations, for the same number, can cause inconsistencies making it difficult to store, search, and validate phone numbers. For instance, searching for 341-678-XXXX might not match if the number was stored as +1-341-678-XXXX, creating challenges in retrieving the correct records.

To address this, we initially focused on normalizing phone numbers. This process involved cleaning up the input by removing all non-numeric characters like parentheses, spaces, and dashes. Additionally, we stripped the +1 country code to store phone numbers in a consistent national format. For example, the following variations were all standardized to the same format:

  • (341) 678-XXXX       →  341678XXXX

  • +1-341-678-XXXX   →  341678XXXX

  • 341 678 XXXX          →   341678XXXX

By removing non-digit characters and the country code +1 when present, we ensured that U.S. numbers were stored consistently, making searches and matching easier across different formats.

However, as we expanded into international markets such as the UK and Ireland, it became clear that this approach wouldn’t work globally. Regional formats introduced further complexity. Consider these examples:

UK Numbers: In the UK, a local phone number might look like:

  • 0751 425 XXXX

  • 751 425 XXXX

  • +44 751 425 XXXX

Irish Numbers: In Ireland, a phone number might be written as:

  • 089 495 XXXX

  • 89495XXXX

  • +353 89 495 XXXX

  • 01 - 8682XXX (local household/premise number)

Additionally, some users might enter numbers using the international access code “00” instead of the “+” sign, resulting in formats like 00353 894 958 XXX. These variations demonstrated that a simple approach of normalizing and removing +1 was no longer sufficient.

Our Solution

We recognized the need to adopt a globally recognized format. The E.164 standard, which defines the international public telecommunication numbering plan, allows us to store phone numbers in a consistent, standardized way. This format includes the country code and supports up to 15 digits, making it ideal for global use.

However, integrating E.164 into our existing systems wasn't straightforward. Legacy products at Toast had been designed with U.S. phone numbers in mind, limiting storage to 10 digits. Transitioning directly to E.164 would have led to inconsistent data across old and new entries. For example, a number like +35387114XXXX from Ireland would exceed the database's 10-digit limit, which was designed for U.S. numbers like 341678XXXX.

Database changes: Country Code field

To resolve this, we introduced a new field, `countryCode`, in our database. This field, while representational in the context of this discussion, is used to store a country's dialing code separately from the phone number itself. For instance:

  • Phone Number: 89495XXXX

  • Country Code: 353

It's important to note that this is a conceptual example and does not reflect the actual implementation in our model. In reality, these fields are tokenized and stored more securely.

By separating the country code from the phone number, we retained the flexibility to handle both legacy U.S. phone numbers and new international ones. This strategy ensured that we didn’t have to retrofit older systems while still meeting the needs of our expanding global operations.

Service Layer

Normalizing phone numbers at the database level was only part of the solution. Equally critical was implementing strong input validation and sanitization mechanisms across multiple layers - frontend, backend and database. At the service layer, where numbers are processed from various sources like user interfaces and external APIs, we ensured that inputs were both sanitized and validated to prevent security vulnerabilities. We leveraged Google’s libphonenumber library to parse and format phone numbers into the E.164 format, ensuring consistency before storage.

We first derive the region code based on the country code and then parsed the number to get the E.164 format:

// Example: Phone number from Ireland
val nationalNumber = "087114XXXX"
val phoneCountryCode = 353
val region = phoneUtil.getRegionCodeForCountryCode(phoneCountryCode) // region = IE
val phoneNumber = phoneUtil.parse(nationalNumber, region)
phoneUtil.format(phoneNumber, PhoneNumberUtil.PhoneNumberFormat.E164) // +35387114XXXX

Next, we extracted the components:

val countryCode = phoneNumberE164.getCountryCode() // 353
val nationalNumber = phoneNumberE164.getNationalNumber() // 87114XXXX

Handling national/E164 format inputs and legacy US numbers

Given that phone numbers were captured from a variety of sources, not all inputs were in national number format. For instance, some systems or APIs supplied phone numbers directly in E.164 format, such as a U.S. number: +1-341-678-XXXX. This introduced complexity, as our system needed to handle both formats seamlessly.

Also we had to ensure that our legacy U.S. phone numbers remained functional and accessible. U.S. numbers had traditionally been stored in formats that did not align with the new E.164 standard, without an accompanying country code.

We addressed this issue by implementing a strategy that attempts to parse the input phone number using both the national number and the country code. If this approach failed (i.e., an invalid number or format), we fell back to parsing the number directly in its E.164 format. If both attempts failed, the number was considered invalid.

Here’s an example implementation of how we handled this at the service layer:

// phoneNumber = "+1212555XXXX", countryCode = ""
fun formatToE164(phoneNumber: String, countryCode: String): String {
    return try {
        // Try to parse as nationalNumber & countryCode and return in E.164 format
        val region = phoneUtil.getRegionCodeForCountryCode(phoneCountryCode)
        val phoneNumber = phoneUtil.parse(nationalNumber, region)
        phoneUtil.format(phoneNumber, PhoneNumberUtil.PhoneNumberFormat.E164)
    } catch (e: IllegalArgumentException) {
        try {
             // Try to parse as E.164 format and return
             val phoneNumber = phoneUtil.parse(phoneNumber)
             phoneUtil.format(phoneNumber, PhoneNumberUtil.PhoneNumberFormat.E164)
        } catch (e: IllegalArgumentException) {
             // If both fail, throw error
             ...
        }
    }
}

This method established a strong fallback mechanism for managing various phone number formats, accommodating both national formats and direct E.164 entries. It also addressed legacy U.S. numbers that lacked a saved country code. By consistently validating and formatting phone numbers at the service layer, we ensured that all numbers in our system conformed to a global standard. This approach simplified downstream operations, including searching, matching, and retrieving phone numbers.

Validation Strategies 

Given the importance of phone numbers for communication, we implemented different validation strategies based on the context in which the phone numbers were used. For scenarios where phone numbers were critical (e.g., for sending transactional messages), we applied stricter validation criteria to ensure correctness. The libphonenumber library played a key role here as well, validating not just the format but also the country-region mappings of phone numbers.

For instance, if we received a national number for the IE along with a country code for US, our stricter validation strategy would look something like: 

// example nationaNumber for IE and countryCode for US
val phoneNumber = phoneUtil.parse(nationalNumber, region)
phoneUtil.isValidNumber(phoneNumber)
// Throws a validation error as this number format doesn't match any valid US phone number

API changes

To address the challenge of modeling international phone numbers in our existing API while maintaining backward compatibility, we made an update to the POST /orders endpoint in the ToastOrders API. We needed to ensure that new dialing codes were treated correctly as distinct types while still accommodating legacy data formats.

We introduced an optional field for countryCode. This change allowed us to support international phone numbers without disrupting existing integrations. Below is an abstract  representation of the updated API schema:

openapi: 3.0.0

components:
  schemas:
    Customer:
      type: object
      required:
 - guid
 - firstName
 - lastName 
        - phone
      properties:
	 ...
        phone:
          type: string
          description: The national significant number (excluding the country code).
          example: "555123XXXX"
        phoneCountryCode:
          type: string
          description: The E.164 country code of the phone number without the plus symbol. This field is optional.
          example: "1"

Frontend Changes

Ideally, we would have updated all user interfaces across our products to prompt users to input phone numbers in a standardized format, including the country code. However, given the variety of products and technologies in use, this approach would have introduced significant delays.

To address this efficiently, we adopted a hybrid approach. If the countryCode wasn’t provided by the frontend, we automatically assigned the country code based on the restaurant’s location. This method covered about 90% of cases, excluding scenarios where tourists might enter phone numbers from different countries. This solution allowed us to manage most cases without the need for an immediate overhaul of the user interface.

fun parse(phoneNumber: String, countryCode: String): String {
val dialingCode = if (countryCode.isNullOrBlank()) {
    getDialingCodeFromRestaurantCountry()
} else {
    countryCode
}

val region = phoneUtil.getRegionCodeForCountryCode(dialingCode)
val phoneNumber = phoneUtil.parse(nationalNumber, region)
return phoneUtil.format(phoneNumber, PhoneNumberUtil.PhoneNumberFormat.E164)
}

Simultaneously, we began updating the UI across our products to include a country code dropdown, allowing users to select their country and automatically apply the correct code. This information was then passed through our APIs and validated on the backend.

Phone normalized at the UI to support international numbers

Final Thoughts

At Toast, we recognize that transitioning to a standardized method for managing international phone numbers is crucial for our operations. By adopting the E.164 format and strategically updating our database and API, we have developed a robust system that efficiently accommodates a variety of phone number formats while ensuring seamless compatibility with our legacy systems.

Our hybrid approach, along with validation strategies driven by Google’s libphonenumber library, has greatly enhanced our ability to accurately capture, store, and retrieve phone numbers. This advancement not only improves the user experience but also streamlines communication across different regions. Our dedication to supporting a multi-regional customer base fuels our continuous innovation and adaptation at Toast.

____________________________

This content is for informational purposes only and not as a binding commitment. Please do not rely on this information in making any purchasing or investment decisions. The development, release and timing of any products, features or functionality remain at the sole discretion of Toast, and are subject to change. Toast assumes no obligation to update any forward-looking statements contained in this document as a result of new information, future events or otherwise. Because roadmap items can change at any time, make your purchasing decisions based on currently available goods, services, and technology.  Toast does not warrant the accuracy or completeness of any information, text, graphics, links, or other items contained within this content.  Toast does not guarantee you will achieve any specific results if you follow any advice herein. It may be advisable for you to consult with a professional such as a lawyer, accountant, or business advisor for advice specific to your situation.