sha-claim
Python SDK for Social Health Authority (SHA, Kenya) claims through the Digital Health Agency's
AfyaConnect HIE eClaims API. Async-first, typed, framework-free — httpx and pydantic, nothing else.
pip install sha-claim
Status:
0.1.18, pre-production. All 49 published endpoints implemented. Eligibility, benefits, consent, the claim lifecycle, discharge and dispensing are verified live against DHA UAT. Two things are not ours to fix:prescribe()is blocked by an unpublishedpatient_instructionvocabulary (§9.8), and the pre-auth nested arrays are unverified (§14). Both are tracked in docs/api/WORKFLOWS.md. See PLAN.md.
This file is the whole guide — every method, its parameters, the HTTP call it makes, the object it returns, end-to-end recipes, and the gotchas that cost us days. Written for a developer who has never touched SHA, the DHA API or this SDK: read §1–§4 once, then come back to the rest as reference.
Contents
- What you are talking to
- Install and configure
- The client
- Errors
- The types you pass around
sha.eligibility— who is covered, for what ·sha.registriessha.consent— patient consent (OTP)sha.claims— opening and resuming a claimClaimSession— everything you do to one claimsha.emergency— emergency casessha.files— standalone uploads- End-to-end recipes
- Testing code that uses the SDK
- Gotchas
- Appendix — every result object, field by field
Beyond this file: ARCHITECTURE (how the SDK is built and why), WORKFLOWS (what UAT actually does, as opposed to what it documents), api/ (reference generated from the official portal), NACARE_INTEGRATION, CERTIFICATION, RELEASING, CHANGELOG.
1. What you are talking to
SHA (Social Health Authority) is Kenya's public health insurer. Facilities get paid by submitting claims to SHA through the Digital Health Agency's AfyaConnect HIE eClaims API. This SDK is a typed Python wrapper around that API.
Four ideas explain almost everything:
| Idea | What it means for you |
|---|---|
| The claim lives on the server. | You don't build a claim locally and post it. You open one on the server, then add things to it call by call, then submit it. |
consent_token is the handle. |
Opening a claim returns a token. Every later call about that claim needs it. The SDK hides this inside a ClaimSession object. Treat the token like a password: it lets anyone bill against that patient's visit. |
| Consent is a two-step OTP dance. | authorize sends the patient an SMS code; open_visit consumes it. Without the code, no claim. |
| Interventions are the unit of everything. | An intervention code (e.g. SHA-12-001 Consultation) is what you authorise, bill against, attach documents to, and pre-authorise. |
Two environments exist: UAT (https://ilm-dev.dha.go.ke/uat-middleware, test data) and
production (URL supplied by DHA at onboarding). Your credentials usually decide which facility you are —
the facility code is embedded in the access token. A credential that covers several facilities sends the code per
request instead; see Facility scoping in §3.
2. Install and configure
pip install sha-claim
Requires Python 3.11+. Runtime dependencies: httpx, pydantic — nothing else.
Configuration is read from environment variables:
| Variable | Required | Default | Meaning |
|---|---|---|---|
SHA_CLIENT_ID |
yes | — | OAuth2 client id from DHA onboarding |
SHA_CLIENT_SECRET |
yes | — | OAuth2 client secret |
SHA_ENVIRONMENT |
no | uat |
uat or production |
SHA_BASE_URL |
production only | UAT URL | Override the base URL (no trailing /api/v1) |
SHA_CONNECT_TIMEOUT |
no | 5 |
seconds |
SHA_READ_TIMEOUT |
no | 30 |
seconds |
SHA_UPLOAD_TIMEOUT |
no | 120 |
seconds, for multipart uploads |
SHA_FACILITY_ID |
no | — | static X-Facility-Id for single-facility deployments (see Facility scoping in §3) |
SHA_FACILITY_ID_TYPE |
no | fr-code |
X-Facility-Id-Type |
Missing or invalid settings raise ConfigurationError when you create the client, not later.
If you prefer code over env vars:
from sha_claim import SHASettings, Environment, Timeouts
settings = SHASettings(
client_id="…",
client_secret="…",
base_url="https://ilm-dev.dha.go.ke/uat-middleware",
environment=Environment.UAT,
timeouts=Timeouts(connect=5, read=30, upload=120),
)
3. The client
Everything starts from AsyncSHAClient. It is async: every call is awaited and you run it
inside an event loop (asyncio.run(...), or your FastAPI/Django-async handler).
import asyncio
from sha_claim import AsyncSHAClient, IdentificationType
async def main() -> None:
async with AsyncSHAClient.from_env() as sha: # reads SHA_* env vars
e = await sha.eligibility.check("12345678", IdentificationType.NATIONAL_ID)
print(e.full_name, e.member_found)
asyncio.run(main())
async with … as sha: opens the HTTP connection pool and closes it at the end. Reuse one client
for many calls; don't create one per request. Tokens are fetched on first use, cached, and refreshed
automatically (single-flight: 100 concurrent callers trigger one refresh).
The client exposes seven resources:
| Attribute | What it's for | Section |
|---|---|---|
sha.auth |
Which facility am I? Is the credential working? | below |
sha.eligibility |
Is this person covered? For which benefits/interventions? Balances. | §6 |
sha.registries |
Turn an identity document into a CR number; can this member receive an OTP? | §6.4 |
sha.consent |
Send the OTP; inspect/reject an authorization | §7 |
sha.claims |
Open a claim (→ ClaimSession) or resume one from a saved token |
§8 |
sha.emergency |
Open an emergency case (→ ClaimSession); list treatment protocols |
§10 |
sha.files |
Upload a file / get a download URL | §11 |
sha.auth is plumbing you rarely touch — the SDK authenticates on its own — but three calls are
useful:
| Method | Returns |
|---|---|
identity() |
Identity — facility, facility_id_type, tenant_id, tenant_name, issuer, client_id, issued_at, expires_at, seconds_remaining (prop). Read off the cached token; never returns the token itself. |
check() |
True if a token can be obtained, else raises. A readiness probe. |
token() |
BearerToken — access_token, expires_in, token_type. For code that must call the HIE directly (legacy paths, bots). Prefer the SDK's own methods: they never need it. |
me = await sha.auth.identity()
print(me.facility, me.tenant_name) # "which facility am I", without parsing a JWT yourself
Constructor options, all optional (used mainly in tests):
AsyncSHAClient(settings, transport=…, tokens=…, clock=…, retry=RetryPolicy(max_attempts=3), http=httpx.AsyncClient())
Observability. Pass on_event= a callable and the SDK calls it once per HTTP attempt with an
SDKEvent — operation, status, duration_ms, attempt, trace_id, redacted consent_token,
error. The SDK stores nothing; your callback is where audit logging, metrics and alerting live.
A hook that raises is logged and ignored — it can never break a claim call.
def audit(e: SDKEvent) -> None:
db.insert(
"sha_events",
operation=e.operation,
status=e.status,
trace_id=e.trace_id,
duration_ms=e.duration_ms,
token=e.consent_token,
at=e.occurred_at,
)
if not e.ok:
metrics.increment("sha.errors", tags={"op": e.operation, "status": e.status})
async with AsyncSHAClient.from_env(on_event=audit) as sha:
...
Facility scoping. DHA scopes claims/preauth/patient calls to one facility
(docs). A facility-specific
credential needs nothing: its token carries facility_id. A multi-facility integration sends
X-Facility-Id + X-Facility-Id-Type on every request; the SDK does that for you:
with facility_scope("FID-47-115307-8"): # everything inside is scoped; safe under concurrency
await sha.eligibility.check(...)
# or, in a per-request web dependency: activate_facility(code) … clear_facility()
# or, one facility for the whole process: SHA_FACILITY_ID=FID-… / SHASettings(facility=...)
Headers override the token claim. The SDK never sends one header without the other.
Retries. Reads (GET, and preview) are retried up to 3 times with jittered backoff on
network errors and 408/429/502/503/504. Writes are never retried automatically. A 401 triggers
one token refresh and one replay.
4. Errors
Every exception the SDK raises is a subclass of sha_claim.SHAClaimError. Catch that if you don't
care which; catch specific ones when you do.
| Exception | When | Useful attributes |
|---|---|---|
ConfigurationError |
bad/missing settings | — |
RequestValidationError |
the SDK refused to send because the request is certainly wrong (empty list, negative price, bad ICD format…) | .violations → list of Violation(field, message) |
BadRequestError |
server said 400 — usually a business rule ("intervention X not supported for OUTPATIENT") | .status, .error, .trace_id |
AuthenticationError |
token could not be obtained, or 401 even after a refresh | .trace_id |
PermissionDeniedError |
403 | .trace_id |
NotFoundError |
404 | .trace_id |
ServerError |
any other 4xx | .status, .trace_id |
RateLimitedError |
429 | .retry_after (seconds, may be None) |
TransportError |
network failure / timeout / 5xx after retries | .trace_id |
SubmissionOutcomeUnknownError |
submit was sent but we never got an answer — see §9 lifecycle |
.trace_id |
UnexpectedResponseError |
the server answered with a shape we don't understand | — |
trace_id is the server's own correlation id. Quote it when you raise a ticket with DHA.
from sha_claim import BadRequestError, RequestValidationError, SHAClaimError
try:
auth = await sha.consent.authorize(patient, ServiceType.OUTPATIENT, ["SHA-12-001"])
except RequestValidationError as e: # nothing was sent
for v in e.violations:
print(v.field, v.message)
except BadRequestError as e: # server refused
log.warning("SHA rejected: %s (trace %s)", e, e.trace_id)
except SHAClaimError: # anything else from the SDK
raise
Server messages are unwrapped for you: the API sometimes nests JSON inside strings
({"error":"{\"error\":\"Kindly note…\"}"}); str(exc) gives you the human sentence.
5. The types you pass around
The SDK uses small typed values instead of bare strings so mistakes fail early and loudly.
Almost every method also accepts a plain str and converts it for you — the types below are
what you get back, and what you use when you want the validation up front.
Identifiers
| Type | What it is | Notes |
|---|---|---|
PatientId |
the beneficiary's CR number (e.g. CR7678914660684-5) |
comes from eligibility.check(...).patient_id; this is patient_id everywhere in the API |
ConsentToken |
the claim handle returned by open_visit |
repr/str are redacted (CR76…G6); use .value when you must persist it |
ClaimGuid, LineGuid, AttachmentId, FileId, InvoiceNumber, FacilityCode |
server ids / your invoice number | non-empty, whitespace-trimmed |
All raise ValueError if empty.
Codes
| Type | Format | Example |
|---|---|---|
InterventionCode |
SHA intervention, upper-cased for you | InterventionCode("sha-12-001") → SHA-12-001 |
Icd11Code |
ICD-11 / ICD-10 stem, upper-cased, validated | "1A00", "BA00.1", "J18.9" ok; "malaria" → ValueError |
SchemeCode, ProtocolCode |
as given by SHA | "UHC", "EP-1" |
Money
from sha_claim import Money
Money.kes("1500") # KES 1,500.00
Money.kes("1500.505") # → 1500.51 (half-up to cents)
Money.kes(1500) * 2 # KES 3,000.00
Money(1500.0) # TypeError — floats are refused on purpose; pass str/int/Decimal
Consent proof
Otp("123456") (digits only, redacted in repr), BiometricGuid("…"), MatchId("…").
open_visit accepts any of the three as proof.
Practitioners
from sha_claim import PractitionerRef, RegulationBody, IdentificationType
PractitionerRef.registered("A1234", RegulationBody.KMPDC) # by registration number (usual)
PractitionerRef("12345678", IdentificationType.NATIONAL_ID, RegulationBody.NCK) # by national ID
RegulationBody: KMPDC (doctors), COC (clinical officers), NCK (nurses).
Attachments
from sha_claim import Attachment, DocumentType
Attachment.from_path("discharge.pdf", DocumentType.DISCHARGE_SUMMARY) # reads the file, guesses content type
Attachment("inv.pdf", pdf_bytes, DocumentType.INVOICE, "application/pdf")
Empty content or > 10 MB → ValueError. DocumentType has 31 values (INVOICE, LAB_RESULTS,
DISCHARGE_SUMMARY, PREAUTH_FORM, PRESCRIPTION, CT_SCAN, … see appendix).
Enums you will pass
| Enum | Values |
|---|---|
ServiceType |
CAPITATION, OUTPATIENT, INPATIENT, EMERGENCY |
IdentificationType |
NATIONAL_ID ("National ID"), ALIEN_ID, REFUGEE_ID, REGISTRATION_NUMBER (practitioners only) |
CancelReason |
WRONG_PATIENT, NO_SERVICE_GIVEN, WRONG_BENEFIT, EXPIRED_VISIT, EXHAUSTED_BENEFIT, TIME_BARRED, OTHER_REASONS |
DischargeReason |
RECOVERED, REFERRED, DECEASED, ABSCONDED, OTHER |
NextOfKinIdType |
NATIONAL_ID, CLIENT_REGISTRY_ID, BIRTH_NOTIFICATION, BIRTH_CERTIFICATE, ALIEN_ID, REFUGEE_ID, MANDATE_NUMBER, TEMPORARY_ID |
BroughtBy |
RELATIVE, UNKNOWN, SAMARITAN, PARAMEDICS |
ModeOfArrival |
AMBULANCE, WALK_IN, OTHER |
DoctorConsentRequestType |
PREAUTH_DOCTOR_APPROVAL, EMERGENCY_CLAIM_DOCTOR_APPROVAL, PRESCRIPTION |
Enums you will read (lenient)
PaymentMechanism, AuthorizationStatus, EligibilityStatus, CoverageStatus come from the
server. Their vocabularies aren't fully documented, so an unknown value does not crash: you get a
member whose .is_known is False and whose value is the raw string. Compare with ==.
extra
Every result object has an extra dict holding server fields the SDK hasn't modelled yet. It's a
read-only escape hatch — log it, don't build logic on it.
6. sha.eligibility
All reads. Safe to call any time; retried automatically on transient failures.
check(identification_number, identification_type) → Eligibility
Start here for every patient. Turns an ID document into the CR number you need everywhere else, and tells you whether they're covered.
| Param | Type | Notes |
|---|---|---|
identification_number |
str |
e.g. national ID number |
identification_type |
IdentificationType |
NATIONAL_ID, ALIEN_ID, REFUGEE_ID (not REGISTRATION_NUMBER — that's for doctors) |
Sends GET /patients/eligibility?identification_number=…&identification_type=National ID.
Returns Eligibility:
| Field | Type | Meaning |
|---|---|---|
patient_id |
PatientId | None |
the CR number — keep it |
full_name |
str |
|
status / status_description |
EligibilityStatus | None / str |
"10" = member found; UAT returns "00" "Member Not found!" for unknown IDs |
schemes |
tuple[Scheme, …] |
e.g. UHC; each has coverage.status, policy_period |
date_of_birth, gender, age, is_alive |
may be empty on UAT | |
whitelisted_for_otp, facility_biometrics_enforced |
bool |
|
member_found (property) |
bool |
status is "member found" and a CR number came back |
is_covered_on(day) |
bool |
member found and at least one scheme is active on that date |
active_schemes_on(day) |
tuple[Scheme, …] |
e = await sha.eligibility.check("00000000", IdentificationType.NATIONAL_ID) # UAT synthetic member
if not e.is_covered_on(date.today()):
raise Exception(f"not covered: {e.status_description}")
patient = e.patient_id
benefits(patient) → tuple[BenefitPackage, …]
Top-level packages the patient can use. GET /patients/benefits?patient_id=….
Each BenefitPackage: code (SHA-12), name (Outpatient Services).
sub_benefits(patient) → tuple[SubBenefit, …]
GET /patients/sub-benefits?patient_id=…. Each SubBenefit: code (SHA-12-SC-01), name,
parent_code (SHA-12), access_point (OP, IP, OP and IP).
interventions(patient, sub_benefit_code) → tuple[InterventionCoverage, …]
The billable things under a sub-benefit, with tariffs and rules.
GET /patients/benefits/interventions?patient_id=…&sub_benefit_code=….
| Field | Meaning |
|---|---|
code |
InterventionCode — what you authorise and bill |
name |
Consultation, Cesarean Section, … |
payment_mechanism |
CAPITATION, CASE BASED, FEE FOR SERVICE (lenient) |
needs_preauth |
must file a pre-auth (§9) before billing |
needs_doctor_authorization |
needs a doctor's sign-off |
access_point |
OP / IP |
overall_tariff |
Money | None — 0 for capitation |
applicable_schemes |
e.g. ("UHC",) |
service_type_for_authorization (property) |
use this as service_type when authorising — see gotcha in §14 |
coverage = await sha.eligibility.interventions(patient, "SHA-12-SC-01")
consultation = next(c for c in coverage if c.name == "Consultation")
consultation.service_type_for_authorization # ServiceType.CAPITATION on UAT
utilization(patient, intervention) → UtilizationBalance
Remaining limits for one intervention. GET /patients/benefits/utilization. Fields:
individual_max, individual_utilised, household_max, household_utilised, available_amount,
eligible, funds (per-fund limits), individual_remaining (property).
On UAT this endpoint returned an upstream 400 in Sept 2026; expect BadRequestError.
pomsf_balances(patient, policy_year, *, principal_member_number=None) → dict
Public Officers Medical Scheme Fund (civil servants). Returned raw as a dict — the payload is large and NaCare has no POMSF members yet.
bed_occupancy(facility) → BedOccupancy
GET /facilities/{code}/beds/occupancy. Fields: facility_name, level, total_beds,
total_inpatient_visits, normal_beds, icu_beds, hdu_beds, dialysis_beds, baby_cots,
occupancy_rate (property, None if no beds). Needs auth on UAT despite the docs.
6.4 sha.registries — who is this person?
sha.eligibility answers what is this member entitled to; the Client Registry answers who are they
and can we reach them. Worth calling before consent: SHA sends the visit OTP only to a confirmed,
active phone contact, and a member with none cannot consent by OTP at all — so this turns a failed
authorize into a question you asked first.
| Method | Sends | Returns |
|---|---|---|
find_patient(identification_number, identification_type=NATIONAL_ID) |
GET /patients |
PatientRecord | None — None, not an exception, when the registry holds nobody with that document |
contacts(patient) |
GET /patients/contacts |
tuple[PatientContact, …] — empty means the OTP path is closed for this member |
can_consent_by_otp(patient) |
as above | bool — any confirmed, active phone on record |
PatientRecord: patient_id (PatientId | None), first_name, middle_name, last_name,
full_name (prop), gender, date_of_birth, identification_type, identification_number, phone,
citizenship, county, sub_county, ward, other_identifications, dependant_ids.
PatientContact: contact_id, value, contact_type, is_confirmed, is_active, is_main,
next_of_kin_full_name, can_receive_otp (prop).
record = await sha.registries.find_patient("12345678", IdentificationType.NATIONAL_ID)
if record is None:
return "not in the Client Registry" # not an error — just nobody with that document
if not await sha.registries.can_consent_by_otp(record.patient_id):
return "no confirmed phone on record — this member cannot consent by OTP"
UAT answers a document it does not hold with
400 "zero results found in client registry"rather than a 404.find_patientfolds that intoNone, so a registry outage still raises and is never mistaken for an unknown patient.
7. sha.consent
Which consent path? Two exist and they don't mix:
- OTP (usual):
send_otp→ patient reads the code →claims.open_visit(..., Otp(code)). Do not callauthorizefirst.- Biometrics (device):
authorize→ the patient verifies on the biometric device →open_visit(..., BiometricGuid(auth.guid)).A
PENDINGauthorization left behind by the biometric path blocks the OTP path. Uselist+rejectto clear it.
send_otp(patient, interventions) → str
POST /claims/otp {patient_id, intervention_codes}. Sends the visit OTP to the beneficiary's registered
phone; returns the server message. (On UAT the message contains the OTP itself — sandbox only.)
list(patient) → tuple[Authorization, …]
GET /claims/authorizations?beneficiary_code=…. Every authorization the beneficiary has at your facility,
newest first, with status ∈ PENDING | AUTHORIZED | SUBMITTED_CLAIM | CLOSED. Undocumented but live.
authorize(patient, service_type, interventions, otp=None) → Authorization
Biometric path. Creates a pending authorization to be verified on a biometric device. If the patient
already has an open visit, UAT returns that AUTHORIZED record instead.
| Param | Type | Notes |
|---|---|---|
patient |
PatientId | str |
CR number |
service_type |
ServiceType |
use InterventionCoverage.service_type_for_authorization |
interventions |
list of InterventionCode | str |
at least one; duplicates removed |
otp |
Otp | None |
leave None — the server generates and sends it |
Sends POST /claims/authorize with {"patient_id", "service_type", "interventions": [...]} (+ "otp" if given).
Returns Authorization: guid, token, auth_code, status (PENDING), label
(UNAUTHORIZED until verified), is_open, benefit_type, beneficiary, beneficiary_name,
provider_fid, interventions (each with needs_preauth), expiry, is_pending (prop),
needs_preauth (prop, any intervention), as_biometric_proof() → BiometricGuid(guid).
Raises RequestValidationError if interventions is empty; BadRequestError e.g.
"intervention Consultation(SHA-12-001) is not supported for service type OUTPATIENT".
get(token, guid, patient=None) → Authorization | None
Re-read an authorization. GET /claims/authorizations?token=…&guid=…[&beneficiary_code=…].
None if the server has nothing.
reject(token) → None
Close a pending authorization you won't use. POST /claims/authorizations/{token}/reject.
8. sha.claims
open_visit(patient, service_type, interventions, proof) → ClaimSession
Step 2. Verifies the OTP and opens the server-side virtual claim.
| Param | Type | Notes |
|---|---|---|
patient |
PatientId | str |
|
service_type |
ServiceType |
same as you used in authorize |
interventions |
list of codes | |
proof |
Otp | BiometricGuid | MatchId |
the code the patient read out, or a verified biometric GUID |
Sends POST /claims/visit with {"patient_id", "service_type", "intervention_codes": [...]} plus
exactly one of "otp" / "auth_guid" / "match_id".
Returns a ClaimSession (§9). session.consent_token is the handle; session.claim is the
VirtualClaim snapshot the server returned.
Typical failures: BadRequestError "OTP was not found for contact: +2547…" (no pending
authorization / wrong code), "Kindly ensure the biometrics visit has been successfully verified".
resume(consent_token) → ClaimSession
Re-attach to a claim you opened earlier (e.g. token persisted in your DB). No network call.
session.claim is None until you preview().
session = sha.claims.resume(saved_token)
claim = await session.preview()
9. ClaimSession
One object = one server-side claim. Every method sends the session's consent_token for you.
Methods that return a VirtualClaim also refresh session.claim.
9.1 Interventions
| Method | Sends | Returns |
|---|---|---|
add_intervention(code) |
POST /claims/interventions {consent_token, intervention_code} |
ClaimIntervention — needs_preauth, preauth_exists, preauth_outstanding (prop), applicable_document_types, required_preauth_document_types, tariff |
retire_intervention(code) |
POST /claims/interventions/retire |
None |
restore_intervention(code) |
POST /claims/interventions/restore |
None |
switch_intervention(existing, new, *, retain_bill_items=True, bill_from=None, bill_to=None) |
POST /claims/interventions/switch |
None |
The bed rebate (PER DIEM). Critical care is not billed by the item. ICU, HDU, NICU and the burns unit
(SHA-03-*) come back with payment_mechanism == PaymentMechanism.PER_DIEM: SHA pays a fixed amount for every
day of the stay and accrues it itself. None of this is in the portal spec — the fields exist with empty
descriptions — so read it off the intervention:
| Field / property | Type | What it is |
|---|---|---|
is_per_diem (prop) |
bool |
this intervention is paid by the day, not the item |
accrued_per_diem_days |
int |
days of stay SHA has accrued so far |
accrued_per_diem |
Money | None |
what those days have earned, as SHA computed it |
keph_level_tariff |
Money | None |
the daily rate for your facility's KEPH level (SHA spells it kephLevelTarrif) |
bill_from / bill_to |
datetime | None |
the window being accrued over |
per_diem_allowance (prop) |
Money | None |
what SHA will pay for the stay so far |
per_diem_allowance prefers SHA's own accrual, falls back to keph_level_tariff × accrued_per_diem_days, and
returns None — never zero — when it knows neither. That matters: on UAT every per-diem intervention returns
overall_tariff 0.00 because no rate is published for the facility's KEPH level, and a zero here would read as
"SHA pays nothing" and push a covered stay onto the patient. Treat None as unknown, and say so on screen.
bed = next((i for i in claim.interventions if i.is_per_diem), None)
if bed:
allowance = bed.per_diem_allowance # Money | None
if allowance is None:
ui.warn(
f"{bed.name}: {bed.accrued_per_diem_days} day(s) accrued, rate not published — confirm with SHA"
)
else:
extras = claim.total_amount - allowance # anything over the per diem is the patient's / an exclusion
9.2 Diagnoses
| Method | Sends | Returns |
|---|---|---|
add_diagnosis(icd, intervention) |
POST /claims/diagnoses {consent_token, icd_code, intervention_code} |
ClaimDiagnosis — code, name, intervention_code, record_id, is_flagged, recorded_on |
remove_diagnosis(icd, intervention) |
PATCH /claims/diagnoses |
None |
icd is validated locally (Icd11Code), so a typo fails before any network call.
9.3 Billing lines
add_line(intervention, unit_price, quantity=1, *, scheme_code=None, charge_date=None, diagnoses=(), service_name="", service_identifier="", practitioner=None, attachments=()) → ClaimLine
| Param | Type | Notes |
|---|---|---|
intervention |
code | which intervention this line bills |
unit_price |
Money |
required, Money not a number |
quantity |
int | Decimal |
> 0 |
scheme_code |
SchemeCode | str | None |
e.g. "UHC" |
charge_date |
date | None |
|
diagnoses |
ICD codes | linked to this line |
service_name |
str |
label for the line (DHA "Add New Line") |
service_identifier |
str |
your own charge/order reference, for reconciliation |
practitioner |
PractitionerRef | None |
attending doctor — one per claim |
attachments |
LineAttachment(document_title, Attachment) … |
files sent with the line ("Add Combined Billing Details") |
Sends POST /claims/lines as multipart/form-data: consent_token, intervention_code, unit_price ("1500.00"), quantity, [scheme_code], [charge_date], [diagnoses = JSON array], [service_name], [service_identifier], [practitioner_identification_type/number, practitioner_regulation_body], [attachments = JSON array of {document_title, document_type, file_field_name}] plus one binary part per
attachment named attachment_0, attachment_1, … (upload timeout applies when files are present).
Returns ClaimLine: guid (needed to remove/edit), intervention_code, item_code, item_name,
quantity (Decimal), unit_price, total_amount, net_amount, copay, scheme_code,
charge_date, is_active, doctor_name.
Plus, on lines read back from preview(), how SHA splits the line — computed by SHA, never sent by you:
| Field | Type | What it is |
|---|---|---|
rebate_amount |
Money | None |
what SHA rebates on this line (the wire still calls it nhifRebateAmount) |
sponsor_net |
Money | None |
the sponsor's share |
patient_net |
Money | None |
what is left to the patient |
benefit_exceeded |
bool |
SHA flagged this line as taking the member past a UHC limit |
All four are None/False on the line add_line returns and populated once SHA has priced the claim, so read
them from preview(), not from the add call.
RequestValidationError if quantity ≤ 0 or price negative.
| Method | Sends | Returns |
|---|---|---|
remove_line(line_guid) |
PATCH /claims/lines {consent_token, line_guid} |
None |
edit_line(line_guid, *, quantity=None, unit_price=None, scheme_code=None) |
PATCH /claims/lines/edit {line_id, …changed fields} — for after payer review |
ClaimLine |
resubmit_lines() |
POST /claims/lines/resubmit {consent_token} — after edit_line |
LineResubmission — line, status, message, resubmitted_at |
9.4 Attachments
| Method | Sends | Returns |
|---|---|---|
attach(attachment, intervention) |
POST /claims/attachments multipart: consent_token, document_type, intervention_code, file_blob (uses the upload timeout) |
ClaimAttachment — attachment_id, title, attachment_type, intervention_code |
remove_attachment(attachment_id, intervention) |
PATCH /claims/attachments |
None |
await session.attach(Attachment.from_path("invoice.pdf", DocumentType.INVOICE), "SHA-12-001")
9.5 Lifecycle
preview() → VirtualClaim
POST /claims/preview {consent_token}. The claim as the server sees it. Safe to call any time
(it's retried like a read). Use it to check totals before submitting, and to resolve an ambiguous submit.
VirtualClaim.submission_blockers() → tuple[Blocker, …]
Pure check over the snapshot preview() returned — nothing is stored or fetched. Each Blocker
has code, message, intervention. Codes: NO_BILLING_LINES, ZERO_TOTAL, NEGATIVE_TOTAL,
PREAUTH_OUTSTANDING, NO_DIAGNOSIS, MISSING_DOCUMENTS, NO_ACTIVE_INTERVENTIONS. It is
conservative (it only interprets what the server said), so an empty tuple means no known blocker,
not a guarantee. Use it to disable the Submit button and tell the biller why.
claim = await session.preview()
if blockers := claim.submission_blockers():
for b in blockers:
print(b) # PREAUTH_OUTSTANDING [SHA-08-006]: intervention needs a pre-authorisation…
else:
await session.submit("INV-1")
add_doctor(practitioner) → str
POST /claims/doctors. Attaches the attending practitioner; the registration is checked against the
Health Worker Registry. Required before submit (UAT: "there is no doctor attached to the claim").
submit(invoice_number=None, *, discharge_reason=None, otp=None, reason_for_unknown_patient=None) → VirtualClaim
POST /claims/submit {consent_token, invoice_number, discharge_reason, otp, [reason_for_unknown_patient]}.
Final. No changes after this.
What UAT actually requires (none of it documented): a discharge_reason, the OTP from
send_discharge_otp() — yes, for outpatient/CAPITATION too — and a doctor on the claim. The server-assigned
invoice_number is on the preview (preview.invoice_number). So the real sequence is:
await session.add_doctor(PractitionerRef.registered("A1234", RegulationBody.KMPDC))
preview = await session.preview()
await session.send_discharge_otp(patient) # OTP to the patient / next of kin
claim = await session.submit(preview.invoice_number, discharge_reason=DischargeReason.RECOVERED, otp=code)
The SDK sends submit exactly once. If the network drops or the server times out after the
request left, you get SubmissionOutcomeUnknownError — the SDK does not guess. Do this:
try:
claim = await session.submit("INV-2026-000123")
except SubmissionOutcomeUnknownError as e:
log.warning("submit outcome unknown, trace %s", e.trace_id)
claim = await session.preview() # the server knows; ask it
if claim.workflow_state not in SUBMITTED_STATES: # your own mapping once you've seen real values
claim = await session.submit("INV-2026-000123")
close(reason, text) → VirtualClaim
POST /claims/close {consent_token, cancel_reason_type, cancel_reason_text}. Abandon a claim that
will never be submitted. reason is a CancelReason; text must be non-empty.
payer_status(provider_claim_no) → tuple[PayerClaimRecord, …]
GET /claims/preview/payer?guid=…&provider_claim_no=… — how the payer's system sees the
submitted claim. Needs the claim GUID; if the session has no snapshot yet it calls preview() first.
Each PayerClaimRecord: guid, provider_claim_no, tracking_number, workflow_state,
workflow_display_name, status (prop), claim_type, is_inpatient, proposed_value,
proposed_value_less_copays, total_copay, member_name, scheme_name, created.
9.6 Inpatient: discharge and next of kin
Every claim needs a discharge OTP and reason before submit. Inpatient claims additionally call discharge; outpatient / CAPITATION pass discharge_reason + otp straight to submit (UAT rejects /claims/discharge for them).
| Method | Sends | Returns |
|---|---|---|
add_next_of_kin(*, full_name, id_number, id_type, contact_value) |
POST /patients/next-of-kin/contacts — register who receives OTPs when the patient can't |
NextOfKinContact — guid, is_verified, is_confirmed, is_main_contact |
send_discharge_otp(patient) |
POST /claims/otp/discharge {consent_token, patient_id} |
str message |
discharge(*, reason, invoice_number, otp, discharged_at=None) |
POST /claims/discharge — INPATIENT only (UAT rejects it for other service types; outpatient/CAPITATION pass the discharge fields to submit instead). discharged_at defaults to now, must be tz-aware |
VirtualClaim |
id_type is a NextOfKinIdType; reason a DischargeReason; otp may be Otp or str.
9.7 Pre-authorisation
Needed when an intervention has needs_preauth (check InterventionCoverage or
ClaimIntervention.preauth_outstanding).
request_preauth(intervention, *, service_start, service_end, items, diagnoses, doctors, notification_email, attachments=()) → Preauthorization
| Param | Type | Notes |
|---|---|---|
intervention |
code | |
service_start, service_end |
datetime (tz-aware) |
end ≥ start |
items |
list[PreauthItem] |
≥ 1; PreauthItem(code, description, quantity, unit_price: Money) |
diagnoses |
ICD codes | ≥ 1 |
doctors |
list[PractitionerRef] |
|
notification_email |
str |
valid email; DHA sends updates here |
attachments |
list[Attachment] |
e.g. PREAUTH_FORM |
Sends POST /preauths as multipart; items/diagnoses/doctors/attachments are JSON strings in form
fields and attachment bytes are extra file parts. ⚠ The portal does not publish the inner schema of
those four arrays; the SDK's encoding follows the API's own vocabulary and is unverified on UAT
(see docs/api/WORKFLOWS.md Q3). If DHA rejects it, the fix is in one function.
Returns Preauthorization: guid, token, status, doctor_review_status,
needs_doctor_approval, doctor_approved, doctors_required, is_request_phase,
is_response_phase, total_estimated, interim_approved, final_approved, service_start/end,
countdown, awaiting_doctor (prop), decided (prop).
| Method | Sends | Returns |
|---|---|---|
preauths() |
GET /preauths?consent_token=… |
tuple[Preauthorization, …] |
remove_preauth_diagnosis(icd, intervention) |
DELETE /preauths/diagnoses/{icd} |
Preauthorization |
remove_preauth_doctor(intervention, registration_number) |
DELETE /preauths/doctors — only before submission |
None |
cancel_preauth(intervention) |
POST /preauths/cancel |
Preauthorization |
request_doctor_consent(intervention, practitioner, *, request_type=PREAUTH_DOCTOR_APPROVAL, service_type=None, emergency_claim_id=None) |
POST /claims/doctor-consent |
str message |
9.8 ePrescriptions
prescribe(intervention, items, *, prescriber=None) → Prescription
items is a list of MedicationOrder:
| Field | Type | Notes |
|---|---|---|
generic_concept_code |
str |
the medication's code |
dose_quantity |
int | Decimal |
> 0 |
dose_unit |
str |
TABLET, ML, … |
frequency |
int |
times per period_unit, > 0 |
period_unit |
str |
DAY, … |
duration |
int |
> 0 |
duration_unit |
str |
DAY, WEEK, … |
start_date |
date |
|
end_date |
date | None |
≥ start |
patient_instruction, additional_instruction |
str |
optional |
needs_refill, refill_count |
bool, int |
refill_count > 0 when needs_refill |
Sends POST /prescriptions JSON {consent_token, intervention_code, items: [...], [identification_number, identification_type, regulation_body]}. Field names match the portal's published example exactly.
Returns Prescription: guid, code, status, doctor_review_status, intervention_code,
dosage (tuple of Dosage: medication, dose_quantity, dose_unit, frequency, period_unit,
duration, route, start_date, end_date, price, status).
| Method | Sends | Returns |
|---|---|---|
prescription() |
GET /prescriptions?consent_token=… |
Prescription | None |
dispense(intervention, products, dispensers) |
POST /prescriptions/dispense (singular — the portal documents /dispenses, which 404s) {consent_token, intervention_code, actual_products: [{actual_product_code, total_quantity, medication_price}], doctors: [{identification_number, identification_type}]} |
Dispense — record_id, status, dosages |
remove_prescription_doctor(intervention, registration_number) |
DELETE /prescriptions/doctors |
None |
products is a list of DispensedProduct(product_code, quantity, price: Money); dispensers a list
of PractitionerRef. Both must be non-empty.
⚠
prescribe()cannot succeed on UAT today.patient_instructionis validated against an enumeration DHA has never published: 36 candidates have been rejected, including every member of its ownPRESCRIPTION-CONDITION-KENYAvalue set, all 40 administrative routes, free text and integer choices. The live portal page carries enums for 14 other fields and none for this one.dose_unitwas solved the same way and accepts 12 lowercase values (tablet,ml, …) that appear in no published registry.GET /prescriptionsanswers 403 for this client.dispense()works — but only once a prescription exists. The full elimination table is indocs/api/WORKFLOWS.md§6.1; it is DHA's to answer, don't re-guess it.One related trap:
generic_concept_codeis not validated — UAT accepts aPH…product code where aGE…generic belongs. A wrong code fails silently rather than loudly, so resolve the generic yourself (DHA's terminology service links them by ahas-genericmapping).
9.9 Emergency (on a session opened via sha.emergency.open_case)
| Method | Sends | Returns |
|---|---|---|
add_protocol(protocol, intervention, unit_price, quantity=1, *, diagnoses=()) |
POST /claims/emergency/protocols multipart (diagnoses comma-separated here) |
ClaimLine |
add_emergency_doctor(doctor) |
POST /claims/doctors {consent_token, identification_number, identification_type, regulation_body} |
str message |
remove_emergency_doctor() |
DELETE /claims/doctors {consent_token} |
None |
open_emt_claim(EmtClaim(...)) |
POST /claims/emt multipart — the ambulance provider's claim |
VirtualClaim |
EmtClaim(protocol_code, case_number, practitioner_registration_number, provider_registration_number, beneficiary, otp, diagnoses, interventions, attachments=()).
10. sha.emergency
open_case(attending, reference_number, brought_by, mode_of_arrival, interventions, *, patient=None, otp=None, notes="") → ClaimSession
Opens an emergency claim without prior OTP consent. patient=None for an unidentified casualty.
| Param | Type |
|---|---|
attending |
PractitionerRef (identification + regulation body) |
reference_number |
your ER reference |
brought_by |
BroughtBy |
mode_of_arrival |
ModeOfArrival |
interventions |
codes, ≥ 1 |
patient |
PatientId | str | None |
otp |
Otp | None (if the patient can consent) |
Sends POST /claims/emergency. Returns a ClaimSession — use add_protocol, add_emergency_doctor,
then the normal preview / submit.
protocols(intervention, *, active=True) → tuple[EmergencyProtocol, …]
GET /claims/emergency/protocols?intervention_code=…&active=true. Each EmergencyProtocol: code
(ProtocolCode), name, protocol_type, classification, status, tariff (Money | None).
11. sha.files
Claim documents normally go through session.attach(...). These are for standalone storage.
| Method | Sends | Returns |
|---|---|---|
upload(filename, content: bytes, content_type="application/octet-stream") |
POST /uploads multipart file |
StoredFile — file_id, path (the documented response is {}; fields appear if the server sends them) |
download_link(file_id) |
GET /uploads/{file_id} |
DownloadLink — url, message |
12. End-to-end recipes
set_coverage(principal, policy_number) → None
POST /authorizations/covers — POMSF schemes only (the DHA process pages; not on the eclaims portal).
Chooses which member's policy pays for this visit: principal is the CR number of the member whose cover
pays (not necessarily the patient), policy_number is Scheme.policy_number from eligibility — never
composed locally. Call before adding lines or pre-auths. RequestValidationError on a blank policy number.
12.1 Outpatient consultation (the common case)
import asyncio
from datetime import date
from sha_claim import (
AsyncSHAClient,
Attachment,
DocumentType,
IdentificationType,
Money,
Otp,
SubmissionOutcomeUnknownError,
)
async def outpatient(id_number: str, read_otp_from_patient) -> str:
async with AsyncSHAClient.from_env() as sha:
# 1. Who is this, and are they covered?
e = await sha.eligibility.check(id_number, IdentificationType.NATIONAL_ID)
if not e.is_covered_on(date.today()):
raise RuntimeError(f"Not covered: {e.status_description}")
patient = e.patient_id
# 2. What can we bill? Pick the intervention and let it tell us the service type.
coverage = await sha.eligibility.interventions(patient, "SHA-12-SC-01")
consultation = next(c for c in coverage if c.name == "Consultation")
service_type = consultation.service_type_for_authorization
# 3. Consent: this sends the SMS.
await sha.consent.authorize(patient, service_type, [consultation.code])
otp = Otp(read_otp_from_patient()) # your UI asks the patient for the code
# 4. Open the claim.
session = await sha.claims.open_visit(patient, service_type, [consultation.code], otp)
token_to_persist = session.consent_token.value # save this if the visit spans requests
# 5. Build it.
await session.add_diagnosis("1A00", consultation.code)
await session.add_line(consultation.code, Money.kes("1500"), quantity=1, diagnoses=["1A00"])
await session.attach(Attachment.from_path("invoice.pdf", DocumentType.INVOICE), consultation.code)
# 6. Check, then submit once.
preview = await session.preview()
print("server total:", preview.total_amount)
try:
claim = await session.submit("INV-2026-000123")
except SubmissionOutcomeUnknownError:
claim = await session.preview()
return claim.consent_token.value
asyncio.run(outpatient("00000000", lambda: input("OTP: ")))
12.2 Inpatient with discharge
session = await sha.claims.open_visit(patient, ServiceType.INPATIENT, ["SHA-08-005"], otp)
await session.add_diagnosis("JB0Z", "SHA-08-005")
await session.add_line("SHA-08-005", Money.kes("10000"))
# patient can't consent to discharge herself → register next of kin first
await session.add_next_of_kin(
full_name="Jane Doe", id_number="12345678", id_type=NextOfKinIdType.NATIONAL_ID, contact_value="+2547…"
)
await session.send_discharge_otp(patient)
await session.discharge(
reason=DischargeReason.RECOVERED,
invoice_number="INV-1",
otp=read_otp(), # discharged_at defaults to now
)
await session.submit("INV-1")
12.2a Critical care, where SHA pays by the day
session = await sha.claims.open_visit(patient, ServiceType.INPATIENT, ["SHA-03-001"], otp) # ICU CARE
await session.add_diagnosis("JB0Z", "SHA-03-001")
await session.add_line("SHA-03-001", Money.kes("25000")) # your own bed charge, as usual
claim = await session.preview()
bed = next(i for i in claim.interventions if i.is_per_diem)
allowance = bed.per_diem_allowance # Money | None — what SHA covers for the stay so far
# anything the bill carries above the allowance is the patient's, or an exclusion on your invoice
if allowance is not None and claim.total_amount and claim.total_amount > allowance:
excess = claim.total_amount - allowance
allowance is None means SHA published neither an accrual nor a KEPH-level rate — unknown, not nothing. §9.1.
12.3 Something that needs pre-authorisation
session = await sha.claims.open_visit(patient, ServiceType.INPATIENT, ["SHA-08-006"], otp) # Cesarean
pa = await session.request_preauth(
"SHA-08-006",
service_start=start,
service_end=end,
items=[PreauthItem("CS", "Cesarean section", 1, Money.kes("30000"))],
diagnoses=["JB0Z"],
doctors=[PractitionerRef.registered("A1234", RegulationBody.KMPDC)],
notification_email="claims@yourfacility.example",
attachments=[Attachment.from_path("preauth.pdf", DocumentType.PREAUTH_FORM)],
)
if pa.awaiting_doctor:
await session.request_doctor_consent(
"SHA-08-006", PractitionerRef.registered("A1234", RegulationBody.KMPDC)
)
# poll later:
for p in await session.preauths():
print(p.status, p.final_approved)
12.4 Emergency
doctor = PractitionerRef.registered("A1234", RegulationBody.KMPDC)
protocols = await sha.emergency.protocols("SHA-19-001")
session = await sha.emergency.open_case(
doctor,
"ER-0917",
BroughtBy.PARAMEDICS,
ModeOfArrival.AMBULANCE,
["SHA-19-001"],
patient=None,
notes="RTA, unconscious on arrival", # notes are mandatory on SHA's side
) # unidentified casualty — interventions must be emergency-fund ones (`InterventionCoverage.is_emergency`)
await session.add_emergency_doctor(doctor)
await session.add_protocol(protocols[0].code, "SHA-19-001", protocols[0].tariff, diagnoses=["NF0A"])
await session.submit(reason_for_unknown_patient="unconscious, no ID")
12.5 Resume a claim tomorrow
session = sha.claims.resume(token_from_your_db)
claim = await session.preview()
status = await session.payer_status(claim.invoice_number.value)
13. Testing code that uses the SDK
You have three levels, cheapest first.
Fake the SDK's port. ClaimSession and the resources depend on Protocols; hand them fakes.
See tests/unit/test_session_and_submit.py for complete fakes you can copy.
Fake the HTTP layer. Give the real client a mocked httpx server with respx:
import httpx, respx
from sha_claim import AsyncSHAClient, SHASettings
settings = SHASettings(client_id="x", client_secret="y", base_url="https://uat.example/uat-middleware")
@respx.mock
async def test_my_code():
respx.post(f"{settings.api_root}/tenants/token").mock(
return_value=httpx.Response(200, json={"access_token": "T", "expires_in": 3600})
)
respx.get(f"{settings.api_root}/patients/eligibility").mock(return_value=httpx.Response(200, json={...}))
async with AsyncSHAClient(settings) as sha:
...
Realistic payloads for every endpoint are in docs/api/spec/examples.json (the portal's samples) and
tests/fixtures/ (recorded from UAT, sanitised).
Hit UAT. Set RUN_LIVE=1 and real credentials in .env; see tests/live/.
14. Gotchas
- CAPITATION interventions must be authorised as
ServiceType.CAPITATION, notOUTPATIENT— the server rejects the latter even though the docs don't list CAPITATION for that endpoint. UseInterventionCoverage.service_type_for_authorizationand you never think about it. authorizewithout an OTP is what sends the SMS. Don't ask the patient for a code before you've called it.open_visitwith an OTP but no pending authorization fails with "OTP was not found".submitis attempted once. OnSubmissionOutcomeUnknownError,preview()— never blindly retry.- Persist
consent_token.value, not the object'sstr()—str()is redacted on purpose. Moneyrefuses floats.Money.kes("1500.50")orMoney.kes(1500).- Diagnosis codes are validated locally.
"malaria"raises before any request. - Server status strings (
workflow_state,claim_auth_status, preauthstatus) are rawstr— DHA hasn't documented the vocabulary. Log what you see; the SDK will promote them to enums as values are confirmed. - Lenient enums:
PaymentMechanism("SOMETHING NEW")doesn't raise; check.is_knownif you care. - Every result has
.extrawith unmodelled server fields. Read-only. - Reads retry, writes don't. If you wrap a write in your own retry, you own the duplicate.
- The facility comes from your credentials, or from the scope you set. No method takes a facility
argument: either the token carries it, or you wrap the call in
facility_scope(...)/ setSHA_FACILITY_ID. The two headers always travel together, never one alone. per_diem_allowanceisNone, not zero, when SHA hasn't published a rate. Don't coerce it — a zero on an ICU stay bills the patient for the bed. §9.1.- ePrescriptions are blocked on UAT by
patient_instruction. 36 candidate values rejected, including every member of DHA's own published value set; no enumeration exists in any published source.dispenseworks once a prescription exists.WORKFLOWS.md§6.1 has the elimination table — don't re-guess it. - Pre-auth nested arrays are unverified on UAT (Q3). Everything else in this guide is either live-verified or asserted against the portal's published examples.
15. Appendix — result objects
Generated from the code. All are frozen dataclasses: read fields, don't mutate. extra holds unmodelled server fields.
15.1 Objects the SDK returns
Eligibility
Eligibility(patient_id: 'PatientId | None', full_name: 'str', status: 'EligibilityStatus | None', status_description: 'str', schemes: 'tuple[Scheme, ...]', date_of_birth: 'date | None' = None, gender: 'str' = '', age: 'int | None' = None, is_alive: 'bool | None' = None, whitelisted_for_otp: 'bool' = False, facility_biometrics_enforced: 'bool' = False, extra: 'Mapping[str, Any]' = )
| Field | Type | Required |
|---|---|---|
patient_id |
`PatientId | None` |
full_name |
str |
yes |
status |
`EligibilityStatus | None` |
status_description |
str |
yes |
schemes |
tuple[Scheme, ...] |
yes |
date_of_birth |
`date | None` |
gender |
str |
no |
age |
`int | None` |
is_alive |
`bool | None` |
whitelisted_for_otp |
bool |
no |
facility_biometrics_enforced |
bool |
no |
extra |
dict |
no |
Properties: member_found
Scheme
Scheme(name: 'str', scheme_id: 'int | None', member_type: 'str', policy_number: 'str', policy_period: 'DateRange', coverage: 'Coverage')
| Field | Type | Required |
|---|---|---|
name |
str |
yes |
scheme_id |
`int | None` |
member_type |
str |
yes |
policy_number |
str |
yes |
policy_period |
DateRange |
yes |
coverage |
Coverage |
yes |
Coverage
Coverage(status: 'CoverageStatus | None', message: 'str', reason: 'str', period: 'DateRange')
| Field | Type | Required |
|---|---|---|
status |
`CoverageStatus | None` |
message |
str |
yes |
reason |
str |
yes |
period |
DateRange |
yes |
DateRange
DateRange(start: 'date | None', end: 'date | None')
| Field | Type | Required |
|---|---|---|
start |
`date | None` |
end |
`date | None` |
BenefitPackage
BenefitPackage(code: 'str', name: 'str')
| Field | Type | Required |
|---|---|---|
code |
str |
yes |
name |
str |
yes |
SubBenefit
SubBenefit(code: 'str', name: 'str', parent_code: 'str', access_point: 'str')
| Field | Type | Required |
|---|---|---|
code |
str |
yes |
name |
str |
yes |
parent_code |
str |
yes |
access_point |
str |
yes |
InterventionCoverage
InterventionCoverage(code: 'InterventionCode', name: 'str', payment_mechanism: 'PaymentMechanism | None', needs_preauth: 'bool', needs_doctor_authorization: 'bool', access_point: 'str', overall_tariff: 'Money | None', applicable_schemes: 'tuple[str, ...]', fund: 'str' = '', sub_benefit_code: 'str' = '', extra: 'Mapping[str, Any]' = )
| Field | Type | Required |
|---|---|---|
code |
InterventionCode |
yes |
name |
str |
yes |
payment_mechanism |
`PaymentMechanism | None` |
needs_preauth |
bool |
yes |
needs_doctor_authorization |
bool |
yes |
access_point |
str |
yes |
overall_tariff |
`Money | None` |
applicable_schemes |
tuple[str, ...] |
yes |
fund |
str |
no |
sub_benefit_code |
str |
no |
extra |
dict |
no |
Properties: service_type_for_authorization
UtilizationBalance
GET /patients/benefits/utilization — how much of a benefit the member (and household) has left.
| Field | Type | Required |
|---|---|---|
intervention_code |
`InterventionCode | None` |
patient_id |
str |
yes |
limit_scope |
str |
yes |
individual_max |
`Money | None` |
individual_utilised |
`Money | None` |
household_max |
`Money | None` |
household_utilised |
`Money | None` |
available_amount |
`Money | None` |
eligible |
`bool | None` |
next_availability |
str |
no |
funds |
tuple[FundLimit, ...] |
no |
extra |
dict |
no |
Properties: individual_remaining
FundLimit
FundLimit(fund_type: 'str', max_amount: 'Money | None', utilised_amount: 'Money | None', available_amount: 'Money | None')
| Field | Type | Required |
|---|---|---|
fund_type |
str |
yes |
max_amount |
`Money | None` |
utilised_amount |
`Money | None` |
available_amount |
`Money | None` |
BedOccupancy
BedOccupancy(facility_name: 'str', level: 'str', total_beds: 'int', total_inpatient_visits: 'int', normal_beds: 'int' = 0, icu_beds: 'int' = 0, hdu_beds: 'int' = 0, dialysis_beds: 'int' = 0, baby_cots: 'int' = 0, extra: 'Mapping[str, Any]' = )
| Field | Type | Required |
|---|---|---|
facility_name |
str |
yes |
level |
str |
yes |
total_beds |
int |
yes |
total_inpatient_visits |
int |
yes |
normal_beds |
int |
no |
icu_beds |
int |
no |
hdu_beds |
int |
no |
dialysis_beds |
int |
no |
baby_cots |
int |
no |
extra |
dict |
no |
Properties: occupancy_rate
Authorization
Snapshot of POST /claims/authorize / GET /claims/authorizations.
| Field | Type | Required |
|---|---|---|
guid |
str |
yes |
token |
str |
yes |
auth_code |
str |
yes |
status |
`AuthorizationStatus | None` |
label |
str |
yes |
is_open |
bool |
yes |
benefit_type |
str |
yes |
beneficiary |
`PatientId | None` |
beneficiary_name |
str |
yes |
provider_fid |
str |
yes |
interventions |
tuple[AuthorizedIntervention, ...] |
yes |
expiry |
`datetime | None` |
overall_preauth_finalised |
bool |
no |
record_id |
`int | None` |
extra |
dict |
no |
Properties: is_pending, needs_preauth
AuthorizedIntervention
AuthorizedIntervention(code: 'InterventionCode', name: 'str', needs_preauth: 'bool', payment_mechanism: 'PaymentMechanism | None', sub_benefit_code: 'str' = '')
| Field | Type | Required |
|---|---|---|
code |
InterventionCode |
yes |
name |
str |
yes |
needs_preauth |
bool |
yes |
payment_mechanism |
`PaymentMechanism | None` |
sub_benefit_code |
str |
no |
VirtualClaim
VirtualClaim(consent_token: 'ConsentToken', guid: 'ClaimGuid | None', claim_id: 'int | None', workflow_state: 'str', claim_auth_status: 'str', service_type: 'ServiceType | None', patient_name: 'str', member_number: 'str', payer_name: 'str', scheme_name: 'str', currency: 'str', total_amount: 'Money | None', net_amount: 'Money | None', total_copay: 'Money | None' = None, total_discount: 'Money | None' = None, invoice_number: 'InvoiceNumber | None' = None, visit_number: 'str' = '', visit_start: 'datetime | None' = None, visit_end: 'datetime | None' = None, interventions: 'tuple[ClaimIntervention, ...]' = (), diagnoses: 'tuple[ClaimDiagnosis, ...]' = (), attachments: 'tuple[ClaimAttachment, ...]' = (), invoices: 'tuple[Invoice, ...]' = (), is_negative: 'bool' = False, is_zero: 'bool' = False, extra: 'Mapping[str, Any]' = )
| Field | Type | Required |
|---|---|---|
consent_token |
ConsentToken |
yes |
guid |
`ClaimGuid | None` |
claim_id |
`int | None` |
workflow_state |
str |
yes |
claim_auth_status |
str |
yes |
service_type |
`ServiceType | None` |
patient_name |
str |
yes |
member_number |
str |
yes |
payer_name |
str |
yes |
scheme_name |
str |
yes |
currency |
str |
yes |
total_amount |
`Money | None` |
net_amount |
`Money | None` |
total_copay |
`Money | None` |
total_discount |
`Money | None` |
invoice_number |
`InvoiceNumber | None` |
visit_number |
str |
no |
visit_start |
`datetime | None` |
visit_end |
`datetime | None` |
interventions |
tuple[ClaimIntervention, ...] |
no |
diagnoses |
tuple[ClaimDiagnosis, ...] |
no |
attachments |
tuple[ClaimAttachment, ...] |
no |
invoices |
tuple[Invoice, ...] |
no |
is_negative |
bool |
no |
is_zero |
bool |
no |
extra |
dict |
no |
Properties: preauth_outstanding, lines
ClaimIntervention
ClaimIntervention(code: 'InterventionCode', name: 'str', payment_mechanism: 'PaymentMechanism | None', needs_preauth: 'bool', preauth_exists: 'bool', workflow_state: 'str', sub_benefit_code: 'str' = '', fund: 'str' = '', overall_tariff: 'Money | None' = None, applicable_document_types: 'tuple[str, ...]' = (), required_preauth_document_types: 'tuple[str, ...]' = (), bill_from: 'datetime | None' = None, bill_to: 'datetime | None' = None, accrued_per_diem_days: 'int' = 0, accrued_per_diem: 'Money | None' = None, keph_level_tariff: 'Money | None' = None, extra: 'Mapping[str, Any]' = )
| Field | Type | Required |
|---|---|---|
code |
InterventionCode |
yes |
name |
str |
yes |
payment_mechanism |
`PaymentMechanism | None` |
needs_preauth |
bool |
yes |
preauth_exists |
bool |
yes |
workflow_state |
str |
yes |
sub_benefit_code |
str |
no |
fund |
str |
no |
overall_tariff |
`Money | None` |
applicable_document_types |
tuple[str, ...] |
no |
required_preauth_document_types |
tuple[str, ...] |
no |
bill_from |
`datetime | None` |
bill_to |
`datetime | None` |
accrued_per_diem_days |
int |
no |
accrued_per_diem |
`Money | None` |
keph_level_tariff |
`Money | None` |
extra |
dict |
no |
Properties: preauth_outstanding, is_per_diem, per_diem_allowance
ClaimDiagnosis
ClaimDiagnosis(code: 'Icd11Code | None', name: 'str', intervention_code: 'InterventionCode | None', record_id: 'int | None' = None, is_flagged: 'bool' = False, recorded_on: 'datetime | None' = None, extra: 'Mapping[str, Any]' = )
| Field | Type | Required |
|---|---|---|
code |
`Icd11Code | None` |
name |
str |
yes |
intervention_code |
`InterventionCode | None` |
record_id |
`int | None` |
is_flagged |
bool |
no |
recorded_on |
`datetime | None` |
extra |
dict |
no |
ClaimLine
ClaimLine(guid: 'LineGuid | None', intervention_code: 'InterventionCode | None', item_code: 'str', item_name: 'str', quantity: 'Decimal', unit_price: 'Money | None', total_amount: 'Money | None', net_amount: 'Money | None', copay: 'Money | None' = None, scheme_code: 'str' = '', charge_date: 'date | None' = None, is_active: 'bool' = True, doctor_name: 'str' = '', rebate_amount: 'Money | None' = None, sponsor_net: 'Money | None' = None, patient_net: 'Money | None' = None, benefit_exceeded: 'bool' = False, extra: 'Mapping[str, Any]' = )
| Field | Type | Required |
|---|---|---|
guid |
`LineGuid | None` |
intervention_code |
`InterventionCode | None` |
item_code |
str |
yes |
item_name |
str |
yes |
quantity |
Decimal |
yes |
unit_price |
`Money | None` |
total_amount |
`Money | None` |
net_amount |
`Money | None` |
copay |
`Money | None` |
scheme_code |
str |
no |
charge_date |
`date | None` |
is_active |
bool |
no |
doctor_name |
str |
no |
rebate_amount |
`Money | None` |
sponsor_net |
`Money | None` |
patient_net |
`Money | None` |
benefit_exceeded |
bool |
no |
extra |
dict |
no |
Invoice
An invoice inside a virtual claim (lines are grouped by invoice on the server side).
| Field | Type | Required |
|---|---|---|
invoice_id |
str |
yes |
invoice_number |
`InvoiceNumber | None` |
invoice_type |
str |
yes |
workflow_state |
str |
yes |
dispatch_status |
str |
yes |
total_amount |
`Money | None` |
net_amount |
`Money | None` |
copay |
`Money | None` |
discount |
`Money | None` |
lines |
tuple[ClaimLine, ...] |
no |
invoice_date |
`date | None` |
extra |
dict |
no |
ClaimAttachment
ClaimAttachment(attachment_id: 'AttachmentId | None', title: 'str', attachment_type: 'str', intervention_code: 'InterventionCode | None', description: 'str' = '', extra: 'Mapping[str, Any]' = )
| Field | Type | Required |
|---|---|---|
attachment_id |
`AttachmentId | None` |
title |
str |
yes |
attachment_type |
str |
yes |
intervention_code |
`InterventionCode | None` |
description |
str |
no |
extra |
dict |
no |
PayerClaimRecord
A row from GET /claims/preview/payer — the claim as the payer's system sees it (camelCase on the wire).
| Field | Type | Required |
|---|---|---|
guid |
str |
yes |
provider_claim_no |
str |
yes |
tracking_number |
str |
yes |
workflow_state |
str |
yes |
workflow_display_name |
str |
yes |
claim_type |
str |
yes |
is_inpatient |
bool |
yes |
proposed_value |
`Money | None` |
proposed_value_less_copays |
`Money | None` |
total_copay |
`Money | None` |
member_name |
str |
no |
member_number |
str |
no |
scheme_name |
str |
no |
created |
`datetime | None` |
extra |
dict |
no |
Properties: status
NextOfKinContact
NextOfKinContact(guid: 'str', full_name: 'str', id_number: 'str', contact_value: 'str', contact_type: 'str', is_verified: 'bool', is_confirmed: 'bool', is_main_contact: 'bool', owner_type: 'str' = '', extra: 'Mapping[str, Any]' = )
| Field | Type | Required |
|---|---|---|
guid |
str |
yes |
full_name |
str |
yes |
id_number |
str |
yes |
contact_value |
str |
yes |
contact_type |
str |
yes |
is_verified |
bool |
yes |
is_confirmed |
bool |
yes |
is_main_contact |
bool |
yes |
owner_type |
str |
no |
extra |
dict |
no |
LineResubmission
LineResubmission(line: 'LineGuid | None', status: 'str', message: 'str', resubmitted_at: 'datetime | None' = None)
| Field | Type | Required |
|---|---|---|
line |
`LineGuid | None` |
status |
str |
yes |
message |
str |
yes |
resubmitted_at |
`datetime | None` |
Preauthorization
Snapshot of a pre-authorisation record (camelCase on the wire).
| Field | Type | Required |
|---|---|---|
guid |
str |
yes |
token |
str |
yes |
intervention_code |
`InterventionCode | None` |
status |
str |
yes |
doctor_review_status |
str |
yes |
needs_doctor_approval |
bool |
yes |
doctor_approved |
bool |
yes |
doctors_required |
int |
yes |
is_request_phase |
bool |
yes |
is_response_phase |
bool |
yes |
total_estimated |
`Money | None` |
interim_approved |
`Money | None` |
final_approved |
`Money | None` |
service_start |
`datetime | None` |
service_end |
`datetime | None` |
provider_notification_email |
str |
no |
member_name |
str |
no |
description |
str |
no |
countdown |
`int | None` |
record_id |
`int | None` |
extra |
dict |
no |
Properties: awaiting_doctor, decided
Prescription
Prescription(guid: 'str', code: 'str', status: 'str', doctor_review_status: 'str', intervention_code: 'InterventionCode | None', dosage: 'tuple[Dosage, ...]', record_id: 'int | None' = None, extra: 'Mapping[str, Any]' = )
| Field | Type | Required |
|---|---|---|
guid |
str |
yes |
code |
str |
yes |
status |
str |
yes |
doctor_review_status |
str |
yes |
intervention_code |
`InterventionCode | None` |
dosage |
tuple[Dosage, ...] |
yes |
record_id |
`int | None` |
extra |
dict |
no |
Dosage
A dosage line as the server stores it.
| Field | Type | Required |
|---|---|---|
medication |
str |
yes |
medication_identifier |
str |
yes |
dose_quantity |
`Decimal | None` |
dose_unit |
str |
yes |
frequency |
`int | None` |
period_unit |
str |
yes |
duration |
str |
yes |
duration_unit |
str |
yes |
route |
str |
no |
start_date |
`date | None` |
end_date |
`date | None` |
price |
`Money | None` |
status |
str |
no |
extra |
dict |
no |
Dispense
Dispense(record_id: 'int | None', status: 'str', dosages: 'tuple[Dosage, ...]', extra: 'Mapping[str, Any]' = )
| Field | Type | Required |
|---|---|---|
record_id |
`int | None` |
status |
str |
yes |
dosages |
tuple[Dosage, ...] |
yes |
extra |
dict |
no |
EmergencyProtocol
EmergencyProtocol(code: 'ProtocolCode', name: 'str', protocol_type: 'str', classification: 'str', status: 'str', tariff: 'Money | None', guid: 'str' = '', extra: 'Mapping[str, Any]' = )
| Field | Type | Required |
|---|---|---|
code |
ProtocolCode |
yes |
name |
str |
yes |
protocol_type |
str |
yes |
classification |
str |
yes |
status |
str |
yes |
tariff |
`Money | None` |
guid |
str |
no |
extra |
dict |
no |
StoredFile
Result of POST /uploads. The documented response is {}; the id/path surface when the server sends them.
| Field | Type | Required |
|---|---|---|
file_id |
`FileId | None` |
path |
str |
no |
extra |
dict |
no |
DownloadLink
DownloadLink(url: 'str', message: 'str' = '', extra: 'Mapping[str, Any]' = )
| Field | Type | Required |
|---|---|---|
url |
str |
yes |
message |
str |
no |
extra |
dict |
no |
15.2 Objects you construct (commands)
Required = yes means you must pass it. Each validates in its constructor and raises ValueError; when passed through a ClaimSession method the same problem surfaces as RequestValidationError.
NewClaimLine
Command for POST /claims/lines.
| Field | Type | Required |
|---|---|---|
intervention_code |
InterventionCode |
yes |
unit_price |
Money |
yes |
quantity |
`Decimal | int` |
scheme_code |
`SchemeCode | None` |
charge_date |
`date | None` |
diagnoses |
tuple[Icd11Code, ...] |
no |
Properties: total
LineEdit
Command for PATCH /claims/lines/edit (after payer review).
| Field | Type | Required |
|---|---|---|
line |
LineGuid |
yes |
quantity |
`int | None` |
unit_price |
`Money | None` |
scheme_code |
`SchemeCode | None` |
Discharge
Command for POST /claims/discharge (inpatient). The OTP comes from send_discharge_otp.
| Field | Type | Required |
|---|---|---|
discharge_date |
date |
yes |
reason |
DischargeReason |
yes |
invoice_number |
InvoiceNumber |
yes |
otp |
Otp |
yes |
NextOfKin
Command for POST /patients/next-of-kin/contacts — who consents when the beneficiary cannot.
| Field | Type | Required |
|---|---|---|
full_name |
str |
yes |
id_number |
str |
yes |
id_type |
NextOfKinIdType |
yes |
contact_value |
str |
yes |
PreauthRequest
Command for POST /preauths. Validates what the server would certainly reject.
| Field | Type | Required |
|---|---|---|
intervention_code |
InterventionCode |
yes |
service_start |
datetime |
yes |
service_end |
datetime |
yes |
items |
tuple[PreauthItem, ...] |
yes |
diagnoses |
tuple[Icd11Code, ...] |
yes |
doctors |
tuple[PractitionerRef, ...] |
yes |
provider_notification_email |
str |
yes |
attachments |
tuple[Attachment, ...] |
no |
Properties: estimated_total
PreauthItem
A billed item on the pre-auth request.
| Field | Type | Required |
|---|---|---|
code |
str |
yes |
description |
str |
yes |
quantity |
`Decimal | int` |
unit_price |
Money |
yes |
Properties: total
DoctorConsentRequest
Command for POST /claims/doctor-consent.
| Field | Type | Required |
|---|---|---|
intervention_code |
InterventionCode |
yes |
request_type |
DoctorConsentRequestType |
yes |
practitioner |
PractitionerRef |
yes |
service_type |
`ServiceType | None` |
emergency_claim_id |
`str | None` |
MedicationOrder
One prescribed medication (an items[] entry of POST /prescriptions).
| Field | Type | Required |
|---|---|---|
generic_concept_code |
str |
yes |
dose_quantity |
`Decimal | int` |
dose_unit |
str |
yes |
frequency |
int |
yes |
period_unit |
str |
yes |
duration |
int |
yes |
duration_unit |
str |
yes |
start_date |
date |
yes |
end_date |
`date | None` |
patient_instruction |
str |
no |
additional_instruction |
str |
no |
needs_refill |
bool |
no |
refill_count |
int |
no |
PrescriptionRequest
PrescriptionRequest(intervention_code: 'InterventionCode', items: 'tuple[MedicationOrder, ...]', prescriber: 'PractitionerRef | None' = None)
| Field | Type | Required |
|---|---|---|
intervention_code |
InterventionCode |
yes |
items |
tuple[MedicationOrder, ...] |
yes |
prescriber |
`PractitionerRef | None` |
DispensedProduct
An actual_products[] entry of POST /prescriptions/dispenses.
| Field | Type | Required |
|---|---|---|
product_code |
str |
yes |
quantity |
`Decimal | int` |
price |
Money |
yes |
DispenseRequest
DispenseRequest(intervention_code: 'InterventionCode', products: 'tuple[DispensedProduct, ...]', dispensers: 'tuple[PractitionerRef, ...]')
| Field | Type | Required |
|---|---|---|
intervention_code |
InterventionCode |
yes |
products |
tuple[DispensedProduct, ...] |
yes |
dispensers |
tuple[PractitionerRef, ...] |
yes |
EmergencyCase
Command for POST /claims/emergency. beneficiary is None for an unidentified patient.
| Field | Type | Required |
|---|---|---|
attending |
PractitionerRef |
yes |
reference_number |
str |
yes |
brought_by |
BroughtBy |
yes |
mode_of_arrival |
ModeOfArrival |
yes |
interventions |
tuple[InterventionCode, ...] |
yes |
beneficiary |
`PatientId | None` |
otp |
`Otp | None` |
notes |
str |
no |
ProtocolLine
Command for POST /claims/emergency/protocols — bills a treatment protocol on an emergency claim.
| Field | Type | Required |
|---|---|---|
protocol_code |
ProtocolCode |
yes |
intervention_code |
InterventionCode |
yes |
unit_price |
Money |
yes |
quantity |
int |
no |
diagnoses |
tuple[Icd11Code, ...] |
no |
EmtClaim
Command for POST /claims/emt — the ambulance / EMT provider's claim for an emergency case.
| Field | Type | Required |
|---|---|---|
protocol_code |
ProtocolCode |
yes |
case_number |
str |
yes |
practitioner_registration_number |
str |
yes |
provider_registration_number |
str |
yes |
beneficiary |
PatientId |
yes |
otp |
Otp |
yes |
diagnoses |
tuple[Icd11Code, ...] |
yes |
interventions |
tuple[InterventionCode, ...] |
yes |
attachments |
tuple[Attachment, ...] |
no |
Attachment
Attachment(filename: 'str', content: 'bytes', document_type: 'DocumentType', content_type: 'str' = 'application/octet-stream')
| Field | Type | Required |
|---|---|---|
filename |
str |
yes |
content |
bytes |
yes |
document_type |
DocumentType |
yes |
content_type |
str |
no |
Properties: size
PractitionerRef
Identifies a doctor by registration number (preferred) or a national identity document.
| Field | Type | Required |
|---|---|---|
identification_number |
str |
yes |
identification_type |
IdentificationType |
yes |
regulation_body |
RegulationBody |
yes |
15.3 Enum values
AuthorizationStatus(lenient — unknown values allowed):PENDING=PENDINGBroughtBy(strict):RELATIVE=RELATIVE,UNKNOWN=UNKNOWN,SAMARITAN=SAMARITAN,PARAMEDICS=PARAMEDICSCancelReason(strict):WRONG_PATIENT=WRONG_PATIENT,NO_SERVICE_GIVEN=NO_SERVICE_GIVEN,WRONG_BENEFIT=WRONG_BENEFIT,EXPIRED_VISIT=EXPIRED_VISIT,EXHAUSTED_BENEFIT=EXHAUSTED_BENEFIT,TIME_BARRED=TIME_BARRED,OTHER_REASONS=OTHER_REASONSCoverageStatus(lenient — unknown values allowed):COVERED=1DischargeReason(strict):RECOVERED=RECOVERED,REFERRED=REFERRED,DECEASED=DECEASED,ABSCONDED=ABSCONDED,OTHER=OTHERDoctorConsentRequestType(strict):PREAUTH_DOCTOR_APPROVAL=PREAUTH_DOCTOR_APPROVAL_REQUEST,EMERGENCY_CLAIM_DOCTOR_APPROVAL=EMERGENCY_CLAIM_DOCTOR_APPROVAL_REQUEST,PRESCRIPTION=PRESCRIPTION_REQUESTEligibilityStatus(lenient — unknown values allowed):MEMBER_FOUND=10IdentificationType(strict):NATIONAL_ID=National ID,ALIEN_ID=Alien ID,REFUGEE_ID=Refugee ID,REGISTRATION_NUMBER=registration_numberModeOfArrival(strict):AMBULANCE=AMBULANCE,WALK_IN=WALK-IN,OTHER=OTHERNextOfKinIdType(strict):NATIONAL_ID=National ID,CLIENT_REGISTRY_ID=ClientRegistry ID,BIRTH_NOTIFICATION=Birth Notification,BIRTH_CERTIFICATE=Birth Certificate,ALIEN_ID=Alien ID,REFUGEE_ID=Refugee ID,MANDATE_NUMBER=Mandate Number,TEMPORARY_ID=Temporary IDPaymentMechanism(lenient — unknown values allowed):CAPITATION=CAPITATION,CASE_BASED=CASE BASED,FEE_FOR_SERVICE=FEE FOR SERVICE,PER_DIEM=PER DIEMServiceType(strict):CAPITATION=CAPITATION,OUTPATIENT=OUTPATIENT,INPATIENT=INPATIENT,EMERGENCY=EMERGENCYDocumentType(strict):BIO_DETAILS=BIO_DETAILS,BIRTH_NOTIFICATION=BIRTH_NOTIFICATION,CARE_PLAN=CARE_PLAN,CASE_NOTE=CASE_NOTE,CASE_SUMMARY=CASE_SUMMARY,CERTIFIED_BURIAL_PERMIT=CERTIFIED_BURIAL_PERMIT,CERTIFIED_COPY_OF_DECEASED_ID=CERTIFIED_COPY_OF_DECEASED_ID,CLAIM_FORM=CLAIM_FORM,COVER_LETTER_FROM_EMPLOYER=COVER_LETTER_FROM_EMPLOYER,CRITICAL_CARE_UNIT_CASE=CRITICAL_CARE_UNIT_CASE,CT_SCAN=CT_SCAN,DEATH_NOTICE=DEATH_NOTICE,DIALYSIS_CHART=DIALYSIS_CHART,DISCHARGE_SUMMARY=DISCHARGE_SUMMARY,ENTRY_EXIT_VISA_STAMP=ENTRY_EXIT_VISA_STAMP,FINAL_BILL=FINAL_BILL,IMAGING_ORDER=IMAGING_ORDER,IMAGING_REPORT=IMAGING_REPORT,INVOICE=INVOICE,LAB_ORDER=LAB_ORDER,LAB_RESULTS=LAB_RESULTS,MAGNETIC_RESONANCE_IMAGING=MAGNETIC_RESONANCE_IMAGING,MEDICAL_REPORT=MEDICAL_REPORT,OTHER=OTHER,POST_SERVICE_IMAGING_REPORT=POST_SERVICE_IMAGING_REPORT,PRE_SERVICE_IMAGING_REPORT=PRE_SERVICE_IMAGING_REPORT,PREAUTH_FORM=PREAUTH_FORM,PRESCRIPTION=PRESCRIPTION,REQUEST_FORM_BY_RELEVANT_CONSULTANT=REQUEST_FORM_BY_RELEVANT_CONSULTANT,RHESUS_FACTOR=RHESUS_FACTOR,THEATRE_NOTES=THEATRE_NOTESRegulationBody(strict):KMPDC=KMPDC,COC=COC,NCK=NCK
Design in one paragraph
The server owns the claim: you open a virtual claim, receive a consent_token, mutate the claim
call by call, then submit. The SDK mirrors that faithfully — it holds snapshots, not a local
state machine — and only rejects locally what the server will certainly reject. Reads are retried
with backoff; submit is attempted exactly once. It stores nothing: no database, no polling, no
background tasks — your on_event hook is where audit logging and metrics live. Layers follow Clean
Architecture and are enforced by import-linter. Details: docs/ARCHITECTURE.md.
Treat the consent_token like a password — it lets anyone bill against that patient's visit. Persist
token.value, never str(token), which is redacted on purpose.
Develop
make install # venv + editable install with dev extras
make check # ruff, mypy --strict, import-linter, pytest (≥ 90 % coverage)
make live # RUN_LIVE=1: smoke tests against DHA UAT (needs .env)
License
Apache-2.0
Release files for sha-claim 0.1.18
For a detailed explanation of source distributions (sdists) and built distributions (wheels), please see the package formats documentation.
Source distribution (sdist)
| File | Size | Uploaded | |
|---|---|---|---|
| sha_claim-0.1.18.tar.gz | 188.6 kB | Details |
Built distribution (wheel)
| File | Interpreter | ABI | Platform | Reset |
|---|---|---|---|---|
| sha_claim-0.1.18-py3-none-any.whl | Python 3 | none | any | Details |
Total release size: 299.2 kB
Release files / sha_claim-0.1.18.tar.gz
| Download URL | sha_claim-0.1.18.tar.gz |
|---|---|
| Size | 188.6 kB |
| Tags | Source |
|
SHA-256 checksum How to use checksums |
d8be24754f061b13c7daf05f6c091d2e0e948e3f0f21870d3adf3ced328e69fc
|
|
BLAKE2b-256 checksum How to use checksums |
dbae9376d0e32ab657c9a01ef85bd835e0ac093ac70f033dcf35801797c922aa
|
| Upload date | |
|
Uploaded using Trusted Publishing? What is trusted publishing? |
Yes |
| Uploaded via |
twine/7.0.0 CPython/3.13.14
|
Provenance
Provenance describes where a file came from. On PyPI, provenance is shared via attestations, which provide a verifiable record of the build or publishing details. View details, limitations and caveats.
PyPI Publish Attestation
PyPI verified that this artifact, at this checksum, originated from the publisher listed below.
Signed by GitHub Actions, verified by PyPI on Sep 24, 2026.
Transparency logRelease files / sha_claim-0.1.18-py3-none-any.whl
| Download URL | sha_claim-0.1.18-py3-none-any.whl |
|---|---|
| Size | 110.6 kB |
| Tags | Python 3 |
|
SHA-256 checksum How to use checksums |
86fb626de9187bcb308f1c226c52fb5610e6abe482658031def33a73f1190529
|
|
BLAKE2b-256 checksum How to use checksums |
7e08d51d3feb1cb8632e1799697f87a361aa9d7b3414a8027c79a2df5d10ee9b
|
| Upload date | |
|
Uploaded using Trusted Publishing? What is trusted publishing? |
Yes |
| Uploaded via |
twine/7.0.0 CPython/3.13.14
|
Provenance
Provenance describes where a file came from. On PyPI, provenance is shared via attestations, which provide a verifiable record of the build or publishing details. View details, limitations and caveats.
PyPI Publish Attestation
PyPI verified that this artifact, at this checksum, originated from the publisher listed below.
Signed by GitHub Actions, verified by PyPI on Sep 24, 2026.
Transparency log