Build on Hakiki
A single HTTPS API for consent-first KYC and KYB across Tanzania's identity, business, tax, banking, and mobile-money sources. Server-to-server. Audit-ready.
#Overview
Every verification names a subject (a person or business), the checks to run against connected sources, and the consent that authorises the lookup. The response carries an outcome for each check, a risk score, and a decision.
#Sandbox status
All sources return deterministic mock data. No data is fetched from NIDA, BRELA, TRA, banks, or mobile-money operators.
telco_identity— sandbox mocknida— sandbox mockbrela— sandbox mocktra— sandbox mockbank_account— coming soon
#Authentication
All /v1/* endpoints require an API key in the X-API-Key header. Keys are scoped to an environment (test or live) and to an organization.
curl https://api.hakiki.co.tz/v1/sources \
-H "X-API-Key: demo-public-key"X-API-Keystringrequireddemo-public-key.#Create verification
/v1/verificationsSubmit a subject with one or more checks to perform. Consent is required on every request.
curl -X POST http://localhost:8080/v1/verifications \
-H "Content-Type: application/json" \
-H "X-API-Key: demo-public-key" \
-d '{
"subject": {
"kind": "person",
"reference": "19900101123456789",
"full_name": "Amina Juma",
"phone": "+255712345678"
},
"checks": ["telco_identity", "nida"],
"consent": {
"method": "ussd_otp",
"granted_at": "2026-07-04T12:00:00Z"
}
}'Body parameters
subject.kind"person" | "business"requiredsubject.referencestringrequiredsubject.full_namestringsubject.phonestringtelco_identity is requested.checksstring[]requiredconsent.methodstringrequiredussd_otp, web_form, in_branch).consent.granted_atISO-8601 datetimerequired{
"id": "b12a8e3f-1a4f-4d6b-9b6a-7e7d4a1c5a92",
"status": "completed",
"subject": {
"kind": "person",
"reference": "19900101123456789",
"full_name": "Amina Juma",
"phone": "+255712345678"
},
"checks": [
{
"check_kind": "telco_identity",
"outcome": "match",
"confidence": 0.92,
"message": "SIM registration matches subject identity"
},
{
"check_kind": "nida",
"outcome": "match",
"confidence": 0.88,
"message": "NIDA record found for reference"
}
],
"decision": "approved",
"risk_score": 12,
"risk_level": "low",
"environment": "test",
"created_at": "2026-07-04T12:00:01Z",
"completed_at": "2026-07-04T12:00:02Z"
}#Get verification
/v1/verifications/{id}Retrieve a previously created verification, including its full audit timeline and webhook deliveries.
curl http://localhost:8080/v1/verifications/b12a8e3f-1a4f-4d6b-9b6a-7e7d4a1c5a92 \
-H "X-API-Key: demo-public-key"iduuidrequiredPOST /v1/verifications.#Sources
Each check_kind routes to a specific upstream source. Use the values below in your request body.
| Check | Category | Status | Supports |
|---|---|---|---|
telco_identity Telco Identity | telco | Sandbox | person |
nida National ID (NIDA) | identity | Sandbox | person |
brela Business Registry (BRELA) | business | Sandbox | business |
tra Tax Authority (TRA) | tax | Sandbox | person, business |
bank_account Bank Account | banking | Coming soon | person, business |
Outcomes
matchno_matchpendingunsupported#Risk & decisions
Every completed verification carries a deterministic risk_score (0–100) and a decision. The engine is transparent and rule-based — the same inputs always produce the same output.
approvedneeds_reviewrejectedThe verification detail screen renders a "Risk Explanation" card that surfaces which checks and flags drove the decision. This is derived from the engine output, not a separate AI service.
#Idempotency
Retry network calls safely by sending an Idempotency-Key header on POST /v1/verifications. The same key will return the original response within a 24-hour window.
curl -X POST http://localhost:8080/v1/verifications \
-H "Content-Type: application/json" \
-H "X-API-Key: demo-public-key" \
-H "Idempotency-Key: signup-flow-8f2b1c-7a3e" \
-d '{ "subject": { ... }, "checks": ["telco_identity"], "consent": { ... } }'409 Conflict. Use a new key for each distinct verification request.#Webhooks
Subscribe to verification lifecycle events. Every delivery is signed with HMAC-SHA256 in the Hakiki-Signature header.
Event types
verification.createdverification.completedverification.approvedverification.rejectedreview.openedreview.resolvedVerifying signatures
import crypto from 'node:crypto'
export function verifyHakikiSignature(rawBody, header, secret) {
if (!header) return false
const [tsPart, sigPart] = header.split(',')
const ts = tsPart?.split('=')[1]
const sig = sigPart?.split('=')[1]
if (!ts || !sig) return false
const expected = crypto
.createHmac('sha256', secret)
.update(ts + '.' + rawBody)
.digest('hex')
return crypto.timingSafeEqual(Buffer.from(sig), Buffer.from(expected))
}#Rate limits
Rate limits are applied per API key. The current default is 60 requests / minute (configurable via HAKIKI_RATE_LIMIT_PER_MINUTE).
HTTP/1.1 429 Too Many Requests
X-RateLimit-Limit: 60
X-RateLimit-Remaining: 0
X-RateLimit-Reset: 1717598400
Retry-After: 12#Errors
All errors return a JSON body with a stable error string.
{
"error": "missing_field:checks"
}| Status | When |
|---|---|
| 400 | Missing or invalid field in the request body |
| 401 | Missing or invalid X-API-Key |
| 409 | Idempotency-Key reuse with a different request body |
| 422 | Requested source not enabled in this environment |
| 429 | Rate limit exceeded |
| 500 | Unexpected server error — safe to retry |
Ready to try it?
Sign in to the sandbox dashboard, create a verification, and inspect the full audit trail.