Storing a phone number so it still works in two years

Store one canonical column in E.164, as a string, and normalise at the edge rather than at the point of sending. Every messaging system that has a "why did this fail" problem has a phone number problem underneath it.

The four ways a number gets destroyed

  1. Stored as an integer. 0712345678 becomes 712345678. The leading zero is not a formatting detail, it is part of the national number.
  2. Excel. A CSV opened and saved in a spreadsheet turns long numbers into scientific notation and strips leading zeros. This destroys more contact lists than any bug.
  3. Local format with no country. 0712345678 is unambiguous only if you know the country, and the day you add a second country it is unambiguous nowhere.
  4. Normalised at send time, not at write time. Now the same person exists three times, and none of the rows know about each other.

Normalise once, at the edge

# Every entry point does this: the form, the CSV import, the API,
# the webhook. Not the send path - by then it is too late to
# deduplicate against what you already have.
def to_e164(raw: str, default_country: str) -> str:
    digits = re.sub(r"[^\d+]", "", raw)
    if digits.startswith("+"):
        return digits
    if digits.startswith("00"):
        return "+" + digits[2:]
    if digits.startswith("0"):
        return default_country + digits[1:]
    return default_country + digits

The default_country argument is not optional and must not have a default. A function that silently assumes a country is a function that will one day put a Kenyan prefix on a Ugandan number, and the message will go to a real person who is not your customer.

Keep what they typed

Two columns: the canonical E.164 you send to, and the raw string the person entered. When a number turns out to be wrong, the raw value is the only evidence of whether it was mistyped or mis-normalised, and those have completely different fixes.

Validate what you can, not what you cannot

  • Length and prefix against the country's plan catches most typos.
  • Whether it is on WhatsApp is a separate question with its own API, and a number can be valid and not on WhatsApp.
  • Whether it belongs to your customer is not checkable at all without sending something to it, which is what a confirmation message is for.

Uniqueness, and the trap in it

A unique index on the canonical number is right for contacts and wrong for people. Numbers are recycled, shared between family members, and reassigned by carriers. Treat the number as a route to a person, not as their identity - which is also why handing back an old customer's history on a recycled number is a real risk.

Questions people actually ask

What is E.164?

The international format: a plus, a country code and the national number, with no spaces, dashes or brackets. +254712345678. It is the only format that is unambiguous everywhere.

Should I store the number as a string or a number?

A string, always. Leading zeros are meaningful, numbers get longer than an integer column expects, and nobody ever does arithmetic on a phone number.

Njiwa

One inbox for WhatsApp, Facebook and Instagram, and an API that sends over either WhatsApp rail. 7 days free, no card.

Start a trial See pricing