The Standard Utility Toolkit for Pakistani Developers
Production-ready TypeScript utilities for CNIC validation, phone parsing, WhatsApp links, IBAN validation, postal lookup, network detection, geographic lookup, and PKR formatting.
Try PakDataKit in your browser, no installation needed:
π pakdatakit-playground.vercel.app
Validate CNICs, analyze phone numbers, detect networks, build WhatsApp links, check IBANs and banks, look up postal codes, cities and provinces, format PKR amounts, and try the text and Hijri utilities. Use it to preview outputs before you integrate the library.
| Package | Install | For |
|---|---|---|
pakdatakit |
npm install pakdatakit |
JavaScript and TypeScript (Node, browsers, Bun, Deno) |
pakdatakit (Python) |
pip install pakdatakit |
Python 3.9+, same functions in snake_case, same data |
pakdatakit-react |
npm install pakdatakit-react |
React inputs that format as you type, and a Raast QR component |
The Python package is checked against the TypeScript library with over 12,000 shared test cases, so both give the same results.
Every Pakistani application eventually needs to solve the same problems:
Most teams implement these repeatedly.
PakDataKit provides a single, typed, reusable solution so you can focus on building your product instead of rebuilding common utilities.
analyzePhone)npm install pakdatakit
Requirements:
import and require()Import everything from pakdatakit, or a single module to keep bundles small:
import { validateCNIC } from 'pakdatakit/cnic';
const { analyzePhone } = require('pakdatakit/phone');
Subpaths: address, cnic, format, geo, hijri, holidays, iban, input, networks, numbers, phone, postal, raast, tax, text, whatsapp, words, plus the form adapters zod, valibot and yup.
import {
validateCNIC,
analyzePhone,
createWhatsAppLink,
validateIBAN,
getBankFromIBAN,
getPostalCode,
getCityByPostalCode,
getNetwork,
formatLakh,
} from 'pakdatakit';
validateCNIC('35202-1234567-1');
// true
analyzePhone('03001234567');
// { valid: true, formatted: "+923001234567", network: "Jazz" }
createWhatsAppLink('03001234567', 'Assalamualaikum');
// "https://wa.me/923001234567?text=Assalamualaikum"
validateIBAN('PK36SCBL0000001123456702');
// true
getBankFromIBAN('PK36SCBL0000001123456702');
// "Standard Chartered Bank (Pakistan) Limited"
getPostalCode('Multan');
// "60000"
getCityByPostalCode('60000');
// "Multan"
getNetwork('03001234567');
// "Jazz"
formatLakh(2500000);
// "25 Lakh"
npx pakdatakit validate-cnic 35202-1234567-1
npx pakdatakit analyze-phone 03001234567
npx pakdatakit parse-address "House 12, Gulberg III, Lahore, Punjab 54660"
npx pakdatakit hijri
npx pakdatakit normalize-urdu "ap kia ker rhe ho"
The CLI exits with status 0 when the input is valid or found, 1 when it is invalid or not found, and 2 for usage errors, so it can be used in scripts. Run npx pakdatakit --help for all commands.
Validates Pakistani CNIC numbers.
validateCNIC(cnic: string): boolean
Examples:
validateCNIC('3520212345671');
// true
validateCNIC('35202-1234567-1');
// true
validateCNIC('35202-123456-1');
// false
Supports:
Separators must sit between the 5-7-1 digit groups, and the first digit must be a province code (1β8). NADRA does not publish a check-digit algorithm, so this checks structure only.
Reads the province of registration and gender encoded in a CNIC.
getCNICInfo(cnic: string): CNICInfo | null
getCNICInfo('35202-1234567-1');
// { digits: "3520212345671", formatted: "35202-1234567-1", province: "Punjab", gender: "male" }
getCNICInfo('invalid');
// null
The first digit is the province where the CNIC was first registered, which is not necessarily where the holder lives now. The last digit is odd for male and even for female.
Validates Pakistani mobile numbers. Accepts 03β¦, +92β¦, 0092β¦, +92 (0) 3β¦ and short 3β¦ forms with spaces, dashes, dots or Urdu digits. The 03XX prefix must belong to a mobile operator.
validatePhone(phone: string): boolean
Examples:
validatePhone('03001234567');
// true
validatePhone('+923001234567');
// true
validatePhone('3001234567');
// true
Converts valid Pakistani mobile numbers to E.164 format.
formatPhone(phone: string): string | null
Examples:
formatPhone('03001234567');
// "+923001234567"
formatPhone('+92 300-123-4567');
// "+923001234567"
formatPhone('03601234567');
// null (unallocated prefix)
Runs validation, formatting, and network detection in one call.
analyzePhone(phone: string): PhoneAnalysis
Returns:
{
valid: boolean;
formatted: string | null;
network: 'Jazz' | 'Zong' | 'Telenor' | 'Ufone' | 'SCO' | 'Unknown';
}
Examples:
analyzePhone('03001234567');
// { valid: true, formatted: "+923001234567", network: "Jazz" }
analyzePhone('invalid');
// { valid: false, formatted: null, network: "Unknown" }
Generates a WhatsApp click-to-chat link for Pakistani mobile numbers.
createWhatsAppLink(phone: string, message?: string): string | null
Examples:
createWhatsAppLink('03001234567');
// "https://wa.me/923001234567"
createWhatsAppLink('03001234567', 'Assalamualaikum');
// "https://wa.me/923001234567?text=Assalamualaikum"
Returns null for invalid phone numbers.
Validates Pakistani IBANs using format and ISO 13616 mod-97 checksum.
validateIBAN(iban: string): boolean
Examples:
validateIBAN('PK36SCBL0000001123456702');
// true
validateIBAN('PK37SCBL0000001123456702');
// false
Returns the bank name from a valid Pakistani IBAN's 4-letter bank code.
getBankFromIBAN(iban: string): string | null
Examples:
getBankFromIBAN('PK36SCBL0000001123456702');
// "Standard Chartered Bank (Pakistan) Limited"
getBankFromIBAN('invalid');
// null
Supported bank codes include HABB, MEZN, SCBL, UNIL, BAHL, NBPA, MUCB, JSBL, SADA (SadaPay), NAYA (NayaPay), RQMI (Raqami), MSHQ (Mashreq), and 48 total entries in the dataset, checked against real IBANs (see DATA.md). Merged or renamed banks are labelled with their current name, e.g. SAUD β "Silkbank Limited (merged into United Bank Limited)".
Returns the city for a Pakistani postal code.
getCityByPostalCode(postalCode: string): string | null
Examples:
getCityByPostalCode('60000');
// "Multan"
getCityByPostalCode('99999');
// null
Returns the primary postal code for a Pakistani city (case-insensitive).
getPostalCode(city: string): string | null
Examples:
getPostalCode('Multan');
// "60000"
getPostalCode('karachi');
// "74200"
Detects network using the original mobile prefix.
getNetwork(phone: string): NetworkName
Returns:
Examples:
getNetwork('03001234567');
// Jazz
getNetwork('03151234567');
// Zong
getNetwork('03331234567');
// Ufone
getNetwork('03451234567');
// Telenor
Note: This is the operator the prefix was originally allocated to. With Mobile Number Portability (MNP), and Telenor Pakistan's 2026 merger into PTML (Ufone), the current carrier may differ.
Identifies any Pakistani number: mobile, landline, UAN (111), toll-free (0800), premium-rate (0900) or personal (0122). Uses the same number patterns as Google's libphonenumber, plus PTCL's area-code list.
analyzeNumber('021-111-825-888');
// { type: 'uan', formatted: '021-111-825-888', e164: '+9221111825888', areaCode: '021', cities: ['Karachi'] }
analyzeNumber('042 35761234');
// { type: 'landline', formatted: '042-35761234', e164: '+924235761234', areaCode: '042', cities: ['Lahore'] }
getNumberType('0800-12345'); // 'toll-free'
validateLandline('051-9260138'); // true
getAreaCode('Mardan'); // '0937'
Returns province for a city.
getProvince(city: string): string | null
Examples:
getProvince('Multan');
// Punjab
getProvince('Karachi');
// Sindh
getProvince('Unknown');
// null
Case-insensitive city search.
searchCities(query: string): string[]
Examples:
searchCities('mul');
// ["Multan"]
searchCities('kar');
// ["Karachi"]
District-level geography lookup across 177 districts, including the 2026 Balochistan and Khyber Pakhtunkhwa changes. Former names such as Quetta and South Waziristan still resolve.
getDistrictProvince('Lahore'); // "Punjab"
searchDistricts('karachi'); // ["Karachi Central", "Karachi East", ...]
Parses free-form Pakistani addresses into structured components.
parseAddress('House 12, Gulberg III, Lahore, Punjab 54660');
// { street: "House 12", area: "Gulberg III", city: "Lahore", province: "Punjab", postalCode: "54660", warnings: [], ... }
parseAddress('12 Model Town Lahore');
// { street: "12 Model Town", city: "Lahore", province: "Punjab", ... }
parseAddress('House 1, Lahore, Sindh').warnings;
// ["Lahore is in Punjab, but the address says Sindh"]
House, plot, street and PO box numbers are not mistaken for postal codes.
Formats structured components back into a mailing address string. Only the components you pass are included.
Normalizes informal Roman Urdu spellings (ap kia ker β aap kya kar).
Convert between English, Urdu, and Arabic-Indic digits.
formatHijri(new Date(2025, 5, 7)); // "10 Dhu al-Hijjah 1446"
isRamadan(new Date()); // true | false
// Shift by whole days to match local moon sighting
formatHijri(new Date(2025, 5, 7), { offset: -1 }); // "9 Dhu al-Hijjah 1446"
Uses the tabular ("islamic-civil") calendar. Dates announced in Pakistan follow moon sighting and can differ by a day; use offset to adjust. Invalid dates throw a RangeError.
Formats PKR amounts with separators, rounded to whole rupees. Negative amounts keep their sign; NaN and Infinity throw a RangeError.
formatPKR(amount: number): string
Examples:
formatPKR(150000);
// "Rs. 150,000"
formatPKR(0);
// "Rs. 0"
formatPKR(-1500);
// "-Rs. 1,500"
Formats values using Pakistan's Lakh / Crore numbering system.
formatLakh(amount: number): string
Examples:
formatLakh(2500000);
// "25 Lakh"
formatLakh(10000000);
// "1 Crore"
formatLakh(50000);
// "Rs. 50,000"
Writes numbers and rupee amounts in words using the Pakistani scale (hundred, thousand, lakh, crore, arab, kharab), in English or Urdu. Useful for cheques, invoices and receipts.
amountInWords(amount: number, options?: { language?: "en" | "ur"; cheque?: boolean }): string
numberToWords(n: number, options?: { language?: "en" | "ur" }): string
amountInWords(125000.5);
// "one lakh twenty-five thousand rupees and fifty paisa"
amountInWords(125000, { cheque: true });
// "Rupees One Lakh Twenty-Five Thousand Only"
amountInWords(125000, { language: 'ur' });
// "Ψ§ΫΪ© ΩΨ§Ϊ©ΪΎ ΩΎΪΫΨ³ ΫΨ²Ψ§Ψ± Ψ±ΩΩΎΫ"
amountInWords(500.5, { language: 'ur', cheque: true });
// "ΩΎΨ§ΩΪ Ψ³Ω Ψ±ΩΩΎΫ Ψ§ΩΨ± ΩΎΪΨ§Ψ³ ΩΎΫΨ³Ϋ Ψ΅Ψ±Ω"
numberToWords(12_34_56_789);
// "twelve crore thirty-four lakh fifty-six thousand seven hundred eighty-nine"
Amounts are rounded to the nearest paisa. Negative or non-finite amounts throw a RangeError.
Validates FBR tax numbers. Individuals use their CNIC as their NTN; companies and AOPs have a 7-digit NTN, often written with a check digit as 1234567-8. STRNs are 13 digits.
analyzeNTN('0622438-9');
// { type: 'business', digits: '06224389', formatted: '0622438-9' }
analyzeNTN('3520212345671');
// { type: 'individual', digits: '3520212345671', formatted: '35202-1234567-1' }
validateSTRN('17-00-3761-882-19'); // true
FBR does not publish its check-digit algorithms, so these check the format only.
Builds and reads payment QR codes following the State Bank of Pakistan's QR standard (Circular Letter No. 1 of 2022). Turn the returned string into an image with any QR library.
// Person-to-person: anyone can pay this IBAN through Raast; the payer enters the amount
createRaastP2PQr({ iban: 'PK36SCBL0000001123456702' });
// With a fixed amount: banking apps require an expiry
const payload = createRaastP2PQr({
iban: 'PK36SCBL0000001123456702',
amount: 1500,
expiresAt: new Date(2026, 10, 13, 23, 59),
});
// β¦or give how long it works: '30m', '12h', '7d' or milliseconds
createRaastP2PQr({
iban: 'PK36SCBL0000001123456702',
amount: 1500,
expiresIn: '1d',
});
parseRaastQr(payload);
// { kind: 'p2p', dynamic: true, crcValid: true, iban: 'PK36SCBL0000001123456702',
// amount: 1500, expiresAt: '2026-11-13T23:59', ... }
// Merchant QR (EMVCo), with Pakistan-specific bill due dates
createMerchantQr({
merchantAccount: {
tag: '28',
fields: {
/* sub-fields from your bank */
},
},
merchantName: 'Aziz General Store',
city: 'Karachi',
amount: 4250,
billNumber: 'INV-1001',
dueDate: new Date(2026, 10, 25),
amountAfterDueDate: 4500,
});
Person-to-person QR codes use the same layout as QR codes from banking apps (Faysal Bank and easypaisa without an amount; JazzCash and Mashreq with one), and scan in the Easypaisa and JazzCash apps. This is handy for accounts whose app cannot generate a receiving QR, such as SadaPay. A QR with an amount must have expiresAt: apps reject amount QRs without an expiry, and ignore an amount in a QR that has none.
SBP reserves merchant-account tags 28β30 for Raast but has shared the layout of the fields inside them only with banks, so createMerchantQr takes those sub-fields from you. parseRaastQr checks the CRC and finds the IBAN in either kind of QR.
Checks a scanned QR before anyone pays, for example to warn about a QR sticker that was replaced or edited.
validateRaastQr(scannedText);
// { valid: false, errors: ['expired'], warnings: [], qr: { iban: 'PK36β¦', amount: 1500, β¦ } }
| Errors | Meaning |
|---|---|
not-a-raast-qr |
Not a Pakistani payment QR |
bad-checksum |
The QR was damaged or its contents were edited |
no-iban / invalid-iban |
No IBAN, or one that fails its checksum |
expired |
A QR with an amount is past its expiry |
missing-expiry |
A QR with an amount has no expiry, so banking apps reject it |
Warnings (payment still works): amount-ignored when a static QR carries an amount that apps ignore, and expires-soon when it expires within an hour (expiresSoonMs changes this). Pass now to check against another time.
Lists Pakistan's federal public holidays. Eid, Ashura and Milad use the dates actually observed after moon sighting (2020 onwards, from Cabinet Division and SBP notices), not a calculation.
getPakistanHolidays(2025).filter((h) => h.id === 'eid-ul-fitr');
// [{ name: 'Eid ul Fitr', date: '2025-03-31', estimated: false, source: 'https://β¦' }, β¦ '2025-04-01', '2025-04-02']
isPakistanHoliday('2026-08-14'); // true (Independence Day)
// Future years are estimated and flagged; apply the official announcement when it comes
getPakistanHolidays(2027, {
announced: { 'eid-ul-fitr': ['2027-03-10', '2027-03-11', '2027-03-12'] },
});
Fixed holidays follow the year they applied in: Iqbal Day was restored in 2022 and Youm-e-Takbeer added in 2024. Pass includeChristianHolidays: true for 26 December. Optional holidays such as Shab-e-Barat and Easter are not included.
Ready-made schemas for Zod, Valibot and Yup. Install the library you use; PakDataKit does not depend on any of them.
import { z } from 'zod';
import { pk } from 'pakdatakit/zod';
const CheckoutForm = z.object({
cnic: pk.cnic(),
phone: pk.phone(),
iban: pk.iban({ message: 'Please enter a valid IBAN' }),
});
import * as v from 'valibot';
import { pk } from 'pakdatakit/valibot';
const CheckoutForm = v.object({ cnic: pk.cnic(), phone: pk.phone() });
import { object } from 'yup';
import { pk } from 'pakdatakit/yup';
const CheckoutForm = object({ cnic: pk.cnic().required(), phone: pk.phone() });
Available schemas: pk.cnic(), pk.phone(), pk.iban(), pk.ntn(), pk.strn() and pk.landline(). Every factory accepts { message } to replace the default error. The Yup schemas let empty values pass so .required() decides whether a field is mandatory.
Format a field while the user types, and tell a half-typed value apart from a wrong one, so you don't show an error before they've finished.
import { formatCNICInput, checkCNICInput } from 'pakdatakit/input';
formatCNICInput('352021234'); // "35202-1234"
checkCNICInput('35202-1234'); // "incomplete" β keep typing
checkCNICInput('05202'); // "invalid" β no CNIC starts with 0
checkCNICInput('35202-1234567-1'); // "valid"
| Field | Formatter | Checker |
|---|---|---|
| CNIC | formatCNICInput β 35202-1234567-1 |
checkCNICInput |
| Mobile | formatPhoneInput β 0300-1234567 or +92 300 1234567 |
checkPhoneInput (flags an unallocated 03XX prefix as soon as it's typed) |
| IBAN | formatIBANInput β PK36 SCBL 0000 0011 2345 6702 |
checkIBANInput |
Checkers return 'empty', 'incomplete', 'valid' or 'invalid'. In React, for example:
const [cnic, setCnic] = useState('');
const state = checkCNICInput(cnic);
<input
value={cnic}
onChange={(e) => setCnic(formatCNICInput(e.target.value))}
aria-invalid={state === 'invalid'}
/>;
import {
validateCNIC,
validatePhone,
formatPhone,
analyzePhone,
createWhatsAppLink,
validateIBAN,
getBankFromIBAN,
getPostalCode,
getCityByPostalCode,
getNetwork,
getProvince,
searchCities,
formatPKR,
formatLakh,
} from 'pakdatakit';
// CNIC validation
const cnicValid = validateCNIC('35202-1234567-1');
// Phone intelligence
const phoneInfo = analyzePhone('03001234567');
// WhatsApp link for ecommerce support
const waLink = createWhatsAppLink('0300 123 4567', 'Assalamualaikum');
// IBAN validation for fintech checkout
const ibanValid = validateIBAN('PK36SCBL0000001123456702');
const bank = getBankFromIBAN('PK36SCBL0000001123456702');
// Postal lookup for shipping forms
const postal = getPostalCode('Multan');
const city = getCityByPostalCode('60000');
// Province lookup
const province = getProvince('Lahore');
// Formatting
const amount = formatPKR(150000);
const budget = formatLakh(2500000);
analyzePhone)parseAddress, formatAddress)getPostalAreas, searchPostalCodes)normalizeRomanUrdu)normalizeText, toUrduDigits, toEnglishDigits)toHijri, fromHijri, formatHijri)getHijriMonthName, isRamadan)pakdatakit command)getCNICInfo)amountInWords, numberToWords)Contributions are welcome.
Ideas, bug reports, improvements, and pull requests help make PakDataKit better for everyone.
Let's build the standard toolkit for Pakistani developers π΅π°
See:
MIT License
Built with β€οΈ for Pakistani Developers π΅π°