NDAS API v1.0.0

Rendered from the frozen contract (ndas.v1.yaml) — the single source of truth. v1 changes are additive-only; clients must ignore unknown fields.

resolve

Coordinate → structured address (the flood-facing read path)

POST/v1/resolve

Resolve a coordinate to a structured address

Public tier. Walks the admin hierarchy (Region → … → Village), returns the standardized physical block, the official postcode (or an explicit `pending_allocation` status — never a placeholder), and a confidence score. Response contains location/status data only.

Request body — ResolveRequest

FieldTypeDescription
point *PointWGS84 coordinate
lat *number
lon *number
gps_accuracy_mnumber | nullReported GPS accuracy in meters, if known

Responses

StatusSchemaDescription
200ResolveResponseResolution outcome (including pending/unresolved statuses)
400ErrorResponseMalformed request
422ErrorResponseInput failed validation (structural code rules, domains)
429ErrorResponseRequest budget exhausted

Try it (sandbox)

curl -X POST \
  'https://sandbox.n.das.ug/v1/resolve' \
  -H 'X-API-Key: <your sandbox key>' \
  -H 'Content-Type: application/json' \
  -d '{ "point": { "lat": 0.3355, "lon": 32.5555 }, "gps_accuracy_m": 4 }'

addresses

Address lookup and registration

POST/v1/addresses

Register an address (allocate a digital address code)

Partner/agent scoped. Allocates the next sequential premise number in the resolved parish and mints the canonical digital address. In a `pending_allocation` parish the capture is stored and `201` returns the record with `status: pending_allocation` and `digital_address`, `postcode`, and `premise` all `null` (the schema enforces this coherence); issuance happens when MoICT allocates the postcode — the partner is notified and can re-fetch via its `unique_reference`. **Requires `Idempotency-Key`, scoped per principal.** The replay ledger key is (authenticated principal, Idempotency-Key): replays by the same principal return the original result; a different principal using the same key string hits its own independent ledger — it can neither read another partner's result nor cause a cross-tenant `409`.

Parameters

NameInTypeDescription
Idempotency-Key *headerstringClient-generated key (UUID recommended). The server's replay ledger is keyed by **(authenticated principal, Idempotency-Key)** — keys are tenant-isolated and can never match, replay, or conflict across principals. Within one principal: replays with the same key return the original result; the same key with a different payload is rejected with 409. Keys are retained for at least 24 hours.

Request body — RegisterAddressRequest

FieldTypeDescription
point *PointWGS84 coordinate
lat *number
lon *number
gps_accuracy_mnumber | null
streetstring | null
house_numberstring | null
unit_designatorstring | null
unique_referencestring | nullThe partner's own id — becomes the crosswalk join key
sourcestring | nullCapture context (defaults to the caller's channel)

Responses

StatusSchemaDescription
201RegisterAddressResponseAddress registered (digital code minted, or explicitly pending)
400ErrorResponseMalformed request
401ErrorResponseMissing or invalid credentials
403ErrorResponseAuthenticated but not permitted (scope/tenant)
409ErrorResponseConflict — either the Idempotency-Key was replayed with a different payload (`idempotency_conflict`), or the supplied `unique_reference` is already bound to a different address for this partner (`reference_conflict`).
422ErrorResponseInput failed validation (structural code rules, domains)
429ErrorResponseRequest budget exhausted

Try it (sandbox)

curl -X POST \
  'https://sandbox.n.das.ug/v1/addresses' \
  -H 'X-API-Key: <your sandbox key>' \
  -H 'Content-Type: application/json' \
  -d '{ "point": { "lat": 0.3355, "lon": 32.5555 }, "gps_accuracy_m": 6.5, "street": "Kafu Road", "house_number": "24", "unique_reference": "partner-abc-000123" }'
GET/v1/addresses/{digital_address}

Look up an address by digital address code

Accepts the canonical frozen format **and any superseded or legacy alias shape** (grandfathered System-A codes included — issued codes resolve forever). The registry, not the URL pattern, decides validity: unknown codes are `404`; only over-length input is structurally rejected (`422`). The response's `digital_address` is **always the canonical current code**; when the query was an alias, `queried_code` echoes what was asked. Anonymous lookups draw from the strict per-IP anti-enumeration budget (`x-ndas-rate-limit-tiers`); bulk lookup requires keyed access. Audience shaping: unauthenticated and public keys receive `PublicAddress`; partner scopes receive `PartnerAddress`.

Parameters

NameInTypeDescription
digital_address *pathstringA digital address code: canonical (`UG-XXXX-XXXXX`), superseded, or legacy alias shape. Free-form by design — legacy System-A codes do not match the frozen pattern but must resolve forever.

Responses

StatusSchemaDescription
200AddressResponseThe address record, shaped for the caller's audience
404ErrorResponseUnknown resource (e.g. code not in the registry)
422ErrorResponseInput failed validation (structural code rules, domains)
429ErrorResponseRequest budget exhausted

Try it (sandbox)

curl -X GET \
  'https://sandbox.n.das.ug/v1/addresses/{digital_address}' \
  -H 'X-API-Key: <your sandbox key>'
GET/v1/addresses/reference/{unique_reference}

Look up an address by partner reference (crosswalk)

The sync join key: a partner passes its own `unique_reference` (supplied at registration) and receives the canonical record. Scoped to the owning partner — references are tenant-isolated.

Parameters

NameInTypeDescription
unique_reference *pathstringThe partner-supplied external reference

Responses

StatusSchemaDescription
200PartnerAddressResponseThe partner-tier address record
401ErrorResponseMissing or invalid credentials
403ErrorResponseAuthenticated but not permitted (scope/tenant)
404ErrorResponseUnknown resource (e.g. code not in the registry)
429ErrorResponseRequest budget exhausted

Try it (sandbox)

curl -X GET \
  'https://sandbox.n.das.ug/v1/addresses/reference/{unique_reference}' \
  -H 'X-API-Key: <your sandbox key>'

standardize

Messy input → canonical address block

POST/v1/standardize

Standardize messy address input into the canonical block

Accepts free text and/or partial components and returns the canonical physical address block (what Posta and partners use to clean their data). Returns candidate interpretations ranked by confidence.

Request body — StandardizeRequest

FieldTypeDescription
raw_textstringFree-form address text to canonicalize
componentsobject
districtstring
countystring
subcountystring
parishstring
villagestring
streetstring
house_numberstring
digital_addressDigitalAddressThe frozen national format: UG- + 4-digit premise + - + 5-digit postcode. All numeric, nothing appended, no check digit.

Responses

StatusSchemaDescription
200StandardizeResponseStandardization candidates, best first
400ErrorResponseMalformed request
422ErrorResponseInput failed validation (structural code rules, domains)
429ErrorResponseRequest budget exhausted

Try it (sandbox)

curl -X POST \
  'https://sandbox.n.das.ug/v1/standardize' \
  -H 'X-API-Key: <your sandbox key>' \
  -H 'Content-Type: application/json' \
  -d '{ "raw_text": "plot 24 kafu rd bwaise kawempe kampala" }'

reference

Admin hierarchy and postcode reference data

GET/v1/reference/admin-units

List administrative units (drill-down)

The Region → District → County → Sub-county → Parish → Village drill-down. Filter by `level` and/or `parent_id`; omit both to list regions. Paginated.

Parameters

NameInTypeDescription
levelqueryAdminLevel
parent_idquerystringReturn children of this unit
pagequeryinteger
limitqueryinteger

Responses

StatusSchemaDescription
200AdminUnitListResponseMatching admin units
400ErrorResponseMalformed request
429ErrorResponseRequest budget exhausted

Try it (sandbox)

curl -X GET \
  'https://sandbox.n.das.ug/v1/reference/admin-units' \
  -H 'X-API-Key: <your sandbox key>'
GET/v1/reference/admin-units/{admin_unit_id}/children

List the direct children of an administrative unit

Parameters

NameInTypeDescription
admin_unit_id *pathstring
pagequeryinteger
limitqueryinteger

Responses

StatusSchemaDescription
200AdminUnitListResponseThe unit's direct children
404ErrorResponseUnknown resource (e.g. code not in the registry)
429ErrorResponseRequest budget exhausted

Try it (sandbox)

curl -X GET \
  'https://sandbox.n.das.ug/v1/reference/admin-units/{admin_unit_id}/children' \
  -H 'X-API-Key: <your sandbox key>'
GET/v1/reference/postcodes/{code}

Look up a postcode (current, superseded, or legacy alias)

Returns the official record for a postcode query. Accepts canonical 5-digit codes, superseded codes, and **legacy alias shapes** (e.g. grandfathered System-A placeholders) — the registry decides validity; unknown queries are `404`, only over-length input is `422`. Superseded 5-digit codes resolve forever: the record carries `status: superseded` plus the canonical successor in `canonical_code`. For legacy-shaped aliases the returned record **is** the canonical target (its `code` keeps the frozen 5-digit format) and `queried_code` echoes the original query.

Parameters

NameInTypeDescription
code *pathstringA postcode query: canonical 5-digit, superseded, or a legacy alias shape. Free-form by design (see endpoint description).

Responses

StatusSchemaDescription
200PostcodeResponseThe postcode record
404ErrorResponseUnknown resource (e.g. code not in the registry)
422ErrorResponseInput failed validation (structural code rules, domains)
429ErrorResponseRequest budget exhausted

Try it (sandbox)

curl -X GET \
  'https://sandbox.n.das.ug/v1/reference/postcodes/{code}' \
  -H 'X-API-Key: <your sandbox key>'

kyc

Consented address-person verification (partner product)

POST/v1/kyc/address-match

Consented person–address match (KYC product)

The monetized verification product for banks/immigration/MDAs. Requires the `kyc.match` scope (OAuth2 bearer — API keys alone do not carry it) **and** a valid consent reference (Uganda DPPA 2019). Consent is bound to the specific check: **(subject, purpose, address-bounded)**. The server MUST reject with `403` (`consent_scope_invalid`) any consent whose scope does not cover the queried `digital_address` — a single consent can never be replayed across arbitrary addresses to discover where a person lives. The response is a match boolean with consent/audit references — **raw PII is never echoed back**. Every lookup is written to the immutable audit trail (who queried whose address, under which consent).

Request body — KycMatchRequest

FieldTypeDescription
digital_address *DigitalAddressThe frozen national format: UG- + 4-digit premise + - + 5-digit postcode. All numeric, nothing appended, no check digit.
subject *objectThe data subject to match (PII is processed, never echoed)
full_namestring
nin_tokenstringTokenized NIN reference issued by the identity provider. The token shape is structural: anything not matching `^tok_[A-Za-z0-9_]+$` — a raw NIN in particular — is rejected with 422 before any processing.
phonestringE.164-style digits only
consent_reference *stringThe consent record authorizing this check (Uganda DPPA 2019). Consent is **(subject, purpose, address-bounded)**: it names the data subject, the address-verification purpose, and the specific digital address(es) it covers. The server MUST reject (403, `consent_scope_invalid`) a consent whose scope does not cover the queried `digital_address` — one subject-scoped consent can never be swept across many addresses to discover residence.

Responses

StatusSchemaDescription
200KycMatchResponseMatch outcome (fail-safe — a wrong code yields no-match)
401ErrorResponseMissing or invalid credentials
403ErrorResponseMissing scope or invalid/revoked consent reference
422ErrorResponseInput failed validation (structural code rules, domains)
429ErrorResponseRequest budget exhausted

Try it (sandbox)

curl -X POST \
  'https://sandbox.n.das.ug/v1/kyc/address-match' \
  -H 'X-API-Key: <your sandbox key>' \
  -H 'Content-Type: application/json' \
  -d '{ "digital_address": "UG-0007-10201", "subject": { "full_name": "Synthetic Party 001", "nin_token": "tok_sandbox_5f2a9c" }, "consent_reference": "consent-2026-000042" }'

system

Health and service metadata

GET/v1/health

Service health and data versions

Responses

StatusSchemaDescription
200HealthResponseService is healthy
503ErrorResponseService is degraded or unavailable

Try it (sandbox)

curl -X GET \
  'https://sandbox.n.das.ug/v1/health' \
  -H 'X-API-Key: <your sandbox key>'

admin

NDAS Admin Console — staff tier (Keycloak ndas-staff realm roles)

GET/v1/admin/addresses

List addresses for moderation (NDAS Staff)

Moderation-oriented listing. Carries NO owner/party data.

Parameters

NameInTypeDescription
statusqueryAddressStatus
districtquerystring
parishquerystring
qquerystringSubstring match on code or street
pagequeryinteger
limitqueryinteger

Responses

StatusSchemaDescription
200StaffAddressListResponseMatching addresses (staff summary projection)
400ErrorResponseMalformed request
401ErrorResponseMissing or invalid credentials
403ErrorResponseAuthenticated but not permitted (scope/tenant)
429ErrorResponseRequest budget exhausted

Try it (sandbox)

curl -X GET \
  'https://sandbox.n.das.ug/v1/admin/addresses' \
  -H 'X-API-Key: <your sandbox key>'
GET/v1/admin/addresses/{digital_address}

Staff address detail (owner block included — the read is audited)

The only endpoint family that shapes owner data into a response (blueprint §9 — staff see more, but everything is audited): each call emits an `admin.address_viewed` audit event naming the staff principal. The NIN token hash never leaves the registry.

Parameters

NameInTypeDescription
digital_address *pathstring

Responses

StatusSchemaDescription
200StaffAddressResponseThe staff-tier address record with verification history
401ErrorResponseMissing or invalid credentials
403ErrorResponseAuthenticated but not permitted (scope/tenant)
404ErrorResponseUnknown resource (e.g. code not in the registry)
422ErrorResponseInput failed validation (structural code rules, domains)
429ErrorResponseRequest budget exhausted

Try it (sandbox)

curl -X GET \
  'https://sandbox.n.das.ug/v1/admin/addresses/{digital_address}' \
  -H 'X-API-Key: <your sandbox key>'
POST/v1/admin/addresses/{digital_address}/moderate

Moderate an address (verify / reject / retire)

Lifecycle transitions: `verify` (unverified → verified), `reject` (stays unverified; the rejection is recorded as a verification event), `retire` (unverified|verified → retired — the code stays resolvable forever and is never reused). Disallowed transitions are `409` `moderation_action_invalid`. Audited.

Parameters

NameInTypeDescription
digital_address *pathstring

Request body — ModerationRequest

FieldTypeDescription
action *string
verify · reject · retire
notestring | null

Responses

StatusSchemaDescription
200StaffAddressResponseThe updated staff-tier record
400ErrorResponseMalformed request
401ErrorResponseMissing or invalid credentials
403ErrorResponseAuthenticated but not permitted (scope/tenant)
404ErrorResponseUnknown resource (e.g. code not in the registry)
409ErrorResponseTransition not allowed from the current status
422ErrorResponseInput failed validation (structural code rules, domains)
429ErrorResponseRequest budget exhausted

Try it (sandbox)

curl -X POST \
  'https://sandbox.n.das.ug/v1/admin/addresses/{digital_address}/moderate' \
  -H 'X-API-Key: <your sandbox key>'
GET/v1/admin/verification/queue

The verification queue (unverified addresses + evidence)

Parameters

NameInTypeDescription
districtquerystring
pagequeryinteger
limitqueryinteger

Responses

StatusSchemaDescription
200VerificationQueueResponseUnverified addresses, oldest first, with their events
401ErrorResponseMissing or invalid credentials
403ErrorResponseAuthenticated but not permitted (scope/tenant)
429ErrorResponseRequest budget exhausted

Try it (sandbox)

curl -X GET \
  'https://sandbox.n.das.ug/v1/admin/verification/queue' \
  -H 'X-API-Key: <your sandbox key>'
GET/v1/admin/disputes

The dispute / claim-conflict queue

Summaries only — claimant contact (PII) appears in detail.

Parameters

NameInTypeDescription
statusqueryDisputeStatus
pagequeryinteger
limitqueryinteger

Responses

StatusSchemaDescription
200DisputeListResponseDispute summaries, oldest first
400ErrorResponseMalformed request
401ErrorResponseMissing or invalid credentials
403ErrorResponseAuthenticated but not permitted (scope/tenant)
429ErrorResponseRequest budget exhausted

Try it (sandbox)

curl -X GET \
  'https://sandbox.n.das.ug/v1/admin/disputes' \
  -H 'X-API-Key: <your sandbox key>'
POST/v1/admin/disputes

Open a dispute against a registered address

Request body — DisputeCreateRequest

FieldTypeDescription
digital_address *string
kindstring
claim_conflict · ownership · data_error
claimant_name *string
claimant_contactstring | null
grounds *string
evidence_refstring | null

Responses

StatusSchemaDescription
201DisputeDetailResponseThe opened dispute (detail projection)
400ErrorResponseMalformed request
401ErrorResponseMissing or invalid credentials
403ErrorResponseAuthenticated but not permitted (scope/tenant)
404ErrorResponseUnknown resource (e.g. code not in the registry)
422ErrorResponseInput failed validation (structural code rules, domains)
429ErrorResponseRequest budget exhausted

Try it (sandbox)

curl -X POST \
  'https://sandbox.n.das.ug/v1/admin/disputes' \
  -H 'X-API-Key: <your sandbox key>'
GET/v1/admin/disputes/{dispute_id}

Dispute detail (claimant contact included — the read is audited)

Parameters

NameInTypeDescription
dispute_id *pathstring

Responses

StatusSchemaDescription
200DisputeDetailResponseThe dispute with grounds, evidence, and decision trail
401ErrorResponseMissing or invalid credentials
403ErrorResponseAuthenticated but not permitted (scope/tenant)
404ErrorResponseUnknown resource (e.g. code not in the registry)
429ErrorResponseRequest budget exhausted

Try it (sandbox)

curl -X GET \
  'https://sandbox.n.das.ug/v1/admin/disputes/{dispute_id}' \
  -H 'X-API-Key: <your sandbox key>'
POST/v1/admin/disputes/{dispute_id}/adjudicate

Decide a dispute (evidence-based; audited)

Allowed from `open`, `under_review`, or `appealed`; the case becomes `resolved`. Decisions move the *claim*, never the code (permanence).

Parameters

NameInTypeDescription
dispute_id *pathstring

Request body — DisputeAdjudicateRequest

FieldTypeDescription
decision *string
uphold_claimant · uphold_holder · dismissed
note *string

Responses

StatusSchemaDescription
200DisputeDetailResponseThe resolved dispute
401ErrorResponseMissing or invalid credentials
403ErrorResponseAuthenticated but not permitted (scope/tenant)
404ErrorResponseUnknown resource (e.g. code not in the registry)
409ErrorResponseThe dispute is not in an adjudicable state
422ErrorResponseInput failed validation (structural code rules, domains)
429ErrorResponseRequest budget exhausted

Try it (sandbox)

curl -X POST \
  'https://sandbox.n.das.ug/v1/admin/disputes/{dispute_id}/adjudicate' \
  -H 'X-API-Key: <your sandbox key>'
POST/v1/admin/disputes/{dispute_id}/appeal

Appeal a resolved dispute (reopens the case; audited)

Parameters

NameInTypeDescription
dispute_id *pathstring

Request body — DisputeAppealRequest

FieldTypeDescription
note *string

Responses

StatusSchemaDescription
200DisputeDetailResponseThe appealed (reopened) dispute
401ErrorResponseMissing or invalid credentials
403ErrorResponseAuthenticated but not permitted (scope/tenant)
404ErrorResponseUnknown resource (e.g. code not in the registry)
409ErrorResponseOnly resolved disputes can be appealed
422ErrorResponseInput failed validation (structural code rules, domains)
429ErrorResponseRequest budget exhausted

Try it (sandbox)

curl -X POST \
  'https://sandbox.n.das.ug/v1/admin/disputes/{dispute_id}/appeal' \
  -H 'X-API-Key: <your sandbox key>'
GET/v1/admin/agents

The field network — per-agent activity and trust scores

Responses

StatusSchemaDescription
200AgentListResponseAgents aggregated from verification events + tasks
401ErrorResponseMissing or invalid credentials
403ErrorResponseAuthenticated but not permitted (scope/tenant)
429ErrorResponseRequest budget exhausted

Try it (sandbox)

curl -X GET \
  'https://sandbox.n.das.ug/v1/admin/agents' \
  -H 'X-API-Key: <your sandbox key>'
GET/v1/admin/field-tasks

List field-agent assignments

Parameters

NameInTypeDescription
agent_idquerystring
statusqueryFieldTaskStatus
pagequeryinteger
limitqueryinteger

Responses

StatusSchemaDescription
200FieldTaskListResponseField tasks, newest first
400ErrorResponseMalformed request
401ErrorResponseMissing or invalid credentials
403ErrorResponseAuthenticated but not permitted (scope/tenant)
429ErrorResponseRequest budget exhausted

Try it (sandbox)

curl -X GET \
  'https://sandbox.n.das.ug/v1/admin/field-tasks' \
  -H 'X-API-Key: <your sandbox key>'
POST/v1/admin/field-tasks

Assign a field agent to an area (audited)

Request body — FieldTaskCreateRequest

FieldTypeDescription
agent_id *string
area_admin_unit_id *stringThe area's public admin-unit id (e.g. adm-parish-bwaise-i)

Responses

StatusSchemaDescription
201FieldTaskResponseThe created assignment
400ErrorResponseMalformed request
401ErrorResponseMissing or invalid credentials
403ErrorResponseAuthenticated but not permitted (scope/tenant)
404ErrorResponseUnknown resource (e.g. code not in the registry)
422ErrorResponseInput failed validation (structural code rules, domains)
429ErrorResponseRequest budget exhausted

Try it (sandbox)

curl -X POST \
  'https://sandbox.n.das.ug/v1/admin/field-tasks' \
  -H 'X-API-Key: <your sandbox key>'
POST/v1/admin/field-tasks/{task_id}/status

Move a field task along its lifecycle (audited)

Parameters

NameInTypeDescription
task_id *pathstring

Request body — FieldTaskStatusRequest

FieldTypeDescription
status *FieldTaskStatus
open · assigned · done

Responses

StatusSchemaDescription
200FieldTaskResponseThe updated task
401ErrorResponseMissing or invalid credentials
403ErrorResponseAuthenticated but not permitted (scope/tenant)
404ErrorResponseUnknown resource (e.g. code not in the registry)
422ErrorResponseInput failed validation (structural code rules, domains)
429ErrorResponseRequest budget exhausted

Try it (sandbox)

curl -X POST \
  'https://sandbox.n.das.ug/v1/admin/field-tasks/{task_id}/status' \
  -H 'X-API-Key: <your sandbox key>'
GET/v1/admin/reports/coverage

Verification coverage per district (or per parish of one district)

Parameters

NameInTypeDescription
districtquerystringDrill into one district (groups become parishes)

Responses

StatusSchemaDescription
200AdminCoverageResponseCoverage/verification statistics
401ErrorResponseMissing or invalid credentials
403ErrorResponseAuthenticated but not permitted (scope/tenant)
429ErrorResponseRequest budget exhausted

Try it (sandbox)

curl -X GET \
  'https://sandbox.n.das.ug/v1/admin/reports/coverage' \
  -H 'X-API-Key: <your sandbox key>'
GET/v1/admin/reports/usage

Usage/audit event volumes (ClickHouse-backed, outbox fallback)

Parameters

NameInTypeDescription
daysqueryinteger

Responses

StatusSchemaDescription
200UsageReportResponsePer-day, per-event-type event volumes; `source` names the store
400ErrorResponseMalformed request
401ErrorResponseMissing or invalid credentials
403ErrorResponseAuthenticated but not permitted (scope/tenant)
429ErrorResponseRequest budget exhausted

Try it (sandbox)

curl -X GET \
  'https://sandbox.n.das.ug/v1/admin/reports/usage' \
  -H 'X-API-Key: <your sandbox key>'
GET/v1/admin/audit-events

Query the immutable audit trail (who did what)

Queries the ClickHouse audit store; when it is unreachable the response falls back to the transactional outbox and says so in `source` — a partial view is never silently presented as the trail.

Parameters

NameInTypeDescription
event_typequerystring
aggregatequerystring
aggregate_idquerystring
principalquerystring
date_fromquerystring
date_toquerystring
pagequeryinteger
limitqueryinteger

Responses

StatusSchemaDescription
200AuditEventListResponseMatching audit events, newest first
400ErrorResponseMalformed request
401ErrorResponseMissing or invalid credentials
403ErrorResponseAuthenticated but not permitted (scope/tenant)
429ErrorResponseRequest budget exhausted

Try it (sandbox)

curl -X GET \
  'https://sandbox.n.das.ug/v1/admin/audit-events' \
  -H 'X-API-Key: <your sandbox key>'
GET/v1/admin/users

NDAS Staff users & roles (Keycloak-backed view; NDAS Admins only)

A read-only projection of the Keycloak `ndas-staff` realm (users + realm roles). Provisioning stays in Keycloak. Requires the `ndas-admin` role; the read is audited. `503` when the identity provider view is unavailable or unconfigured.

Responses

StatusSchemaDescription
200StaffUserListResponseStaff accounts with their realm roles
401ErrorResponseMissing or invalid credentials
403ErrorResponseAuthenticated but not permitted (scope/tenant)
429ErrorResponseRequest budget exhausted
503ErrorResponseIdentity provider view unavailable

Try it (sandbox)

curl -X GET \
  'https://sandbox.n.das.ug/v1/admin/users' \
  -H 'X-API-Key: <your sandbox key>'

boundary

Boundary & Postcode Dashboard — staff tier GIS editing

GET/v1/boundary/changes

List boundary changes (draft → review → publish pipeline)

Parameters

NameInTypeDescription
statusqueryBoundaryChangeStatus
admin_unit_idquerystring
pagequeryinteger
limitqueryinteger

Responses

StatusSchemaDescription
200BoundaryChangeListResponseBoundary changes, newest first (no geometry payloads)
400ErrorResponseMalformed request
401ErrorResponseMissing or invalid credentials
403ErrorResponseAuthenticated but not permitted (scope/tenant)
429ErrorResponseRequest budget exhausted

Try it (sandbox)

curl -X GET \
  'https://sandbox.n.das.ug/v1/boundary/changes' \
  -H 'X-API-Key: <your sandbox key>'
POST/v1/boundary/changes

Draft a boundary change (hand-drawn or dissolve-from-children)

`geometry_edit` takes a GeoJSON Polygon/MultiPolygon drawn in the editor. `dissolve_from_children` computes the geometry server-side as the union of the unit's current children — the standard way to fill missing upper-level (region/county/subcounty) polygons — and rejects a supplied geometry. Both create a reviewable, audited draft; nothing touches the live map until publish.

Request body — BoundaryChangeCreateRequest

FieldTypeDescription
admin_unit_id *string
kindBoundaryChangeKind
geometry_edit · dissolve_from_children
geometrynull | GeoJsonPolygonInputRequired for geometry_edit; forbidden for dissolve
notestring | null

Responses

StatusSchemaDescription
201BoundaryChangeResponseThe created draft (with geometry)
400ErrorResponseMalformed request
401ErrorResponseMissing or invalid credentials
403ErrorResponseAuthenticated but not permitted (scope/tenant)
404ErrorResponseUnknown resource (e.g. code not in the registry)
422ErrorResponseInput failed validation (structural code rules, domains)
429ErrorResponseRequest budget exhausted

Try it (sandbox)

curl -X POST \
  'https://sandbox.n.das.ug/v1/boundary/changes' \
  -H 'X-API-Key: <your sandbox key>'
GET/v1/boundary/changes/{change_id}

Boundary change detail (with geometry + validation report)

Parameters

NameInTypeDescription
change_id *pathstring

Responses

StatusSchemaDescription
200BoundaryChangeResponseThe change, its geometry, and the last validation report
401ErrorResponseMissing or invalid credentials
403ErrorResponseAuthenticated but not permitted (scope/tenant)
404ErrorResponseUnknown resource (e.g. code not in the registry)
429ErrorResponseRequest budget exhausted

Try it (sandbox)

curl -X GET \
  'https://sandbox.n.das.ug/v1/boundary/changes/{change_id}' \
  -H 'X-API-Key: <your sandbox key>'
POST/v1/boundary/changes/{change_id}/submit

Submit a draft for review (topology validation gates it)

Re-runs topology validation and stores the report on the change. A failing report blocks review (`422 geometry_invalid`, status stays `draft`); a passing one moves the change to `in_review`. Audited.

Parameters

NameInTypeDescription
change_id *pathstring

Responses

StatusSchemaDescription
200BoundaryChangeResponseThe change, now in review
401ErrorResponseMissing or invalid credentials
403ErrorResponseAuthenticated but not permitted (scope/tenant)
404ErrorResponseUnknown resource (e.g. code not in the registry)
409ErrorResponseNot a draft
422ErrorResponseInput failed validation (structural code rules, domains)
429ErrorResponseRequest budget exhausted

Try it (sandbox)

curl -X POST \
  'https://sandbox.n.das.ug/v1/boundary/changes/{change_id}/submit' \
  -H 'X-API-Key: <your sandbox key>'
POST/v1/boundary/changes/{change_id}/publish

Publish a reviewed change (NDAS Admins; four-eyes; versioned)

Requires the `ndas-admin` role AND a reviewer different from the author. Re-runs topology validation inside the publish transaction (a neighbouring boundary may have changed since review — TOCTOU guard), then applies the geometry, snapshots the prior geometry on the change, advances the unit's version, and bumps the live boundary data version (cache invalidation). `409` when: not in review, the draft is stale (the unit's version moved since drafting), or topology no longer validates against the current map.

Parameters

NameInTypeDescription
change_id *pathstring

Responses

StatusSchemaDescription
200BoundaryChangeResponseThe published change (prior geometry retained)
401ErrorResponseMissing or invalid credentials
403ErrorResponseAuthenticated but not permitted (scope/tenant)
404ErrorResponseUnknown resource (e.g. code not in the registry)
409ErrorResponseNot in review, the draft is stale, or topology regressed
429ErrorResponseRequest budget exhausted

Try it (sandbox)

curl -X POST \
  'https://sandbox.n.das.ug/v1/boundary/changes/{change_id}/publish' \
  -H 'X-API-Key: <your sandbox key>'
POST/v1/boundary/changes/{change_id}/reject

Reject a change in review (audited)

Parameters

NameInTypeDescription
change_id *pathstring

Request body — BoundaryRejectRequest

FieldTypeDescription
notestring | null

Responses

StatusSchemaDescription
200BoundaryChangeResponseThe rejected change
401ErrorResponseMissing or invalid credentials
403ErrorResponseAuthenticated but not permitted (scope/tenant)
404ErrorResponseUnknown resource (e.g. code not in the registry)
409ErrorResponseNot in review
429ErrorResponseRequest budget exhausted

Try it (sandbox)

curl -X POST \
  'https://sandbox.n.das.ug/v1/boundary/changes/{change_id}/reject' \
  -H 'X-API-Key: <your sandbox key>'
POST/v1/boundary/validate

Ad-hoc topology validation for the editor

Validates a proposed geometry for a unit against its level's invariants — OGC validity (with an auto-repaired geometry offered back), no sibling overlap (borders may touch, interiors may not), containment within the parent. PostGIS/GEOS-backed.

Request body — BoundaryValidateRequest

FieldTypeDescription
admin_unit_id *string
geometry *GeoJsonPolygonInputEditor input — Polygon or MultiPolygon (normalized server-side)
type *string
Polygon · MultiPolygon
coordinates *array<array<object>>

Responses

StatusSchemaDescription
200TopologyReportResponseThe topology report
400ErrorResponseMalformed request
401ErrorResponseMissing or invalid credentials
403ErrorResponseAuthenticated but not permitted (scope/tenant)
404ErrorResponseUnknown resource (e.g. code not in the registry)
422ErrorResponseInput failed validation (structural code rules, domains)
429ErrorResponseRequest budget exhausted

Try it (sandbox)

curl -X POST \
  'https://sandbox.n.das.ug/v1/boundary/validate' \
  -H 'X-API-Key: <your sandbox key>'
GET/v1/boundary/coverage

Coverage intelligence — the heatmap + the fix-next list

Where the map is missing or wrong: per-level geometry coverage, the capped list of units without polygons, registered addresses falling OUTSIDE every parish polygon (per district), parishes awaiting an official postcode, and duplicate admin codes.

Responses

StatusSchemaDescription
200BoundaryCoverageResponseThe coverage report
401ErrorResponseMissing or invalid credentials
403ErrorResponseAuthenticated but not permitted (scope/tenant)
429ErrorResponseRequest budget exhausted

Try it (sandbox)

curl -X GET \
  'https://sandbox.n.das.ug/v1/boundary/coverage' \
  -H 'X-API-Key: <your sandbox key>'
GET/v1/boundary/admin-units/{admin_unit_id}/geometry

One unit's editable geometry (GeoJSON) for the editor

Parameters

NameInTypeDescription
admin_unit_id *pathstring

Responses

StatusSchemaDescription
200UnitGeometryResponseThe unit with its current MultiPolygon (null when missing)
401ErrorResponseMissing or invalid credentials
403ErrorResponseAuthenticated but not permitted (scope/tenant)
404ErrorResponseUnknown resource (e.g. code not in the registry)
429ErrorResponseRequest budget exhausted

Try it (sandbox)

curl -X GET \
  'https://sandbox.n.das.ug/v1/boundary/admin-units/{admin_unit_id}/geometry' \
  -H 'X-API-Key: <your sandbox key>'
GET/v1/boundary/postcodes

Postcode registry oversight (search + paginate)

Parameters

NameInTypeDescription
statusqueryPostcodeRecordStatus
qquerystringPrefix match on code, substring on parish/district
pagequeryinteger
limitqueryinteger

Responses

StatusSchemaDescription
200PostcodeOversightListResponseRegistry rows (official MoICT allocation)
400ErrorResponseMalformed request
401ErrorResponseMissing or invalid credentials
403ErrorResponseAuthenticated but not permitted (scope/tenant)
429ErrorResponseRequest budget exhausted

Try it (sandbox)

curl -X GET \
  'https://sandbox.n.das.ug/v1/boundary/postcodes' \
  -H 'X-API-Key: <your sandbox key>'
GET/v1/boundary/pending-parishes

The pending_allocation queue (parishes without official codes)

Parameters

NameInTypeDescription
qquerystring
pagequeryinteger
limitqueryinteger

Responses

StatusSchemaDescription
200PendingParishListResponseParishes awaiting a MoICT allocation
401ErrorResponseMissing or invalid credentials
403ErrorResponseAuthenticated but not permitted (scope/tenant)
429ErrorResponseRequest budget exhausted

Try it (sandbox)

curl -X GET \
  'https://sandbox.n.das.ug/v1/boundary/pending-parishes' \
  -H 'X-API-Key: <your sandbox key>'
POST/v1/boundary/pending-parishes/{pending_id}/allocate

Record an official MoICT allocation for a pending parish

NDAS never mints postcodes — this RECORDS what MoICT allocated (blueprint §3.3), and is fail-closed on authenticity and permanence: - **Four-eyes:** `ndas-admin` only (not a lone boundary-editor). - **Authenticity:** the code MUST be in the official MoICT allocation (the complete April-2019 schedule plus any loaded refresh master). A well-formed but unofficial code is rejected `422 code_not_official` — a fabricated code can never become a permanent digital address. - **Permanence / one-code-per-parish:** `409 postcode_conflict` if the code is already registered OR the parish already holds a code. On success the code enters the registry (bound to the parish admin-unit by id), the parish leaves the queue, the registry data version bumps (engine + caches reload), and the allocation is audited with the MoICT source reference.

Parameters

NameInTypeDescription
pending_id *pathstring

Request body — PostcodeAllocateRequest

FieldTypeDescription
code *stringThe code MoICT allocated (never minted by NDAS). Validated against the official allocation set — an unofficial code is rejected.
effective_fromstring | null
source_referencestring | nullThe MoICT source-document reference for this allocation (schedule or refresh master). Recorded in the audit event.

Responses

StatusSchemaDescription
201PostcodeAllocateResponseThe recorded allocation
400ErrorResponseMalformed request
401ErrorResponseMissing or invalid credentials
403ErrorResponseAuthenticated but not permitted (scope/tenant)
404ErrorResponseUnknown resource (e.g. code not in the registry)
409ErrorResponseCode already registered, or the parish already has a code
422ErrorResponseMalformed code, bad date, or code not in the official allocation
429ErrorResponseRequest budget exhausted

Try it (sandbox)

curl -X POST \
  'https://sandbox.n.das.ug/v1/boundary/pending-parishes/{pending_id}/allocate' \
  -H 'X-API-Key: <your sandbox key>'

partner

Partner self-service (additive, W5) — the Developer Portal and Partner/MDA Dashboard backend: tenancy, API-key lifecycle, team management, webhooks, usage, invoices, and clerk-recorded consent. All endpoints require a signed-in partner user (OAuth2, ndas-partners realm) whose Keycloak subject is bound to a partner tenant; key / user / webhook management additionally requires the partner-admin role. Plaintext secrets appear exactly once, in the response of the request that created them.

GET/v1/partner/me

The signed-in partner user's tenant, role, and environment

Responses

StatusSchemaDescription
200PartnerProfileResponseTenant profile. In sandbox deployments a first-login user is auto-provisioned a tenant (self-service sandbox); production tenancy is provisioned by NDAS Staff.
401ErrorResponseMissing or invalid credentials
403ErrorResponseAuthenticated but not permitted (scope/tenant)
429ErrorResponseRequest budget exhausted

Try it (sandbox)

curl -X GET \
  'https://sandbox.n.das.ug/v1/partner/me' \
  -H 'X-API-Key: <your sandbox key>'
GET/v1/partner/keys

List the tenant's API keys and production key requests

Responses

StatusSchemaDescription
200PartnerKeyListResponseKeys (hashed at rest; prefix only) + open requests
401ErrorResponseMissing or invalid credentials
403ErrorResponseAuthenticated but not permitted (scope/tenant)
429ErrorResponseRequest budget exhausted

Try it (sandbox)

curl -X GET \
  'https://sandbox.n.das.ug/v1/partner/keys' \
  -H 'X-API-Key: <your sandbox key>'
POST/v1/partner/keys

Issue a sandbox key instantly, or file a production key request

partner-admin only. `sandbox: true` (default) mints immediately and returns the plaintext ONCE. `sandbox: false` files a request that NDAS Staff approve; the partner then claims the approved request (the plaintext is minted in the partner's own session — staff never see it). Sandbox deployments always mint sandbox keys regardless of the flag. `kyc.match` can never be carried by an API key.

Request body — CreatePartnerKeyRequest

FieldTypeDescription
name *string
scopes *array<string>kyc.match is OAuth-only and can never be carried by a key
sandboxboolean
justificationstringRequired context for production key requests

Responses

StatusSchemaDescription
201PartnerKeyIssuedResponseSandbox key minted — plaintext shown once
202KeyRequestFiledResponseProduction key request filed (await staff decision)
400ErrorResponseMalformed request
401ErrorResponseMissing or invalid credentials
403ErrorResponseAuthenticated but not permitted (scope/tenant)
422ErrorResponseInput failed validation (structural code rules, domains)
429ErrorResponseRequest budget exhausted

Try it (sandbox)

curl -X POST \
  'https://sandbox.n.das.ug/v1/partner/keys' \
  -H 'X-API-Key: <your sandbox key>'
POST/v1/partner/keys/{key_id}/rotate

Rotate a key (revoke + mint a successor with the same scopes)

Parameters

NameInTypeDescription
key_id *pathstringThe API key id (tenant-scoped)

Responses

StatusSchemaDescription
201PartnerKeyIssuedResponseSuccessor key — plaintext shown once
401ErrorResponseMissing or invalid credentials
403ErrorResponseAuthenticated but not permitted (scope/tenant)
404ErrorResponseUnknown resource (e.g. code not in the registry)
429ErrorResponseRequest budget exhausted

Try it (sandbox)

curl -X POST \
  'https://sandbox.n.das.ug/v1/partner/keys/{key_id}/rotate' \
  -H 'X-API-Key: <your sandbox key>'
POST/v1/partner/keys/{key_id}/revoke

Revoke a key immediately

Parameters

NameInTypeDescription
key_id *pathstringThe API key id (tenant-scoped)

Responses

StatusSchemaDescription
200PartnerKeyResponseThe revoked key record
401ErrorResponseMissing or invalid credentials
403ErrorResponseAuthenticated but not permitted (scope/tenant)
404ErrorResponseUnknown resource (e.g. code not in the registry)
429ErrorResponseRequest budget exhausted

Try it (sandbox)

curl -X POST \
  'https://sandbox.n.das.ug/v1/partner/keys/{key_id}/revoke' \
  -H 'X-API-Key: <your sandbox key>'
POST/v1/partner/key-requests/{request_id}/claim

Claim an approved production key request (mints the key once)

Parameters

NameInTypeDescription
request_id *pathstring

Responses

StatusSchemaDescription
201PartnerKeyIssuedResponseProduction key minted — plaintext shown once
401ErrorResponseMissing or invalid credentials
403ErrorResponseAuthenticated but not permitted (scope/tenant)
404ErrorResponseUnknown resource (e.g. code not in the registry)
409ErrorResponseRequest not in a claimable state (request_state_invalid)
429ErrorResponseRequest budget exhausted

Try it (sandbox)

curl -X POST \
  'https://sandbox.n.das.ug/v1/partner/key-requests/{request_id}/claim' \
  -H 'X-API-Key: <your sandbox key>'
GET/v1/partner/users

List the tenant's team members

Responses

StatusSchemaDescription
200PartnerUserListResponseTeam members (partner-admin / partner-clerk)
401ErrorResponseMissing or invalid credentials
403ErrorResponseAuthenticated but not permitted (scope/tenant)
429ErrorResponseRequest budget exhausted

Try it (sandbox)

curl -X GET \
  'https://sandbox.n.das.ug/v1/partner/users' \
  -H 'X-API-Key: <your sandbox key>'
POST/v1/partner/users

Add a team member (partner-admin invites partner-clerk)

Request body — AddPartnerUserRequest

FieldTypeDescription
subject *string
rolestring
admin · clerk
labelstring

Responses

StatusSchemaDescription
201PartnerUserResponseMember added
400ErrorResponseMalformed request
401ErrorResponseMissing or invalid credentials
403ErrorResponseAuthenticated but not permitted (scope/tenant)
422ErrorResponseInput failed validation (structural code rules, domains)
429ErrorResponseRequest budget exhausted

Try it (sandbox)

curl -X POST \
  'https://sandbox.n.das.ug/v1/partner/users' \
  -H 'X-API-Key: <your sandbox key>'
POST/v1/partner/users/{user_id}/remove

Remove a team member (cannot remove yourself)

Parameters

NameInTypeDescription
user_id *pathstring

Responses

StatusSchemaDescription
200RemovedResponseMember removed
401ErrorResponseMissing or invalid credentials
403ErrorResponseAuthenticated but not permitted (scope/tenant)
404ErrorResponseUnknown resource (e.g. code not in the registry)
429ErrorResponseRequest budget exhausted

Try it (sandbox)

curl -X POST \
  'https://sandbox.n.das.ug/v1/partner/users/{user_id}/remove' \
  -H 'X-API-Key: <your sandbox key>'
GET/v1/partner/webhooks

List webhook endpoints (signing secrets are never re-shown)

Responses

StatusSchemaDescription
200WebhookListResponseConfigured webhook endpoints
401ErrorResponseMissing or invalid credentials
403ErrorResponseAuthenticated but not permitted (scope/tenant)
429ErrorResponseRequest budget exhausted

Try it (sandbox)

curl -X GET \
  'https://sandbox.n.das.ug/v1/partner/webhooks' \
  -H 'X-API-Key: <your sandbox key>'
POST/v1/partner/webhooks

Register a webhook endpoint (HMAC secret shown once)

Deliveries are signed with `X-NDAS-Signature` (HMAC-SHA256 over the raw body using the returned secret). HTTPS destinations only.

Request body — CreateWebhookRequest

FieldTypeDescription
url *string
events *array<string>

Responses

StatusSchemaDescription
201WebhookCreatedResponseWebhook registered — secret shown once
400ErrorResponseMalformed request
401ErrorResponseMissing or invalid credentials
403ErrorResponseAuthenticated but not permitted (scope/tenant)
422ErrorResponseInput failed validation (structural code rules, domains)
429ErrorResponseRequest budget exhausted

Try it (sandbox)

curl -X POST \
  'https://sandbox.n.das.ug/v1/partner/webhooks' \
  -H 'X-API-Key: <your sandbox key>'
POST/v1/partner/webhooks/{webhook_id}/delete

Delete a webhook endpoint

Parameters

NameInTypeDescription
webhook_id *pathstring

Responses

StatusSchemaDescription
200DeletedResponseWebhook deleted
401ErrorResponseMissing or invalid credentials
403ErrorResponseAuthenticated but not permitted (scope/tenant)
404ErrorResponseUnknown resource (e.g. code not in the registry)
429ErrorResponseRequest budget exhausted

Try it (sandbox)

curl -X POST \
  'https://sandbox.n.das.ug/v1/partner/webhooks/{webhook_id}/delete' \
  -H 'X-API-Key: <your sandbox key>'
GET/v1/partner/usage

Usage aggregates (per day / per key / per endpoint)

Served from the ClickHouse metering store; when it is unreachable the endpoint fails SOFT to daily Postgres rollups (`source: rollup`) with calls only — error/latency fields are null.

Parameters

NameInTypeDescription
fromquerystring
toquerystring

Responses

StatusSchemaDescription
200UsageSummaryResponseUsage aggregates for the requested range (default 30d)
401ErrorResponseMissing or invalid credentials
403ErrorResponseAuthenticated but not permitted (scope/tenant)
422ErrorResponseInput failed validation (structural code rules, domains)
429ErrorResponseRequest budget exhausted

Try it (sandbox)

curl -X GET \
  'https://sandbox.n.das.ug/v1/partner/usage' \
  -H 'X-API-Key: <your sandbox key>'
GET/v1/partner/invoices

The tenant's invoices, newest first

Responses

StatusSchemaDescription
200InvoiceListResponseInvoices
401ErrorResponseMissing or invalid credentials
403ErrorResponseAuthenticated but not permitted (scope/tenant)
429ErrorResponseRequest budget exhausted

Try it (sandbox)

curl -X GET \
  'https://sandbox.n.das.ug/v1/partner/invoices' \
  -H 'X-API-Key: <your sandbox key>'
POST/v1/partner/consents

Record an explicitly captured consent (clerk verification tool)

The clerk attests the data subject granted consent for an address verification of the named address (Uganda DPPA 2019). The record stores hashed/normalized subject identifiers only — a raw NIN is rejected before any processing. The returned `consent_reference` is quoted by the subsequent `/v1/kyc/address-match` call, which enforces BOTH the address-bounded and subject-bounded scope of the consent. Recording is audited.

Request body — RecordConsentRequest

FieldTypeDescription
digital_address *DigitalAddressThe frozen national format: UG- + 4-digit premise + - + 5-digit postcode. All numeric, nothing appended, no check digit.
subject *objectThe data subject granting consent. Identifiers are stored hashed/normalized only; a raw NIN is rejected with 422.
full_namestring
nin_tokenstring
phonestring
expires_in_daysinteger

Responses

StatusSchemaDescription
201ConsentReceiptResponseConsent recorded
400ErrorResponseMalformed request
401ErrorResponseMissing or invalid credentials
403ErrorResponseAuthenticated but not permitted (scope/tenant)
422ErrorResponseInput failed validation (structural code rules, domains)
429ErrorResponseRequest budget exhausted

Try it (sandbox)

curl -X POST \
  'https://sandbox.n.das.ug/v1/partner/consents' \
  -H 'X-API-Key: <your sandbox key>'