Updates & versioning
TL;DR
Every location and administrative area keeps a
versionshistory — an ordered list of{ versionId, name, externalId, status, effectiveFrom }entries.Identity (
id, parent/administrative area, location type) is fixed at creation and never changes. Only name, code and status are versioned.Each record and certificate resolves the version that was in effect at the record's own date (its anchor), not the current name.
Updates only append a new version — nothing is edited or removed in place. A future-dated version can be withdrawn before it takes effect.
Re-parenting a location or administrative area is not supported. Moving one to a different parent means inactivating the old one and creating a new one.
Writing locations or administrative areas after go-live requires the
location.editscope.
Why versioning exists
Names, codes and active/inactive status of offices, facilities and administrative areas change over time — a health facility closes, a district office is renamed, a village is merged into another. Historical records must keep showing the name and status that applied when they were captured, while current forms and searches must reflect what's true today. A single mutable name/status field cannot do both at once, so each location and administrative area instead keeps a small history of versions.
The versions array
Each element of versions has:
versionId— a UUID identifying the version itself.effectiveFrom— a plain date (YYYY-MM-DD) from which this version applies.nameexternalId— optional, used for point-in-time code uniqueness (see below).status—activeorinactive.
Versions are sorted ascending by effectiveFrom. Unless an explicit effectiveFrom is given at creation, a location's first version defaults to 0001-01-01 — a beginning-of-time sentinel — so that every location always resolves to some version, however far back a record's date reaches.
Everything else about a location or administrative area — its id, its parent (administrativeAreaId / parentId), and a location's locationType — is set once at creation and cannot change. Renames and status changes only ever append a new version; nothing is edited or deleted in place.
Resolving a version — the anchor date
Because a location's name and status vary over time, any UI or API that renders one needs an anchor date: the date the name/status should be resolved at. Which anchor applies depends on the surface:
Record views, review screens and a certificate's declaration fields use the record anchor — the record's own date of event, or
createdAtwhen the configured date-of-event field is empty (e.g. a partial notification), the same fallback convention used elsewhere for date-of-event resolution.Form selectors resolve against today unless the field opts in with
anchorToDateOfEvent.
"Today" isn't the same clock everywhere: the events service resolves it as the UTC calendar date, while a client anchoring to today uses the browser's local calendar date. The two can differ by a day for a user near midnight UTC.
Resolution takes the version with the greatest effectiveFrom that is still on or before the anchor, falling back to the earliest version when the anchor precedes all of them. In practice this means:
A record captured before a rename shows the old name in the record view and on the certificate, even after the location has since been renamed.
A record captured before a location is deactivated still shows it as it was, even though fields configured with
activeOnlyno longer offer it in new declarations.A location whose first version's
effectiveFromhas not yet arrived is likewise hidden from fields configured withactiveOnly— a location scheduled for the future stays hidden until then. Fields that don't set the flag keep listing every location regardless of version status.
The wire format for a location or administrative area still carries flat name/status/externalId fields (resolved as of today) alongside versions — some consumers read these directly; the reference country config's analytics sync, for instance, writes the flat fields straight into its own database rather than resolving per-record history. The registrar-facing client is stricter: its cached location map strips the flat fields entirely, so no client code can read a location's current name without going through resolveVersion/resolvePath at the anchor. That cache is kept for up to a day (a 24-hour stale time), so a location created or renamed elsewhere can take up to a day to reach a given device.
Changed in v2.1: the validUntil field has been removed from the Location and AdministrativeArea wire models. Read the top-level status for whether an entity is active today, or derive when a version stopped applying from the effectiveFrom of the next element in versions.
Withdrawing a pending change
A version that has not taken effect yet (its effectiveFrom is strictly in the future) can be withdrawn — this removes it from the history outright, as if it had never been scheduled. Once a version's effectiveFrom is today or earlier it counts as already in effect and can no longer be withdrawn; a further version must be appended instead to change course. A version can only be withdrawn if it is not the only one a location or administrative area has — a row must always keep at least one version, so withdrawing the last remaining one is rejected instead.
Transfers: no re-parenting
A location's administrative area, and an administrative area's parent, are part of its immutable identity — they cannot be changed by appending a version. Moving a health facility or office to a different administrative area (or moving an administrative area under a different parent) is therefore done as two separate, independent operations:
Inactivate the existing location/area (append an
inactiveversion).Create a new location/area under the correct parent.
Both operations are idempotent and safe to retry individually; they are not combined into a single atomic call.
Nothing in this recipe carries over automatically. Say District Office A closes and District Office B opens as its replacement, same jurisdiction:
No cascade. Anything that pointed at District Office A — locations assigned to it, or sub-areas beneath it, if it's an administrative area — keeps pointing at District Office A. None of it is moved or relinked to District Office B.
No data migration. Records already registered at District Office A keep referencing District Office A's UUID forever. They do not move to District Office B, even though B is meant to replace A.
externalIdreuse is order-sensitive. If both offices share the same reference code, inactivate District Office A first, then create District Office B. Create B before A is inactive (or with an earliereffectiveFromthan A's inactivation) and the request is rejected — the code is still active at A.
Point-in-time code uniqueness
Where an externalId (an external reference code) is set, it must be unique among active holders at any given point in time — not across all of history. A new location can reuse a code that a different, since-inactivated location used to hold, but it cannot take over a code that's still active (or scheduled to become active) elsewhere from the same date onward. Locations and administrative areas enforce this independently, each only against its own kind — a location and an administrative area may hold the same externalId at the same time without conflict.
Where history comes from
Existing rows didn't always have a versions history. The upgrade to this model backfilled every location and administrative area with a 0001-01-01 active version built from its old flat name and code, plus — for a row that had a legacy valid_until date set — a further inactive version dated at that cutover. The initial hierarchy seed (see How to populate administrative hierarchy) can also supply a pre-built versions array directly; re-running the seed against the same row replaces its stored history wholesale rather than merging into it, so a caller that means to keep a history it built has to resend it every time.
Audit trail
Every create, update and withdraw is written to the audit log. An idempotent replay — the request was already applied before — is deliberately not re-audited, since nothing new happened.
Configuring form selectors
LOCATION, ADMINISTRATIVE_AREA and ADDRESS fields have their own activeOnly / anchorToDateOfEvent configuration options for controlling which versions a form selector offers — see How to limit location and administrative area options in event declaration.
Access control
Locations and administrative areas can only be written through the write API described in How to add new locations & administrative areas and How to update & deactivate locations & administrative areas. Both require the location.edit scope, which must be assigned to a role in your country configuration before any user can manage locations this way.
Last updated