temetro is in beta and under active development. Expect things to change.
temetro

Patient Portal

Public kiosk API for self-service booking and results.

The Patient Portal is a public, unauthenticated kiosk surface (a clinic iPad in the waiting room). There is no session — every request is scoped by the clinic slug in the URL. The matching frontend lives at /portal/{clinic} and renders with no sidebar or auth guard.

MethodPathNotes
GET/api/portal/{clinic}Clinic name for the kiosk header
GET/api/portal/{clinic}/linkRelay-based pairing descriptor for the wallet app
GET/api/portal/{clinic}/doctorsPublic list of the clinic's doctors
GET/api/portal/{clinic}/availabilityTaken time slots for a doctor on a day
POST/api/portal/{clinic}/patientsRegister a new (demographics-only) patient
POST/api/portal/{clinic}/appointmentsSelf-service booking
GET/api/portal/{clinic}/resultsMinimal status lookup

The portal is reachable two ways: patients open the web page at /portal/{clinic}, or they scan the clinic's Patient Portal QR (Settings → Signing → Patient Portal, or the portal's own "Link my wallet" card). The web page uses the JSON endpoints below directly; the Temetro wallet app instead connects over the Temetro Network relay and runs the same actions there (so it works from any phone without the clinic exposing its API).

GET /api/portal/{clinic}/link returns the non-secret descriptor the wallet app needs to reach this clinic over the relay: the clinic's Ed25519 signing public key (the relay's routing id) and the relay URL. The frontend encodes it into a temetro-portal: QR.

{
  "clinicId": "<clinic signing public key, hex>",
  "relay": "https://network.temetro.com",
  "slug": "sunrise-clinic",
  "name": "Sunrise Clinic"
}

After scanning, the wallet app links by its wallet number — the clinic attaches that number to the patient's file ahead of time (via Import from a patient app / QR pairing), so the patient types nothing. It then books appointments and downloads results over the relay — all attributed to that patient on the clinic's web app. (If the wallet isn't paired yet, linking returns a friendly "ask the front desk to add your wallet number" message.)

These endpoints are public. PHI exposure is deliberately minimal: every lookup requires both a file number and a matching name, and results return only appointment status and whether results exist — never clinical values. A per-kiosk token or one-time verification code is the recommended hardening for a real deployment.

List doctors

GET /api/portal/{clinic}/doctors returns the clinic's providers so a patient can choose who to see. Only display-safe fields leave this public surface — no ids, emails, or usernames.

[
  { "name": "Dr. Amina Yusuf", "specialty": "Cardiology" },
  { "name": "Dr. Omar Ali", "specialty": null }
]

Check availability

GET /api/portal/{clinic}/availability?provider=Dr.%20Amina%20Yusuf&date=2026-07-01 returns the taken time slots for that provider on that day, so the portal (and the wallet app) can render only free times.

{ "date": "2026-07-01", "provider": "Dr. Amina Yusuf", "taken": ["09:30", "11:00"] }

The filter mirrors the booking conflict check (an appointment with no provider blocks the slot clinic-wide). Booking still re-validates server-side, so a slot taken between the availability check and the booking is rejected with 409.

Register a new patient

POST /api/portal/{clinic}/patients lets a first-time visitor create a record so they can then book. It writes demographics only (no clinical PHI) from this unauthenticated surface.

{ "name": "Hodan Warsame", "sex": "F", "age": 34 }

Returns 201 with { "fileNumber": "10005", "name": "Hodan Warsame" }. The kiosk then books using that file number.

Book an appointment

POST /api/portal/{clinic}/appointments

{
  "fileNumber": "10293",
  "name": "Hodan Warsame",
  "date": "2026-07-01",
  "time": "09:30",
  "type": "Follow-up",
  "provider": "Dr. Amina Yusuf"
}

provider is optional — the chosen doctor from the picker; when omitted it falls back to the patient's primary provider. The booking is verified against an existing patient (fileNumber + matching name) in that clinic, must be a future date, and is rejected with 409 if the slot is already taken (same date + time for the same provider). On success it creates a confirmed appointment that appears on the clinic's Appointments page. Returns 201 with the confirmed date, time, type, and provider. Unknown name/file number → 404.

View results

GET /api/portal/{clinic}/results?fileNumber=10293&name=Hodan%20Warsame

{
  "name": "Hodan Warsame",
  "upcoming": [
    { "date": "2026-07-01", "time": "09:30", "type": "Follow-up", "provider": "Dr. Amina Yusuf", "status": "confirmed" }
  ],
  "hasResults": true,
  "resultCount": 3
}

Returns the patient's upcoming appointments plus whether results are on file. It never returns lab values — the kiosk tells the patient to ask a staff member to review results with them.

On this page