Test Scores API
Base URL: /api/v1/testscores
Manage standardized test types, applicant test scores, score policies, verification workflows, and superscoring.
Note: Score submission and retrieval is also available through the People domain. See Cross-Domain Routes at the bottom. The admissions-scoped route was removed with that domain.
Endpoints Overview
Test Type Configuration
| Method | Endpoint | Description |
|---|---|---|
| GET | /types | List test types |
| POST | /types | Create test type |
| GET | /types/{testTypeID} | Get test type details |
| PATCH | /types/{testTypeID} | Update test type |
| DELETE | /types/{testTypeID} | Delete test type |
Test Sections
| Method | Endpoint | Description |
|---|---|---|
| POST | /types/{testTypeID}/sections | Create test section |
| PATCH | /sections/{sectionID} | Update test section |
| DELETE | /sections/{sectionID} | Delete test section |
Program Requirements
Removed (August 2026). The five /requirements endpoints read and wrote program_test_requirements, a table that exists in no schema, so every one of them failed at runtime and nothing in the interface called them. Institution-wide score policies are a separate feature and do work.
Score Policies
| Method | Endpoint | Description |
|---|---|---|
| GET | /policies | List score policies |
| POST | /policies | Create score policy |
| GET | /policies/{policyID} | Get policy details |
| PATCH | /policies/{policyID} | Update policy |
| DELETE | /policies/{policyID} | Delete policy |
Applicant Test Scores
| Method | Endpoint | Description |
|---|---|---|
| GET | /scores | List test scores |
| POST | /scores | Submit test score |
| GET | /scores/{scoreID} | Get score details |
| PATCH | /scores/{scoreID} | Update score |
| DELETE | /scores/{scoreID} | Delete score |
Score Verification
| Method | Endpoint | Description |
|---|---|---|
| POST | /scores/{scoreID}/verify | Verify a score |
| POST | /scores/{scoreID}/reject | Reject a score |
| POST | /scores/{scoreID}/mark-official | Mark official score received |
Cross-Domain Routes
| Method | Endpoint | Description |
|---|---|---|
| GET | /people/persons/{personID}/test-scores | List scores by person |
| POST | /people/persons/{personID}/test-scores | Submit score for person |
| GET | /people/persons/{personID}/superscore/{testTypeID} | Calculate superscore |
Test Types
List Test Types
GET /api/v1/testscores/typesQuery Parameters
| Parameter | Type | Description |
|---|---|---|
active_only | boolean | Only return active types |
search | string | Search in code, name |
ordering | string | Sort by: sort_order (default) |
Success Response (200 OK)
{
"count": 5,
"results": [
{
"test_type_id": 1,
"code": "SAT",
"name": "SAT",
"min_score": 400,
"max_score": 1600,
"score_increment": 10,
"validity_months": 60,
"is_active": true,
"sort_order": 1
},
{
"test_type_id": 2,
"code": "ACT",
"name": "ACT",
"min_score": 1,
"max_score": 36,
"score_increment": 1,
"validity_months": 60,
"is_active": true,
"sort_order": 2
}
]
}Get Test Type Details
GET /api/v1/testscores/types/{testTypeID}Returns the test type with all its sections.
Success Response (200 OK)
{
"test_type_id": 1,
"code": "SAT",
"name": "SAT",
"description": "Scholastic Assessment Test",
"min_score": 400,
"max_score": 1600,
"score_increment": 10,
"validity_months": 60,
"is_active": true,
"sort_order": 1,
"created_at": "2024-01-01T00:00:00Z",
"created_by": "admin",
"updated_at": "2024-01-01T00:00:00Z",
"updated_by": "admin",
"sections": [
{
"section_id": 1,
"test_type_id": 1,
"code": "EBRW",
"name": "Evidence-Based Reading and Writing",
"min_score": 200,
"max_score": 800,
"is_required": true,
"sort_order": 1
},
{
"section_id": 2,
"test_type_id": 1,
"code": "MATH",
"name": "Math",
"min_score": 200,
"max_score": 800,
"is_required": true,
"sort_order": 2
}
]
}Create Test Type
POST /api/v1/testscores/typesRequest Body
| Field | Type | Required | Description |
|---|---|---|---|
code | string | Yes | Unique code (max 20) |
name | string | Yes | Display name (max 100) |
description | string | No | Description (max 500) |
min_score | decimal | Yes | Minimum possible score |
max_score | decimal | Yes | Maximum possible score |
score_increment | decimal | No | Score increment step |
validity_months | integer | No | How long scores are valid |
is_active | boolean | No | Active status (default: true) |
sort_order | integer | No | Display order |
Update Test Type
PATCH /api/v1/testscores/types/{testTypeID}Accepts the same fields as create, all optional.
Delete Test Type
DELETE /api/v1/testscores/types/{testTypeID}Test Sections
Create Test Section
POST /api/v1/testscores/types/{testTypeID}/sectionsRequest Body
| Field | Type | Required | Description |
|---|---|---|---|
code | string | Yes | Section code (max 30) |
name | string | Yes | Section name (max 100) |
min_score | decimal | Yes | Minimum section score |
max_score | decimal | Yes | Maximum section score |
is_required | boolean | No | Whether section is required |
sort_order | integer | No | Display order |
Example Request
{
"code": "EBRW",
"name": "Evidence-Based Reading and Writing",
"min_score": 200,
"max_score": 800,
"is_required": true,
"sort_order": 1
}Update Test Section
PATCH /api/v1/testscores/sections/{sectionID}Accepts the same fields as create, all optional.
Delete Test Section
DELETE /api/v1/testscores/sections/{sectionID}Score Policies
List Score Policies
GET /api/v1/testscores/policiesSuccess Response (200 OK)
{
"count": 2,
"results": [
{
"policy_id": 1,
"test_type_id": 1,
"test_type_code": "SAT",
"test_type_name": "SAT",
"score_selection_method": "SUPERSCORE",
"allow_superscoring": true,
"superscore_method": "BEST_SECTION",
"notes": "Use best section scores across all attempts",
"is_active": true
}
]
}Create Score Policy
POST /api/v1/testscores/policiesRequest Body
| Field | Type | Required | Description |
|---|---|---|---|
test_type_id | integer | Yes | Test type |
score_selection_method | string | Yes | HIGHEST, MOST_RECENT, SUPERSCORE, or MANUAL |
allow_superscoring | boolean | No | Allow superscoring |
superscore_method | string | No | BEST_SECTION, WEIGHTED_AVERAGE, or CUSTOM |
notes | string | No | Policy notes (max 500) |
is_active | boolean | No | Active status |
Update Score Policy
PATCH /api/v1/testscores/policies/{policyID}Accepts the same fields as create (except test_type_id), all optional.
Delete Score Policy
DELETE /api/v1/testscores/policies/{policyID}Applicant Test Scores
List Scores
GET /api/v1/testscores/scoresQuery Parameters
| Parameter | Type | Description |
|---|---|---|
person_id | string (UUID) | Filter by person |
application_id | integer | Filter by application |
test_type_id | integer | Filter by test type |
verification_status_id | integer | Filter by verification status |
is_official | boolean | Filter official scores |
is_self_reported | boolean | Filter self-reported scores |
ordering | string | Sort by: -test_date (default) |
Success Response (200 OK)
{
"count": 3,
"results": [
{
"score_id": 1,
"person_id": "550e8400-e29b-41d4-a716-446655440000",
"application_id": 42,
"test_type_id": 1,
"test_type_code": "SAT",
"test_type_name": "SAT",
"composite_score": 1350,
"test_date": "2024-06-01T00:00:00Z",
"is_self_reported": false,
"is_official": true,
"verification_status_id": 1,
"verification_status_code": "VERIFIED",
"verification_status_display": "Verified",
"created_at": "2024-07-15T10:00:00Z",
"created_by": "admin"
}
]
}Get Score Details
GET /api/v1/testscores/scores/{scoreID}Success Response (200 OK)
{
"score_id": 1,
"person_id": "550e8400-e29b-41d4-a716-446655440000",
"application_id": 42,
"test_type_id": 1,
"test_type_code": "SAT",
"test_type_name": "SAT",
"composite_score": 1350,
"test_date": "2024-06-01T00:00:00Z",
"score_reported_at": "2024-07-01T00:00:00Z",
"is_self_reported": false,
"is_official": true,
"verification_status_id": 1,
"verification_status_code": "VERIFIED",
"verification_status_display": "Verified",
"verified_by": 5,
"verified_at": "2024-07-15T10:00:00Z",
"verification_notes": "Official score report received",
"official_score_received_at": "2024-07-01T00:00:00Z",
"score_send_confirmation": "CB-2024-123456",
"notes": null,
"created_at": "2024-07-15T10:00:00Z",
"created_by": "admin",
"updated_at": "2024-07-15T10:00:00Z",
"updated_by": "admin",
"section_scores": [
{
"score_section_id": 1,
"score_id": 1,
"section_id": 1,
"section_code": "EBRW",
"section_name": "Evidence-Based Reading and Writing",
"score": 680,
"percentile": 92,
"min_score": 200,
"max_score": 800
},
{
"score_section_id": 2,
"score_id": 1,
"section_id": 2,
"section_code": "MATH",
"section_name": "Math",
"score": 670,
"percentile": 88,
"min_score": 200,
"max_score": 800
}
]
}Submit Score
POST /api/v1/testscores/scoresRequest Body
| Field | Type | Required | Description |
|---|---|---|---|
person_id | string (UUID) | Yes | Person ID |
application_id | integer | No | Associated application |
test_type_id | integer | Yes | Test type |
composite_score | decimal | Yes | Composite/total score |
test_date | datetime | Yes | Date test was taken |
score_reported_at | datetime | No | When score was reported |
is_self_reported | boolean | No | Self-reported flag |
score_send_confirmation | string | No | Score send confirmation (max 100) |
notes | string | No | Notes (max 1000) |
section_scores | array | No | Individual section scores |
section_scores[].section_id | integer | Yes | Section ID |
section_scores[].score | decimal | Yes | Section score |
section_scores[].percentile | integer | No | Percentile rank |
Example Request
{
"person_id": "550e8400-e29b-41d4-a716-446655440000",
"application_id": 42,
"test_type_id": 1,
"composite_score": 1350,
"test_date": "2024-06-01T00:00:00Z",
"is_self_reported": false,
"section_scores": [
{ "section_id": 1, "score": 680, "percentile": 92 },
{ "section_id": 2, "score": 670, "percentile": 88 }
]
}Update Score
PATCH /api/v1/testscores/scores/{scoreID}Request Body
| Field | Type | Required | Description |
|---|---|---|---|
composite_score | decimal | No | Updated composite score |
test_date | datetime | No | Updated test date |
score_reported_at | datetime | No | When score was reported |
score_send_confirmation | string | No | Confirmation code (max 100) |
notes | string | No | Notes (max 1000) |
section_scores | array | No | Updated section scores |
Delete Score
DELETE /api/v1/testscores/scores/{scoreID}Score Verification
Verify Score
POST /api/v1/testscores/scores/{scoreID}/verifyMarks a score as verified.
Request Body
| Field | Type | Required | Description |
|---|---|---|---|
verified_by | integer | Yes | User ID of verifier |
verification_notes | string | No | Notes (max 1000) |
Reject Score
POST /api/v1/testscores/scores/{scoreID}/rejectMarks a score as rejected.
Request Body
| Field | Type | Required | Description |
|---|---|---|---|
verified_by | integer | Yes | User ID of reviewer |
verification_notes | string | Yes | Reason for rejection (max 1000) |
Mark Official Score Received
POST /api/v1/testscores/scores/{scoreID}/mark-officialRecords that an official score report has been received.
Request Body
| Field | Type | Required | Description |
|---|---|---|---|
verified_by | integer | Yes | User ID of reviewer |
official_score_received_at | datetime | Yes | Date official score was received |
document_id | integer | No | Associated document ID |
verification_notes | string | No | Notes (max 1000) |
Cross-Domain Routes
Scores by Person
GET /api/v1/people/persons/{personID}/test-scores
POST /api/v1/people/persons/{personID}/test-scoresList or submit test scores for a specific person. The POST endpoint accepts the same body as POST /testscores/scores (the person_id is inferred from the path).
Scores by Application
Removed in August 2026 with the admissions domain (migration 167). Scores are reached by person instead: GET /api/v1/people/persons/{personID}/test-scores.
Superscoring
GET /api/v1/people/persons/{personID}/superscore/{testTypeID}Calculate the superscore for a person across all their test attempts of the given type. Takes the best section score from each attempt and combines them.
Success Response (200 OK)
{
"person_id": "550e8400-e29b-41d4-a716-446655440000",
"test_type_id": 1,
"test_type_code": "SAT",
"test_type_name": "SAT",
"superscore": 1400,
"calculated_at": "2024-12-20T10:00:00Z",
"source_scores": [
{
"score_id": 1,
"test_date": "2024-03-01T00:00:00Z",
"composite_score": 1350
},
{
"score_id": 2,
"test_date": "2024-06-01T00:00:00Z",
"composite_score": 1380
}
],
"section_bests": [
{
"section_id": 1,
"section_code": "EBRW",
"section_name": "Evidence-Based Reading and Writing",
"best_score": 720,
"from_score_id": 2,
"from_test_date": "2024-06-01T00:00:00Z"
},
{
"section_id": 2,
"section_code": "MATH",
"section_name": "Math",
"best_score": 680,
"from_score_id": 1,
"from_test_date": "2024-03-01T00:00:00Z"
}
]
}TypeScript Types
interface TestType {
test_type_id: number
code: string
name: string
description: string | null
min_score: number
max_score: number
score_increment: number | null
validity_months: number | null
is_active: boolean
sort_order: number
sections?: TestSection[]
}
interface TestSection {
section_id: number
test_type_id: number
code: string
name: string
min_score: number
max_score: number
is_required: boolean
sort_order: number
}
interface ProgramTestRequirement {
requirement_id: number
test_type_id: number
test_type_code: string
test_type_name: string
application_type_id: number | null
program_id: number | null
is_required: boolean
minimum_score: number | null
preferred_score: number | null
accept_superscoring: boolean
alternative_test_type_id: number | null
alternative_test_code: string | null
notes: string | null
is_active: boolean
}
interface ScorePolicy {
policy_id: number
test_type_id: number
test_type_code: string
test_type_name: string
score_selection_method: 'HIGHEST' | 'MOST_RECENT' | 'SUPERSCORE' | 'MANUAL'
allow_superscoring: boolean
superscore_method: 'BEST_SECTION' | 'WEIGHTED_AVERAGE' | 'CUSTOM' | null
notes: string | null
is_active: boolean
}
interface ApplicantTestScore {
score_id: number
person_id: string
application_id: number | null
test_type_id: number
test_type_code: string
test_type_name: string
composite_score: number
test_date: string
score_reported_at: string | null
self_reported_at: string | null
is_self_reported: boolean
is_official: boolean
verification_status_id: number | null
verification_status_code: string | null
verification_status_display: string | null
verified_by: number | null
verified_at: string | null
verification_notes: string | null
official_score_received_at: string | null
score_send_confirmation: string | null
document_id: number | null
notes: string | null
section_scores?: SectionScore[]
}
interface SectionScore {
score_section_id: number
score_id: number
section_id: number
section_code: string
section_name: string
score: number
percentile: number | null
min_score: number
max_score: number
}
interface Superscore {
person_id: string
test_type_id: number
test_type_code: string
test_type_name: string
superscore: number
calculated_at: string
source_scores: SuperscoreSource[]
section_bests: SectionBest[]
}
interface SuperscoreSource {
score_id: number
test_date: string
composite_score: number
}
interface SectionBest {
section_id: number
section_code: string
section_name: string
best_score: number
from_score_id: number
from_test_date: string
}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/testscores. 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 | /policies | TESTSCORES.POLICIES | List policies |
| POST | /policies | TESTSCORES.POLICIES | Create a policy |
| DELETE | /policies/{policyID} | TESTSCORES.POLICIES | Delete a policy |
| GET | /policies/{policyID} | TESTSCORES.POLICIES | Get one policy |
| PATCH | /policies/{policyID} | TESTSCORES.POLICIES | Update a policy |
| GET | /scores | STUDENTS.TAB.TEST_SCORES | List scores |
| POST | /scores | STUDENTS.SCORES.EDIT | Create a score |
| DELETE | /scores/{scoreID} | STUDENTS.SCORES.EDIT | Delete a score |
| GET | /scores/{scoreID} | STUDENTS.TAB.TEST_SCORES | Get one score |
| PATCH | /scores/{scoreID} | STUDENTS.SCORES.EDIT | Update a score |
| POST | /scores/{scoreID}/mark-official | ADMISSIONS.SCORES.VERIFY | Mark official for a score |
| POST | /scores/{scoreID}/reject | ADMISSIONS.SCORES.VERIFY | Reject for a score |
| POST | /scores/{scoreID}/verify | ADMISSIONS.SCORES.VERIFY | Verify for a score |
| DELETE | /sections/{sectionID} | TESTSCORES.TYPES | Delete a section |
| PATCH | /sections/{sectionID} | TESTSCORES.TYPES | Update a section |
| GET | /types | TESTSCORES.TYPES | List types |
| POST | /types | TESTSCORES.TYPES | Create a type |
| DELETE | /types/{testTypeID} | TESTSCORES.TYPES | Delete a type |
| GET | /types/{testTypeID} | TESTSCORES.TYPES | Get one type |
| PATCH | /types/{testTypeID} | TESTSCORES.TYPES | Update a type |
| POST | /types/{testTypeID}/sections | TESTSCORES.TYPES | Add sections to a type |