Grading API
Base URL: /api/v1/grading
Manage grade entry, GPA calculations, and transcripts.
Endpoints Overview
| Method | Endpoint | Description |
|---|---|---|
| GET | /term-gpa | Calculate term GPA |
| GET | /cumulative-gpa | Calculate cumulative GPA |
| GET | /transcript | Get student transcript |
| POST | /transcript/official | Issue an official transcript (logged) |
| GET | /roster | Get grade roster for a section |
| GET | /roster/anonymous | Anonymous grading sheet (student numbers only) |
| GET | /roster/export | Roster and photos as a ZIP, for a seating chart |
| GET | /roster/terms, /roster/sections | Pickers for the roster screen |
| POST | /submit | Submit final or midterm grades in bulk |
| POST | /recalculate | Recalculate student GPA |
| GET | /grade-summary | Get grade distribution summary |
Term GPA
GET /api/v1/grading/term-gpaCalculate GPA for a specific term.
Query Parameters
| Parameter | Type | Required | Description |
|---|---|---|---|
student_id | integer | Yes | Student ID |
term_id | integer | Yes | Term ID |
Example Request
curl -X GET "http://localhost:8000/api/v1/grading/term-gpa?student_id=1001&term_id=8" \
-b cookies.txtSuccess Response (200 OK)
{
"student_id": 1001,
"term_id": 8,
"term_code": "FA2024",
"gpa": 3.67,
"credits_attempted": 15.0,
"credits_earned": 15.0,
"quality_points": 55.05,
"courses": [
{
"course_code": "MATH 101",
"course_title": "College Algebra",
"credits": 3.0,
"grade": "A",
"quality_points": 12.0,
"counts_toward_gpa": true
},
{
"course_code": "CS 101",
"course_title": "Intro to Programming",
"credits": 4.0,
"grade": "A-",
"quality_points": 14.8,
"counts_toward_gpa": true
}
]
}TypeScript Types
interface TermGPA {
student_id: number
term_id: number
term_code: string
gpa: number
credits_attempted: number
credits_earned: number
quality_points: number
courses: GradedCourse[]
}
interface GradedCourse {
course_code: string
course_title: string
credits: number
grade: string
quality_points: number
counts_toward_gpa: boolean
}Cumulative GPA
GET /api/v1/grading/cumulative-gpaCalculate overall cumulative GPA across all terms.
Query Parameters
| Parameter | Type | Required | Description |
|---|---|---|---|
student_id | integer | Yes | Student ID |
Example Request
curl -X GET "http://localhost:8000/api/v1/grading/cumulative-gpa?student_id=1001" \
-b cookies.txtSuccess Response (200 OK)
{
"student_id": 1001,
"cumulative_gpa": 3.45,
"credits_attempted": 48.0,
"credits_earned": 45.0,
"quality_points": 165.6,
"terms": [
{
"term_id": 8,
"term_code": "FA2024",
"term_gpa": 3.67,
"credits_attempted": 15.0,
"credits_earned": 15.0
},
{
"term_id": 7,
"term_code": "SU2024",
"term_gpa": 3.25,
"credits_attempted": 6.0,
"credits_earned": 6.0
}
]
}Transcript
GET /api/v1/grading/transcriptGet full academic transcript for a student. This is the on-screen copy; printed, it is unofficial and carries an UNOFFICIAL watermark. Transcript holds do not withhold it.
Query Parameters
| Parameter | Type | Required | Description |
|---|---|---|---|
student_id | integer | Yes | Student ID |
include_in_progress | boolean | No | Include current term enrollments |
Example Request
curl -X GET "http://localhost:8000/api/v1/grading/transcript?student_id=1001&include_in_progress=true" \
-b cookies.txtSuccess Response (200 OK)
{
"student": {
"student_id": 1001,
"student_number": "STU001234",
"name": "John Robert Smith",
"date_of_birth": "2000-05-15",
"program": "Bachelor of Science in Computer Science",
"major": "Computer Science",
"minor": null,
"initial_term": "FA2024",
"expected_graduation": "2028-05-15"
},
"cumulative": {
"gpa": 3.45,
"credits_attempted": 48.0,
"credits_earned": 45.0,
"quality_points": 165.6
},
"terms": [
{
"term_id": 8,
"term_code": "FA2024",
"term_name": "Fall 2024",
"gpa": 3.67,
"credits_attempted": 15.0,
"credits_earned": 15.0,
"courses": [
{
"course_code": "MATH 101",
"course_title": "College Algebra",
"credits": 3.0,
"grade": "A",
"grade_date": "2024-12-20",
"quality_points": 12.0
},
{
"course_code": "CS 101",
"course_title": "Intro to Programming",
"credits": 4.0,
"grade": "A-",
"grade_date": "2024-12-20",
"quality_points": 14.8
},
{
"course_code": "ENGL 101",
"course_title": "English Composition",
"credits": 3.0,
"grade": "B+",
"grade_date": "2024-12-20",
"quality_points": 9.9
}
]
}
],
"transfer_credits": [],
"notes": [],
"generated_at": "2024-12-21T10:00:00Z"
}A graduate's response also carries graduation_records, the degrees conferred: graduation_date, degree_name, degree_code, major_name (omitted when the major did not resolve), graduation_type, title, title_html, honors_display, with_honors and other_honors. A double major has one entry per major, because each is its own Colleague credential (migration 301; see Audit API).
TypeScript Types
interface Transcript {
student: TranscriptStudent
cumulative: CumulativeGPA
terms: TranscriptTerm[]
transfer_credits: TransferCredit[]
notes: string[]
generated_at: string
}
interface TranscriptTerm {
term_id: number
term_code: string
term_name: string
gpa: number
credits_attempted: number
credits_earned: number
courses: TranscriptCourse[]
}
interface TranscriptCourse {
course_code: string
course_title: string
credits: number
grade: string
grade_date: string | null
quality_points: number
}Official Transcript
POST /api/v1/grading/transcript/officialBody: {"student_id": 1001}. Gated on GRADING.TRANSCRIPT.OFFICIAL (migration 306), held initially by REGISTRAR and SUPER_ADMIN; superusers pass through the guard's bypass.
- Refused with 403 while the student has an active hold whose type has
prevents_transcript(e.g. LCTRA, LCTUI, FATRA). Holds withhold only the official transcript. - Each successful call writes a row to
records.transcript_issuances(student, issuing user, impersonator if any, time). - Returns the same body as
GET /transcript, plusofficial: true,issuance_idandissued_at. The transcript screen prints exactly this response, without the watermark, with the issue date and issuance number in the header.
Grade Roster
GET /api/v1/grading/rosterThe students holding a seat in a section (every enrollment not dropped, withdrawn or cancelled), for grade entry. The caller must be able to see the section: faculty reach their own sections, deans their units, the registrar everything.
Query Parameters
| Parameter | Type | Required | Description |
|---|---|---|---|
section_id | integer | Yes | Section ID |
Example Request
curl -X GET "http://localhost:8000/api/v1/grading/roster?section_id=512" -b cookies.txtSuccess Response (200 OK)
An array, one entry per student:
[
{
"enrollment_id": 1,
"student_id": 1001,
"person_id": "6f1c...",
"student_name": "John Smith",
"student_number": "1234567",
"email": "jsmith@example.edu",
"major_names": "BS Biology",
"enrollment_date": "2026-09-01T00:00:00Z",
"current_grade_id": null,
"midterm_grade_id": 14,
"midterm_grade": "C-",
"midterm_grade_date": "2026-10-20T00:00:00Z",
"credit_hours": 4,
"is_audit": false,
"is_pass_fail": false,
"status_display": "Enrolled",
"has_photo": true
}
]The pickers that lead to a roster are GET /roster/terms and GET /roster/sections?term_id=. Each section carries its roster count (the same predicate as this endpoint) and how many are graded or incomplete; they deliberately do not use course_sections.current_enrollment, which reads 0 for a term whose enrollments have completed.
Anonymous Grading Sheet
GET /api/v1/grading/roster/anonymous?section_id=512The roster with identity stripped, for blind grading. Same students as the roster above, by student number only, ordered by student number -- never by name. A student without a student number cannot be identified on a sheet and is left off. Same node and section check as the roster.
{
"section_id": 512,
"course_code": "MATH 101",
"course_title": "College Algebra",
"section_code": "01",
"term_name": "Fall Quarter 2026",
"entries": [ { "student_number": "1234567" }, { "student_number": "1234570" } ]
}There is deliberately no name, email, person_id, student_id or photo field.
Roster Export for a Seating Chart
GET /api/v1/grading/roster/export?section_id=512Returns application/zip (roster-{course}-{section}-{date}.zip, Cache-Control: no-store), built in memory, containing:
roster.csv(UTF-8 with a BOM, so Excel reads accented names):last_name,first_name,preferred_name,student_number,email,majors,photo_filephotos/Last_First_Number.jpg, one display-size JPEG per student with an approved photo
The students are the roster's. A student with a FERPA opt-out stays in the CSV with no photo; a student whose photo is missing has an empty photo_file. Authority is the section check, not PEOPLE.VIEW: photos are read through the photo service, not the HTTP photo route. Each export is logged with the counts of photos written, withheld and missing.
Bulk Grade Submission
POST /api/v1/grading/submitSubmit final or midterm grades for several enrollments in one section.
Request Body
| Field | Type | Required | Description |
|---|---|---|---|
section_id | integer | Yes | Section ID |
grades | array | Yes | Array of grade entries |
grades[].enrollment_id | integer | Yes | Enrollment ID; must belong to the section |
grades[].grade_id | integer or null | Yes | Grade ID from the section's grading scale. null clears a midterm grade; a final grade is always required |
grades[].notes | string | No | Note recorded with the change |
is_midterm | boolean | No | true writes the midterm grade instead of the final grade |
Example Request
curl -X POST http://localhost:8000/api/v1/grading/submit -b cookies.txt -H "Content-Type: application/json" -d '{
"section_id": 512,
"is_midterm": false,
"grades": [
{ "enrollment_id": 1, "grade_id": 1 },
{ "enrollment_id": 2, "grade_id": 2 }
]
}'Success Response (200 OK)
{
"total_submitted": 2,
"total_succeeded": 2,
"total_failed": 0
}A grade that fails is reported in errors (one message per failure) and counted in total_failed; the others still save.
Rules
| Rule | Final grade | Midterm grade |
|---|---|---|
| Grade must be on the section's grading scale (section, else course, else the level's default) | Yes | Yes |
| Window | Refused after the term's grades_due_date unless the caller is a superuser or holds GRADING.RELEASE | Refused unless MIDTERM_GRADES_ENABLED is on, and outside the term's midterm_grade_start_date..midterm_grade_end_date |
| Enrollment | Any on the roster | Only students holding a seat; not dropped or withdrawn |
| GPA | Recalculated afterwards | Not affected |
A midterm grade written here is marked midterm_grade_source = 'SIS' (migration 292), and so is a midterm cleared here. The nightly Colleague import writes midterm grades only where the source is empty or ODS, so a grade entered or cleared in the SIS is never overwritten.
Recalculate GPA
POST /api/v1/grading/recalculateForce recalculation of a student's GPA.
Request Body
| Field | Type | Required | Description |
|---|---|---|---|
student_id | integer | Yes | Student ID |
term_id | integer | No | Recalculate specific term only |
Example Request
curl -X POST http://localhost:8000/api/v1/grading/recalculate \
-b cookies.txt \
-H "Content-Type: application/json" \
-d '{
"student_id": 1001,
"term_id": 8
}'Success Response (200 OK)
{
"success": true,
"data": {
"student_id": 1001,
"previous_cumulative_gpa": 3.42,
"new_cumulative_gpa": 3.45,
"changed": true,
"recalculated_at": "2024-12-21T10:30:00Z"
}
}Grade Summary
GET /api/v1/grading/grade-summaryThe pivot behind the Summary Grade Report: grade distribution across a set of periods, one column per term or per academic year. (This is not a per-section summary; the documentation here used to describe a section_id parameter that the endpoint has never accepted.)
The drill-down behind each number is GET /reporting/grade-summary/enrollments. The two are separate implementations over the same population -- the pivot aggregates the raw tables, the drill-down reads vw_grade_enrollment_base -- and grade_summary_parity_live_test.go exists to keep them equal. Any filter added to one must be added to the other.
What it counts
Enrollments that are part of the academic record. Dropped and cancelled ones are not: DROPPED, CANCELLED, and rows carrying Colleague grade X (legend "Used for classes deleted") are all excluded. Withdrawn enrollments are excluded by default and can be asked for.
Query Parameters
| Parameter | Type | Description |
|---|---|---|
group_by | string | term (default) or year -- the column axis |
term_ids | string | Comma-separated term ids. Send with group_by=term |
academic_year_ids | string | Comma-separated academic year ids. Send with group_by=year |
major_id | integer | Filter to students holding this major |
minor_id | integer | Filter to students holding this minor |
include_withdrawn | boolean | true to count WITHDRAWN enrollments; default false. Pass the same value to the drill-down or a cell will not match the list behind it |
unit_view | string | courses (default) or students; only meaningful for UNIT-scoped grants |
Send term_ids or academic_year_ids, not both -- together they silently AND.
Example Request
curl -X GET "http://localhost:8000/api/v1/grading/grade-summary?group_by=term&term_ids=8,9" \
-b cookies.txtSuccess Response (200 OK)
{
"columns": [
{ "id": 8, "code": "24/WI", "name": "Winter 2024" },
{ "id": 9, "code": "24/SP", "name": "Spring 2024" }
],
"grades": [
{ "letter_grade": "A", "display_name": "A", "grading_system": "Standard" },
{ "letter_grade": "(none)", "display_name": "Not Graded", "grading_system": "" }
],
"data": {
"A": { "24/WI": 412, "24/SP": 388 },
"(none)": { "24/WI": 7, "24/SP": 51 }
},
"column_totals": { "24/WI": 419, "24/SP": 439 },
"grand_total": 858,
"group_by": "term"
}(none) is the bucket for an enrollment with no grade row. It is a real enrollment, not a missing one, and the report counts it.
GPA Calculation Rules
Quality Points
Quality points are calculated as: credits × grade_quality_points
| Grade | Quality Points |
|---|---|
| A | 4.0 |
| A- | 3.7 |
| B+ | 3.3 |
| B | 3.0 |
| B- | 2.7 |
| C+ | 2.3 |
| C | 2.0 |
| C- | 1.7 |
| D+ | 1.3 |
| D | 1.0 |
| F | 0.0 |
GPA Formula
GPA = Total Quality Points / Total GPA CreditsNon-GPA Grades
These grades do not affect GPA:
| Grade | Description |
|---|---|
| W | Withdrawal |
| I | Incomplete |
| P | Pass (Pass/Fail) |
| NP | No Pass (Pass/Fail) |
| AU | Audit |
| TR | Transfer Credit |
Repeat Policy
When a course is repeated:
- Only the highest grade counts toward GPA
- Both attempts appear on transcript
- Previous attempt marked as "Repeated"
Complete endpoint reference
Every route this domain registers, with the permission node that gates it. Generated from the route table, so it is complete by construction — the sections above explain behaviour, this one guarantees nothing is missing.
Paths are relative to /api/v1/grading. A node in italics is an exemption rather than a grant: selfservice acts only on the caller's own record, public needs no session at all, and custom carries its own wrapper.
| Method | Endpoint | Permission node | Description |
|---|---|---|---|
| GET | /academic-levels | GRADING.GPA | List academic levels |
| GET | /cumulative-gpa | GRADING.GPA | Get cumulative GPA |
| GET | /grade-changes | GRADING.GRADE_HISTORY | List grade changes |
| GET | /grade-summary | REPORTS.GRADE_SUMMARY | Get grade summary |
| POST | /recalculate | GRADING.GPA | Recalculate |
| POST | /release | GRADING.RELEASE | Release grades |
| POST | /release/bulk | GRADING.RELEASE | Release all pending grades |
| GET | /release/history | GRADING.RELEASE | List grade releases |
| GET | /release/pending | GRADING.RELEASE | List grades awaiting release |
| GET | /roster | GRADING.ROSTER | Get roster |
| GET | /roster/anonymous | GRADING.ROSTER | Get the anonymous grading sheet (student IDs only) |
| GET | /roster/export | GRADING.ROSTER | Download the roster with photos as a ZIP |
| GET | /roster/sections | GRADING.ROSTER | List gradeable sections for the roster picker |
| GET | /roster/terms | GRADING.ROSTER | List terms for the roster picker |
| GET | /student-terms | GRADING.GPA | List the terms a student has grades in |
| POST | /submit | GRADING.ROSTER | Submit |
| GET | /term-gpa | GRADING.GPA | Get term GPA |
| GET | /transcript | GRADING.TRANSCRIPT | Get transcript |
| POST | /transcript/official | GRADING.TRANSCRIPT.OFFICIAL (VIEW) | Issue an official transcript |