Repository navigation
API Reference
A guided tour of the HTTP and WebSocket APIs, with examples. The complete, authoritative contracts are in the repository: REST API, Device API, WebSocket API. A running server also serves interactive documentation at /docs (Swagger) and /redoc. A repository test fails if any route is missing from those reference pages.
- Conventions
- Authentication
- Users
- Courses and classes
- Students and enrollment
- Quizzes and polls
- CSV imports
- Modules, firmware and deployments
- Device API
- WebSocket API
- Health
- A complete scripted session
| Topic | Rule |
|---|---|
| Base URL | http(s)://<host>:<port> |
| Bodies | JSON, unless marked multipart (file uploads) |
| Auth |
Authorization: Bearer <access_token> for users; X-API-Key: <key> for devices |
| Timestamps | ISO-8601, naive IST |
| Validation errors |
422 with FastAPI's {"detail":[{"loc":[…],"msg":…,"type":…}]}
|
| Other errors | {"detail": "<message>"} |
| Status | Meaning |
|---|---|
| 400 | a business rule (a duplicate code, the wrong state such as "Quiz is already active") |
| 401 | missing or invalid credentials; an expired session (detail: "SESSION_EXPIRED", header X-Session-Code: idle|hard); an unknown device key |
| 403 | authenticated but not allowed: role, class, a disabled account, the wrong device |
| 404 | not found |
| 409 | conflict: a section that already exists, a version with different bytes, a state change from the wrong state |
| 413 | upload too large |
| 422 | invalid input |
| 503 | the database was busy or failed; nothing was saved; retry |
Guards used below: public, session (any user), teacher+, class access (the class's teachers or an admin), admin, super admin, device.
curl -s -X POST https://impress.example.edu/api/auth/login \
-H 'content-type: application/json' -d '{"username":"admin","password":"…"}'{"access_token": "impress_Q3h…", "token_type": "bearer",
"expires_at": "2026-10-09T03:35:16", "created_at": "2026-10-08T21:35:16",
"idle_timeout_s": 3600, "hard_timeout_s": 21600, "server_time": "2026-10-08T21:35:16"}| Method & path | Guard | Purpose |
|---|---|---|
POST /api/auth/login |
public | sign in; 401 wrong credentials, 403 disabled account |
GET /api/auth/me, PUT /api/auth/me
|
session | your profile; change your full name |
POST /api/auth/change-password |
session | {current_password, new_password} |
POST /api/auth/logout |
session | end this session |
GET /api/auth/session, GET /api/auth/session-status
|
token | remaining idle and hard time; never extend the session |
POST /api/auth/activity |
session | an explicit "user is active" ping |
| Method & path | Guard | Notes |
|---|---|---|
POST /api/admin/users |
admin |
{username, email, password, full_name, role}; granting admin roles needs super admin |
GET /api/admin/users?role= |
admin | list |
PUT /api/admin/users/{id} |
admin | {full_name?, email?, is_active?, role?} |
POST /api/admin/users/{id}/deactivate |
admin | ends their sessions |
POST /api/admin/users/{id}/reset-password |
admin | {new_password} |
DELETE /api/admin/users/{id} |
admin | soft delete |
POST /api/admin/users/import |
admin | multipart CSV |
| Method & path | Guard | Notes |
|---|---|---|
POST /api/courses/, GET, GET /{id}, PUT /{id}, DELETE /{id}
|
admin (read: teacher+) | the course catalog |
POST /api/admin/classes |
admin | create a class (ClassCreate, below); created inactive; returns timetable warnings
|
GET /api/admin/classes, PUT /{id}, DELETE /{id}
|
admin | |
PUT /api/admin/classes/{id}/faculty |
admin |
{teacher_ids:[…]}; the first is primary |
GET /api/classes/ |
teacher+ | your classes (all, for admins) |
POST /api/classes/ |
teacher+ | create; you are the primary teacher |
GET /api/classes/{id} |
class access | |
POST /api/classes/{id}/activate, /deactivate
|
class access | |
POST /api/classes/join |
teacher+ |
{code}: become co-faculty |
GET /api/classes/{id}/presence |
class access | live devices and student modules |
DELETE /api/classes/{id} |
primary teacher or admin | deletes the class and everything in it |
{"name": "Physics", "code": "PHY101", "course_id": 3, "course_section": "A",
"term": "Monsoon", "year": 2026, "start_date": "2026-07-15", "end_date": "2026-11-15",
"meeting_schedule": [{"day": "Mon", "start": "09:00", "end": "10:00"}],
"location": "Block B", "capacity": 60, "teacher_ids": [2, 5], "classroom_code": "RM-201"}| Method & path | Guard | Notes |
|---|---|---|
POST /api/students/ |
teacher+ | {roll_number (10 alphanumerics), student_name, email, phone, program, enrollment_year, graduation_year} |
GET /api/students/?search= |
teacher+ | search by roll number or name |
PUT /api/students/{id}, DELETE /api/students/{id}
|
teacher+ | soft delete |
DELETE /api/students/{id}/hard-delete |
admin | permanent; answers anonymised |
POST /api/students/import |
teacher+ | multipart CSV |
POST /api/students/classes/{id}/import-students |
teacher+ | CSV, plus enroll in the class |
POST /api/admin/classes/{id}/enroll |
teacher+ | {student_id} |
DELETE /api/admin/classes/{id}/unenroll/{student_id} |
teacher+ | |
GET /api/admin/students/{id}/participation |
teacher+ | quizzes, polls, attendance |
curl -s -X POST https://…/api/quizzes/ -H "$H" -H 'content-type: application/json' -d '{
"class_session_id": 7, "title": "Warm-up", "quiz_mode": "planned", "timing_mode": "manual",
"questions": [{"question_text": "2 + 2?", "options": ["3","4","5","6"], "correct_option": 1}]}'| Method & path | Guard | Notes |
|---|---|---|
POST /api/quizzes/ |
class access | 1+ questions, 2–4 options, correct_option < len(options); warnings for text a module truncates |
GET /api/quizzes/class/{class_id}, GET /api/quizzes/{id}
|
class access | |
POST /api/quizzes/{id}/start · /next · /stop
|
class access | broadcasts quiz_question / quiz_end. start works only on a draft: completed → 409. stop on a draft → 409; on a finished quiz it changes nothing (#76). next works in every timing mode |
| (timer) | – |
per_question: the server advances when question_time_limit runs out. total: it ends the quiz when total_time_limit runs out. Answers more than 2 s after a deadline get 409 (HTTP) or are skipped (batch) (#73) |
GET /api/quizzes/{id}/results |
class access | per question: total_answers, option_counts
|
POST /api/polls/ |
class access | {class_session_id, title, options (2–4), poll_mode: live|planned} |
POST /api/polls/{id}/start · /end
|
class access | broadcasts poll_start / poll_end. start works only on a draft: closed → 409. end on a draft → 409 (#76) |
GET /api/polls/{id}/results |
class access |
option_counts, total_votes
|
| Method & path | Guard | Notes |
|---|---|---|
GET /api/quizzes/questions/template.csv |
public | example |
POST /api/quizzes/questions/parse |
teacher+ | multipart; validates; stores nothing |
POST /api/quizzes/{id}/questions/import?dry_run= |
class access | draft quizzes; dry_run defaults to true; all-or-nothing |
GET /api/admin/classes/import-template.csv |
public | example |
POST /api/admin/classes/import?mode=create|update&dry_run= |
admin | sections; dry_run defaults to true; all-or-nothing |
GET /api/admin/classes/export.csv |
admin | sections, in the import layout |
The question report:
{"total": 3, "valid": 2, "invalid": 1, "duplicate": 0, "imported": 0, "skipped": 3, "committed": false,
"rows": [{"line": 2, "status": "valid", "question_text": "What is 2 + 2?", "options": ["3","4","5","6"],
"correct_option": 1, "errors": [], "warnings": [], "duplicate_of": null}]}A commit with any invalid row returns 422 {"message", "report"} and stores nothing. Formats: CSV Imports.
| Method & path | Guard | Notes |
|---|---|---|
GET /api/admin/modules, GET /api/admin/modules/{id}
|
admin |
DeviceResponse: identity, presence, telemetry, OTA state, class, health, health_reasons, diagnostics, key_state
|
POST /api/admin/modules/{id}/access |
admin | {is_active} |
POST /api/admin/modules/{id}/reset-key |
admin | forget the device's own key |
POST /api/admin/modules/{id}/ota |
admin |
{version, allow_downgrade?}: a one-device deployment |
GET /api/admin/student-modules?class_id=&gateway_id=&state=connected|seen&q= |
admin | the inventory |
GET /api/admin/classes/{id}/devices |
admin | {node, student_devices, student_modules} |
POST /api/admin/firmware |
admin |
multipart file, channel?, release_notes?; facts read from the image; 422 with the reason; 409 for a different image under an existing version |
GET /api/admin/firmware?target= |
admin | images, with usage, latest_approved, signer, signed_by_site_key
|
GET /api/admin/firmware/{id} |
admin | the image, its devices and its history |
POST /api/admin/firmware/{id}/approve · /deprecate; PATCH; DELETE
|
admin | |
POST /api/admin/deployments?dry_run= |
admin | {artifact_id, device_ids?, class_id?, device_type?, running_version?, all_compatible?, strategy?, allow_downgrade?, idempotency_key?} |
GET /api/admin/deployments, GET /api/admin/deployments/{id}
|
admin | progress and targets |
POST /api/admin/deployments/{id}/pause · /resume · /cancel
|
admin |
A deployment, previewed and then started:
curl -s -X POST "https://…/api/admin/deployments?dry_run=true" -H "$H" -H 'content-type: application/json' \
-d '{"artifact_id": 4, "device_type": "c6", "all_compatible": true, "strategy": {"canary": 1, "batch_size": 5}}'
# {"included":[{"device_id":12,"mac_address":"…","stage":0,"from_version":"2.1.0"},…],
# "excluded":[{"device_id":15,"mac_address":"…","reason":"already running 2.2.0"}]}
curl -s -X POST "https://…/api/admin/deployments" -H "$H" -H 'content-type: application/json' \
-d '{"artifact_id": 4, "device_type": "c6", "all_compatible": true, "idempotency_key": "release-2.2.0"}'For gateways and hubs. Every call carries X-API-Key: the shared provisioning key when registering, then the device's own key. See Security → Per-device keys.
| Method & path | Caller | Purpose |
|---|---|---|
POST /api/device/register |
gateway at boot and on reconnect; hub on its OTA hop | create or update the device, mark it online, link a gateway to a class, issue a device key |
POST /api/device/heartbeat |
gateway every 15 s | liveness, telemetry, diagnostics |
POST /api/device/ping |
gateway every 120 s | classroom status (activity log) |
POST /api/device/batch |
gateway | student joins, leaves, answers, votes, heartbeats, OTA results |
POST /api/device/attendance |
(available) | a check-in |
POST /api/device/firmware/check |
OTA clients | is an update pending? (version, SHA-256, size) |
GET /api/device/firmware/download?mac_address&version |
OTA clients | the pending image only (403 otherwise); header X-Firmware-SHA256
|
POST /api/device/ota/status |
OTA clients | report each step (forward only) |
POST /api/device/firmware/applied |
pre-v2.1 clients | treated as success
|
POST /api/polls/{id}/vote, POST /api/quizzes/{id}/answer
|
direct submissions (not used by current firmware) | {device_id, selected_option} |
Registration:
// request
{"mac_address": "48:F6:EE:FF:FE:C7", "device_type": "c6", "device_name": "imPress C6",
"firmware_version": "2.1.0", "classroom_code": "RM-201"}
// response (device_key only when registering with the shared key and no key is active)
{"device_id": 12, "status": "registered", "class_id": 3, "device_key": "kq3V…"}A batch:
{"device_type": "c6", "messages": [
{"type": "student_join", "enrollment_number": "ABCDE12345", "device_mac": "48:F6:EE:FF:FE:C7", "device_id": 3577735345},
{"type": "quiz_answer", "quiz_id": 4, "question_order": 0, "enrollment_number": "ABCDE12345",
"selected_option": 1, "response_time_ms": 900, "device_id": 3577735345},
{"type": "heartbeat", "device_mac": "48:F6:EE:FF:FE:C7", "device_id": 3577735345, "battery_pct": 76, "rssi": -58}
]}
// → {"status": "ok", "processed": 3, "skipped": 0}An OTA report: {"mac_address": "…", "state": "verifying"}, and at the end {"mac_address": "…", "state": "success", "version": "2.2.0"}. The states are precheck, downloading, verifying, installing, rebooting, health_check, success, rolled_back and failed. Going backwards is a 409; repeating the current state is a no-op.
One room per class: ws(s)://<host>/ws/class/{class_id}?role=teacher|device.
| Role | Credentials | Refused with |
|---|---|---|
teacher |
?token=<session token> (browsers can't set WebSocket headers) |
close 4401 |
device |
X-API-Key header (or legacy ?api_key=); the shared key, or an active device key of an enabled device |
close 4401 |
On connect the server sends {"event": "connected", "class_id": 7, "role": "teacher", …}.
Event (event / type) |
Payload | Audience |
|---|---|---|
quiz_question |
quiz_id, title, question_order, total_questions, question_text, options, timing_mode, time_limit_s (seconds for this question: the per-question limit, or what is left of a total limit; 0 = none) |
everyone |
quiz_end / quiz_ended
|
quiz_id, title |
everyone |
poll_start / poll_started
|
poll_id, title, options |
everyone |
poll_end / poll_ended
|
poll_id, title, option_counts, total_votes |
everyone |
quiz_answer (type) |
quiz_id, question_order, total_answers |
everyone |
poll_vote (type) |
poll_id, selected_option, total_votes |
everyone |
presence |
data: the presence snapshot (devices and student modules) |
teachers only |
device_command |
command, payload, e.g. ota_update {device_type, version, mac_address}
|
everyone (gateways act on it) |
pong |
– | the sender of {"event":"ping"}
|
Events aren't replayed: after reconnecting, read the current state over REST. The keys a gateway reads are pinned by a committed fixture (firmware/contract/device_ws_frames.json), shared by the backend and firmware tests.
curl -s https://impress.example.edu/health
# {"status":"healthy","version":"2.1.0","schema_version":8}/api/health is the same. Both are public.
BASE=https://impress.example.edu
TOKEN=$(curl -s -X POST $BASE/api/auth/login -H 'content-type: application/json' \
-d '{"username":"admin","password":"…"}' | python3 -c 'import sys,json;print(json.load(sys.stdin)["access_token"])')
H="Authorization: Bearer $TOKEN"
J='content-type: application/json'
# a class with a room code, activated
CID=$(curl -s -X POST $BASE/api/admin/classes -H "$H" -H "$J" \
-d '{"name":"Physics","code":"PHY101","classroom_code":"RM-201"}' | python3 -c 'import sys,json;print(json.load(sys.stdin)["id"])')
curl -s -X POST $BASE/api/classes/$CID/activate -H "$H"
# a student, enrolled
SID=$(curl -s -X POST $BASE/api/students/ -H "$H" -H "$J" \
-d '{"roll_number":"ABCDE12345","student_name":"Asha Rao"}' | python3 -c 'import sys,json;print(json.load(sys.stdin)["id"])')
curl -s -X POST $BASE/api/admin/classes/$CID/enroll -H "$H" -H "$J" -d "{\"student_id\": $SID}"
# a quiz, started
QID=$(curl -s -X POST $BASE/api/quizzes/ -H "$H" -H "$J" -d "{\"class_session_id\": $CID, \"title\": \"Check\",
\"questions\": [{\"question_text\": \"2+2?\", \"options\": [\"3\",\"4\"], \"correct_option\": 1}]}" \
| python3 -c 'import sys,json;print(json.load(sys.stdin)["id"])')
curl -s -X POST $BASE/api/quizzes/$QID/start -H "$H"
# … students answer through the gateway …
curl -s $BASE/api/quizzes/$QID/results -H "$H"A simulated gateway, for testing without hardware, is in the Development Guide.
imPress v2.1 · Repository · Issues · Source docs: docs/ · Not yet verified on hardware: see Testing and CI
Understand
Set up
Use
Operate
Develop
More