SaKiDeveloper API Reference← Back to site

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

text
https://saki.co.ke

All 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:

http
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.

POST /api/auth/login-any
bash
curl -s https://saki.co.ke/api/auth/login-any \
  -H "Content-Type: application/json" \
  -d '{"email":"desk@nabuala.co.ke","password":"••••••••"}'
json
{
  "access_token": "eyJhbGciOi...",
  "refresh_token": "eyJhbGciOi...",
  "portal": "provider",
  "expires_in": 900
}
Member endpoints require a provider token. They are the caller who identifies a patient at the point of care. An insurer token is used for the SHA, eTIMS, and queue endpoints.

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.

StatusMeaning
401Missing, invalid, or expired token
403Provider is not linked to the selected insurer
404No active member matches (the number is never echoed back, so the endpoint cannot be used to enumerate members)
409Conflict — e.g. an admission form already submitted
422No lookup key given, or no member and no phone on an admission
429Rate or usage quota exceeded

Member lookup

GET /api/members/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 paramRequiredDescription
insurer_org_idyesWhich insurer the member belongs to. The caller must hold an active link to it.
member_numberone ofThe membership number, e.g. SAKI-0002
id_numberone ofThe 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.

bash
curl -s "https://saki.co.ke/api/members/lookup?insurer_org_id=$ORG&member_number=SAKI-0002" \
  -H "Authorization: Bearer $TOKEN"

200 response

json
{
  "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

POST /api/agents/admission/generate-link

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.

json
{
  "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.

The provider never types the patient's phone. The number the SMS goes to is the one on file in the insurer's registry — a facility cannot redirect a member's link to an arbitrary phone.

200 response

json
{
  "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

GET /api/documents/{token}

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.

json
{
  "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.

EndpointPurpose
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-checkFlag SHA + private overlap
GET /api/agents/sha/modeWhich 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.

POST /api/etims/verify
bash
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:

statusMeaning
verified_liveKRA confirmed the invoice; amount reconciled
format_ok_mockFormat valid, not verified against KRA (mock mode)
invalid_formatNot a well-formed CUIN
not_foundLive mode: KRA has no such invoice
amount_mismatchLive mode: claim amount ≠ KRA's recorded total
kra_unreachableLive 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.

EndpointPurpose
GET /member/{member_number}Member identity (for SaKi's cache sync)
GET /member/{member_number}/cobRealtime benefit position (COB)
SaKi sends 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

GET {base}/member/{member_number}

Return the member's identity. No live balances here — this is what SaKi caches.

json
{
  "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

GET {base}/member/{member_number}/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).

json
{
  "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:

SettingMeaning
member_api_baseYour API root, e.g. https://api.your-insurer.co.ke
member_api_key_encThe bearer key SaKi sends you (stored encrypted at rest)
cob_modelive 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.

A reference implementation of this exact API — the two endpoints, the bearer check, and drifting balances — ships with SaKi as a standalone dummy_insurer service you can run to validate your own before going live.

Questions or an integration key: hello@saki.co.ke.