Ogowkey v1
← All articles
12 min read Ogowkey team

Integrating with NIRA: a developer's guide to verifying Somali IDs against the national registry

How to integrate with the NIRA national registry to verify Somali IDs - auth, ePIN lookup, biometric match, error handling, and reference Python + TypeScript code.

nirasomaliaapideveloperidentity

The National Identification and Registration Authority (NIRA) is the authoritative registry of legal identities in Somalia. If you operate a regulated financial service, a SIM-registration desk, or any KYC-bearing product in Somalia, NIRA is the source of truth that turns a presented ID card from a piece of plastic into a verifiable claim about a real person.

This guide is the developer-side companion to our Somali national ID verification deep-dive. We cover what the NIRA API actually exposes, how authentication works, the request/response shape, error semantics, and how to integrate cleanly in Python and TypeScript with retry, timeout and audit behaviour worth shipping to production.

What NIRA is, briefly

NIRA was established by the parliamentary act of 2020 and began civilian enrolment in late 2022. The authority operates:

  • A national biometric enrolment system that captures fingerprints, iris and a face image at enrolment.
  • The polycarbonate national ID card, issued against that enrolment.
  • A central registry that maps a national ID number to the enrolled biometric data.
  • A verification API that licensed entities can query to confirm whether a card and a fresh biometric capture match the registered record.

The verification API is what we focus on here. Onboarding to it requires a Memorandum of Understanding (MoU) with NIRA, signed at the institutional level. We won't walk through the legal side - your compliance lead and NIRA's onboarding team handle that - but every technical detail below assumes the MoU is in place and your service has been issued production credentials.

For the public-facing context and the latest official guidance, see nira.gov.so.

The flow at a high level

The NIRA verification flow is a request-response between your service and NIRA, with a fresh biometric capture from the user as the active credential.

User deviceYour serviceOgowkey / NIRANIRA registry1. ID image + selfie2. POST /v1/nira/verify3. ePIN lookup + biometric match4. signed result5. { outcome, score }6. result to user

The numbered steps:

  1. The user device captures the ID and a fresh selfie under your capture SDK.
  2. Your service forwards both to Ogowkey (or your direct NIRA integration if you're licensed for that).
  3. The intermediary calls NIRA's API with the ID number plus the captured biometric.
  4. NIRA returns a signed match result.
  5. The intermediary unpacks and returns the decision to your service.
  6. Your service makes the call (open the account, allow the transaction, etc.) and returns to the user.

The whole roundtrip should complete in under 800ms p95 when everything is healthy.

Authentication

NIRA uses mutual TLS for transport-layer authentication, plus signed request bodies for application-layer authentication.

Mutual TLS

You receive a client certificate and a key from NIRA during onboarding. Every outbound request uses these. Pin NIRA's server certificate on your side - the public cert is published on nira.gov.so - to prevent man-in-the-middle attacks.

In Python with httpx:

import httpx

client = httpx.AsyncClient(
 base_url="https://api.nira.gov.so/v1",
 cert=("/secrets/nira-client.pem", "/secrets/nira-client.key"),
 verify="/secrets/nira-server-ca.pem",
 timeout=httpx.Timeout(connect=2.0, read=5.0, write=2.0, pool=2.0),
)

In Node.js with undici:

import { Agent, fetch } from 'undici';
import { readFileSync } from 'node:fs';

const dispatcher = new Agent({
 connect: {
 cert: readFileSync('/secrets/nira-client.pem'),
 key: readFileSync('/secrets/nira-client.key'),
 ca: readFileSync('/secrets/nira-server-ca.pem'),
 rejectUnauthorized: true
 }
});

const res = await fetch('https://api.nira.gov.so/v1/verify', {
 method: 'POST',
 dispatcher,
 headers: { 'content-type': 'application/json' },
 body: JSON.stringify(payload)
});

Signed request bodies

Every request body is signed with a per-integration HMAC-SHA256 key issued by NIRA. The signature is sent in an X-NIRA-Signature header. NIRA verifies the signature before doing any work, so a request that doesn't sign correctly fails fast with a 401.

import hashlib
import hmac
import time

def sign(body: bytes, secret: bytes) -> str:
 ts = str(int(time.time()))
 payload = ts.encode() + b"." + body
 mac = hmac.new(secret, payload, hashlib.sha256).hexdigest()
 return f"t={ts}, v1={mac}"

The verified timestamp prevents replay attacks; NIRA rejects signatures older than 5 minutes.

The verify endpoint

The core operation is POST /v1/verify. Request body:

{
 "national_id": "8989012345",
 "biometric": {
 "type": "selfie",
 "image_base64": "/9j/4AAQSkZJRgABA...",
 "format": "jpeg"
 },
 "purpose": "kyc_onboarding",
 "reference": "req_2026_05_18_a4b2c"
}

Field semantics:

  • national_id - the 10-digit ID number printed on the card. Strip any spaces or dashes before sending.
  • biometric.type - selfie, fingerprint, or iris. Most fintech flows use selfie because it's the only modality consumer phones support.
  • biometric.image_base64 - the captured image, base64-encoded. JPEG at 80–95% quality is the sweet spot; PNG works too but adds size for no accuracy gain.
  • purpose - a machine-readable enum from NIRA's purpose taxonomy. Common values: kyc_onboarding, account_recovery, sim_registration, payment_authorization.
  • reference - your own correlation ID, returned verbatim in the response. Use it to tie the NIRA call to your internal audit log.

Successful response (HTTP 200):

{
 "outcome": "match",
 "score": 0.94,
 "registry_status": "active",
 "verified_at": "2026-05-18T14:22:31Z",
 "reference": "req_2026_05_18_a4b2c",
 "signature": "MEUCIQDx...",
 "expires_at": "2026-05-18T14:32:31Z"
}

outcome values:

  • match - the biometric matches the registered template with sufficient confidence.
  • no_match - the ID number exists in the registry but the biometric doesn't match.
  • not_found - no record exists for that ID number.
  • revoked - the record exists but has been revoked (death certificate, fraud finding, etc.).

Always check outcome, not just HTTP status. A 200 response with no_match is a failed verification, not a successful one.

The signature is over the entire response body, signed with NIRA's private key. Verify it against NIRA's published public key before trusting the decision - this is the non-repudiation property that makes the response defensible in audit.

Error handling and retries

NIRA returns standard HTTP error codes plus a structured error body:

{
 "error": "rate_limited",
 "code": "RATE_001",
 "details": { "limit_per_minute": 60, "retry_after_seconds": 8 }
}

Retry policy that handles real-world failure modes:

import asyncio
import httpx
from typing import Any

RETRYABLE_CODES = {408, 429, 502, 503, 504}

async def verify_with_retry(payload: dict[str, Any], max_attempts: int = 3) -> httpx.Response:
 for attempt in range(max_attempts):
 try:
 res = await client.post(
 "/verify",
 json=payload,
 headers={"X-NIRA-Signature": sign(...)},
 )
 except (httpx.ConnectTimeout, httpx.ReadTimeout) as exc:
 if attempt == max_attempts - 1:
 raise
 await asyncio.sleep(0.4 * (2 ** attempt))
 continue

 if res.status_code in RETRYABLE_CODES:
 ra = int(res.headers.get("retry-after", 1))
 if attempt == max_attempts - 1:
 return res
 await asyncio.sleep(min(ra, 10))
 continue

 return res

A few non-obvious points:

  • Idempotency. A retried request must be safe to retry. NIRA dedupes by reference for 24 hours, so re-sending the same reference returns the cached prior result rather than billing you twice. Always set a reference and reuse it on retry.
  • Open circuit breaker. If NIRA is down, do not block your sign-up flow indefinitely. After 3 failed attempts, fall back to a degraded mode - accept the customer at a lower KYC tier with a flag in your audit log to re-verify when NIRA returns.
  • Async, not sync. Don't await the NIRA call inline in a critical-path request. Push it to a background worker, mark the user as pending_verification, and notify your front-end when the result lands. Average latency is fine; tail latency is the killer.

A complete Python integration

A minimal but production-shaped integration in FastAPI:

from fastapi import FastAPI, HTTPException
from pydantic import BaseModel, Field
import httpx, hmac, hashlib, time, base64, secrets, asyncio

app = FastAPI()
client = httpx.AsyncClient(
 base_url="https://api.nira.gov.so/v1",
 cert=("/secrets/nira-client.pem", "/secrets/nira-client.key"),
 verify="/secrets/nira-server-ca.pem",
 timeout=httpx.Timeout(connect=2.0, read=5.0),
)
NIRA_SECRET = open("/secrets/nira-hmac.key", "rb").read().strip()


class VerifyRequest(BaseModel):
 national_id: str = Field(min_length=10, max_length=10)
 selfie_b64: str
 purpose: str = "kyc_onboarding"


def sign(body: bytes) -> str:
 ts = str(int(time.time()))
 mac = hmac.new(NIRA_SECRET, ts.encode() + b"." + body, hashlib.sha256).hexdigest()
 return f"t={ts}, v1={mac}"


@app.post("/api/kyc/verify")
async def verify(req: VerifyRequest):
 body = {
 "national_id": req.national_id,
 "biometric": {"type": "selfie", "image_base64": req.selfie_b64, "format": "jpeg"},
 "purpose": req.purpose,
 "reference": f"req_{secrets.token_hex(8)}",
 }
 body_bytes = httpx._content.json.dumps(body).encode()
 res = await client.post(
 "/verify",
 content=body_bytes,
 headers={
 "X-NIRA-Signature": sign(body_bytes),
 "content-type": "application/json",
 },
 )
 if res.status_code >= 500:
 raise HTTPException(503, "Registry unavailable")
 data = res.json()
 return {
 "verified": data["outcome"] == "match",
 "outcome": data["outcome"],
 "score": data.get("score"),
 "reference": data["reference"],
 }

Hand-rolling this is fine for small operations. For most teams, going through an aggregator like Ogowkey gets you the same result with the credential-management, retry, audit and SDK ergonomics pre-built - see the Ogowkey docs for the API surface.

TypeScript SDK example

The same call through the Ogowkey TypeScript SDK:

import { Ogowkey } from '@ogowkey/sdk';

const client = new Ogowkey({ apiKey: process.env.OGOWKEY_KEY! });

const result = await client.identity.verify({
 nationalId: '8989012345',
 selfie: { imageBase64: selfieB64, format: 'jpeg' },
 documentImage: { imageBase64: idCardB64, format: 'jpeg' },
 purpose: 'kyc_onboarding'
});

if (result.outcome === 'approved' && result.niraMatch === true) {
 await db.users.update({ id: userId, verified: true, niraReference: result.niraReference });
}

Under the hood this runs NIRA verification, document OCR, MRZ validation, face match and AML screening in parallel, returning a single decision JSON.

Audit and replay

NIRA verification is auditable. The signed response body is your evidence. Store, at minimum:

  • The request body (with the biometric redacted to a hash for storage minimisation).
  • The full signed response.
  • The reference, NIRA's signature, and the public key fingerprint used to verify it.
  • Timestamps for issue, response, and your downstream decision.

For the broader audit-log architecture, see On-continent data residency for African fintechs - the storage requirements interact with where this data is allowed to live.

Common integration mistakes

Three patterns we see:

Synchronous calls on the critical path

Putting the NIRA call inside a user-facing HTTP request is fine when NIRA is healthy and a problem when it isn't. Queue the verification and return a "verification in progress" state. Push the result via WebSocket or SSE when it lands.

Caching biometric results aggressively

Tempting because biometrics are expensive. Dangerous because the value of NIRA verification is that it's fresh. A 24-hour cache on a SIM swap or fraud attempt defeats the point. Cache the registry status (active / revoked) for ~6 hours; do not cache the biometric match.

Ignoring revoked

A revoked outcome means NIRA has determined the record should no longer be honoured - death certificate filed, fraud finding, duplicate enrolment. Treating this as a match because the score is high is a compliance failure. The decision tree should branch on outcome first, score second.

Closing

NIRA is a remarkably solid piece of national infrastructure for an economy at Somalia's stage, and integrating with it well is a competitive advantage for fintechs. Get the auth right, handle errors with patience, store audit trails, and treat the freshness of the verification as a feature, not an inconvenience.

For the document-side details that pair with this API, see our Somali national ID verification deep-dive. For the broader KYC operating picture, the KYC in Somalia guide is the entry point.

If you want a sanity check on your NIRA integration, run it through the playground or [reach out](mailto:olow304@gmail.com?subject=Ogowkey%20 - %20NIRA%20integration).