Every Kenyan dev I know has the same folder on their laptop: a half-finished utils/ directory with validatePhone.ts, kraPin.ts, counties.json, and a M-PESA SMS parser that mostly works. We've all written it. We've all copied it into the next project. Then the regex breaks because Safaricom released a new prefix, and we patch it in one project and forget the other two.
I got tired of doing this. So I packaged it.
kenya-utils is a free, MIT-licensed TypeScript library with the Kenya-specific helpers you actually use: phones, KRA PINs, national IDs (including Maisha cards), the 47 counties, M-PESA, and shilling formatting. Zero dependencies. Works everywhere modern JavaScript runs.
npm install kenya-utils
This article walks through what's inside, where each piece is useful, and the design choices that should make it boring to adopt. Whether you're a student building your first form or a senior engineer shipping fintech at scale, there's something here that'll save you an afternoon.
The 30-second tour
import { parsePhone, isValidKraPin, findCounty, formatKes } from "kenya-utils";
parsePhone("0712345678");
// { e164: "+254712345678", network: "Safaricom", ... }
isValidKraPin("A123456789B"); // true
findCounty("nairobi");
// { code: 47, name: "Nairobi", capital: "Nairobi", ... }
formatKes(1234567); // "Ksh 1,234,567.00"
That's it. Four imports, four common problems solved.
You can also import only the module you need, so unused code doesn't ship to your bundle:
import { parsePhone } from "kenya-utils/phone";
import { findCounty } from "kenya-utils/counties";
Each module is a separate entry. If you only need county data, your final bundle won't carry the M-PESA parser.
Phones — the one everyone needs
Every signup form in Kenya needs phone validation. Every one of them gets it slightly wrong.
import { parsePhone, isValidKePhone, formatPhone, detectNetwork } from "kenya-utils/phone";
parsePhone("+254 712 345 678");
// {
// e164: "+254712345678",
// national: "0712345678",
// international: "+254 712 345 678",
// subscriber: "712345678",
// network: "Safaricom",
// countryCode: "254"
// }
isValidKePhone("0712345678"); // true
isValidKePhone("0202345678"); // false (landline)
formatPhone("0712345678"); // "+254712345678"
formatPhone("+254712345678", "national"); // "0712345678"
detectNetwork("0730123456"); // "Airtel"
detectNetwork("0747123456"); // "Faiba"
Things that took me longer than I'd like to admit to get right:
-
Accepting every format users actually type.
+254...,254...,0..., bare 9-digit subscriber numbers. Whitespace, hyphens, dots, and parens are all stripped. -
Returning
nullon garbage, not throwing. You can call it on any input withouttry/catchceremony. -
Network detection by prefix. Useful for UX hints ("we'll send a Safaricom STK push"). But — and this matters — Kenya has number portability. A
0722number could now be on Airtel. Treat detection as a hint, not a guarantee. The README is honest about this.
KRA PINs — for any system that touches tax
If you're building anything for businesses, payroll, invoicing, or e-citizen integrations, you'll need KRA PIN validation eventually.
import { isValidKraPin, parseKraPin, formatKraPin } from "kenya-utils/kra-pin";
isValidKraPin("A123456789B"); // true (Individual)
isValidKraPin("P987654321Z"); // true (Non-Individual)
isValidKraPin("a 123-456-789 b"); // true — case and whitespace tolerant
parseKraPin("A123456789B");
// {
// pin: "A123456789B",
// type: "Individual",
// prefix: "A",
// sequence: "123456789",
// checkChar: "B"
// }
A prefix = individuals. P = everything else (companies, partnerships, trusts, societies). The format is [A|P] + 9 digits + 1 letter. The lenient parsing (a 123-456-789 b works) matters because users will paste PINs from PDFs, emails, and screenshots, and they will have stray whitespace every time.
National IDs and Maisha cards
The old IDs are 7 digits. The newer ones are 8. The Maisha Namba uses the same numeric format, so the validator handles all three.
import {
isValidKeId,
parseKeId,
parseMaishaCard,
isMaishaCardExpired,
} from "kenya-utils/national-id";
isValidKeId("12345678"); // true
parseKeId("1234567"); // { id: "1234567", digits: 7, isLegacy: true }
parseMaishaCard({ id: "12345678", expiry: "2030-01-01" });
// { id: "12345678", expiryDate: ..., isExpired: false, daysUntilExpiry: 1234 }
isMaishaCardExpired("2020-01-01"); // true
The Maisha Card is different from the Maisha Namba — the namba is the number, the card is the physical credential with an expiry date. Most apps care about both, so both are covered.
Counties — all 47, properly structured
Building a county dropdown shouldn't require maintaining your own JSON. It always does.
import {
counties,
findCounty,
countiesByRegion,
findCountyBySubCounty,
} from "kenya-utils/counties";
counties.length; // 47
findCounty(47); // by code
findCounty("Mombasa"); // by name
findCounty("tana-river"); // by slug
countiesByRegion("Coast");
// [Mombasa, Kwale, Kilifi, Tana River, Lamu, Taita Taveta]
findCountyBySubCounty("Westlands")?.name; // "Nairobi"
Each county object looks like:
{
code: 47,
name: "Nairobi",
capital: "Nairobi",
slug: "nairobi",
region: "Nairobi",
subCounties: ["Westlands", "Dagoretti North", ...]
}
The slug field is what you want for URLs. The code is what you want when integrating with government APIs that use numeric codes. The subCounties list is what you want when a user picks a county and you need to narrow them to a constituency.
If you spot a sub-county that's missing or wrong, please PR it. Boundaries shift and I'd rather the data be correct than my pride preserved.
Currency — formatting and parsing shillings
import { formatKes, parseKes, toKesWords } from "kenya-utils/currency";
formatKes(1234567); // "Ksh 1,234,567.00"
formatKes(1234, { decimals: 0 }); // "Ksh 1,234"
formatKes(1234, { symbol: "KES" }); // "KES 1,234.00"
parseKes("Ksh 1,234,567.50"); // 1234567.5
parseKes("500 shillings"); // 500
toKesWords(1234.50);
// "one thousand two hundred thirty four shillings and fifty cents"
toKesWords is the one most people don't realize they need until they're building a cheque-printing module or a formal receipt. Locale-aware number-to-words is harder than it looks. This handles it for KES, including the "and X cents" tail.
M-PESA — paybill validation and SMS parsing
Two practical things: validate paybill/till numbers before submission, and parse the SMS confirmations Safaricom sends.
import { isValidPaybill, isValidTillNumber, parseMpesaSms } from "kenya-utils/mpesa";
isValidPaybill(123456); // true
const r = parseMpesaSms(
"QHJ7K8L9M0 Confirmed. Ksh1,000.00 sent to JOHN DOE 0712345678 on 9/5/26 at 10:30 AM. New M-PESA balance is Ksh5,000.00. Transaction cost, Ksh23.00.",
);
// {
// transactionId: "QHJ7K8L9M0",
// type: "sent",
// amount: 1000,
// party: "JOHN DOE",
// partyPhone: "0712345678",
// balance: 5000,
// transactionCost: 23,
// ...
// }
The parser handles the common formats: sent, received, paybill, buygoods, withdraw, airtime. Safaricom occasionally tweaks the wording, so it's regex-driven and forgiving. If a real SMS format breaks the parser, open an issue with a redacted sample — that's exactly the kind of contribution that makes the library better for everyone.
This is useful for:
- Personal finance apps that read SMS via the Android API.
- Reconciliation tools that need to match incoming payments to invoices.
- Bookkeeping bots that turn M-PESA receipts into ledger entries.
- Hackathon projects where you don't want to write a parser at 2 AM.
What's intentionally not here yet
Being honest matters in a launch post:
- Wards. There are 1,450+ wards across 290 constituencies. I want them sourced properly rather than typed from memory. Coming in v1.2.
- VAT and withholding tax helpers. Useful but I want to verify rates against a current KRA source. v1.3.
- More M-PESA edge cases — Pochi la Biashara, M-Shwari, KCB-MPESA. The big formats are covered; the long tail is incoming.
Bundle and runtime
- Zero runtime dependencies.
- ESM + CJS builds, both ship with TypeScript types.
-
sideEffects: falsefor tree-shaking. - Runs in Node 20+, modern browsers, Next.js (server and client components), Cloudflare Workers, Deno, and Bun.
If you import one module, you get that module's bytes. If you import nothing, you ship nothing. This sounds obvious but a surprising number of "utility" libraries don't bother.
FAQ
Will the network detection work for ported numbers?
It returns the original prefix-owner. Number portability means ~5% of detections will be technically wrong. Use it for UX hints, not for billing logic.
Does it work offline / on the edge?
Yes. No network calls, no DOM, no Node-only APIs. Cloudflare Workers, Vercel Edge, Deno Deploy, and React Native all work.
Can I use it without TypeScript?
Yes. Plain JS works the same. Types are bundled for editors that care.
What about KSH vs Ksh vs KES?
formatKes defaults to "Ksh" but accepts a symbol option for "KES", "KSh", or whatever your finance team prefers.
Is the M-PESA parser official?
No. It's a community-maintained regex against the SMS format Safaricom uses. The Daraja API is the right move for real integrations. The parser is for cases where you're reading the SMS after the fact (personal finance, reconciliation, etc.).
Why not split each module into its own package?
Because every Kenyan project ends up needing at least three of these, and seven kenya-* dependencies is worse than one with sub-imports. Tree-shaking handles the bundle-size concern.
Try it
npm install kenya-utils
# or
yarn add kenya-utils
# or
pnpm add kenya-utils
Collins87mbathi
/
kenya-utils
Kenya utilities for JavaScript & TypeScript — phone numbers, KRA PIN, national IDs (Maisha cards), the 47 counties, M-PESA, and shilling formatting. Zero deps, fully typed.
kenya-utils
Helpers for Kenya-specific data in JavaScript and TypeScript — phone numbers, KRA PINs, national IDs (including Maisha cards), the 47 counties, M-PESA, and shilling formatting.
I built this because I kept copying the same regexes between projects. If you've been doing the same, this is for you.
Install
npm install kenya-utils
yarn add kenya-utils
pnpm add kenya-utils
Works in Node 20+, browsers, Next.js (server and client), Cloudflare Workers, Deno, Bun. No dependencies.
A quick taste
import { parsePhone, isValidKraPin, findCounty, formatKes } from "kenya-utils";
parsePhone("0712345678");
// { e164: "+254712345678", network: "Safaricom", ... }
isValidKraPin("A123456789B"); // true
findCounty("nairobi");
// { code: 47, name: "Nairobi", capital: "Nairobi", ... }
formatKes(1234567); // "Ksh 1,234,567.00"
You can also import only the module you need so unused code doesn't ship with your bundle:
import…If you ship a Kenyan-market app this month, swap one of these in and see how it feels. If you find a phone prefix that's missing, an M-PESA SMS format that breaks the parser, or a sub-county that's wrong, open an issue or a PR. The library gets better with every report.
And if it saves you the 20 minutes you usually spend rewriting validatePhone.ts from scratch, that's the whole point.
Top comments (0)