Base URL: https://<device-ip>/api. Requests and responses are JSON. Mutating endpoints require Authorization: Bearer <token> (the token model is explained in REST design). Errors use HTTP status codes plus {"error":"human readable"}.
Legend: Auth = requires the bearer token. Limit = rate limit class beyond the default.
Playback
| Method | Path | Auth | Purpose |
|---|
| GET | /playback/status | no | Current playback snapshot |
| POST | /playback/play | yes | Play a file or a stream |
| POST | /playback/pause | yes | Pause |
| POST | /playback/resume | yes | Resume from pause |
| POST | /playback/stop | yes | Stop |
| POST | /playback/seek | yes | Seek within the current file |
| POST | /playback/volume | yes | Set volume |
| POST | /playback/resume-saved | yes | Resume the persisted position |
| POST | /playback/next | yes | Play the next available surah of the current reciter |
| POST | /playback/prev | yes | Play the previous available surah |
| GET | /playback/mode | no | Current advance mode (plus hifz config when active) |
| POST | /playback/mode | yes | Set the advance mode |
{
"state": "playing",
"mode": "file",
"position": 128470,
"duration": 431000,
"volume": 50,
"underrun_count": 0,
"track": "/sdcard/Al-Husary/001_Al-Fatihah.mp3",
"reciter": "Al-Husary",
"surah_number": 1,
"advance_mode": "continuous",
"hasResumeState": false
}
state: playing, paused, buffering, stopped. mode: file, stream, idle. position/duration in milliseconds; live streams report duration 0. underrun_count is the audio health counter. track/reciter/surah_number describe the current source; advance_mode is one of off, continuous, repeat_one, hifz_repeat, hifz_ab. In the hifz modes the response gains a hifz object with repeat_done, repeat_target, pause_s, waiting and, for hifz_ab, ab_start/ab_end.
POST /api/playback/mode
{"mode": "continuous"} // or "off", "repeat_one"
{"mode": "hifz_repeat", "repeat": 3, "pause_s": 5} // repeat current surah
{"mode": "hifz_ab", "repeat": 3, "ab": {"from": 78, "to": 114}, "pause_s": 5}
repeat 0 means endless (hifz_repeat only). pause_s (0-600) inserts silence between repetitions; the display keeps the now-playing screen during the gap. Missing files inside an A-B range are skipped and the range wraps from to back to from.
POST /api/playback/play
{"path": "/sdcard/Al-Husary/001.mp3"} // file form
{"url": "https://qurango.net/radio/tarateel"} // stream form
POST /api/playback/seek
{"position": 60000}
Seek is rejected with 400 for live streams. POST /playback/volume takes {"volume": 0..100} and answers with the applied value.
Library
| Method | Path | Auth | Purpose |
|---|
| GET | /library/reciters | no | Scanned reciter list |
| GET | /library/surahs?reciter=NAME | no | The 114-slot availability map plus names for one reciter |
Favorites
| Method | Path | Auth | Purpose |
|---|
| GET | /favorites | no | All favorites with play counts |
| POST | /favorites | yes | Add ({"type":"surah","reciter":...,"surah":n} or {"type":"stream","url":...,"name":...}) |
| DELETE | /favorites/{id} | yes | Remove |
| POST | /favorites/toggle | yes | Add or remove in one call, body as in add |
Radio stations
| Method | Path | Auth | Purpose |
|---|
| GET | /radio/stations | no | Saved stations |
| POST | /radio/stations | yes | Add {"name":..., "url":...} (URL validated, see streaming) |
| DELETE | /radio/stations/{id} | yes | Remove |
NFC cards
| Method | Path | Auth | Purpose |
|---|
| GET | /rfid/cards | yes | Registered cards |
| POST | /rfid/cards | yes | Register {"uid":..., "name":..., binding} |
| DELETE | /rfid/cards/{uid} | yes | Delete |
| GET | /rfid/scan | yes | Last tapped UID, for the registration form's scan button. Returns {"uid":..., "uid_len":n, "registered":bool, "age_ms":n}, or {"uid":"", "age_ms":-1} when nothing has been tapped yet. A tap also arrives live over the WebSocket nfc message; this endpoint is the polling fallback |
Prayer times
| Method | Path | Auth | Purpose |
|---|
| GET | /prayer | no | Today's times, next prayer with countdown, Hijri date, config echo |
| POST | /prayer/config | yes | Location, method, Asr school, offsets, adhan settings |
| POST | /prayer/test-adhan | yes | Play the configured adhan now |
POST /api/prayer/config
{
"lat": 30.0444, "lng": 31.2357,
"method": 2, "asr_hanafi": false,
"offsets_min": [0, 0, 0, 0, 0, 0],
"adhan": {"enabled": true, "prayer_mask": 61, "volume": 80,
"path": "/sdcard/adhan.mp3"}
}
method: 0 MWL, 1 ISNA, 2 Egyptian, 3 Umm al-Qura, 4 Karachi. offsets_min adjusts Fajr, Sunrise, Dhuhr, Asr, Maghrib, Isha by minutes. prayer_mask is a bitmask over the same order (bit 1, Sunrise, is ignored); volume 0 keeps the current volume during the adhan. Times are computed offline from the solar position, so no network is needed after the location is set; the Hijri date is tabular (about one day of tolerance versus sighting-based announcements).
Scheduler
| Method | Path | Auth | Purpose |
|---|
| GET | /schedules | no | All schedules |
| POST | /schedules | yes | Create {"hour":..,"minute":..,"days":mask,"target":...,"volume":optional} |
| DELETE | /schedules/{id} | yes | Delete |
| POST | /schedules/{id}/enable | yes | {"enabled": bool} |
Settings and system
| Method | Path | Auth | Purpose |
|---|
| GET | /settings | no | All settings, credentials redacted, plus current time, time_synced, time_source (none / rtc / sntp / manual), time_format_24h and rtc_temperature when a DS3231 is fitted |
| POST | /settings/wifi | yes | Set {"ssid":..., "password":...}; device reconnects |
| POST | /settings/brightness | yes | {"brightness": 0..255} |
| POST | /settings/timezone | yes | {"timezone": "POSIX rule"} (validated printable ASCII; the dashboard offers a preset list plus a custom entry) |
| POST | /settings/time-format | yes | {"time_format_24h": bool}; picks 12-hour (default) or 24-hour clock, applied to the display immediately |
| POST | /settings/time | yes | {"epoch": unix seconds}; sets the system clock and the RTC, for offline installs |
| POST | /settings/auto-resume | yes | {"enabled": bool} |
| POST | /settings/auto-off | yes | {"auto_off_minutes": 0..240}; deep sleep after idle, 0 disables. Playback, hifz gaps, drills, OTA and connected dashboard clients keep the device awake |
| POST | /settings/card-removal | yes | {"policy": "continue" / "pause" / "stop"} |
| GET | /system | no | Version, uptime, free heap, NVS stats, boot_failures array |
| GET | /battery | no | {"present":..., "voltage":..., "percentage":..., "charging":...}; present false means no battery is attached (floating or unwired sense) and the other fields read zero |
| POST | /system/sleep | yes | Deep sleep now; wakes on the button or, when the RTC is fitted, at the next enabled schedule |
| POST | /reset | yes | Factory reset |
{
"version": "2.1.0",
"uptime": 15,
"free_heap": 7772024,
"boot_failures": [
{"component": "SD", "error": "ESP_ERR_TIMEOUT"}
]
}
OTA
| Method | Path | Auth | Limit | Purpose |
|---|
| GET | /ota/status | no | | {"in_progress":.., "progress":0-100, "mode":"pull" / "upload" / "idle", "status":..., "error":...} |
| POST | /ota/upload | yes | 2/min | Body is the raw Safi.bin |
| POST | /ota/pull | yes | 2/min | {"url": "https://..."}, subject to OTA hardening |
Debug
| Method | Path | Auth | Limit | Purpose |
|---|
| GET | /debug/screenshot | yes | 10/10 s | The live screen as a 24-bit BMP (design) |
Non-API routes
GET /ws upgrades to the WebSocket; GET /* serves the dashboard from LittleFS with API routes taking precedence.
Status code summary
| Code | Meaning here |
|---|
| 200 | Success; body is the result object |
| 400 | Malformed body or invalid value; error says which field |
| 401 | Missing or wrong bearer token |
| 404 | Unknown entity (favorite id, card uid, station id) |
| 429 | Rate limit exceeded for that endpoint class |
| 500 | Device-side failure; the serial log has the detail |