Hallo Stroke

API Reference

Complete REST API endpoint reference.

All routes are under /api. Protected routes require Authorization: Bearer <JWT>. Admin routes additionally require role == "admin". Super-admin routes require a role: "superadmin" JWT.

Auth

Public

MethodPathBodyReturns
POST/login{username, password}{access_token, refresh_token}
POST/register{username, email?, password, nama_ktp, nama_samaran, nomor_hp, role?, nik?, domicile?, preferences?, cf-turnstile-response}{access_token, refresh_token}

nik is optional. It was previously required (16 digits) but has been deprecated to simplify registration and improve privacy — legacy users' NIK data is retained but new users are never asked for it. domicile is a domicile_province enum value (see Provinces).

| POST | /refresh | {refresh_token} | {access_token} | | POST | /auth/check-username | {username} | {available, message} | | GET | /send-verification-code?phone= | — | sends WhatsApp reset link | | POST | /update-password | {code, new_password(min6)} | {message} | | POST | /superadmin/login | {username, password} | {access_token, message} |

Protected (JWT required)

MethodPathDescription
POST/logoutClears stored refresh token
GET/profileReturns user (NIK decrypted)
PUT/profileEdit profile (multipart: nik, nama_ktp, nama_samaran, nomor_hp, email, password, preferences, role_ids, image)
GET/users/:idRoles for the authenticated user
POST/caregiver/patientsBody {patient_id} — caller must have pendamping role
GET/caregiver/patientsList the caller's patients
GET/patient/caregiversList the caller's caregivers
GET/patientsAll users with pasien role
GET/caregiversAll users with pendamping role
DELETE/caregiver/patients/:idRemove a patient link

Roles

MethodPathBodyNotes
GET/roles—List all roles
POST/roles{name, is_editable}
GET/roles/:id—
PUT/roles/:id{name, is_editable?}Non-editable roles rejected
DELETE/roles/:id—Non-editable roles rejected
POST/admins/:id/roles{role_ids:[]}ON CONFLICT DO NOTHING
GET/users/:id/roles—Roles for a user
DELETE/admins/:id/roles{role_ids:[]}

Kategori (dynamic content categories)

MethodPathBody
POST/kategori{nama_kategori}
GET/kategori—
PUT/kategori/:id—
DELETE/kategori/:id—

Faskes (health facilities)

MethodPathQueryNotes
GET/faskes/nearbylat, lng, radius?(m, default 5000)Haversine + Google Places fallback; in-memory grid cache (4h, 1km hit radius); follows Google next_page_token pagination (max 3 pages = 60 places)
GET/faskes/—{results, status}
GET/faskes/locationlat, lngDB Haversine within 50 km
GET/faskes/searchkeywordLIKE on nama
GET/faskes/:id—:id is Google place_id

Admin: POST/PUT/DELETE /faskes[/:id].

Forum

Public read uses OptionalJWT (populates is_liked/is_saved if logged in).

MethodPathAuthDescription
GET/forum/categoriesoptionalList categories
GET/forum/threads?category_id=optionalList threads
GET/forum/threads/:idoptionalThread detail + nested replies + counts
POST/forum/threadsJWTCreate (multipart: category_id, title, content, use_pseudonym, image) → fires WhatsApp broadcast
PUT/forum/threads/:idJWTEdit (multipart)
DELETE/forum/threads/:idJWTOwner only
POST/forum/threads/:id/replyJWTReply (multipart: content, use_pseudonym, parent_id?, image) → notifies thread creator
POST/forum/threads/:id/likeJWTToggle → {is_liked}
POST/forum/threads/:id/saveJWTToggle → {is_saved}
DELETE/forum/replies/:idJWTOwner only
POST/forum/replies/:id/likeJWTToggle
POST/forum/replies/:id/saveJWTToggle
PUT/forum/replies/:idJWTEdit (multipart)

Articles

MethodPathAuthDescription
GET/articles/—Scrape Kemenkes live → [{title, url, is_saved}]
GET/articles/detail?url=—Scrape full article by URL
GET/articles/all—DB-backed list
GET/articles/search?title=—
GET/articles/url?url=—
GET/articles/:id—Numeric DB ID
POST/articles/saveJWTBody {url, title} → {is_saved}
GET/articles/savedJWT

Admin: POST/PUT/DELETE /articles[/:id] (multipart add).

YouTube

MethodPathAuthDescription
GET/youtube/videos—DB list. thumbnail is the largest real resolution: maxres when YouTube's API reports the video has HD, else hqdefault — never a guessed URL, so no 404s on low-res videos
GET/youtube/videos/:id—DB by ID
GET/youtube/videos/:id/detail—Live YouTube API
GET/youtube/channel/:channelId—Latest 20
GET/youtube/search?q=—Search
POST/youtube/saveJWTBody {video_id, title, channel_title?, thumbnail_url?} → {is_saved}
GET/youtube/savedJWT

Admin: POST/PUT/DELETE /youtube/video[/:id].

Komunitas

MethodPathAuth
GET/komunitas/—
GET/komunitas/:id—

Admin: POST/PUT/DELETE /komunitas[/:id] (multipart with image).

Dokter

MethodPathAuthDescription
GET/dokter/nearby?lat=&lng=—Doctors practicing in the user's province (reverse geocoded), each with distance_km to their nearest faskes, sorted nearest-first. Doctors with no faskes (or none in the province) are filtered out. Radius filtering is client-side. 400 on missing/invalid params
GET/dokter/—All doctors
GET/dokter/:id—One doctor

Admin: POST/PUT/DELETE /dokter[/:id]. PUT /dokter/:id re-syncs the faskes association when faskes_ids is sent (transactional); a nil faskes_ids leaves associations untouched.

Emergency numbers (per-province)

Per-province emergency phone numbers. Public nearby reverse-geocodes GPS to a province and returns that province's numbers; admin CRUD manages the list. The frontend falls back to tel:112 (Indonesia national emergency) when no match.

MethodPathAuthDescription
GET/emergency/nearby?lat=&lng=—{province, phones:[{id, province, phone, title}]}
GET/emergency/adminList all
POST/emergency/adminBody {province, phone, title?} → 201
PUT/emergency/:idadminBody {province, phone, title?}
DELETE/emergency/:idadmin204

province is plain varchar (not the domicile_province enum column type); matching against the enum value set is done in Go via auth.IsValidDomicileProvince.

Remote config (per-province home layout)

Per-province configuration of the home page section order and visibility. One row per province; the admin PUT replaces the sections array wholesale. Province "default" controls the logged-out home page (/, no province context) and is an allowed special case — it is not a domicile_province enum value.

MethodPathAuthDescription
GET/province-config/nearby?lat=&lng=—Reverse-geocode → province config; 404 on GPS fail / no config
GET/province-config/:province—404 if no config saved for that province
PUT/province-config/:provinceadminBody {sections:[{id, enabled, order}]} → full config
DELETE/province-config/:provinceadmin204

sections[].id matches the frontend registry in src/utils/homeSections.js (video, articles, and the five menu buttons). order is the render index; the service re-stamps order from the array position so order === index always holds. /nearby is registered before /:province so Fiber matches it first.

On 404 (GPS failed, province unmatched, or no saved config), the frontend falls back to the user's inputted domicile province, then to the built-in DEFAULT_CONFIG. Public GET never requires auth — home pages render logged-out.

Provinces

domicile and province fields use the domicile_province Postgres enum. The full value set lives in internal/auth/provinces.go (IsValidDomicileProvince) and database/enum.go. Use the public GET /province-config/:province and GET /emergency/nearby with the province name (e.g. DKI Jakarta, URL-encoded).

Pengingat (reminders)

See Backend Reminders for the scheduler. All protected (JWT).

MethodPathBody / Query
POST/pengingat/{tipe(makan/terapi/kontrol), judul, waktu(RFC3339), timezone?, details(json)}
POST/pengingat/obat{judul, timezone?, details:{nama_obat, frekuensi, jadwal:[{jam, jumlah, satuan}], aturan}}
GET/pengingat/?tipe=optional tipe filter
GET/pengingat/:id—
PUT/pengingat/:idpartial update
DELETE/pengingat/:id—

Push notifications

MethodPathBodyNotes
POST/push/subscribe{endpoint, keys:{p256dh, auth}, user_id?}no JWT
POST/push/send{user_id, payload:{title, body, url?}}no JWT

Logs

MethodPathBodyNotes
POST/log/user-logLogUserasync-queued, returns 202
POST/log/admin-logLogAdminasync-queued, returns 202

Admin

All require role == "admin".

MethodPathDescription
GET/admins/List admins
POST/admins/Create admin (multipart: email, password, nama_ktp, role?, nomor_hp?, image)
GET/admins/usersList all users
GET/admins/users/:idUser detail (NIK decrypted)
PUT/admins/users/:id{is_verified?, role_ids?}
GET/admins/overview?start=&end=Aggregate stats: totals, by-province, growth, activity, forum/reminder categories, recent activity. start/end are YYYY-MM-DD (optional)
GET/admins/export/:report?format=&start=&end=report ∈ users|admins|activity-logs|domicile, format ∈ csv|pdf|xlsx. Returns a file download (Content-Disposition: attachment)
GET/admins/caregiver-patient/allAll caregiver–patient relations
GET/admins/caregiver-patient?caregiver_id=Caregiver's patients
POST/admins/caregiver-patient{caregiver_id, patient_id} (validates pendamping + pasien)
DELETE/admins/caregiver-patient{caregiver_id, patient_id}
GET/admins/:username
PUT/admins/:idEdit admin (multipart)
DELETE/admins/:idDelete admin

Super-admin

All require a role: "superadmin" JWT, re-verified in each handler.

MethodPathDescription
POST/superadmin/queryBody {query} — arbitrary SQL
GET/superadmin/tablesList tables
GET/superadmin/tables/:tableNameTable data
GET/superadmin/tables/:tableName/exporttext/csv
POST/superadmin/query/exportBody {query} → CSV

Arbitrary SQL

/superadmin/query executes arbitrary SQL. It is the most privileged endpoint in the system and should be tightly network-restricted in production.

On this page