Skip to content

API Reference

Varun Karthic edited this page Oct 10, 2026 · 2 revisions

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.

Contents


Conventions

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.

Authentication

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

Users

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

Courses and classes

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"}

Students and enrollment

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

Quizzes and polls

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

CSV imports

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.

Modules, firmware and deployments

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"}'

Device API

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.

WebSocket API

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.

Health

curl -s https://impress.example.edu/health
# {"status":"healthy","version":"2.1.0","schema_version":8}

/api/health is the same. Both are public.

A complete scripted session

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.

Clone this wiki locally