npm package discovery and stats viewer.

Discover Tips

  • General search

    [free text search, go nuts!]

  • Package details

    pkg:[package-name]

  • User packages

    @[username]

Sponsor

Optimize Toolset

I’ve always been into building performant and accessible sites, but lately I’ve been taking it extremely seriously. So much so that I’ve been building a tool to help me optimize and monitor the sites that I build to make sure that I’m making an attempt to offer the best experience to those who visit them. If you’re into performant, accessible and SEO friendly sites, you might like it too! You can check it out at Optimize Toolset.

About

Hi, 👋, I’m Ryan Hefner  and I built this site for me, and you! The goal of this site was to provide an easy way for me to check the stats on my npm packages, both for prioritizing issues and updates, and to give me a little kick in the pants to keep up on stuff.

As I was building it, I realized that I was actually using the tool to build the tool, and figured I might as well put this out there and hopefully others will find it to be a fast and useful way to search and browse npm packages as I have.

If you’re interested in other things I’m working on, follow me on Twitter or check out the open source projects I’ve been publishing on GitHub.

I am also working on a Twitter bot for this site to tweet the most popular, newest, random packages from npm. Please follow that account now and it will start sending out packages soon–ish.

Open Software & Tools

This site wouldn’t be possible without the immense generosity and tireless efforts from the people who make contributions to the world and share their work via open source initiatives. Thank you 🙏

© 2026 – Pkg Stats / Ryan Hefner

africa-phone

v0.1.1

Published

Offline-first parsing, validation, normalization, and mobile network/carrier detection for African phone numbers.

Readme

africa-phone

CI npm version license

African-first phone number parsing, validation, normalization, and mobile network/carrier detection — 100% offline, zero runtime dependencies.

import { parsePhone } from 'africa-phone';

parsePhone('08031234567', 'NG');
// {
//   input: '08031234567', iso2: 'NG', countryCallingCode: '234',
//   nationalNumber: '8031234567', e164: '+2348031234567',
//   national: '0803 123 4567', international: '+234 803 123 4567',
//   isPossible: true, isValid: true, validationIssue: 'NONE',
//   network: {
//     operatorId: 'ng-mtn', name: 'MTN Nigeria', brand: 'MTN',
//     mobileMoney: 'MTN MoMo', ussd: { balance: '*556#', buyAirtime: '*312#', data: '*131#' },
//     portedNumberPossible: true,
//   },
// }

Why not libphonenumber-js?

General-purpose libraries validate length and formatting for every country on Earth, but treat African numbers as an afterthought — none of them tell you the carrier, whether a mobile money product exists on that network, or which USSD code checks the balance. africa-phone is built the other way around: a small set of African countries, covered deeply.

  • Carrier/network detection — MTN, Airtel, Safaricom, Vodacom, and more, from the number prefix.
  • Mobile money awareness — M-Pesa, MTN MoMo, Airtel Money, Orange Money, T-Kash, telebirr, and others, mapped per operator.
  • Extensible USSD codes — balance-check and other shortcodes per operator.
  • Honest about number portability — where a country has mandated mobile number portability (Nigeria, Kenya, and others), results carry a portedNumberPossible flag instead of silently pretending prefix-based carrier detection is ground truth.
  • Fully offline — no network calls, works identically in Node and the browser.

Install

npm install africa-phone

Quick start

import { parsePhone, isValidPhone, getNetwork, normalize, getPhoneInfo } from 'africa-phone';

parsePhone('+254712345678');
// iso2: 'KE', network.brand: 'Safaricom', network.mobileMoney: 'M-Pesa', ...

isValidPhone('0803 123 4567', 'NG');
// true

getNetwork('0712345678', 'KE');
// { operatorId: 'ke-safaricom', name: 'Safaricom', brand: 'Safaricom',
//   mobileMoney: 'M-Pesa', ussd: { balance: '*144#', mpesaMenu: '*334#', data: '*544#' },
//   portedNumberPossible: true }

normalize('0803-123-4567', 'international', 'NG');
// '+234 803 123 4567'

getPhoneInfo('0712345678', 'KE');
// same shape as parsePhone() — "return everything" convenience alias

defaultCountry is only needed for numbers with no + or 00 international prefix (a bare local number). International-form input (+234..., 00234...) is detected automatically and defaultCountry is ignored.

Supported countries (launch set)

Every operator's prefixes carry a source URL and lastVerified date in the data files (src/countries/*.ts) — see CONTRIBUTING.md for the full research notes, including what was deliberately left out.

| Country | ISO2 | Calling code | Operators | Mobile money | | ------------ | ---- | ------------ | ------------------------------------------- | ------------------------------------------------- | | Nigeria | NG | +234 | MTN, Airtel, Glo, 9mobile | MTN MoMo, Airtel Money | | Kenya | KE | +254 | Safaricom, Airtel, Telkom, Equitel | M-Pesa, Airtel Money, T-Kash, Equitel Money | | Ghana | GH | +233 | MTN, Vodafone, AirtelTigo, Expresso | MTN MoMo, Vodafone Cash, AirtelTigo Money | | South Africa | ZA | +27 | Vodacom, MTN, Cell C, Telkom | MTN MoMo | | Tanzania | TZ | +255 | Vodacom, Tigo, Airtel, Halotel, TTCL, Smile | M-Pesa, Tigo Pesa, Airtel Money | | Rwanda | RW | +250 | MTN, Airtel | MTN MoMo, Airtel Money | | Ethiopia | ET | +251 | Ethio Telecom, Safaricom | telebirr, M-Pesa | | Senegal | SN | +221 | Orange, Free, Expresso | Orange Money, Free Money | | Ivory Coast | CI | +225 | Orange, MTN, Moov | Orange Money, MTN MoMo, Moov Money | | Egypt | EG | +20 | Vodafone, Etisalat, Orange, WE | Vodafone Cash, Etisalat Cash, Orange Cash, WE Pay | | Algeria | DZ | +213 | Ooredoo, Mobilis, Djezzy | not verified — none listed | | Tunisia | TN | +216 | Ooredoo, Orange, Tunisie Telecom, Elissa | Mobicash, TelecoMoney | | Gambia | GM | +220 | Africell, QCell, Comium | Afrimoney, QMoney |

Gambia is mid-migration from 7-digit to 9-digit numbers (PURA, effective 2026-09-04, old format stops working 2026-11-30). This package models the new 9-digit format only (87/83/86 prefixes) — a 7-digit input reports TOO_SHORT during the transition window. See CONTRIBUTING.md.

Not included in this release: Uganda and Cameroon were researched but held back — Uganda's operator/prefix sources conflicted (including a stale claim about an operator that exited the market in 2021), and Cameroon's per-operator prefix breakdown couldn't be confirmed from any source checked. Morocco has detailed prefix data from a secondary source that hasn't been cross-checked against the official regulator yet. Gambia's Gamcel is left out — PURA's migration notice names only Africell, QCell, and Comium as compliant operators, and Gamcel had not been assigned a new prefix as of this writing. See CONTRIBUTING.md.

Call getSupportedCountries() at runtime for the full, structured list (including operator prefixes and USSD codes).

More countries welcome — see CONTRIBUTING.md.

Tree-shaking / selective imports

The default africa-phone entry auto-registers all 13 countries — simplest to use, and small enough in practice that most consumers don't need to think about it. If you only ship to 1–2 countries and want a bundler to drop the rest entirely, use the /core entry plus per-country subpaths:

import { registerCountry, parsePhone } from 'africa-phone/core';
import ng from 'africa-phone/countries/ng';

registerCountry(ng);
parsePhone('08031234567', 'NG');

You can also register your own custom country the same way — see src/countries/_template.ts for the shape.

API reference

function parsePhone(number: string, defaultCountry?: string): PhoneInfo;
function getPhoneInfo(number: string, defaultCountry?: string): PhoneInfo; // alias of parsePhone
function isValidPhone(number: string, defaultCountry?: string): boolean;
function getNetwork(number: string, defaultCountry?: string): NetworkInfo | null;
function normalize(
  number: string,
  format?: 'e164' | 'national' | 'international', // default: 'e164'
  defaultCountry?: string,
): string | null;
function getSupportedCountries(): CountryDefinition[];
function registerCountry(definition: CountryDefinition, options?: { override?: boolean }): void;

PhoneInfo — the "everything" object:

interface PhoneInfo {
  input: string;
  iso2: string | null;
  countryCallingCode: string | null;
  nationalNumber: string | null;
  e164: string | null;
  national: string | null;
  international: string | null;
  isPossible: boolean; // digits-only, correct length for the detected country
  isValid: boolean; // structural validity (see caveats below)
  validationIssue: 'NONE' | 'COUNTRY_UNKNOWN' | 'TOO_SHORT' | 'TOO_LONG' | 'INVALID_CHARACTERS';
  network: NetworkInfo | null;
}

interface NetworkInfo {
  operatorId: string;
  name: string;
  brand: string;
  mobileMoney: string | null;
  ussd: Record<string, string> | null;
  portedNumberPossible: boolean;
}

Every function is non-throwing except registerCountry, which throws on malformed country data — that's a setup-time developer error, not a parse of untrusted user input.

Caveats — read this before relying on carrier detection

  • isValid is structural only (correct length, digits-only). It does not mean the prefix is a recognized operator block — a structurally valid number with an unrecognized prefix still comes back isValid: true with network: null. This is intentional: it means a future patch release that adds newly-allocated prefixes can never silently flip a previously- invalid number to valid.
  • network.portedNumberPossible is true for countries with mandated mobile number portability (Nigeria, Kenya, and others). Operator detection is a heuristic based on original numbering-plan allocation — it is not a live lookup and does not reflect a number that has since been ported to a different network.
  • 00 as an international dialing prefix is treated as equivalent to +, which is a common but not universal convention — documented here so it isn't a silent surprise.
  • Data freshness: each country carries a lastVerified date. Regulators periodically reallocate prefix blocks; see CONTRIBUTING.md if you spot stale or incorrect data.
  • getSupportedCountries() returns a snapshot, not a live view. The country/operator objects it returns are deep copies — mutating one (e.g. pushing a new prefix onto an operator) has no effect on parsing or network detection, since the actual lookup index was built once at registerCountry() time. Call registerCountry() again (with { override: true }) to change a country's data instead.
  • Dual ESM/CJS "package hazard": this package keeps its registered-country state in one module-level singleton. If a single process somehow loads both the ESM build and the CJS build of the same installed copy (rare — typically only if one dependency require()s africa-phone while your app code imports it), each gets its own independent registry, and registerCountry() calls made through one won't be visible through the other. This is a general limitation of stateful dual-format packages, not specific to africa-phone.

Contributing

New countries, corrected prefixes, and additional USSD codes are welcome — see CONTRIBUTING.md for the data-authoring guide and VERSIONING.md for how changes map to semver.

License

MIT — see LICENSE.