Utangulizi
Hii ndiyo API moja inayotumiwa na kila kitu kwenye mfumo huu: konsoli ya hospitali, mlango wa mgonjwa, na app yoyote ya simu au software ya desktop utakayojenga. Hakuna API ya pili iliyofichwa mahali pengine.
Kila endpoint kwenye ukurasa huu imesomwa moja kwa moja kutoka kwenye msimbo wa seva
(backend/internal/router/router.go na backend/internal/handlers/).
Hakuna kitu kilichoandikwa kwa mkono hapa, kwa hiyo kinachoandikwa ndicho seva inachofanya.
Ikiwa endpoint imebadilika, ukurasa huu unabadilika inapotengenezwa upya.
Kanuni tatu za kukumbuka:
- Kila jibu lina muundo mmoja:
{success, message, data, meta}. - Karibu kila njia inahitaji tokeni na ruhusa mahususi. Kificha kitufe kwenye app si ulinzi — seva ndiyo inayozuia.
- Ujumbe wa makosa umeandikwa kwa lugha ya binadamu. Uonyeshe kama ulivyo badala ya kubuni ujumbe wako mwenyewe.
Introduction
This is the single API behind everything in this system: the hospital console, the patient portal, and any mobile app or desktop program you build. There is no second, hidden API somewhere else.
Every endpoint on this page was read straight out of the server's own source
(backend/internal/router/router.go and backend/internal/handlers/).
Nothing here is hand-maintained, so what it says is what the server does.
Three rules to carry with you:
- Every reply has one shape:
{success, message, data, meta}. - Almost every route needs a token and a specific permission. Hiding a button in your app is manners; the server is the thing that protects the record.
- Error messages are written for a human to read. Show them as they are rather than inventing your own wording.
Anwani za seva
Trafiki yote ya umma hupitia api-gateway. Njia hazibadilishwi njiani — unachokiona
hapa (/api/v1/...) ndicho unachotuma.
Base URLs
All public traffic goes through the api-gateway. Paths are not rewritten on the way —
what you see here (/api/v1/...) is what you send.
| Environment | Base URL | Note |
|---|---|---|
| Local stack | http://localhost:4000 | api-gateway, the only public port |
| Backend direct | http://localhost:8080 | Development and the liveness probe only |
| Deployed hospital | https://<host> | The console's nginx proxies /api/ to the gateway |
| Emulator (Android) | http://10.0.2.2:4000 | localhost inside the emulator is the emulator |
GET /api/v1/settings/system — inajibu bila tokeni na inarudisha
jina la hospitali na nembo, kwa hiyo mtumiaji anaona mara moja kama ameandika anwani sahihi.
Do not bake the host into the app. A different hospital is a different
server. Ask for the address on first run, store it, and let it be changed in settings. Then
confirm it with GET /api/v1/settings/system — it answers without a token and returns
the hospital's name and logo, so the user can see at once whether they typed the right address.
Kuingia na tokeni
Mtiririko ni huu: login → (labda 2FA) → tokeni → refresh → logout.
POST /api/v1/auth/loginukiwa naidentifier(jina la mtumiaji, barua pepe au simu) napassword.- Ikiwa akaunti ina 2FA, jibu litakuwa
requires_2fa: truepamoja nasession_token. Hakuna tokeni bado. Tuma msimbo kwaPOST /api/v1/auth/verify-2fa. - Utapokea
data.tokens:access_token,refresh_token,expires_in. TumaAuthorization: Bearer <access_token>kwenye kila ombi linalofuata. - Ukipata 401, tumia
POST /api/v1/auth/refreshmara moja (soma onyo hapa chini), kisha rudia ombi. Refresh ikishindwa, mrudishe mtumiaji kwenye skrini ya kuingia. POST /api/v1/auth/logoutinafunga kikao upande wa seva. Futa tokeni zako pia.
Jibu la login pia linabeba kila kitu app yako inahitaji kujichora:
permissions (orodha kamili), roles, facility (tawi la mtumiaji),
branches kama anasimamia matawi yote, na country kwa ajili ya namba za simu.
Chora menyu yako kutoka permissions, si kutoka orodha uliyoandika kwenye app.
Sign-in and tokens
The flow is: login → (maybe 2FA) → tokens → refresh → logout.
POST /api/v1/auth/loginwith anidentifier(username, email or phone) andpassword.- If the account has two-factor on, the reply is
requires_2fa: truewith asession_token. No tokens are issued yet. Answer with the code atPOST /api/v1/auth/verify-2fa. - You receive
data.tokens:access_token,refresh_token,expires_in. SendAuthorization: Bearer <access_token>on every request after that. - On a 401, call
POST /api/v1/auth/refreshonce (see the warning below) and retry. If the refresh fails, send the user back to sign-in. POST /api/v1/auth/logoutcloses the session server-side. Clear your stored tokens too.
The login reply also carries everything your app needs to draw itself: the full
permissions list, roles, the user's facility,
branches when the account spans them, and country for phone-number
formatting. Draw your menu from permissions, not from a list you wrote into
the app.
# 1. sign in
curl -X POST http://localhost:4000/api/v1/auth/login \
-H 'Content-Type: application/json' \
-d '{"identifier":"admin","password":"secret"}'
# 2. use the access token
curl http://localhost:4000/api/v1/patients?page=1&per_page=20 \
-H 'Authorization: Bearer eyJhbGciOi...'
# 3. refresh when a call comes back 401
curl -X POST http://localhost:4000/api/v1/auth/refresh \
-H 'Content-Type: application/json' \
-d '{"refresh_token":"eyJhbGciOi..."}'const BASE = 'https://hospital.example.org';
let tokens = null;
let refreshing = null; // ONE in flight, shared by every caller
async function call(path, init = {}, retry = true) {
const headers = { 'Content-Type': 'application/json', ...(init.headers || {}) };
if (tokens) headers.Authorization = 'Bearer ' + tokens.access_token;
const res = await fetch(BASE + '/api/v1' + path, { ...init, headers });
const body = await res.json().catch(() => ({}));
if (res.status === 401 && retry && tokens?.refresh_token) {
refreshing = refreshing || refresh();
const ok = await refreshing;
refreshing = null;
if (ok) return call(path, init, false);
}
if (!res.ok || body.success === false) {
throw Object.assign(new Error(body.message || 'Request failed'), { status: res.status });
}
return body; // { success, message, data, meta? }
}
async function refresh() {
const res = await fetch(BASE + '/api/v1/auth/refresh', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ refresh_token: tokens.refresh_token }),
});
if (!res.ok) { tokens = null; return false; }
tokens = (await res.json()).data.tokens;
return true;
}
export async function login(identifier, password) {
const body = await call('/auth/login', {
method: 'POST', body: JSON.stringify({ identifier, password }),
});
if (body.data.requires_2fa) return { needsCode: true, sessionToken: body.data.session_token };
tokens = body.data.tokens;
return { needsCode: false, session: body.data }; // permissions live here
}import 'dart:convert';
import 'package:http/http.dart' as http;
class Hms {
Hms(this.base);
final String base; // e.g. https://hospital.example.org
Map<String, dynamic>? tokens;
Future<bool>? _refreshing;
Uri _u(String path, [Map<String, String>? q]) =>
Uri.parse('$base/api/v1$path').replace(queryParameters: q);
Future<Map<String, dynamic>> call(String path, {
String method = 'GET', Object? body, Map<String, String>? query, bool retry = true,
}) async {
final headers = {'Content-Type': 'application/json'};
if (tokens != null) headers['Authorization'] = 'Bearer ${tokens!['access_token']}';
final req = http.Request(method, _u(path, query))..headers.addAll(headers);
if (body != null) req.body = jsonEncode(body);
final res = await http.Response.fromStream(await req.send());
final decoded = res.body.isEmpty ? {} : jsonDecode(res.body) as Map<String, dynamic>;
if (res.statusCode == 401 && retry && tokens != null) {
_refreshing ??= _refresh();
final ok = await _refreshing!;
_refreshing = null;
if (ok) return call(path, method: method, body: body, query: query, retry: false);
}
if (res.statusCode >= 400 || decoded['success'] == false) {
throw HmsError(decoded['message'] ?? 'Request failed', res.statusCode);
}
return decoded;
}
Future<bool> _refresh() async {
final res = await http.post(_u('/auth/refresh'),
headers: {'Content-Type': 'application/json'},
body: jsonEncode({'refresh_token': tokens!['refresh_token']}));
if (res.statusCode >= 400) { tokens = null; return false; }
tokens = jsonDecode(res.body)['data']['tokens'] as Map<String, dynamic>;
return true;
}
}
class HmsError implements Exception {
HmsError(this.message, this.status);
final String message; final int status;
bool get isForbidden => status == 403;
@override String toString() => message; // show the server's own wording
}// Kotlin + OkHttp. The Authenticator handles 401 for every call in one place,
// and OkHttp serialises them for you, so nine parallel 401s spend one refresh.
val client = OkHttpClient.Builder()
.addInterceptor { chain ->
val request = chain.request().newBuilder()
.apply { store.access?.let { header("Authorization", "Bearer $it") } }
.build()
chain.proceed(request)
}
.authenticator { _, response ->
if (response.request.header("Authorization") == null) return@authenticator null
val refreshed = synchronized(store) { store.refreshOnce() } ?: return@authenticator null
response.request.newBuilder().header("Authorization", "Bearer $refreshed").build()
}
.build()
data class Envelope<T>(val success: Boolean, val message: String, val data: T?, val meta: PageMeta?)
data class PageMeta(val current_page: Int, val per_page: Int, val last_page: Int, val total: Int)
interface Api {
@POST("api/v1/auth/login")
suspend fun login(@Body body: LoginRequest): Envelope<Session>
@GET("api/v1/patients")
suspend fun patients(
@Query("search") search: String? = null,
@Query("page") page: Int = 1,
@Query("per_page") perPage: Int = 50,
): Envelope<List<Patient>>
}struct Envelope<T: Decodable>: Decodable {
let success: Bool; let message: String; let data: T?
}
actor Hms {
private let base: URL
private var tokens: Tokens?
private var refreshTask: Task<Bool, Never>? // one refresh, shared
init(base: URL) { self.base = base }
func call<T: Decodable>(_ path: String, method: String = "GET",
body: Encodable? = nil, retry: Bool = true) async throws -> T {
var req = URLRequest(url: base.appendingPathComponent("api/v1" + path))
req.httpMethod = method
req.setValue("application/json", forHTTPHeaderField: "Content-Type")
if let token = tokens?.accessToken {
req.setValue("Bearer \(token)", forHTTPHeaderField: "Authorization")
}
if let body { req.httpBody = try JSONEncoder().encode(AnyEncodable(body)) }
let (data, response) = try await URLSession.shared.data(for: req)
let status = (response as? HTTPURLResponse)?.statusCode ?? 0
if status == 401, retry, tokens != nil {
let task = refreshTask ?? Task { await self.refresh() }
refreshTask = task
let ok = await task.value
refreshTask = nil
if ok { return try await call(path, method: method, body: body, retry: false) }
}
let envelope = try JSONDecoder().decode(Envelope<T>.self, from: data)
guard envelope.success, let payload = envelope.data else {
throw HmsError(message: envelope.message, status: status) // show it verbatim
}
return payload
}
}// .NET — a DelegatingHandler keeps the retry out of every call site.
public sealed class HmsAuthHandler : DelegatingHandler {
private readonly TokenStore _store;
private readonly SemaphoreSlim _gate = new(1, 1); // one refresh at a time
protected override async Task<HttpResponseMessage> SendAsync(
HttpRequestMessage request, CancellationToken ct) {
request.Headers.Authorization = new("Bearer", _store.AccessToken);
var response = await base.SendAsync(request, ct);
if (response.StatusCode != HttpStatusCode.Unauthorized) return response;
await _gate.WaitAsync(ct);
try {
if (!await _store.RefreshAsync(ct)) return response; // session is gone
} finally { _gate.Release(); }
var retry = await CloneAsync(request);
retry.Headers.Authorization = new("Bearer", _store.AccessToken);
return await base.SendAsync(retry, ct);
}
}
public record Envelope<T>(bool Success, string Message, T? Data, PageMeta? Meta);
public record PageMeta(int Current_page, int Per_page, int Last_page, int Total);import requests
class Hms:
def __init__(self, base):
self.base, self.tokens = base.rstrip('/'), None
self.http = requests.Session()
def call(self, path, method='GET', json=None, params=None, _retry=True):
headers = {}
if self.tokens:
headers['Authorization'] = 'Bearer ' + self.tokens['access_token']
res = self.http.request(method, f'{self.base}/api/v1{path}',
json=json, params=params, headers=headers, timeout=30)
body = res.json() if res.content else {}
if res.status_code == 401 and _retry and self.tokens:
if self.refresh():
return self.call(path, method, json, params, _retry=False)
if res.status_code >= 400 or body.get('success') is False:
raise HmsError(body.get('message', 'Request failed'), res.status_code)
return body
def login(self, identifier, password):
body = self.call('/auth/login', 'POST', {'identifier': identifier, 'password': password})
if body['data'].get('requires_2fa'):
return body['data'] # answer it with /auth/verify-2fa
self.tokens = body['data']['tokens']
return body['data']
def refresh(self):
res = self.http.post(f'{self.base}/api/v1/auth/refresh',
json={'refresh_token': self.tokens['refresh_token']}, timeout=30)
if res.status_code >= 400:
self.tokens = None
return False
self.tokens = res.json()['data']['tokens']
return True
class HmsError(Exception):
def __init__(self, message, status):
super().__init__(message)
self.status = statusMuundo wa majibu
Kila endpoint — ikifanikiwa au ikishindwa — inajibu kwa muundo mmoja. Andika kichanganuzi kimoja, kitumie kila mahali.
Response envelope
Every endpoint — success or failure — replies in one shape. Write one parser and use it everywhere.
{
"success": true,
"message": "Patients retrieved",
"data": [ ... ], // always present; null when there is nothing to send
"meta": { // only on paged lists
"current_page": 1,
"per_page": 50,
"last_page": 4,
"total": 173
}
}
data haipotei kamwe — ni null ikiwa hakuna kitu, kwa hiyo
unaweza kuiingia bila kuangalia kwanza. meta inatokea tu kwenye orodha zenye kurasa.
data is never absent — it is null when there is nothing,
so you can index it without a nil check. meta appears only on paged lists.
Makosa
Jibu la kosa ni bahasha ile ile, ikiwa na success: false, data: null
na message iliyoandikwa kwa ajili ya mtu — "A patient is already registered under
that national ID". Onyesha ujumbe huo kama ulivyo.
Errors
A failure is the same envelope with success: false, data: null and a
message written for a person — "A patient is already registered under that
national ID". Show that message as it stands.
| Status | Meaning | What your client should do |
|---|---|---|
400 | The body could not be read, or a required field is missing | Show the message against the form |
401 | No token, invalid, or expired | Refresh once, retry, then sign out |
403 | Signed in, but not permitted — or the branch has the module switched off | Show the message; do not retry. The two read differently on purpose |
404 | No such record | Refresh the list — somebody may have removed it |
409 | A conflict with what is already recorded | Show the message; reload before retrying |
429 | Rate limit (open routes only) | Back off and retry later |
500 | The server failed | Show the message; allow a retry |
503 | A service behind the backend is down | Retry with backoff; say the feature is temporarily unavailable |
Kurasa na uchujaji
Orodha zote hutumia maneno yale yale: ?search=, ?page=,
?per_page= (chaguo-msingi 50, kikomo 200). page_size inakubalika pia kwa
daftari la wagonjwa. Soma meta.last_page ili kujua kama kuna ukurasa mwingine —
usihesabu kwa kukisia urefu wa orodha.
Vichujio vya kawaida: status, from, to,
patient_id, staff_id. Kila endpoint inaorodhesha vyake hapa chini.
Paging and filters
Every list speaks the same words: ?search=, ?page=,
?per_page= (default 50, capped at 200). page_size is also accepted on the
patient register. Read meta.last_page to know whether there is another page — do not
infer it from the length of the array.
Common filters are status, from, to,
patient_id, staff_id. Each endpoint lists its own below.
GET /api/v1/patients?search=juma&status=active&page=2&per_page=50
GET /api/v1/billing/bills?status=unpaid&from=2026-01-01&to=2026-01-31
Ruhusa
Kila njia (isipokuwa chache zilizo wazi) inalindwa na msimbo wa ruhusa kama
patient:read au payment:reverse. Orodha kamili ya ruhusa za mtumiaji
inakuja ndani ya jibu la kuingia (data.permissions) — na pia kutoka
GET /api/v1/auth/me.
Namna sahihi ya kujenga app: ficha au zima vitufe ambavyo mtumiaji hana ruhusa navyo, lakini usitegemee hiyo kama ulinzi. Seva itakataa vyovyote vile. Kuficha ni adabu; seva ndiyo ngome.
Ruhusa moja ni mahususi kabisa: portal:use. Ndiyo pekee akaunti ya mgonjwa
inayoshikilia, na haifungui chochote nje ya /portal. Ukijenga app ya mgonjwa, hizo
ndizo njia zako — na zote zinamtambua mgonjwa kutoka kwenye tokeni, si kutoka kwenye ombi.
Permissions
Every route but a handful of open ones is gated on a permission code such as
patient:read or payment:reverse. The caller's full list arrives inside
the sign-in reply (data.permissions) and from GET /api/v1/auth/me.
The right way to build against this: hide or disable what the user cannot do, but never treat that as the protection. The server refuses regardless. Hiding is manners; the server is the wall.
One permission is deliberately narrow: portal:use. It is the only one a patient
account holds, and it opens nothing outside /portal. If you are building a patient
app, those are your routes — and every one of them resolves the patient from the token, never
from the request.
Swichi za moduli
Tawi linaweza kuzima moduli lisilo nayo — chumba cha upasuaji, benki ya damu, chumba cha maiti, gari la wagonjwa, kliniki za TB/CTC, huduma za uzazi. Endpoints zake hujibu 403 hapo, hata kama mtumiaji ana ruhusa.
403 mbili zinatofautiana kwa makusudi: moja inasema mtumiaji hana ruhusa (nenda kwa msimamizi apewe ruhusa), nyingine inasema tawi halina huduma hiyo (nenda kwa msimamizi aiwashe). Onyesha ujumbe wa seva ili mtumiaji ajue anaenda kwa nani.
Endpoints zenye swichi zimewekewa alama module hapa chini.
Module switches
A branch can switch off what it does not run — the theatre, the blood bank, the mortuary, ambulance dispatch, TB/CTC clinics, maternity. Their endpoints answer 403 there even for a user who holds the permission.
The two 403s read differently on purpose: one says the person lacks a permission (go to an administrator to be granted it), the other says the branch does not run that service (go to an administrator to have it switched on). Show the server's message so the user knows who to see.
Endpoints behind a switch are marked module below.
Faili, picha na PDF
- Viambatisho:
POST /api/v1/attachmentskwamultipart/form-data(file,entity_type,entity_id). Kupakua niGET /api/v1/attachments/:id/download. - Picha:
GET|PUT /api/v1/photos/:subject/:id. Ruhusa inatokana na rekodi husika — anayeweza kufungua faili la mgonjwa anaweza kuona uso wake. - PDF: ankara, risiti, noti za krediti, taarifa za mgonjwa, vocha, oda za manunuzi na
slipu za mishahara hurudi kama
application/pdfsi JSON. Hifadhi baiti kama zilivyo, kisha zifungue kwa kionyeshi cha PDF cha mfumo.
Kila ombi hapo linahitaji kichwa cha Authorization pia — usiweke anwani hizo
moja kwa moja kwenye tagi ya <img> ambayo haipeleki tokeni.
Files, photos and PDFs
- Attachments:
POST /api/v1/attachmentsasmultipart/form-data(file,entity_type,entity_id). Download withGET /api/v1/attachments/:id/download. - Photos:
GET|PUT /api/v1/photos/:subject/:id. The permission comes from the subject — whoever may open the patient may see the patient's face. - PDFs: invoices, receipts, credit notes, patient statements, expense vouchers,
purchase orders and payslips return
application/pdf, not JSON. Keep the bytes as they are and hand them to the platform's PDF viewer.
All of these still need the Authorization header — so do not point a bare
<img> tag at them, because it will not send one.
Kujenga app ya simu — mwanzo mwisho
Hatua zifuatazo zinatosha kutoka sifuri hadi app inayofanya kazi wodini.
- Anza stack:
docker compose up, kisha thibitisha kwacurl http://localhost:4000/api/v1. - Tengeneza mteja (client): pakua
openapi.jsonna tumiaopenapi-generator(angalia sehemu ya zana), au andika safu ndogo ya HTTP kama mfano wa Dart hapo juu. Kwa app ndogo, kuandika mwenyewe ni rahisi zaidi. - Skrini ya anwani ya seva: mtumiaji aandike anwani ya hospitali; thibitisha kwa
GET /settings/systemna uonyeshe jina na nembo iliyorudi. - Kuingia:
POST /auth/login→ shughulikiarequires_2fa→ hifadhi tokeni kwenye hifadhi salama (Keychain / EncryptedSharedPreferences / flutter_secure_storage). Usihifadhi tokeni kwenye faili la kawaida. - Chora menyu kutoka
permissionszilizorudi, si kutoka orodha ya ndani. - Skrini za kwanza zenye thamani: foleni (
GET /visits?status=...), daftari la wagonjwa (GET /patients), kumbukumbu za mgonjwa (GET /patients/:id/visits,/vitals,/prescriptions,/diagnostics), na majibu ya dharura (GET /diagnostics/critical). - Kuandika: anza na vitals (
POST /visits/:id/vitals) na kusonga foleni (POST /visits/:id/advance) — vifupi, salama, na vinavyoonekana mara moja. - Arifa:
GET /notificationsnaPOST /notifications/:id/read. - Shughulikia 401/403 mahali pamoja (interceptor), si kwenye kila skrini.
- Jaribu kwa tawi lililozima moduli ili uone 403 ya moduli inavyoonekana kwa mtumiaji.
/portal/* pekee — ndizo zinazomtambua mgonjwa kutoka kwenye tokeni.Building a mobile app — start to finish
These steps take you from nothing to an app that is useful on a ward.
- Bring the stack up:
docker compose up, then checkcurl http://localhost:4000/api/v1. - Get a client: take
openapi.jsonand runopenapi-generator(see tools), or hand-write a small HTTP layer like the Dart example above. For a focused app, hand-writing is usually less work. - Server-address screen: let the user type the hospital's address, then confirm it
with
GET /settings/systemand show the name and logo that come back. - Sign in:
POST /auth/login→ handlerequires_2fa→ store the tokens in secure storage (Keychain / EncryptedSharedPreferences / flutter_secure_storage). Never in a plain file. - Draw the menu from the returned
permissions, not from a list compiled into the app. - The screens worth building first: the queue (
GET /visits?status=...), the register (GET /patients), one patient's record (GET /patients/:id/visits,/vitals,/prescriptions,/diagnostics), and critical results (GET /diagnostics/critical). - First writes: vitals (
POST /visits/:id/vitals) and moving the queue along (POST /visits/:id/advance) — short, safe, and immediately visible. - Notifications:
GET /notificationsandPOST /notifications/:id/read. - Handle 401/403 in one place (an interceptor), not on every screen.
- Test against a branch with a module switched off so you see what that 403 looks like to a user.
/portal/* only — those are the routes that resolve the patient from the token.Kujenga software ya desktop — mwanzo mwisho
Desktop ina faida mbili hospitalini: printa (risiti, ankara, vitambulisho) na vifaa (kisoma-bakodi, mizani, printa ya lebo). API ni ile ile; tofauti iko kwenye jinsi unavyoihifadhi tokeni na jinsi unavyochapisha.
- Chagua msingi: Electron au Tauri (JS/TS — tumia mfano wa TypeScript hapo juu), .NET WPF/WinUI (mfano wa C#), au Qt/Python. Tauri ni ndogo na nyepesi; .NET ni rahisi zaidi kwenye Windows za hospitali zilizopo.
- Hifadhi tokeni kwenye hifadhi ya siri ya mfumo — Windows Credential Manager,
macOS Keychain, libsecret. Si kwenye
localStorageya Electron, na si kwenye faili ya usanidi. - CORS haihusiki ukituma maombi kutoka mchakato mkuu (main process) au kutoka .NET.
Ukituma kutoka renderer ya Electron, ongeza anwani yako kwenye
ALLOWED_ORIGINSya seva au tuma kupitia main process. - Uchapishaji: endpoints za PDF zinarudi baiti tayari kuchapishwa — ankara
(
/billing/bills/:id/invoice), risiti (/billing/payments/:id/receipt), taarifa ya mgonjwa (/billing/patients/:id/statement), vocha (/billing/expenses/:id/voucher), oda ya manunuzi (/procurement/orders/:id/pdf), slipu ya mshahara (/hr/payslips/:id/pdf). Zihifadhi kwenye faili la muda kisha uzipeleke kwa printa ya mfumo. - Kituo cha mauzo (POS): mauzo yote ni
POST /billing/sales— ombi moja linalotengeneza ankara na kupokea malipo. Usitengeneze njia yako ya pili ya kupokea fedha. - Kubadilisha tawi: akaunti inayosimamia matawi yote inapata
brancheskwenye jibu la kuingia; onyesha kichagua-tawi kwa hao tu. - Sasisho la programu: weka nambari ya toleo lako kwenye kichwa cha
User-Agentili kumbukumbu za seva zionyeshe ni toleo lipi lilipiga simu.
Building desktop software — start to finish
Desktop earns its place in a hospital for two reasons: printers (receipts, invoices, wristbands) and devices (barcode scanners, scales, label printers). The API is identical; what differs is how you store the token and how you print.
- Pick a base: Electron or Tauri (JS/TS — use the TypeScript example above), .NET WPF/WinUI (the C# example), or Qt/Python. Tauri ships small; .NET is the easier fit on the Windows machines hospitals already own.
- Store tokens in the OS secret store — Windows Credential Manager, macOS Keychain,
libsecret. Not Electron's
localStorage, and not a config file. - CORS does not apply when you call from a main process or from .NET. If you call from
an Electron renderer, either add your origin to the server's
ALLOWED_ORIGINSor route requests through the main process. - Printing: the PDF endpoints return print-ready bytes — invoice
(
/billing/bills/:id/invoice), receipt (/billing/payments/:id/receipt), patient statement (/billing/patients/:id/statement), expense voucher (/billing/expenses/:id/voucher), purchase order (/procurement/orders/:id/pdf), payslip (/hr/payslips/:id/pdf). Write them to a temp file and hand that to the system printer. - Point of sale: a counter sale is
POST /billing/sales— one request that raises the bill and takes the money. Do not invent a second place where money is taken; two day-end figures that disagree is exactly what that endpoint exists to prevent. - Branch switching: an account that spans branches receives
branchesin the sign-in reply. Draw a branch picker only for those accounts. - Releases: put your build number in the
User-Agentso the server's logs say which version made a call.
Mtandao ukikatika
Hospitali nyingi zina mtandao unaokatika. Usidanganye mtumiaji. Kama ombi halijafika seva, sema hivyo — usionyeshe alama ya "imehifadhiwa".
- Kusoma: ni salama kuhifadhi nakala (cache) na kuonyesha ikiwa na tahadhari "ilipakuliwa saa fulani, huenda imepitwa na wakati".
- Kuandika: usiweke foleni ya maandishi ya kliniki kimya kimya. Rekodi ya mgonjwa iliyoandikwa saa mbili baadaye, baada ya mgonjwa kuondoka, ni hatari zaidi kuliko ujumbe wa wazi wa "hakuna mtandao — iandike kwenye karatasi".
- Ubaguzi: vipimo visivyo vya kliniki (mfano kusoma tu, au arifa) vinaweza kusubiri.
When the link drops
Hospital networks drop. Do not lie to the user. If a request never reached the server, say so — never show a "saved" tick for something that was not.
- Reads: caching is fine, shown with a plain warning that it was loaded earlier and may be out of date.
- Writes: do not silently queue clinical writes. A patient record that lands two hours later, after the patient has gone home, is more dangerous than an honest "no connection — write it on paper and enter it when the link returns".
- The exception is non-clinical traffic — reads, notification acknowledgements — which can safely wait.
OpenAPI, Postman na codegen
Faili tatu zinasafiri pamoja na ukurasa huu, kwenye folda ile ile:
OpenAPI, Postman and codegen
Three files travel with this page, in the same folder:
| File | What it is for |
|---|---|
openapi.json | OpenAPI 3.0.3 — import into Swagger UI, Insomnia, Stoplight, or feed a code generator |
hms.postman_collection.json | Postman collection — every endpoint, grouped as the router groups them |
hms.postman_environment.json | Postman environment — base_url and where the tokens land |
# Postman: import both files, pick the environment, then run
# "Authentication → POST /auth/login". Its test script stores the tokens
# for every other request in the collection.
# A typed Dart client for Flutter
openapi-generator generate -i openapi.json -g dart-dio -o ./lib/hms_api
# Kotlin for Android
openapi-generator generate -i openapi.json -g kotlin -o ./hms-api
# Swift for iOS
openapi-generator generate -i openapi.json -g swift5 -o ./HmsApi
# C# for a Windows desktop client
openapi-generator generate -i openapi.json -g csharp -o ./HmsApi
# TypeScript types only, for Electron/Tauri
npx openapi-typescript openapi.json -o src/hms-api.d.ts
cd backend && go run ./cmd/apidocs.
Jaribio la go test ./cmd/apidocs hushindwa ikiwa zimepitwa na wakati, kwa hiyo
haziwezi kubaki nyuma ya msimbo kimya kimya.
Regenerate all of this with cd backend && go run ./cmd/apidocs.
go test ./cmd/apidocs fails when what is committed is stale, so these files cannot
quietly fall behind the code.
Orodha ya kabla ya kuzindua
- Anwani ya seva inawekwa na mtumiaji, si ndani ya msimbo.
- Tokeni ziko kwenye hifadhi salama ya mfumo.
- Refresh moja tu kwa wakati; ikishindwa, mtumiaji anarudishwa kwenye kuingia.
- Menyu inatokana na
permissionsza seva. - Ujumbe wa makosa unaonyeshwa kama ulivyo, si "Kuna hitilafu".
- 403 ya ruhusa na 403 ya moduli zinaonekana tofauti kwa mtumiaji.
- Hakuna alama ya "imehifadhiwa" kabla seva haijathibitisha.
- Namba za simu zinatumia E.164 (
+255…) — tumiacountrykutoka kwenye kikao. - Saa zote zinatumwa kama UTC ISO-8601 na kuonyeshwa kwa saa za eneo.
- App imejaribiwa kwa akaunti yenye ruhusa chache, si ya msimamizi pekee.
Go-live checklist
- The server address is set by the user, not compiled in.
- Tokens live in the OS secret store.
- One refresh at a time; on failure the user is returned to sign-in.
- The menu is drawn from the server's
permissions. - Error messages are shown verbatim, never as "Something went wrong".
- A permission 403 and a module 403 look different to the user.
- Nothing shows as saved before the server has confirmed it.
- Phone numbers are E.164 (
+255…) — usecountryfrom the session. - Times are sent as UTC ISO-8601 and displayed in local time.
- Tested with a low-permission account, not only an administrator.
Marejeo: kila endpointReference: every endpoint
Bofya endpoint yoyote kuona vigezo, mwili wa ombi na mfano wa curl. Open any endpoint for its parameters, request body and a curl example.