Skip to main content

Command Palette

Search for a command to run...

Phone numbers and SMS OTP in the Gulf: a production guide

How to parse, verify and recover phone-number accounts across the UAE and Saudi Arabia.

Updated
8 min readView as Markdown
Phone numbers and SMS OTP in the Gulf: a production guide
H
I have lead the Engineering for multiple startups in UAE. I also have my own agency qualascend.com.

Phone-number onboarding looks simple until a local number, a portable prefix and an account-recovery flow meet the same production system. A parser can accept a string that no handset can receive. An SMS provider can accept a request that never reaches the user. A successful code proves that someone controlled the destination at that moment, not that the number permanently belongs to them.

For products operating in the UAE and Saudi Arabia, the safest design is a sequence of explicit claims. Format the input, parse it, test its metadata, verify reachability and present control, assess risk, then bind it to the account. Do not collapse those steps into one isValidPhoneNumber flag.

Cover credit: SultanByte editorial artwork.

Start with five different questions

Phone systems often fail because one field called phone_verified carries too much meaning. Split the problem into five questions:

  1. Formatting: Can the input be represented consistently, preferably in the international form defined around the ITU-T E.164 recommendation?
  2. Validity: Does the number have a plausible length and a prefix that current numbering metadata recognises?
  3. Reachability: Can the destination receive an SMS now?
  4. Control: Can the person in this session return the challenge code now?
  5. Current carrier: Which network serves the number today, after any port?

Each answer has a different source and shelf life. Formatting is deterministic. Library metadata changes. Reachability can change by the minute. Control can move to another person. Carrier data can become stale after a port.

That distinction should appear in your schema and events. Store a canonical number, but attach verification time, channel, provider reference and risk decision as separate attributes. A boolean alone loses the evidence you need when investigating fraud or deciding whether a sensitive action needs a fresh challenge.

Normalize without guessing too much

Keep the raw input for support and audit work. Produce a canonical E.164 value for comparison, storage and provider calls. Remove presentation punctuation through a tested parser, not a home-grown chain of string replacements.

Country context matters. A number beginning with 05 is not globally self-describing. If the user has selected UAE or Saudi Arabia as the market, pass the corresponding region as parsing context. If the context is unknown, ask for it rather than inferring it from browser language, IP address or a previously selected currency.

The UAE's TDRA National Numbering Plan, version 5.3 dated 3 June 2015 defines the national mobile form as 0 + 5 + Z + seven digits. The leading zero is a national prefix, so it does not survive unchanged in the international representation. Let a maintained library apply that rule. Do not teach every client application to splice country codes by hand.

A small TypeScript boundary can keep raw input and canonical output separate:

import { parsePhoneNumberFromString } from "libphonenumber-js";

type GulfRegion = "AE" | "SA";

type ParsedPhone = {
  raw: string;
  e164: string;
  region?: string;
  possible: boolean;
  valid: boolean;
};

export function parseGulfPhone(raw: string, region: GulfRegion): ParsedPhone {
  const phone = parsePhoneNumberFromString(raw, region);
  if (!phone) throw new Error("PHONE_PARSE_FAILED");

  return {
    raw,
    e164: phone.number,
    region: phone.country,
    possible: phone.isPossible(),
    valid: phone.isValid(),
  };
}

Pin and update the metadata package deliberately. The upstream Google libphonenumber project makes an important distinction: a number can be "possible" based on length while still failing "valid" checks based on length and prefix metadata. Neither result says that a SIM is active, that SMS is enabled or that the current user controls it.

Treat validation as a cheap gate

Metadata validation belongs before an OTP send because it catches obvious mistakes without spending a message or exposing a provider endpoint to junk traffic. It should still be a forgiving user experience.

Show the user the formatted number before sending. Keep error messages broad enough that the endpoint does not become a number-enumeration oracle. Log structured reason codes internally, such as parse failure, impossible length, unsupported region and metadata-invalid number. Avoid logging the full raw number in general application logs. Mask it or use a keyed, access-controlled identifier for correlation.

Do not reject a number solely because its prefix appears to belong to the "wrong" operator. TDRA's plan warns that portability can change the provider behind a UAE mobile prefix. Saudi Arabia's CST portability regulation likewise allows users to retain a number when changing providers and describes a central portability database. Prefix tables are useful historical allocation data, not reliable current-carrier truth.

Make OTP verification a state machine

An OTP flow is an asynchronous protocol, not a pair of endpoints called send and check. Model the states. This prevents retries, late callbacks and concurrent sessions from turning into accidental approvals.

Internal state Enter when Allowed next state Product behaviour
created Request passes local policy pending, rejected Allocate an idempotency key; do not expose account existence
pending Provider accepts the verification request approved, failed, expired, canceled, locked Allow checks within rate and attempt limits
approved Correct code is accepted none Bind evidence once; ignore duplicate callbacks
locked Attempt ceiling is reached none Require a new verification after cooldown or step-up
failed Provider or channel reports a terminal failure none Offer a safe retry or another approved channel
expired Verification lifetime ends none Start a new challenge; never revive the old one
canceled User or system cancels the request none Reject later checks and callbacks

The exact provider vocabulary must remain behind an adapter. Twilio's Verify API documentation requires an E.164 destination and documents pending, approved, canceled, max_attempts_reached, deleted, failed and expired. Map max_attempts_reached to your locked terminal state. Preserve the raw provider status for debugging, but make product policy depend on your internal model.

Use one active challenge per purpose, phone and account or session. An idempotency key should make repeated client requests return the same active challenge rather than send another code. Apply limits by phone, account, session, device and network boundary. Keep the limits and code lifetime configurable because provider behaviour, abuse patterns and sender rules can change.

Do not interpret "provider accepted" or "message sent" as verification. Approval requires a successful code check tied to the same challenge, purpose and session. Store the minimum evidence needed: canonical number, verification timestamp, purpose, provider reference, terminal status and policy version. If you generate codes yourself, never store them in plaintext. Use a keyed digest and constant-time comparison.

Six-stage production path for Gulf phone-number normalization, validation, OTP verification, portability risk checks, account binding and recovery

Sources: ITU-T E.164, UAE TDRA National Numbering Plan v5.3, Saudi CST portability regulation, NIST SP 800-63B, Google libphonenumber and Twilio Verify API. Credit: SultanByte editorial artwork.

Put portability and SIM risk after basic checks

A live OTP answers a narrow question: the claimant can receive or access the code now. It does not establish legal identity, account ownership or durable control. Recycled numbers, shared family phones, compromised devices and social engineering all sit outside a formatting check.

For higher-risk actions, add a risk decision between OTP approval and account change. NIST SP 800-63B treats PSTN out-of-band authentication as restricted and advises verifiers to consider signals such as SIM change, device swap and number porting. Those signals should trigger policy, not automatic accusations. A recent port may require a passkey, an existing authenticated device, a cooling-off period or support review.

Carrier and portability lookups are operational inputs. They can help route a message or flag a recent change, but they do not prove identity. Record when the lookup occurred and which decision consumed it. A cached carrier value from sign-up should not silently authorize a number change months later.

Bind evidence, then design for loss of control

After approval and any risk step, bind the canonical number to the account with its evidence. Protect number replacement more strongly than initial sign-up because an attacker who changes the destination may capture future codes. Notify the old channel when policy permits, revoke outstanding challenges, and treat callbacks for superseded challenges as no-ops.

Recovery deserves its own threat model. SMS OTP should not be the only recovery route for high-risk accounts. Offer stronger options such as passkeys, recovery codes stored by the user, or support-assisted recovery with documented checks. Do not let a newly added number immediately become the sole recovery factor for sensitive accounts.

Re-verification should follow risk, not a blanket timer. Trigger it when the user changes the number, performs a sensitive action, returns after suspicious device activity or presents a material portability or SIM signal. A verified timestamp remains useful evidence, but it is not a lifetime guarantee.

Ship the boundaries, not one magic flag

The production API should expose the claim each stage has earned. formatted means canonical syntax. valid means the numbering metadata accepts the structure. pending means a challenge exists. approved means the claimant completed it. A carrier result is a dated routing or risk signal. None of those fields alone means "this person owns this identity."

Once those boundaries are explicit, UAE and Saudi numbering differences stay in the parsing layer, portability stays in routing and risk, and OTP remains one authentication signal rather than the foundation of account recovery. That architecture is less convenient than a single boolean, but it is much easier to operate when numbers move, devices change and delivery fails.