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
| Method | Path | Body | Returns |
|---|---|---|---|
| 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)
| Method | Path | Description |
|---|---|---|
| POST | /logout | Clears stored refresh token |
| GET | /profile | Returns user (NIK decrypted) |
| PUT | /profile | Edit profile (multipart: nik, nama_ktp, nama_samaran, nomor_hp, email, password, preferences, role_ids, image) |
| GET | /users/:id | Roles for the authenticated user |
| POST | /caregiver/patients | Body {patient_id} — caller must have pendamping role |
| GET | /caregiver/patients | List the caller's patients |
| GET | /patient/caregivers | List the caller's caregivers |
| GET | /patients | All users with pasien role |
| GET | /caregivers | All users with pendamping role |
| DELETE | /caregiver/patients/:id | Remove a patient link |
Roles
| Method | Path | Body | Notes |
|---|---|---|---|
| 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)
| Method | Path | Body |
|---|---|---|
| POST | /kategori | {nama_kategori} |
| GET | /kategori | — |
| PUT | /kategori/:id | — |
| DELETE | /kategori/:id | — |
Faskes (health facilities)
| Method | Path | Query | Notes |
|---|---|---|---|
| GET | /faskes/nearby | lat, 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/location | lat, lng | DB Haversine within 50 km |
| GET | /faskes/search | keyword | LIKE 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).
| Method | Path | Auth | Description |
|---|---|---|---|
| GET | /forum/categories | optional | List categories |
| GET | /forum/threads?category_id= | optional | List threads |
| GET | /forum/threads/:id | optional | Thread detail + nested replies + counts |
| POST | /forum/threads | JWT | Create (multipart: category_id, title, content, use_pseudonym, image) → fires WhatsApp broadcast |
| PUT | /forum/threads/:id | JWT | Edit (multipart) |
| DELETE | /forum/threads/:id | JWT | Owner only |
| POST | /forum/threads/:id/reply | JWT | Reply (multipart: content, use_pseudonym, parent_id?, image) → notifies thread creator |
| POST | /forum/threads/:id/like | JWT | Toggle → {is_liked} |
| POST | /forum/threads/:id/save | JWT | Toggle → {is_saved} |
| DELETE | /forum/replies/:id | JWT | Owner only |
| POST | /forum/replies/:id/like | JWT | Toggle |
| POST | /forum/replies/:id/save | JWT | Toggle |
| PUT | /forum/replies/:id | JWT | Edit (multipart) |
Articles
| Method | Path | Auth | Description |
|---|---|---|---|
| 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/save | JWT | Body {url, title} → {is_saved} |
| GET | /articles/saved | JWT |
Admin: POST/PUT/DELETE /articles[/:id] (multipart add).
YouTube
| Method | Path | Auth | Description |
|---|---|---|---|
| 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/save | JWT | Body {video_id, title, channel_title?, thumbnail_url?} → {is_saved} |
| GET | /youtube/saved | JWT |
Admin: POST/PUT/DELETE /youtube/video[/:id].
Komunitas
| Method | Path | Auth |
|---|---|---|
| GET | /komunitas/ | — |
| GET | /komunitas/:id | — |
Admin: POST/PUT/DELETE /komunitas[/:id] (multipart with image).
Dokter
| Method | Path | Auth | Description |
|---|---|---|---|
| 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.
| Method | Path | Auth | Description |
|---|---|---|---|
| GET | /emergency/nearby?lat=&lng= | — | {province, phones:[{id, province, phone, title}]} |
| GET | /emergency/ | admin | List all |
| POST | /emergency/ | admin | Body {province, phone, title?} → 201 |
| PUT | /emergency/:id | admin | Body {province, phone, title?} |
| DELETE | /emergency/:id | admin | 204 |
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.
| Method | Path | Auth | Description |
|---|---|---|---|
| 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/:province | admin | Body {sections:[{id, enabled, order}]} → full config |
| DELETE | /province-config/:province | admin | 204 |
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).
| Method | Path | Body / 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/:id | partial update |
| DELETE | /pengingat/:id | — |
Push notifications
| Method | Path | Body | Notes |
|---|---|---|---|
| POST | /push/subscribe | {endpoint, keys:{p256dh, auth}, user_id?} | no JWT |
| POST | /push/send | {user_id, payload:{title, body, url?}} | no JWT |
Logs
| Method | Path | Body | Notes |
|---|---|---|---|
| POST | /log/user-log | LogUser | async-queued, returns 202 |
| POST | /log/admin-log | LogAdmin | async-queued, returns 202 |
Admin
All require role == "admin".
| Method | Path | Description |
|---|---|---|
| GET | /admins/ | List admins |
| POST | /admins/ | Create admin (multipart: email, password, nama_ktp, role?, nomor_hp?, image) |
| GET | /admins/users | List all users |
| GET | /admins/users/:id | User 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/all | All 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/:id | Edit admin (multipart) |
| DELETE | /admins/:id | Delete admin |
Super-admin
All require a role: "superadmin" JWT, re-verified in each handler.
| Method | Path | Description |
|---|---|---|
| POST | /superadmin/query | Body {query} — arbitrary SQL |
| GET | /superadmin/tables | List tables |
| GET | /superadmin/tables/:tableName | Table data |
| GET | /superadmin/tables/:tableName/export | text/csv |
| POST | /superadmin/query/export | Body {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.