Skip to content

Dates of birth and calendars

Saudi government identity services use two calendars, and Verity stores them in two columns because they are genuinely different kinds of value.

The rule

Identifier typeCalendarColumnType
national_id (Saudi citizen)Hijridate_of_birth_hijristring
resident_id (Iqama), everything elseGregoriandate_of_birthdate

Elm's Yakeen by-NIN services match citizens on a Hijri date and residents on a Gregorian one, and Verity sends whatever is stored for that person.

Why Hijri is a string

A Hijri date is not a point on the Gregorian calendar. Stored in a date column, 1379-07-01 is read by both Ruby and Postgres as a Gregorian date in the year 1379 CE — roughly 650 years ago.

Everything computing with it is then wrong while looking perfectly ordinary: age arithmetic, sorting, comparison, and any plausibility rule. A "no older than 120 years" validation rejects legitimate citizen rows, which is exactly what happened when one was first added.

Worse, Focal's individual screening sends the date of birth to an international provider that has no notion of a Hijri calendar. A Hijri value there quietly degraded name-and-date matching on one of the most decision-critical checks in the product.

Splitting the columns makes both problems impossible rather than merely unlikely.

Consequences worth knowing

A date on file is not necessarily a usable one. A Saudi citizen carrying a Gregorian date has a date of birth, and it still cannot run an Elm by-NIN check. Verity asks date_of_birth_recorded? — "is there one in the calendar this check matches on" — rather than "is there one at all", so the Identity and Address buttons stay blocked and say why.

Validation differs by column. The Gregorian column rejects future dates and anything over 120 years old. The Hijri column checks the format (YYYY-MM-DD or YYYY-MM) and that the year falls in 1300–1500, which catches a Gregorian year pasted into it.

Elm receives YYYY-MM. Only year and month are sent, in the calendar that service matches on.

If identity checks are failing

The most likely cause is a date of birth in the wrong calendar. A Hijri value is easy to spot — the year is around 1400 rather than 1900–2000. A Gregorian date sitting on a citizen is the harder case, because it looks perfectly normal.

The person page names it directly. A wrong-calendar date is its own state, separate from an absent one:

What is on fileThe page says
A date in the calendar the check matches onDate of birth on file
A date in the other calendarDate of birth in the wrong calendar — with the value shown, and which calendar is wanted
NothingDate of birth missing

The middle row is the one that matters. The two need opposite work: one is a correction to a value already on file, the other a request for a value nobody holds — and reporting the first as missing sends an operator to retype the date already sitting in the field. The identity panel offers the field to correct it in both cases.

This was wrong until 12 August 2026

The page read the Gregorian column directly, so for a citizen it answered the opposite of the question Yakeen would be asked: a correct Hijri date read as DOB missing beside a live identity button, and a Gregorian date read as DOB on file beside the same button dead with Needs date of birth. Nothing failed, because each surface was internally consistent — they simply asked different questions.

An invariant now holds every surface reporting on a date of birth to date_of_birth_recorded?, the predicate the check itself runs on.