REF-API-001: HTTP Interface Reference
| Reference Number | Version | Effective Date | Supersedes | Authority Level |
|---|---|---|---|---|
| REF-API-001 | 1.0 | 2026-09-05 | ARC-OPS-005_Api_Reference (v1, archived) |
Informative |
I. Purpose
Describe every JSON-returning HTTP endpoint the application exposes, identify which client calls it, and record the disposition of each one — live, operational, stale, broken, or prohibited. The v1 predecessor (ARC-OPS-005_Api_Reference) documented a route table that no longer matches the running application and is quarantined with the rest of the v1 archive.
II. Scope
Covers the 63 endpoints that return application/json, out of 198 registered rules. It does not cover HTML page routes, static assets, or the /docs site.
This document is descriptive, not normative. It records what the application currently exposes. It does not grant authority to any endpoint; authority flows from INV-CORE-* → INV-ARC-* → DOM-* → FEAT-* as always. Where this document and a domain contract disagree, the domain contract wins and this document is the thing that is wrong.
III. Authority Level
Informative. Subordinate to INV-ARC-000, INV-ARC-006, and INV-ARC-007.
III-A. Dependencies
docs/INVARIANT/ARCHITECTURE/INV-ARC-000_EXECUTION_MODEL.mddocs/INVARIANT/ARCHITECTURE/INV-ARC-004_CROSS_TENANT_ISOLATION.mddocs/INVARIANT/ARCHITECTURE/INV-ARC-006_COMMAND_BOUNDARY_FOR_MUTATION.mddocs/INVARIANT/ARCHITECTURE/INV-ARC-007_GET_MUST_BE_PURE.mddocs/DOMAIN/DOM-CORE-001_DOMAIN_AUTHORITY_SUMMARY.md
IV. Interface Model
There is no public API. Every endpoint below is a same-origin XHR endpoint serving the application’s own front end. This has consequences that a reader arriving from the v1 document may not expect:
- No versioning, no deprecation window, no stability contract. The client ships in the same deploy as the server. An endpoint may change shape in any release, because the only caller changes with it.
- Authentication is the session cookie, not a bearer token or API key. There is no token-issuing endpoint and no machine-to-machine credential.
- Mutating requests require a CSRF token. Same-origin XHR carries
X-CSRFToken; a caller outside the browser session has no way to obtain one. - The
/apiprefix is not a boundary. JSON endpoints exist under/admin,/student,/sysadmin, and/as well. Sixteen of the 63 live outside/api. Treat the prefix as historical, not architectural.
Consequently, “the API” is not an integration surface anyone outside the application may build against. Requests for an integration surface are a product decision, not a documentation gap.
IV-A. Response shape
The dominant convention is {"status": "success"|"error", ...} with the HTTP status carrying the real signal. It is not universal — some endpoints return a bare object or a bare array (/api/tips/<user_type>). Do not write a client that assumes the envelope without checking the endpoint.
V. Authentication and Authorization
| Guard | Meaning |
|---|---|
@admin_required |
Teacher session. |
@login_required |
Student session. |
@system_admin_required |
Operator session. |
| (none) | Reachable without a session. Twelve endpoints; each is justified in §VII or flagged as a defect. |
Authorization beyond the session — that a teacher owns this class, that a student holds this entitlement — is enforced inside the view via resolve_canonical_context() and class scoping, never by the decorator. A decorator tells you the actor is some teacher; it never tells you they may touch the record named in the URL.
Rate limiting defaults to 500/day, 200/hour globally (app/extensions.py). Endpoints that override it are noted below.
VI. Endpoint Inventory
Auth column: A = @admin_required, S = @login_required (student), O = @system_admin_required, — = none.
VI-A. Attendance and tap
| Method | Path | Auth | Client |
|---|---|---|---|
| POST | /api/tap |
— † | attendance.js |
| GET | /api/student-status |
S | attendance.js |
| GET | /api/attendance/history |
A | admin_attendance_log.html |
| POST | /admin/tap-in-students |
A | admin_students.html |
| POST | /admin/tap-out-students |
A | admin_students.html |
† /api/tap carries no auth decorator but is not unauthenticated: it resolves g.canonical_context and returns 401 when absent, then requires a PIN check against User.pin_hash before doing anything. Rate limited to 100/min. The risk is not present-tense exposure but that the guarantee is imperative — there is no decorator whose removal a reviewer would notice. See §VIII-D.
/api/student-status is rate limited to 30/min per seat, keyed on (class_id, seat_id) from the context @login_required validated, not on the client address: attendance.js polls it every 10 seconds per visible tab, and students share school addresses. It is exempt from the address-keyed defaults; a request without a session gets 401 before the limit applies. Every /api/* 429 is JSON with Retry-After.
VI-B. Hall passes
The largest cluster, and the only one with a sanctioned cross-class exception.
| Method | Path | Auth | Client |
|---|---|---|---|
| POST | /api/hall-pass/request |
S | attendance.js, admin_hall_pass.html |
| POST | /api/hall-pass/request/<request_id>/cancel |
S | attendance.js, admin_hall_pass.html |
| POST | /api/hall-pass/request/<request_id>/<action> |
A | attendance.js, admin_hall_pass.html |
| POST | /api/hall-pass/<int:pass_id>/<action> |
A | attendance.js, admin_hall_pass.html, hall_pass_setup.html |
| POST | /api/hall-pass/checkout |
S | attendance.js |
| POST | /api/hall-pass/checkin |
S | attendance.js |
| GET | /api/hall-pass/available-types |
S | attendance.js |
| GET | /api/hall-pass/history |
A | admin_hall_pass.html |
| GET · POST | /api/hall-pass/settings |
A | admin_hall_pass.html |
| GET · POST | /api/hall-pass/setup |
A | hall_pass_setup.html |
| POST | /api/hall-pass/verify-token/rotate |
A | admin_hall_pass.html |
| POST | /admin/students/bulk-adjust-hall-pass-entitlements |
A | admin_students.html |
| GET | /api/hall-pass/verification/active |
— | removed 2026-09-27 — see §VII-D |
The public hall-pass verification capability that office staff actually use is not in this table: it is the server-rendered POST /verify/hallpass/<teacher_public_token> in main.py, rate limited to 60/min. /api/hall-pass/verification/active is a JSON reimplementation of the same capability with no caller.
VI-C. Store and entitlements
| Method | Path | Auth | Client |
|---|---|---|---|
| POST | /api/purchase-item |
S | student_shop.html |
| POST | /api/use-item |
S | student_shop.html |
| POST | /api/approve-redemption |
A | admin_dashboard.html, admin_store.html |
| POST | /api/reject-redemption |
A | admin_dashboard.html, admin_store.html |
VI-D. Economy balance checker
Driven entirely by static/js/economy-balance.js, which builds request URLs from apiBaseUrl = '/admin/api/economy'. The script is loaded by admin_store.html, admin_rent_settings.html, admin_edit_item.html, and admin_payroll.html.
| Method | Path | Auth | Client |
|---|---|---|---|
| POST | /admin/api/economy/calculate-cwi |
A | removed 2026-09-27 — see §VII-D |
| POST | /admin/api/economy/analyze |
A | economy-balance.js |
| POST | /admin/api/economy/validate/<feature> |
A | economy-balance.js |
VI-E. Roster and class administration
| Method | Path | Auth | Client |
|---|---|---|---|
| POST | /admin/current-class |
A | layout_admin.html |
| POST | /admin/feature-settings/update |
A | admin_feature_settings.html |
| POST | /admin/upload-students |
A | admin_students.html |
| POST | /admin/students/bulk-delete |
A | admin_students.html |
| POST | /admin/pending-students/delete |
A | removed 2026-09-27 — see §VII-D (the roster had moved to /admin/students/bulk-delete) |
| POST | /admin/void-transaction/<int:transaction_id> |
A | admin_banking.html, student_detail.html |
| POST | /admin/announcements/toggle/<int:announcement_id> |
A | admin_announcements.html |
| POST · DELETE | /admin/join-code/delete · /admin/join-code |
A | admin_account_delete.html |
VI-F. Onboarding
| Method | Path | Auth | Client |
|---|---|---|---|
| GET | /admin/onboarding/status |
A | components/getting_started_widget.html |
| POST | /admin/onboarding/skip |
A | removed 2026-09-27 — see §VII-D |
| POST | /admin/onboarding/skip-task |
A | components/getting_started_widget.html |
VI-G. Passkeys (WebAuthn)
Teacher and operator ceremonies are structurally identical and separately registered.
| Method | Path | Auth | Client |
|---|---|---|---|
| POST | /admin/passkey/auth/start · /finish |
— ‡ | admin_login.html |
| POST | /admin/passkey/register/start · /finish |
A | admin_passkey_settings.html |
| DELETE | /admin/passkey/<int:passkey_id>/delete |
A | admin_passkey_settings.html |
| POST | /sysadmin/passkey/auth/start · /finish |
— ‡ | system_admin_login.html |
| POST | /sysadmin/passkey/register/start · /finish |
O | system_admin_passkey_settings.html |
| POST | /sysadmin/passkey/<int:credential_id>/delete |
O | system_admin_passkey_settings.html |
| GET | /admin/passkey/list |
A | removed 2026-09-27 — see §VII-D |
| GET | /sysadmin/passkey/list |
O | removed 2026-09-27 — see §VII-D |
‡ The auth/start and auth/finish ceremonies are unauthenticated by definition — they are the login. Both are rate limited to 20/min; registration and deletion to 10/min.
VI-H. Hybrid page routes
These are HTML page routes that return JSON only on their XHR branch (student.py contains 21 such content-negotiation checks). They appear in a jsonify scan but are not API endpoints, and should not be treated as a stable JSON surface.
| Path | Notes |
|---|---|
/student/login |
Rate limited 60/min; JSON on the XHR failure branch. |
/student/transfer |
Page; JSON validation errors. |
/student/switch-class/<class_id> |
POST returns JSON. |
VI-I. Platform and operations
| Method | Path | Auth | Disposition |
|---|---|---|---|
| GET | /health |
— | Live. Liveness probe, returns ok. No in-app caller by design. |
| GET | /health/status |
— | Live. Bounded capability/platform status signals. No tenant data or raw diagnostics. |
/debug/filters and /debug/admin-db-test were registered here at the time of the
audit and have since been removed — see §VIII-A.
VII. Disposition Summary
Counts reflect the 2026-09-27 removals (§VII-D): the six stale endpoints, and three that §VI had listed as live but had no caller (/admin/api/economy/calculate-cwi, /admin/onboarding/skip, /admin/pending-students/delete).
| Disposition | Count | Meaning |
|---|---|---|
| Live | 49 | Reachable from a shipped client. 52 at the 2026-09-05 audit. |
| Operational | 2 | No in-app caller by design (/health, /health/status). |
| Stale | 0 | Registered, functional, zero callers. 6 at the 2026-09-05 audit, all since removed (§VII-C). |
| Broken | 0 | Was 1 before §VIII-B was resolved. |
| Prohibited | 0 | Was 2 before §VIII-A was resolved. |
VII-C. Stale endpoints
At the 2026-09-05 audit, each of these was registered and reachable, had an intact auth guard, and had no caller in templates/, static/, or any other application code. All six were removed on 2026-09-27 (§VII-D), so none is registered now.
| Path | Assessment |
|---|---|
/admin/payroll/transactions/<int:transaction_id>/void |
Removed 2026-09-27 (§VII-D). A payroll-specific void, parallel to the live /admin/void-transaction/<id> used by admin_banking.html. Two implementations of one capability; this is the unused one. |
/admin/payroll/transactions/void-bulk |
Removed 2026-09-27 (§VII-D). Bulk form of the same dead path. |
/admin/pending-students/bulk-delete |
Removed 2026-09-27 (§VII-D). Referenced only by a test. At the audit the UI called the singular /admin/pending-students/delete; that route was removed too, and the roster now deletes through /admin/students/bulk-delete. |
/admin/passkey/list |
Removed 2026-09-27 (§VII-D). Both settings pages render the credential list server-side; nothing fetches it. |
/sysadmin/passkey/list |
Removed 2026-09-27 (§VII-D). As above. |
/api/hall-pass/verification/active |
Removed 2026-09-27 (§VII-D). Superseded by the server-rendered /verify/hallpass/<token>. Carries the highest residual risk of the six — see §VIII-C. |
VII-D. Removed 2026-09-27
Re-verified against app.url_map and every template and script, then removed. Each had no caller, no normative reference, and no test.
| Rule | Why it had no caller |
|---|---|
GET /api/hall-pass/verification/active |
§VIII-C. Reproduced before removal: with only the teacher’s hall_pass_verify_token, it returned up to ten of the day’s passes across that teacher’s classes, with names, internal seat_id and class_id, without the class and full-name inputs the verification page requires (INV-ARC-019 §X). |
POST /admin/api/economy/calculate-cwi |
The row above listed economy-balance.js as its caller. That file defined a calculateCWI() wrapper, but nothing ever invoked it; the wrapper is removed too. /analyze and /validate/<feature> are live and stay. |
POST /admin/onboarding/skip |
A no-op. The row above listed the Getting Started widget as its caller; the widget only ever calls /admin/onboarding/skip-task. |
GET /sysadmin/passkey/list |
§VII-C. |
POST /admin/payroll/transactions/<int:transaction_id>/void, POST /admin/payroll/transactions/void-bulk |
§VII-C. /admin/void-transaction/<id> is the live void. |
GET /admin/export-class-roster (CSV, not JSON) |
A re-import roster export with no import to feed and no link. /admin/export-students is the live export. |
POST /student/switch-period/<int:user_id> (redirect) |
Already disabled: it logged a warning and redirected. Class switching is /student/switch-class/<class_id>. |
POST /student/dismiss-recovery/<int:code_id> (redirect) |
The dashboard banner closes client-side (data-bs-dismiss) and never posted here. Its only service function, recovery_service.dismiss_recovery_code, is removed with it. |
Second batch, same day. These carried tests that exercised live behaviour, so each test was first moved to the live route (and watched fail against a broken copy of it), then the route was removed. Nothing requested any of them in the retained production access logs (2026-09-19 onward).
| Rule | What replaced it |
|---|---|
POST /admin/student/delete, /admin/student/archive (form) |
/admin/students/deletion-preview + /admin/students/bulk-delete, which share _dispatch_student_deletion but require the destruction gate at seat scope too. /archive was the same hard delete under another name. |
POST /admin/pending-students/delete, /admin/pending-students/bulk-delete |
The same live route. Its all_pending mode had no replacement and went with it. |
POST /admin/student/add-individual (form) |
/admin/upload-students. v2 has no single/bulk split. The retired route made seats without claim material (unclaimable), skipped repeated names, ran under FEAT-IDEN-001, and built a profile it never added to the session (read from source; the route was not run end to end). |
POST /admin/bonuses (form) |
Nothing. Its command, execute_admin_adjustments, is removed too: it posted ledger effects directly, without the plan resolution FEAT-LED-000 requires or a command-level unclaimed-seat refusal (DOM-IDEN-002 §VIII), and accepted any transaction type. |
GET /admin/passkey/list |
The Passkey Settings page renders the list server-side. |
GET /switch-view (redirect) |
Nothing. The force_desktop flag it set was read nowhere. |
GET /sysadmin/auth-check |
/sysadmin/grafana/auth-check, the only auth_request target in the deployed nginx. |
Reset codes, same day. POST /recovery/admin/generate-code/<int:seat_id> was uncalled and ran FEAT-IDEN-003, but checked only that the teacher owned the seat’s class. The live reset was a switch on the two Edit Student forms, which issued the code inside FEAT-IDEN-006. Both are replaced by POST /admin/student/reset-code (form field seat_id). It runs FEAT-IDEN-003 against the request’s active class, requires a claimed student seat and the actor’s own teacher seat in that class, rejects trivially weak codes, and is rate limited per teacher and per teacher-and-student (FEAT-IDEN-003 §VIII). The student detail page carries its button.
VIII. Findings
VIII-A. /debug/* endpoints were unauthenticated and registered in every environment (prohibited — resolved)
Both routes are declared on main_bp with no auth decorator, no rate limit beyond the global default, and no environment gate. Verified against a running instance with credentials omitted:
GET /debug/filters→ 200, returns the complete Jinja filter list. A framework- and version-fingerprinting aid, including the application’s custom filters.GET /debug/admin-db-test→ executes aUserquery filtered toUserRole.TEACHERand returns the row count. Its own docstring calls it a “Temporary route.”
Neither has a caller. debug_admin_db_test in particular is an unauthenticated endpoint whose entire function is to report on the users table.
Resolution: both were deleted from app/routes/main.py. tests/test_route_registration_contract.py::test_debug_routes_are_not_registered now asserts neither path can reappear in the URL map. If a Jinja-filter dump is wanted for local work, it belongs behind @system_admin_required or a FLASK_DEBUG guard, not on the public router.
Removing debug_admin_db_test exposed a second latent defect it had been sharing with /health/deep: both referenced UserRole, which main.py never imported. The deep health probe’s teachers-table check therefore raised NameError rather than reporting status — which is also why /debug/admin-db-test answered 500 rather than leaking the count. The import was added.
VIII-B. /admin/payroll/rewards/add was a dangling decorator that always 500s (resolved)
app/routes/admin.py:7868 carries a route decorator whose function body was removed. The decorator now binds to the next function in the file:
@admin_bp.route('/payroll/rewards/add', methods=['POST']) # ← body deleted; binds below
@admin_bp.route('/payroll/transactions/<int:transaction_id>/void', methods=['POST'])
@admin_required
def void_payroll_transaction(transaction_id):The rule supplies no transaction_id, but the view requires one, so every request raises TypeError before reaching any application logic. Proven by comparing Rule.arguments against the view signature across all 198 rules — this is the only structural mismatch in the application:
/admin/payroll/rewards/add url_args=[] view_requires=['transaction_id'] MISSING=['transaction_id']Authorization is not bypassed — decorators apply bottom-up, so @admin_required still wraps the function. The defect is a guaranteed 500, plus a route table that advertises a capability (“add payroll reward”) the application does not implement.
Resolution: the orphaned decorator line was deleted. The signature check is now tests/test_route_registration_contract.py::test_every_rule_supplies_the_arguments_its_view_requires, with a companion asserting no rule passes an argument its view cannot accept. Together they cover the whole route table, so a future body deletion fails at test time instead of at request time.
VIII-C. /api/hall-pass/verification/active is dead code holding a live PII surface
The endpoint is token-authorized rather than session-authorized, and carries an explicit sanctioned-exception comment citing INV-ARC-004 §V.3 — it is the one runtime path permitted to span a teacher’s classes. That justification is sound for the capability. It is not sound for a second, unused copy of the capability:
- It returns student first names and last initials, destinations, and timestamps across every class the token’s teacher owns.
- Its only credential is the
hall_pass_verify_tokenin a query string. - The server-rendered page implementing the same capability is rate limited to 60/min; this endpoint inherits only the global default of 200/hour, and query-string credentials are the kind that end up in proxy and referrer logs.
Nothing calls it. It is an unmonitored, more permissive twin of a surface that was deliberately hardened.
Recommendation: delete it. If a JSON verification surface is wanted later, it should be re-derived from the hardened page’s guards rather than resurrected from this one.
Resolution (2026-09-27): removed, per the recommendation above. tests/test_route_registration_contract.py::test_removed_dead_routes_stay_removed keeps it out of the URL map.
VIII-D. /api/tap enforces authentication imperatively
handle_tap is correct today — it 401s without a canonical context and 403s on a bad PIN. But it is the only mutating endpoint whose authentication lives entirely in the body, with no decorator. A refactor that reorders the early returns silently removes the check, and no reviewer scanning decorators would see it.
Recommendation: no behavioural change; add a regression test asserting 401 without a session and 403 on a wrong PIN, so the guarantee is pinned by something other than reading order.
IX. Rules for Adding an Endpoint
- Justify the JSON. If the page can render the state server-side, render it server-side. Every JSON endpoint is a second surface to authorize, scope, and keep alive.
- Carry an explicit auth decorator. Enforce authentication declaratively even when the view re-checks it. §VIII-D is what the alternative costs.
- Scope by
class_id. The decorator authorizes the actor, never the record. Resolve canonical context and scope the query. - Mutate through a FEAT (
INV-ARC-006). Nodb.session.commit()in a view. - Keep GET pure (
INV-ARC-007). No writes, no lazy reconciliation, no expiry sweeps. - Ship the caller in the same change. An endpoint with no caller is how every entry in §VII-C started.
- Delete rather than deprecate. There is no external consumer, so there is nothing to deprecate for.
X. Regenerating This Inventory
The tables above are derived, not hand-maintained. Rebuild them from app.url_map rather than by grepping route decorators — the decorator scan cannot see stacked or dynamically-built routes, and produced two false results during the initial audit:
/admin/api/economy/*looked orphaned becauseeconomy-balance.jscomposes its URLs from a base constant (apiBaseUrl = '/admin/api/economy'), so the full path never appears in any file./admin/payroll/rewards/addlooked like an ordinary route because a decorator scan attributes it to the function beneath it, which is exactly the bug.
Cross-reference callers against templates/**/*.html and static/js/**/*.js, and treat a zero-caller result as a question rather than a verdict.
Last verified: 2026-09-05 against 198 registered rules (63 JSON). Removals recorded in §VII-D (2026-09-27) postdate these counts.
Last updated: 2026-09-30