SaKi API Reference
Integrate a provider or insurer system with SaKi. This reference covers both directions: the REST API your system calls on SaKi, and the member API SaKi calls on an insurer's own system for realtime coordination of benefits.
Overview
SaKi uses a hybrid data model. Member identity (name, phone, cover) is cached inside SaKi and served fast; the benefit position (COB) changes constantly and is never cached — it is fetched live from the insurer's own system on every lookup. So the identity is local; the money is realtime.
Every figure that could be mistaken for a live confirmation carries a source field, so a demo or cached value is never confused for a real one.
Inbound → SaKi REST API
What your provider or insurer system calls on SaKi: member lookup, opening an admission, the patient letter, SHA/SHIF, and eTIMS verification.
Outbound → Your insurer API
What SaKi calls on an insurer's own system: two read-only endpoints returning member identity and the realtime benefit position.
Base URL
https://saki.co.keAll request and response bodies are JSON. All timestamps are ISO-8601 UTC. Currency is Kenyan Shillings (KSh), sent as plain numbers.
Authentication
Every endpoint (except the public patient letter) requires a bearer token:
Authorization: Bearer <access_token>Obtain one from POST /api/auth/login-any, which resolves any portal (provider, insurer, TPA) by email. Access tokens live 15 minutes; refresh with POST /api/auth/refresh.
curl -s https://saki.co.ke/api/auth/login-any \
-H "Content-Type: application/json" \
-d '{"email":"desk@nabuala.co.ke","password":"••••••••"}'{
"access_token": "eyJhbGciOi...",
"refresh_token": "eyJhbGciOi...",
"portal": "provider",
"expires_in": 900
}Errors
Errors use standard HTTP status codes with a JSON detail string. An invalid-but-well-formed result (an unverifiable eTIMS invoice, an unreachable insurer) is returned as a 200 with a status field, not an error — the caller always gets something to show the user.
| Status | Meaning |
|---|---|
| 401 | Missing, invalid, or expired token |
| 403 | Provider is not linked to the selected insurer |
| 404 | No active member matches (the number is never echoed back, so the endpoint cannot be used to enumerate members) |
| 409 | Conflict — e.g. an admission form already submitted |
| 422 | No lookup key given, or no member and no phone on an admission |
| 429 | Rate or usage quota exceeded |
Member lookup
Resolve a member of a given insurer by membership number or national ID. Returns a de-identified view — the phone is masked, and the national ID and SHA number are never included — plus a realtime COB snapshot from the insurer's system.
| Query param | Required | Description |
|---|---|---|
| insurer_org_id | yes | Which insurer the member belongs to. The caller must hold an active link to it. |
| member_number | one of | The membership number, e.g. SAKI-0002 |
| id_number | one of | The member's national ID |
member_number is tried first when both are given. The member is resolved under the insurer's tenant — which is why the insurer must be named and the link is checked.
curl -s "https://saki.co.ke/api/members/lookup?insurer_org_id=$ORG&member_number=SAKI-0002" \
-H "Authorization: Bearer $TOKEN"200 response
{
"member_id": "e6776f37-b30a-451c-9457-8b327b33b7c2",
"member_number": "SAKI-0002",
"full_name": "George",
"phone_masked": "+2547•••••298",
"has_phone": true,
"scheme": "Standard",
"relationship": "principal",
"cover_limit_kes": 500000.0,
"copay_kes": 500.0,
"status": "active",
"has_sha_link": true,
"cob": {
"available": true,
"source": "live",
"sha_benefit_cap": 1100000,
"sha_used_ytd": 981673,
"sha_remaining": 118327,
"private_limit_kes": 500000,
"private_used_kes": 421584,
"private_remaining_kes": 78416,
"as_of": "2026-08-25T09:08:41Z"
}
}Fields deliberately absent: phone (full), email, id_number, sha_number. The provider gets what they need to confirm the right person and see the benefit position — nothing they could misuse.
The cob object always carries available and source:
- live a real figure from the insurer's system, with
as_of - mock a demo figure (insurer not yet integrated); carries a
note available: false— the insurer's system was unreachable; the lookup still returns 200 so the provider can proceed, they just get no live split
Open an admission
Open an admission and dispatch the patient form / consent SMS. Identify the member by number or ID — SaKi resolves the name and the phone on file and sends the SMS to the registered number.
{
"member_number": "SAKI-0002",
"insurer": "Saki Insurance",
"insurer_org_id": "aa63c9ab-51e4-4744-8cf5-278ecf996271",
"provider_type": "hospital",
"service_description": "Appendicectomy"
}member_id_number may be used instead of member_number. When a member resolves, patient_name, membership_number, copay and cover_limit are filled from the registry, and the SMS goes to the registered phone.
200 response
{
"token": "…",
"form_link": "https://saki.co.ke/encounter/…",
"status": "link_sent",
"sms_status": "sent",
"admission_id": "ce830126-…"
}The provider must hold an active link to insurer_org_id, else 403. If no member resolves, 404.
Patient letter
Public — no login. When an officer reviews and sends a pre-authorisation decision, the member receives a tokenised link to their own reviewed letter, durable for the appeal window (30 days). The link token is the credential (HMAC-signed, expiring, phone-bound); it resolves to exactly one letter, whose hash — never the token — is what the database holds.
{
"subject": "Pre-Authorisation — Declined",
"body": "Dear George, ...",
"decision": "declined",
"patient_name": "George",
"pa_reference": "PA-9",
"issued_at": "2026-08-25T18:08:52Z"
}Opening it records first_viewed_at and increments view_count — evidence the member received it, without storing a raw IP. A bad, expired, or wrong-purpose token returns 404 (never distinguishing which).
SHA / SHIF — coordination of benefits
COB answers: of this bill, how much should SHA/SHIF carry before the private insurer pays? The member's SHA number links to their record in the national register.
| Endpoint | Purpose |
|---|---|
| GET /api/agents/sha/member/{sha_number} | Benefit lookup by SHA number |
| GET /api/agents/sha/facility/{code} | Facility accreditation status |
| POST /api/agents/sha/double-billing-check | Flag SHA + private overlap |
| GET /api/agents/sha/mode | Which SHA_API_MODE is active |
Every COB response carries source = the active mode (live / manual / mock), so a caller can never mistake a demo figure for a live SHA confirmation.
KRA eTIMS — invoice verification
A claim is backed by a KRA eTIMS tax invoice, identified by a CUIN (Control Unit Invoice Number). Verifying it confirms the invoice is real before an insurer pays against it.
curl -s -X POST https://saki.co.ke/api/etims/verify \
-H "Authorization: Bearer $TOKEN" -H "Content-Type: application/json" \
-d '{"cuin":"KRAMWX20260814ABCD1234","claim_ref":"CLM-1","amount":150000}'The status names exactly what was confirmed:
| status | Meaning |
|---|---|
| verified_live | KRA confirmed the invoice; amount reconciled |
| format_ok_mock | Format valid, not verified against KRA (mock mode) |
| invalid_format | Not a well-formed CUIN |
| not_found | Live mode: KRA has no such invoice |
| amount_mismatch | Live mode: claim amount ≠ KRA's recorded total |
| kra_unreachable | Live mode: KRA did not respond |
An invalid or unverifiable CUIN is a result (HTTP 200,valid:false), not an error. Also: GET /api/etims/mode andGET /api/etims/log.
Your insurer API — what SaKi calls
For realtime COB, SaKi calls your system. If you are an insurer, you expose two read-only endpoints and give SaKi a base URL and a bearer key. SaKi sends the key on every request; it is your secret, stored encrypted at rest in SaKi and never shown again.
| Endpoint | Purpose |
|---|---|
| GET /member/{member_number} | Member identity (for SaKi's cache sync) |
| GET /member/{member_number}/cob | Realtime benefit position (COB) |
Authorization: Bearer <your-key> and times out after 6 seconds. A timeout or 5xx degrades to an "unavailable" COB result — it never blocks the member lookup. Return 404 for an unknown member.The member key in the path is what the provider typed — it may be a membership number or a national ID. Resolve either.
Member identity
Return the member's identity. No live balances here — this is what SaKi caches.
{
"member_number": "SAKI-0002",
"full_name": "George",
"id_number": "30010002",
"phone": "+254720728298",
"email": "george@example.com",
"scheme": "Standard",
"cover_limit_kes": 500000,
"sha_number": "SHIF-00400102",
"status": "active"
}Realtime COB
Return the member's live benefit position, recomputed each call — that live figure is the whole point of the integration. The SHA block is optional (omit it for a member with no SHA linkage).
{
"member_number": "SAKI-0002",
"private_limit_kes": 500000,
"private_used_kes": 421584,
"private_remaining_kes": 78416,
"as_of": "2026-08-25T09:08:41Z",
"sha_number": "SHIF-00400102",
"sha_benefit_cap": 1100000,
"sha_used_ytd": 981673,
"sha_remaining": 118327
}SaKi reads exactly these keys and passes them through to the provider under cob.source: "live". Any key you omit simply appears as null in the lookup.
Connecting it to SaKi
Each insurer's outbound connection is three values on SaKi's side:
| Setting | Meaning |
|---|---|
| member_api_base | Your API root, e.g. https://api.your-insurer.co.ke |
| member_api_key_enc | The bearer key SaKi sends you (stored encrypted at rest) |
| cob_mode | live call your API mock demo figures, no call |
With cob_mode = mock, an onboarded-but-not-yet-integrated insurer still demos with deterministic figures — every one tagged mock so it is never mistaken for a live confirmation. Flipping to live is a config change: set the base and key, switch the mode. No code or schema change.
dummy_insurer service you can run to validate your own before going live.Questions or an integration key: hello@saki.co.ke.