saudi-utils is a small, strict TypeScript library for offline validation and explicit
normalization of common Saudi identity, banking, business, telecom, and National Address data.
It provides deterministic errors, evidence-labelled rules, and no runtime dependencies.
- Improved TSDoc in published declarations for IDE IntelliSense.
- Opt-in
validateXDetailed()functions add a machine-readable failurecodeand a shortmessage. normalizeAndValidateIban(),normalizeAndValidatePhoneNumber(), andnormalizeAndValidateShortAddress()combine conversion with detailed validation.- New
canonicalizeX()functions returnstring | nullcandidates without domain validation. The v0.1.0normalizeX()functions remain available for compatibility and are deprecated.
npm install saudi-utilssaudi-utils is ESM-only, supports Node.js 18 and newer, and includes TypeScript declarations.
There is no default export.
import {
isSaudiIban,
canonicalizeIban,
normalizeAndValidateIban,
validateSaudiIban,
validateSaudiIbanDetailed,
} from "saudi-utils";
canonicalizeIban("sa03 8000 0000 6080 1016 7519"); // "SA0380000000608010167519"
isSaudiIban("SA0380000000608010167519"); // true (boolean validation)
validateSaudiIban("SA0380000000608010167518"); // { valid: false, code: "INVALID_CHECKSUM" }
validateSaudiIbanDetailed("SA0380000000608010167518");
// { valid: false, code: "INVALID_CHECKSUM", message: "Saudi IBAN has an invalid checksum." }
normalizeAndValidateIban("sa03 8000 0000 6080 1016 7519");
// { valid: true, value: "SA0380000000608010167519" }The package has one side-effect-free ESM root, supports tree shaking, and has zero runtime dependencies.
Validators are deliberately strict. They accept unknown, but only primitive strings can be
valid identifiers. They do not trim, coerce, remove punctuation, change case, or translate
Unicode numerals. Canonical numeric values use ASCII digits and preserve leading zeroes.
The APIs have distinct responsibilities:
| Operation | Example | Return |
|---|---|---|
| Canonical normalization | canonicalizeIban(input) |
Candidate string | null; no domain validation |
| Boolean validation | isSaudiIban(input) |
boolean |
| Detailed validation | validateSaudiIbanDetailed(input) |
{ valid: true, value } or { valid: false, code, message } |
| Normalize and validate | normalizeAndValidateIban(input) |
Detailed result; failures may include normalizedValue |
| Legacy normalization | normalizeIban(input) |
Validated IBAN or null (deprecated) |
canonicalizeIban removes ASCII spaces and uppercases ASCII letters;
canonicalizePhoneNumber accepts supported unseparated Saudi national and country-code forms;
canonicalizeShortAddress uppercases ASCII letters and removes at most one permitted ASCII space.
They return null only when conversion is unsupported, and are idempotent on successful candidates.
Normalization does not determine domain validity:
canonicalizeIban("sa03 8000 0000 6080 1016 7518"); // "SA0380000000608010167518"
normalizeIban("sa03 8000 0000 6080 1016 7518"); // null (v0.1.0 behavior)
normalizeAndValidateIban("sa03 8000 0000 6080 1016 7518");
// { valid: false, code: "INVALID_CHECKSUM", message: "Saudi IBAN has an invalid checksum.", normalizedValue: "SA0380000000608010167518" }The deprecated normalizeIban and normalizeShortAddress return null when the converted value
fails validation. Deprecated normalizePhoneNumber returns a validated
{ kind: "mobile" | "landline" | "toll-free", value } object or null.
For a single operation with structured errors, use normalizeAndValidateIban,
normalizeAndValidatePhoneNumber, or normalizeAndValidateShortAddress. These use canonical
normalization before validation. See the API reference.
formatIban accepts a valid canonical IBAN and returns null otherwise.
Existing validateX() functions return the shared result:
type ValidationResult =
{ valid: true; value: string } | { valid: false; code: ValidationErrorCode };Errors use deterministic precedence:
REQUIREDINVALID_TYPEINVALID_LENGTHINVALID_FORMATINVALID_PREFIXINVALID_CHECKSUM
undefined, null, and "" are required failures. Whitespace is not silently treated as empty.
Boolean is* helpers return the validity of the corresponding validateX() result.
Every public value is a named root export. See the detailed API reference for exact signatures, accepted representations, normalization rules, and return behavior.
| Domain | Public exports |
|---|---|
| Identity | isNationalId, validateNationalId, isIqama, validateIqama, isSaudiId, validateSaudiId, getSaudiIdType, isBorderId, validateBorderId |
| Banking | isSaudiIban, validateSaudiIban, canonicalizeIban, normalizeIban, formatIban |
| Business and tax | isVatNumber, validateVatNumber, isTin, validateTin, isUnifiedNationalNumber, validateUnifiedNationalNumber, isCommercialRegistration, validateCommercialRegistration |
| Telecom | isMobileNumber, validateMobileNumber, isLandlineNumber, validateLandlineNumber, isTollFreeNumber, validateTollFreeNumber, canonicalizePhoneNumber, normalizePhoneNumber |
| National Address | isPostalCode, validatePostalCode, isBuildingNumber, validateBuildingNumber, isAdditionalNumber, validateAdditionalNumber, isShortAddress, validateShortAddress, canonicalizeShortAddress, normalizeShortAddress, validateNationalAddress |
| Types | ValidationErrorCode, ValidationResult, ValidationEvidence, NormalizedSaudiPhone, NationalAddress, NationalAddressValidationResult |
Every listed validateX also has a validateXDetailed export. The combined exports are
normalizeAndValidateIban, normalizeAndValidatePhoneNumber, and
normalizeAndValidateShortAddress. Their shared result type is DetailedValidationResult.
| Value | Accepted canonical representation | Checksum |
|---|---|---|
| National ID | 10 ASCII digits beginning with 1 |
Alternating-double checksum |
| Iqama | 10 ASCII digits beginning with 2 |
Alternating-double checksum |
| Border ID | 10 ASCII digits beginning with 3 or 4 |
None |
| Saudi IBAN | 24 uppercase ASCII alphanumeric characters beginning with SA; positions 3–6 are digits |
ISO 13616 MOD-97-10 |
| VAT Registration Number | 15 ASCII digits beginning and ending with 3 |
None |
| TIN | 10 ASCII digits | None |
| Unified National Number | 10 ASCII digits beginning with 7 |
None |
| Legacy Commercial Registration | 10 ASCII digits | None |
| Mobile | 05XXXXXXXX or +9665XXXXXXXX |
None |
| Landline | National or +966 form using 011, 012, 013, 014, 016, or 017 |
None |
| Toll-free | National 800XXXXXXX only |
None |
| Postal code | 5 ASCII digits | None |
| Building/additional number | 4 ASCII digits | None |
| Short Address | 4 uppercase ASCII letters followed by 4 ASCII digits | None |
Rules are classified as official-checksum, official-structural,
official-structural-community-checksum, or best-known-structural. The complete source and
limitation register is in the
validation evidence document.
- Border ID is provisional: only the best-known 10-digit structure and
3/4prefixes are checked. No checksum or complete public normative schema is claimed. - TIN is structural only: any 10 ASCII digits pass. No checksum or richer authoritative offline grammar is claimed.
- Commercial Registration means the legacy structural format: the API checks only a 10-digit legacy representation. It is not current registry verification and is not an alias for the Unified National Number.
- Phone prefixes describe numbering structure, not the subscriber's current carrier. Number portability makes current-carrier inference invalid.
The values labelled synthetic below demonstrate offline rules only; they are not presented as issued identifiers. The repository's typechecked quick-start fixture also executes its expected results.
import { getSaudiIdType, validateBorderId, validateNationalId } from "saudi-utils";
validateNationalId("1000000008"); // { valid: true, value: "1000000008" } (synthetic)
getSaudiIdType("1000000008"); // "national-id"
validateBorderId("3000000000"); // structural success only (synthetic)import { formatIban, normalizeIban } from "saudi-utils";
const iban = normalizeIban("sa03 8000 0000 6080 1016 7519");
formatIban(iban); // "SA03 8000 0000 6080 1016 7519"
formatIban("sa03 8000 0000 6080 1016 7519"); // null: formatting does not normalizeimport { validateTin, validateUnifiedNationalNumber, validateVatNumber } from "saudi-utils";
validateVatNumber("300000000000003").valid; // true: structural result
validateTin("0123456789").valid; // true: best-known structural result
validateUnifiedNationalNumber("7123456789").valid; // true: structural resultimport { canonicalizePhoneNumber, normalizePhoneNumber, validateMobileNumber } from "saudi-utils";
validateMobileNumber("0501234567").valid; // true: no reachability or carrier claim
canonicalizePhoneNumber("0501234567"); // "+966501234567"
normalizePhoneNumber("0501234567"); // { kind: "mobile", value: "+966501234567" }import { normalizeShortAddress, validateNationalAddress } from "saudi-utils";
normalizeShortAddress("abcd 0123"); // "ABCD0123"
validateNationalAddress({
buildingNumber: "0123",
street: "King Fahd Road",
district: "Al Olaya",
city: "Riyadh",
postalCode: "01234",
additionalNumber: "0000",
applicationField: "ignored",
});
// Success returns a fresh object containing only the six required fields.validateNationalAddress requires those six own data properties, ignores extra properties, does
not invoke accessors, and returns a fresh object containing only required fields. Its specialized
NationalAddressValidationResult returns a NationalAddress object on success. Text fields must
be non-empty primitive strings but are not trimmed or linguistically restricted; cross-field and
address-existence checks are outside the contract.
npm ci
npm run docs:check
npm run checkAvailable focused commands are npm test, npm run test:coverage, npm run typecheck,
npm run lint, npm run format:check, and npm run build. Node.js 18 or newer can run the packed package; repository development/build tooling requires a newer Node version (use Node 24).