Student Notes API
Base URLs: /api/v1/student-notes, /api/v1/notifications
Notes on a student record (migration 308). Anyone who can open a student's record can add a note to it: rich text (tables included, as in staff email) and attached files. The author decides who else may read it, and the people that reaches are notified by the header bell and by email. The note itself is never sent by email: the message says who wrote a note and about which student, and links to it.
Who sees what
| Reader | Sees |
|---|---|
| The author | Their own notes, always, including private and retracted ones |
| A reader acting under a role the author chose | The note, if their own grant scope on STUDENTS.TAB.NOTES reaches the student |
Holders of STUDENTS.NOTES.VIEW_ALL (registrar) | Every note that is not private, including retracted ones |
Holders of STUDENTS.NOTES.VIEW_PRIVATE (registrar) | Private notes too, but only after a reveal: a written reason, logged, opening them for 30 minutes |
| The student | Notes shared with the STUDENT role that are neither private nor retracted (My Notes) |
- A chosen role is scoped. Choosing Advisor reaches the student's own advisors, not every advisor; choosing Academic Administrator reaches those whose unit covers the student's program. The same rule decides who is notified.
- Roles are matched against the role the reader is acting under. Someone who is both an advisor and an instructor sees an advisor-only note while acting as an advisor.
- Instructors hold
STUDENTS.TAB.NOTESatOWN_SECTIONS: they may add and read notes about students actively enrolled in a section they teach in a term that has not ended. That opens the notes only; the rest of the record stays closed to them. - Impersonation is not a reveal. An administrator impersonating an author does not see that author's private notes, and private notes cannot be revealed while impersonating.
- Anything the caller may not reach answers
404, never403.
Notes are permanent
A trigger refuses any change to a note's subject, body, category, term or author, and refuses every DELETE. The author may only:
- add roles (never remove them), which notifies the people newly reached;
- make a private note shared (never the reverse);
- retract the note, which hides it from everyone it was shared with. The author and
VIEW_ALLholders still see it, marked as retracted.
None of the note tables are row-audited: the generic auditor would copy every note body, private ones included, into core.audit_log. The tables carry their own who and when.
Endpoints
All staff routes are gated on STUDENTS.TAB.NOTES at VIEW, writes included, because anyone who can see a student's record may write a note on it. {personID} is the student's person UUID.
| Method | Path | Notes |
|---|---|---|
GET | /student-notes/students/{personID}/notes | The notes the caller may see. ?include_private=1 adds private notes while a reveal is open. Also returns can_view_all, can_reveal_private, can_read_access_log, hidden_private_count and private_revealed_until |
GET | /student-notes/students/{personID}/compose-options | Roles a note may be shared with, recent terms, and the NOTES attachment limits |
POST | /student-notes/students/{personID}/notes | JSON, or multipart with the JSON in a note part and files in files parts |
POST | /student-notes/students/{personID}/private-reveal | {"reason": "..."} (5–1000 characters). Needs STUDENTS.NOTES.VIEW_PRIVATE |
GET | /student-notes/students/{personID}/private-access-log | Needs STUDENTS.NOTES.ACCESS_LOG |
GET | /student-notes/private-access-log | Every reveal, every student. Needs STUDENTS.NOTES.ACCESS_LOG |
POST | /student-notes/notes/{noteID}/read | Marks the note read, and clears its bell entry |
POST | /student-notes/notes/{noteID}/audience | Author only. {"viewer_role_ids": [..], "make_shared": bool} |
POST | /student-notes/notes/{noteID}/retract | Author only |
GET | /student-notes/notes/{noteID}/attachments/{attachmentID} | Always a download |
GET | /student-notes/mine | The student's own (STUDENT.MY_NOTES) |
POST | /student-notes/mine/{noteID}/read | |
GET | /student-notes/mine/{noteID}/attachments/{attachmentID} |
Creating a note
{
"subject": "Probation meeting",
"body_html": "<p>Met to discuss <strong>probation</strong>.</p><table>...</table>",
"note_category_id": 2,
"term_id": 6123,
"is_private": false,
"viewer_role_ids": [4, 7]
}body_htmlis cleaned with the staff-email allowlist (sanitizehtml.CleanEmail). It may be empty only if files are attached.- A private note must have no
viewer_role_ids. note_category_idis aNOTE_CATEGORYlookup value (records.lookup_note_category).- Files must pass the
NOTESattachment surface, which an administrator sets on Admin > Email Sending > Attachments, inside the organizational ceiling. The denylist and the content check apply as for email.
Notifications
core.notifications is a general per-user inbox shown as the header bell. Rows hold a kind, an English title, params that the bell puts into its own translated wording, and a link_path. They never hold content.
| Method | Path |
|---|---|
GET | /notifications (paginated) |
GET | /notifications/unread-count |
POST | /notifications/{notificationID}/read |
POST | /notifications/read-all |
All four are self-service: they read and write only the session user's own rows. The two writes are allowed in restricted write mode, like other per-user state.
A note notifies each user once, however many of the chosen roles they hold and however often its audience is widened (uq_notifications_recipient_source). Email uses the templates STUDENT_NOTE_ADDED (staff) and STUDENT_NOTE_SHARED_WITH_YOU (the student), editable under Admin > Email Templates. The link in them is built from APP_PUBLIC_URL, which defaults to SAML_SP_BASE_URL.