africa-phone
v0.1.1
Published
Offline-first parsing, validation, normalization, and mobile network/carrier detection for African phone numbers.
Maintainers
Readme
africa-phone
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
portedNumberPossibleflag 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-phoneQuick 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 aliasdefaultCountry 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
isValidis 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 backisValid: truewithnetwork: 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.portedNumberPossibleistruefor 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.00as 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
lastVerifieddate. 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 atregisterCountry()time. CallregisterCountry()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 codeimports it), each gets its own independent registry, andregisterCountry()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.
