Core API
Base URL: /api/v1/core/
System reference data including lookup values, currencies, institution settings, countries, email templates, credit conversions, and health checks.
Endpoints Overview
Lookup Values
| Method | Endpoint | Auth | Description |
|---|---|---|---|
| GET | /lookup-types | Yes | List lookup types |
| GET | /lookup-types/by-code/{code} | Yes | Get lookup type by code |
| POST | /lookup-values | Yes | Create lookup value |
| GET | /lookup-values/by-type/{typeCode} | Yes | Get values for a type |
| PATCH | /lookup-values/{typeCode}/{id} | Yes | Update lookup value |
| DELETE | /lookup-values/{typeCode}/{id} | Yes | Delete lookup value |
| GET | /lookups | No | Get all lookups (frontend alias) |
| GET | /lookups/by-type/{typeCode} | No | Get values by type (frontend alias) |
Currencies
| Method | Endpoint | Auth | Description |
|---|---|---|---|
| GET | /currencies | Yes | List currencies |
| POST | /currencies | Yes | Create currency |
| GET | /currencies/{id} | Yes | Get currency details |
| PATCH | /currencies/{id} | Yes | Update currency |
| DELETE | /currencies/{id} | Yes | Delete currency |
Institution Settings
| Method | Endpoint | Auth | Description |
|---|---|---|---|
| GET | /settings | Yes | List all settings |
| POST | /settings | Yes | Create setting |
| GET | /settings/{id} | Yes | Get setting by ID |
| PATCH | /settings/{id} | Yes | Update setting by ID |
| GET | /settings/branding | No | Get branding settings |
| GET | /settings/by_key | Yes | Get setting by key |
| PATCH | /settings/by_key | Yes | Update setting by key |
| PUT | /settings/{key} | Yes | Upsert setting by key |
| POST | /settings/bulk_update | Yes | Update multiple settings |
| PATCH | /settings/bulk_update | Yes | Update multiple settings (alternative) |
| GET | /settings/by_category | Yes | Get settings by category |
Countries
| Method | Endpoint | Auth | Description |
|---|---|---|---|
| GET | /countries | No | List countries |
| GET | /countries/dropdown | No | Minimal country list for dropdowns |
| GET | /countries/{id} | No | Get country details |
| POST | /countries | Yes | Create country |
| PATCH | /countries/{id} | Yes | Update country |
| DELETE | /countries/{id} | Yes | Delete country |
Email Templates
| Method | Endpoint | Auth | Description |
|---|---|---|---|
| GET | /email-templates | Yes | List email templates |
| POST | /email-templates | Yes | Create email template |
| GET | /email-templates/by-code/{code} | Yes | Get template by code |
| GET | /email-templates/{id} | Yes | Get template by ID |
| PATCH | /email-templates/{id} | Yes | Update email template |
| DELETE | /email-templates/{id} | Yes | Delete email template |
| GET | /email-templates/attachments/{id} | Yes | A template's fixed attachments, its allowance and the limits that apply |
| POST | /email-templates/attachments/{id} | Yes | Add files to a template (multipart) |
| PUT | /email-templates/attachments/{id}/settings | Yes | Set the template's attachment allowance |
| GET | /email-templates/attachments/{id}/{attachmentID} | Yes | Download one attached file |
| DELETE | /email-templates/attachments/{id}/{attachmentID} | Yes | Remove one attached file |
Credit Conversions
| Method | Endpoint | Auth | Description |
|---|---|---|---|
| GET | /credit-conversions | Yes | List credit system conversions |
| POST | /credit-conversions | Yes | Create conversion rule |
| POST | /credit-conversions/convert | Yes | Convert credits between systems |
| PUT | /credit-conversions/{conversionID} | Yes | Update conversion rule |
| DELETE | /credit-conversions/{conversionID} | Yes | Delete conversion rule |
Health
| Method | Endpoint | Auth | Description |
|---|---|---|---|
| GET | /health | Yes | Health check |
Lookup Values
The lookup system provides reference data for dropdowns, statuses, and types throughout the application.
Get All Lookups
GET /api/v1/core/lookupsGet all lookup types with their active values. Publicly accessible.
Query Parameters
| Parameter | Type | Description |
|---|---|---|
type_codes | string | Comma-separated list of type codes to filter |
Example Request
# Get all lookups
curl -X GET http://localhost:8000/api/v1/core/lookups
# Get specific types
curl -X GET "http://localhost:8000/api/v1/core/lookups?type_codes=TERM_TYPE,COURSE_STATUS,NAME_TYPE"Success Response (200 OK)
{
"success": true,
"data": [
{
"lookup_type_id": 1,
"type_code": "TERM_TYPE",
"description": "Academic term types",
"values": [
{
"lookup_value_id": 1,
"code": "FALL",
"display_value": "Fall",
"sort_order": 1
},
{
"lookup_value_id": 2,
"code": "SPRING",
"display_value": "Spring",
"sort_order": 2
},
{
"lookup_value_id": 3,
"code": "SUMMER",
"display_value": "Summer",
"sort_order": 3
}
]
},
{
"lookup_type_id": 2,
"type_code": "COURSE_STATUS",
"description": "Course statuses",
"values": [
{
"lookup_value_id": 10,
"code": "ACTIVE",
"display_value": "Active",
"sort_order": 1
},
{
"lookup_value_id": 11,
"code": "INACTIVE",
"display_value": "Inactive",
"sort_order": 2
}
]
}
]
}TypeScript Types
interface LookupValue {
lookup_value_id: number
code: string
display_value: string
sort_order: number
}
interface LookupType {
lookup_type_id: number
type_code: string
description: string | null
values: LookupValue[]
}Get Values by Type Code
GET /api/v1/core/lookups/by-type/{typeCode}Get all active values for a specific lookup type. Publicly accessible.
Path Parameters
| Parameter | Type | Description |
|---|---|---|
typeCode | string | The lookup type code |
Example Request
curl -X GET http://localhost:8000/api/v1/core/lookups/by-type/GENDERSuccess Response (200 OK)
{
"success": true,
"data": [
{
"lookup_value_id": 20,
"lookup_type_id": 5,
"type_code": "GENDER",
"code": "M",
"display_value": "Male",
"is_active": true,
"sort_order": 1
},
{
"lookup_value_id": 21,
"lookup_type_id": 5,
"type_code": "GENDER",
"code": "F",
"display_value": "Female",
"is_active": true,
"sort_order": 2
},
{
"lookup_value_id": 22,
"lookup_type_id": 5,
"type_code": "GENDER",
"code": "NB",
"display_value": "Non-Binary",
"is_active": true,
"sort_order": 3
}
]
}Error Response (404 Not Found)
{
"success": false,
"error": {
"code": "lookup_type_not_found",
"message": "Lookup type \"INVALID_TYPE\" not found"
}
}List Lookup Types
GET /api/v1/core/lookup-typesList all lookup types with value counts.
Query Parameters
| Parameter | Type | Description |
|---|---|---|
search | string | Search in type_code, description |
ordering | string | Sort by: type_code |
Example Request
curl -X GET http://localhost:8000/api/v1/core/lookup-types \
-b cookies.txtSuccess Response (200 OK)
{
"count": 25,
"next": null,
"previous": null,
"results": [
{
"lookup_type_id": 1,
"type_code": "TERM_TYPE",
"description": "Academic term types",
"is_system": true,
"value_count": 3,
"created_at": "2024-01-01T00:00:00Z",
"updated_at": "2024-01-01T00:00:00Z"
}
]
}Get Lookup Type by Code
GET /api/v1/core/lookup-types/by-code/{code}Get a specific lookup type by its code, including its values.
Path Parameters
| Parameter | Type | Description |
|---|---|---|
code | string | The lookup type code |
Example Request
curl -X GET http://localhost:8000/api/v1/core/lookup-types/by-code/TERM_TYPE \
-b cookies.txtGet Values for a Type
GET /api/v1/core/lookup-values/by-type/{typeCode}Get all values for a specific lookup type.
Path Parameters
| Parameter | Type | Description |
|---|---|---|
typeCode | string | The lookup type code |
Example Request
curl -X GET http://localhost:8000/api/v1/core/lookup-values/by-type/ENROLLMENT_STATUS \
-b cookies.txtSuccess Response (200 OK)
{
"count": 5,
"next": null,
"previous": null,
"results": [
{
"lookup_value_id": 100,
"lookup_type_id": 15,
"type_code": "ENROLLMENT_STATUS",
"code": "REGISTERED",
"display_value": "Registered",
"is_active": true,
"sort_order": 1,
"created_at": "2024-01-01T00:00:00Z",
"updated_at": "2024-01-01T00:00:00Z"
}
]
}Create Lookup Value
POST /api/v1/core/lookup-valuesCreate a new lookup value.
Example Request
curl -X POST http://localhost:8000/api/v1/core/lookup-values \
-b cookies.txt \
-H "Content-Type: application/json" \
-d '{
"type_code": "ENROLLMENT_STATUS",
"code": "WITHDRAWN",
"display_value": "Withdrawn",
"sort_order": 6
}'Update Lookup Value
PATCH /api/v1/core/lookup-values/{typeCode}/{id}Update an existing lookup value.
Path Parameters
| Parameter | Type | Description |
|---|---|---|
typeCode | string | The lookup type code |
id | integer | The lookup value ID |
Example Request
curl -X PATCH http://localhost:8000/api/v1/core/lookup-values/ENROLLMENT_STATUS/100 \
-b cookies.txt \
-H "Content-Type: application/json" \
-d '{
"display_value": "Registered (Active)",
"sort_order": 1
}'Delete Lookup Value
DELETE /api/v1/core/lookup-values/{typeCode}/{id}Delete a lookup value.
Path Parameters
| Parameter | Type | Description |
|---|---|---|
typeCode | string | The lookup type code |
id | integer | The lookup value ID |
Example Request
curl -X DELETE http://localhost:8000/api/v1/core/lookup-values/ENROLLMENT_STATUS/100 \
-b cookies.txtCommon Lookup Types
| Type Code | Description |
|---|---|
TERM_TYPE | Academic term types (Fall, Spring, Summer) |
COURSE_STATUS | Course statuses (Active, Inactive) |
PROGRAM_STATUS | Program statuses |
BLOCK_TYPE | Requirement block types |
REQUISITE_TYPE | Course requisite types (Prerequisite, Corequisite) |
NAME_TYPE | Person name types (Legal, Preferred, Maiden) |
ADDRESS_TYPE | Address types (Home, Mailing, Permanent) |
PHONE_TYPE | Phone types (Mobile, Home, Work) |
EMAIL_TYPE | Email types (Personal, Work, Institutional) |
GENDER | Gender options |
ETHNICITY | Ethnicity categories |
RACE | Race categories |
ENROLLMENT_STATUS | Enrollment statuses |
GRADE | Grade values |
INSTRUCTION_MODE | Instruction modes (In Person, Online, Hybrid) |
SECTION_STATUS | Section statuses (Open, Closed, Waitlist) |
Currencies
List Currencies
GET /api/v1/core/currenciesList all currencies. By default returns only active currencies.
Query Parameters
| Parameter | Type | Description |
|---|---|---|
is_active | boolean | Filter by active status (default: true) |
search | string | Search in iso_code, name |
ordering | string | Sort by: iso_code, name |
Example Request
curl -X GET http://localhost:8000/api/v1/core/currencies \
-b cookies.txtSuccess Response (200 OK)
{
"count": 3,
"next": null,
"previous": null,
"results": [
{
"currency_id": 1,
"iso_code": "USD",
"name": "US Dollar",
"symbol": "$",
"decimal_places": 2,
"is_active": true,
"created_at": "2024-01-01T00:00:00Z",
"updated_at": "2024-01-01T00:00:00Z"
},
{
"currency_id": 2,
"iso_code": "EUR",
"name": "Euro",
"symbol": "\u20ac",
"decimal_places": 2,
"is_active": true,
"created_at": "2024-01-01T00:00:00Z",
"updated_at": "2024-01-01T00:00:00Z"
}
]
}TypeScript Types
interface Currency {
currency_id: number
iso_code: string
name: string
symbol: string
decimal_places: number
is_active: boolean
}Institution Settings
Get Branding Settings
GET /api/v1/core/settings/brandingGet all branding-related settings. Publicly accessible.
Example Request
curl -X GET http://localhost:8000/api/v1/core/settings/brandingSuccess Response (200 OK)
{
"success": true,
"data": {
"institution_name": "University of Example",
"institution_short_name": "UOE",
"system_name": "Student Information System",
"logo_url": "/static/images/logo.png",
"logo_dark_url": "/static/images/logo-dark.png",
"primary_color": "#1E40AF",
"secondary_color": "#7C3AED",
"accent_color": "#10B981",
"institution_address_line1": "123 University Ave",
"institution_city": "Example City",
"institution_state": "EX",
"institution_postal_code": "12345",
"institution_phone": "(555) 123-4567",
"institution_email": "info@university.edu",
"institution_website": "https://university.edu"
}
}Get Setting by Key
GET /api/v1/core/settings/by_key?key={setting_key}Get a specific setting by its key.
Query Parameters
| Parameter | Type | Required | Description |
|---|---|---|---|
key | string | Yes | The setting key |
Example Request
curl -X GET "http://localhost:8000/api/v1/core/settings/by_key?key=GPA_DECIMAL_PLACES" \
-b cookies.txtSuccess Response (200 OK)
{
"success": true,
"data": {
"setting_id": 15,
"setting_key": "GPA_DECIMAL_PLACES",
"setting_value": "2",
"typed_value": 2,
"setting_type": "number",
"description": "Number of decimal places for GPA display",
"created_at": "2024-01-01T00:00:00Z",
"updated_at": "2024-03-15T10:30:00Z"
}
}TypeScript Types
interface InstitutionSetting {
setting_id: number
setting_key: string
setting_value: string | null
typed_value: any
setting_type: 'string' | 'number' | 'boolean' | 'json'
description: string | null
}EMAIL_NOREPLY_ADDRESS is validated
The no-reply sender (migration 268) is the From of every system notification, and staff whose role allows it may send from it. Every write path — create, update by id or key, upsert, bulk update — refuses a value that is blank, is not a plain email address, or is not at an active, protected institution domain, with 400 and field: setting_value. A bulk update validates every key before writing any, so a bad value does not leave the batch half applied. See Communications.
Get Setting by ID
GET /api/v1/core/settings/{id}Get a specific setting by its ID.
Path Parameters
| Parameter | Type | Description |
|---|---|---|
id | integer | The setting ID |
Example Request
curl -X GET http://localhost:8000/api/v1/core/settings/15 \
-b cookies.txtCreate Setting
POST /api/v1/core/settingsCreate a new institution setting.
Request Body
| Field | Type | Required | Description |
|---|---|---|---|
setting_key | string | Yes | Unique setting key |
setting_value | string | Yes | The setting value |
setting_type | string | No | Type: string, number, boolean, json |
description | string | No | Description of the setting |
Example Request
curl -X POST http://localhost:8000/api/v1/core/settings \
-b cookies.txt \
-H "Content-Type: application/json" \
-d '{
"setting_key": "MAX_WAITLIST_SIZE",
"setting_value": "25",
"setting_type": "number",
"description": "Maximum number of students on a section waitlist"
}'Update Setting by ID
PATCH /api/v1/core/settings/{id}Update a setting by its ID.
Path Parameters
| Parameter | Type | Description |
|---|---|---|
id | integer | The setting ID |
Example Request
curl -X PATCH http://localhost:8000/api/v1/core/settings/15 \
-b cookies.txt \
-H "Content-Type: application/json" \
-d '{
"setting_value": "3"
}'Upsert Setting by Key
PUT /api/v1/core/settings/{key}Create or update a setting by its key. If the setting exists, it is updated; otherwise, it is created.
Path Parameters
| Parameter | Type | Description |
|---|---|---|
key | string | The setting key |
Example Request
curl -X PUT http://localhost:8000/api/v1/core/settings/GPA_DECIMAL_PLACES \
-b cookies.txt \
-H "Content-Type: application/json" \
-d '{
"setting_value": "3",
"setting_type": "number",
"description": "Number of decimal places for GPA display"
}'Update Setting by Key
PATCH /api/v1/core/settings/by_keyUpdate a setting's value by its key.
Request Body
| Field | Type | Required | Description |
|---|---|---|---|
key | string | Yes | The setting key |
setting_value | string | Yes | The new value |
Example Request
curl -X PATCH http://localhost:8000/api/v1/core/settings/by_key \
-b cookies.txt \
-H "Content-Type: application/json" \
-d '{
"key": "GPA_DECIMAL_PLACES",
"setting_value": "3"
}'Bulk Update Settings
POST /api/v1/core/settings/bulk_updateUpdate multiple settings at once.
Request Body
| Field | Type | Required | Description |
|---|---|---|---|
settings | object | Yes | Key-value pairs to update |
Example Request
curl -X POST http://localhost:8000/api/v1/core/settings/bulk_update \
-b cookies.txt \
-H "Content-Type: application/json" \
-d '{
"settings": {
"primary_color": "#1E40AF",
"secondary_color": "#7C3AED",
"institution_name": "New University Name"
}
}'Success Response (200 OK)
{
"success": true,
"data": {
"updated_keys": ["primary_color", "secondary_color", "institution_name"]
}
}Get Settings by Category
GET /api/v1/core/settings/by_category?category={category}Get settings grouped by category.
Query Parameters
| Parameter | Type | Required | Description |
|---|---|---|---|
category | string | Yes | Category: system or academic |
System Settings:
SESSION_TIMEOUT_MINUTESPAPER_SIZE
Academic Settings:
GPA_REPEAT_POLICYGPA_DECIMAL_PLACESMAX_CREDITS_UNDERGRADMAX_CREDITS_UNDERGRAD_OVERLOADMAX_CREDITS_GRADUATEMAX_CREDITS_GRADUATE_OVERLOAD
Countries
List Countries
GET /api/v1/core/countriesList all countries. Publicly accessible.
Query Parameters
| Parameter | Type | Description |
|---|---|---|
is_active | boolean | Filter by active status (default: true) |
visa_required | boolean | Filter by visa requirement |
search | string | Search in iso_code, country_name |
ordering | string | Sort by: sort_order, country_name, iso_code |
Example Request
curl -X GET http://localhost:8000/api/v1/core/countriesSuccess Response (200 OK)
{
"count": 195,
"next": "http://localhost:8000/api/v1/core/countries?page=2",
"previous": null,
"results": [
{
"country_id": 1,
"iso_code": "US",
"iso_code_3": "USA",
"country_name": "United States",
"visa_required": false,
"sort_order": 1,
"is_active": true,
"created_at": "2024-01-01T00:00:00Z",
"updated_at": "2024-01-01T00:00:00Z"
}
]
}Country Dropdown
GET /api/v1/core/countries/dropdownGet minimal country list for dropdowns. Publicly accessible.
Example Request
curl -X GET http://localhost:8000/api/v1/core/countries/dropdownSuccess Response (200 OK)
{
"success": true,
"data": [
{
"country_id": 1,
"iso_code": "US",
"country_name": "United States",
"visa_required": false
},
{
"country_id": 2,
"iso_code": "CA",
"country_name": "Canada",
"visa_required": false
}
]
}TypeScript Types
interface Country {
country_id: number
iso_code: string
iso_code_3: string | null
country_name: string
visa_required: boolean
sort_order: number
is_active: boolean
}
interface CountryMinimal {
country_id: number
iso_code: string
country_name: string
visa_required: boolean
}Email Templates
List Email Templates
GET /api/v1/core/email-templatesList all email templates.
Example Request
curl -X GET http://localhost:8000/api/v1/core/email-templates \
-b cookies.txtSuccess Response (200 OK)
{
"count": 10,
"next": null,
"previous": null,
"results": [
{
"template_id": 1,
"template_code": "ADMISSION_ACCEPTED",
"name": "Admission Acceptance Letter",
"subject": "Congratulations! You have been accepted",
"body_html": "<p>Dear {{student_name}},</p>...",
"body_text": "Dear {{student_name}},\n...",
"is_active": true,
"created_at": "2024-01-01T00:00:00Z",
"updated_at": "2024-01-01T00:00:00Z"
}
]
}TypeScript Types
interface EmailTemplate {
template_id: number
template_code: string
name: string
subject: string
body_html: string | null
body_text: string | null
is_active: boolean
created_at: string
updated_at: string
}Create Email Template
POST /api/v1/core/email-templatesCreate a new email template.
Request Body
| Field | Type | Required | Description |
|---|---|---|---|
template_code | string | Yes | Unique template code |
name | string | Yes | Template display name |
subject | string | Yes | Email subject line |
body_html | string | No | HTML email body |
body_text | string | No | Plain text email body |
is_active | boolean | No | Active status (default: true) |
Example Request
curl -X POST http://localhost:8000/api/v1/core/email-templates \
-b cookies.txt \
-H "Content-Type: application/json" \
-d '{
"template_code": "REGISTRATION_CONFIRMATION",
"name": "Registration Confirmation",
"subject": "Registration Confirmed for {{term_name}}",
"body_html": "<p>Dear {{student_name}},</p><p>Your registration has been confirmed.</p>",
"body_text": "Dear {{student_name}},\nYour registration has been confirmed."
}'Get Email Template by Code
GET /api/v1/core/email-templates/by-code/{code}Get an email template by its unique code.
Path Parameters
| Parameter | Type | Description |
|---|---|---|
code | string | The template code |
Example Request
curl -X GET http://localhost:8000/api/v1/core/email-templates/by-code/ADMISSION_ACCEPTED \
-b cookies.txtGet Email Template by ID
GET /api/v1/core/email-templates/{id}Get an email template by its ID.
Path Parameters
| Parameter | Type | Description |
|---|---|---|
id | integer | The template ID |
Example Request
curl -X GET http://localhost:8000/api/v1/core/email-templates/1 \
-b cookies.txtUpdate Email Template
PATCH /api/v1/core/email-templates/{id}Update an existing email template.
Path Parameters
| Parameter | Type | Description |
|---|---|---|
id | integer | The template ID |
Example Request
curl -X PATCH http://localhost:8000/api/v1/core/email-templates/1 \
-b cookies.txt \
-H "Content-Type: application/json" \
-d '{
"subject": "Updated Subject Line",
"body_html": "<p>Updated HTML content</p>"
}'Delete Email Template
DELETE /api/v1/core/email-templates/{id}Delete an email template.
Path Parameters
| Parameter | Type | Description |
|---|---|---|
id | integer | The template ID |
Example Request
curl -X DELETE http://localhost:8000/api/v1/core/email-templates/1 \
-b cookies.txtTemplate Bodies: Tables
The template editor has a table toolbar. When a template is used, its body is given fixed inline borders and padding on every table, th and td (email.InlineTableStyles), because mail clients ignore <style> blocks. A style the template already has on those tags is kept after the defaults, so it still wins.
Email Template Attachments
Fixed files sent with every use of a template — a form a drop notice refers to, a policy PDF with the probation alert (migration 305). All five routes are gated on ADMIN.EMAIL_TEMPLATES, like the rest of the template editor; no superuser is needed.
Why not /email-templates/{id}/attachments
Go's ServeMux refuses that pattern next to /email-templates/by-code/{code}: neither is more specific than the other. The template id therefore comes after attachments.
What a template may carry is three layers, intersected (types every layer allows, the smallest of each number):
- the organizational ceiling,
core.email_attachment_limits; - the
TEMPLATEsurface policy,core.email_attachment_policies— the most any template may allow, and the only source of allowed file types. Both are edited superuser-only through the Communications API; - the template's own allowance,
core.email_template_attachment_settings— on/off, file count, per-file and total MB. No row means no attachments.
Files are stored as bytea in core.email_template_attachments, which opts out of row auditing. They are checked when added and again at every send (email.FetchTemplateAttachments) against the limits as they stand then; files that no longer pass are left off that email with a logged warning, and the email still goes.
Get a Template's Attachments
GET /api/v1/core/email-templates/attachments/{id}{
"attachments": [
{ "attachment_id": 4, "filename": "drop-form.pdf", "content_type": "application/pdf", "size_bytes": 92160, "created_at": "2026-10-29T16:40:00Z" }
],
"settings": { "is_enabled": true, "max_files": 2, "max_file_mb": 2, "max_total_mb": 4 },
"available": { "is_enabled": true, "allowed_extensions": ["pdf"], "max_file_mb": 2, "max_files": 2, "max_total_mb": 4 },
"limits": { "is_enabled": true, "allowed_extensions": ["pdf"], "max_file_mb": 2, "max_files": 2, "max_total_mb": 4 }
}| Field | Description |
|---|---|
settings | The template's own allowance as saved. For a template never set up it is off, with the numbers prefilled to available |
available | The TEMPLATE surface inside the ceiling: the most this template may be set to. is_enabled: false means no template may take attachments |
limits | What actually applies: settings inside available |
An unknown template is 404.
Set the Allowance
PUT /api/v1/core/email-templates/attachments/{id}/settings{ "is_enabled": true, "max_files": 2, "max_file_mb": 2, "max_total_mb": 4 }Returns the same object as the GET. Turning attachments off is always accepted, and keeps the numbers already saved so that turning them back on restores them. Turning them on needs the TEMPLATE surface on (400 errors.attachmentsNotAllowed otherwise), and each number must fit inside available (per-field validation errors). Files already attached are kept even if they no longer fit; they are left off each email until the allowance covers them again.
Add Files
POST /api/v1/core/email-templates/attachments/{id}
Content-Type: multipart/form-dataEach file in a part named files. Files already on the template count toward the number and total size allowed. Checks are those of every attachment: allowed type, never a denied type (executables, macro Office files, HTML/SVG/XML, archives), bytes matching the extension, not empty, within the per-file and total limits. Errors are 400 with message_key errors.attachmentsNotAllowed, errors.tooManyAttachments, errors.fileTypeNotAllowed, errors.fileTooLarge, errors.attachmentsTooLarge, errors.attachmentEmpty or errors.attachmentContentMismatch. Returns 201 with the same object as the GET.
curl -X POST http://localhost:8000/api/v1/core/email-templates/attachments/12 \
-b cookies.txt \
-H "X-CSRF-Token: <csrf_token_from_cookie>" \
-F 'files=@drop-form.pdf'Download or Remove a File
GET /api/v1/core/email-templates/attachments/{id}/{attachmentID}
DELETE /api/v1/core/email-templates/attachments/{id}/{attachmentID}The download is sent with Content-Disposition: attachment, X-Content-Type-Options: nosniff and Cache-Control: private, no-store. The delete returns 204, and takes effect on the next email sent from the template. Both answer 404 when the file is not on that template.
Credit Conversions
Manage credit system conversion rules (e.g., semester-to-quarter, ECTS-to-semester) and perform credit conversions.
List Conversions
GET /api/v1/core/credit-conversionsGet all credit system conversion rules.
Example Request
curl -X GET http://localhost:8000/api/v1/core/credit-conversions \
-b cookies.txtSuccess Response (200 OK)
[
{
"conversion_id": 1,
"from_system_id": 1,
"from_system_code": "SEMESTER",
"from_system_display": "Semester Credits",
"to_system_id": 2,
"to_system_code": "QUARTER",
"to_system_display": "Quarter Credits",
"conversion_factor": "1.5",
"is_active": true
},
{
"conversion_id": 2,
"from_system_id": 3,
"from_system_code": "ECTS",
"from_system_display": "ECTS Credits",
"to_system_id": 1,
"to_system_code": "SEMESTER",
"to_system_display": "Semester Credits",
"conversion_factor": "0.5",
"is_active": true
}
]TypeScript Types
interface CreditConversion {
conversion_id: number
from_system_id: number
from_system_code: string
from_system_display: string
to_system_id: number
to_system_code: string
to_system_display: string
conversion_factor: string
is_active: boolean
}Create Conversion
POST /api/v1/core/credit-conversionsCreate a new credit conversion rule.
Request Body
| Field | Type | Required | Description |
|---|---|---|---|
from_system_id | integer | Yes | Source credit system lookup ID |
to_system_id | integer | Yes | Target credit system lookup ID |
conversion_factor | decimal | Yes | Multiplication factor |
is_active | boolean | No | Active status (default: true) |
Example Request
curl -X POST http://localhost:8000/api/v1/core/credit-conversions \
-b cookies.txt \
-H "Content-Type: application/json" \
-d '{
"from_system_id": 2,
"to_system_id": 1,
"conversion_factor": "0.667"
}'Success Response (201 Created)
Returns the created conversion DTO.
Update Conversion
PUT /api/v1/core/credit-conversions/{conversionID}Update a credit conversion rule.
Path Parameters
| Parameter | Type | Description |
|---|---|---|
conversionID | integer | The conversion rule ID |
Request Body
Same fields as Create.
Example Request
curl -X PUT http://localhost:8000/api/v1/core/credit-conversions/1 \
-b cookies.txt \
-H "Content-Type: application/json" \
-d '{
"from_system_id": 1,
"to_system_id": 2,
"conversion_factor": "1.5",
"is_active": true
}'Delete Conversion
DELETE /api/v1/core/credit-conversions/{conversionID}Delete a credit conversion rule.
Path Parameters
| Parameter | Type | Description |
|---|---|---|
conversionID | integer | The conversion rule ID |
Example Request
curl -X DELETE http://localhost:8000/api/v1/core/credit-conversions/1 \
-b cookies.txtSuccess Response (204 No Content)
Convert Credits
POST /api/v1/core/credit-conversions/convertConvert a credit value from one system to another.
Request Body
| Field | Type | Required | Description |
|---|---|---|---|
credits | decimal | Yes | Number of credits to convert |
from_system | string | Yes | Source system code (e.g., SEMESTER) |
to_system | string | Yes | Target system code (e.g., QUARTER) |
Example Request
curl -X POST http://localhost:8000/api/v1/core/credit-conversions/convert \
-b cookies.txt \
-H "Content-Type: application/json" \
-d '{
"credits": "3.0",
"from_system": "SEMESTER",
"to_system": "QUARTER"
}'Success Response (200 OK)
{
"original_credits": "3.0",
"converted_credits": "4.5",
"from_system": "SEMESTER",
"to_system": "QUARTER",
"conversion_factor": "1.5"
}TypeScript Types
interface CreditConvertRequest {
credits: string
from_system: string
to_system: string
}
interface CreditConvertResponse {
original_credits: string
converted_credits: string
from_system: string
to_system: string
conversion_factor: string
}Audit Trail
The federated audit trail behind Administration → Audit Logs, gated on ADMIN.AUDIT. It merges three independent logs into one list: core.audit_log (DB_CHANGE, generic row changes on every table auditing is enabled for), core.permission_audit_log (PERMISSION) and records.grade_change_log (GRADE_CHANGE).
List Entries
GET /api/v1/core/audit-trailQuery Parameters
| Parameter | Description |
|---|---|
page, page_size | Pagination |
log_source | DB_CHANGE, PERMISSION or GRADE_CHANGE |
table_name | Real schema.table for DB_CHANGE; entity type for PERMISSION. GET /audit-trail/categories lists the values that have rows |
field_name | Only rows where this column's value changed. Excludes GRADE_CHANGE rows |
action | INSERT, UPDATE or DELETE (INSERT matches CREATE in the PERMISSION source) |
person_id | The person who made the change |
actor_source | WEB, AUTH, SYSTEM, PUBLIC_FORM, COLLEAGUE_SYNC, ... PERMISSION rows have none, so setting it excludes them |
search | Partial match on table/entity name and record id |
date_from, date_to | Date range |
Success Response (200 OK)
{
"entries": [
{
"log_source": "DB_CHANGE",
"log_id": 918273,
"table_name": "core.auth_users",
"record_id": "412",
"action": "UPDATE",
"changed_fields": ["last_login"],
"changed_by_person_id": "550e8400-e29b-41d4-a716-446655440000",
"changed_by_name": "Jane Doe",
"changed_at": "2026-10-26T14:03:11Z",
"ip_address": "10.0.50.17",
"client_hostname": "lab-14.example.edu",
"user_agent": "Mozilla/5.0 ...",
"actor_source": "AUTH"
}
],
"total_count": 1,
"page": 1,
"page_size": 25
}log_id is the log row's own key in its source table, not the changed record's id (record_id). ip_address and user_agent are recorded on every audited web write; client_hostname (migration 264) is resolved by reverse DNS on the pre-session AUTH routes only, so a successful sign-in — recorded as the UPDATE of core.auth_users.last_login — carries one. client_hostname and user_agent exist only on DB_CHANGE rows. Rows written by background jobs and CLI tools have no client fields. Failed sign-ins write no row.
Delete Entries
DELETE /audit-trail takes { "entries": [{ "log_source", "log_id" }, ...] }, at most 500 per call; DELETE /audit-trail/bulk deletes everything matching the same filters as the list. Each deletion is itself recorded, without the original payload, in core.audit_trail_deletion_log, which no endpoint can delete from.
Health Check
GET /api/v1/core/healthReturns the health status of the core service.
Example Request
curl -X GET http://localhost:8000/api/v1/core/health \
-b cookies.txtSuccess Response (200 OK)
{
"status": "healthy",
"service": "core"
}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/core. 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 | /audit-config | ADMIN.AUDIT_CONFIG | Get audit config |
| POST | /audit-config/apply | ADMIN.AUDIT_CONFIG | Apply |
| PUT | /audit-config/master | ADMIN.AUDIT_CONFIG | Replace master |
| PUT | /audit-config/schema-defaults | ADMIN.AUDIT_CONFIG | Replace schema defaults |
| PUT | /audit-config/tables | ADMIN.AUDIT_CONFIG | Replace tables |
| DELETE | /audit-trail | ADMIN.AUDIT | Delete audit trail |
| GET | /audit-trail | ADMIN.AUDIT | Get audit trail |
| DELETE | /audit-trail/bulk | ADMIN.AUDIT | Delete bulk |
| GET | /audit-trail/categories | ADMIN.AUDIT | List categories |
| GET | /countries | selfservice | List countries |
| POST | /countries | ADMIN.COUNTRIES | Create a country |
| GET | /countries/dropdown | selfservice | List options for a picker |
| DELETE | /countries/{id} | ADMIN.COUNTRIES | Delete a country |
| GET | /countries/{id} | selfservice | Get one country |
| PATCH | /countries/{id} | ADMIN.COUNTRIES | Update a country |
| GET | /credit-conversions | ACADEMIC.CREDIT_CONVERSIONS | List credit conversions |
| POST | /credit-conversions | ACADEMIC.CREDIT_CONVERSIONS | Create a credit conversion |
| POST | /credit-conversions/convert | ACADEMIC.CREDIT_CONVERSIONS | Create convert |
| DELETE | /credit-conversions/{conversionID} | ACADEMIC.CREDIT_CONVERSIONS | Delete a credit conversion |
| PUT | /credit-conversions/{conversionID} | ACADEMIC.CREDIT_CONVERSIONS | Replace a credit conversion |
| GET | /currencies | selfservice | List currencies |
| POST | /currencies | ADMIN.CURRENCIES | Create a currency |
| DELETE | /currencies/{id} | ADMIN.CURRENCIES | Delete a currency |
| GET | /currencies/{id} | selfservice | Get one currency |
| PATCH | /currencies/{id} | ADMIN.CURRENCIES | Update a currency |
| GET | /demo-accounts | public | The demo's published logins; an empty list unless DEMO_MODE=true |
| GET | /email-templates | ADMIN.EMAIL_TEMPLATES | List email templates |
| POST | /email-templates | ADMIN.EMAIL_TEMPLATES | Create a email template |
| GET | /email-templates/by-code/{code} | ADMIN.EMAIL_TEMPLATES | Get one by code |
| DELETE | /email-templates/{id} | ADMIN.EMAIL_TEMPLATES | Delete a email template |
| GET | /email-templates/{id} | ADMIN.EMAIL_TEMPLATES | Get one email template |
| PATCH | /email-templates/{id} | ADMIN.EMAIL_TEMPLATES | Update a email template |
| GET | /email-templates/attachments/{id} | ADMIN.EMAIL_TEMPLATES | Get a template's attachments and limits |
| POST | /email-templates/attachments/{id} | ADMIN.EMAIL_TEMPLATES | Add files to a template |
| PUT | /email-templates/attachments/{id}/settings | ADMIN.EMAIL_TEMPLATES | Set a template's attachment allowance |
| DELETE | /email-templates/attachments/{id}/{attachmentID} | ADMIN.EMAIL_TEMPLATES | Remove a template attachment |
| GET | /email-templates/attachments/{id}/{attachmentID} | ADMIN.EMAIL_TEMPLATES | Download a template attachment |
| GET | /filter-presets | selfservice | List filter presets |
| POST | /filter-presets | selfservice | Create a filter preset |
| DELETE | /filter-presets/{presetID} | selfservice | Delete a filter preset |
| PATCH | /filter-presets/{presetID} | selfservice | Update a filter preset |
| GET | /health | public | Get health |
| GET | /instance | public | Write mode, environment and whether demo logins are published |
| GET | /institution-config | ADMIN.BRANDING | Get institution config |
| PUT | /institution-config | ADMIN.BRANDING | Replace institution config |
| GET | /lookup-types | selfservice | List lookup types |
| GET | /lookup-types/by-code/{code} | selfservice | Get one by code |
| POST | /lookup-values | ADMIN.LOOKUPS | Create a lookup value |
| GET | /lookup-values/by-type/{typeCode} | selfservice | Get one by type |
| DELETE | /lookup-values/{typeCode}/{id} | ADMIN.LOOKUPS | Delete a |
| PATCH | /lookup-values/{typeCode}/{id} | ADMIN.LOOKUPS | Update a |
| GET | /lookups | selfservice | List lookups |
| GET | /lookups/by-type/{typeCode} | selfservice | Get one by type |
| GET | /settings | selfservice | List settings |
| POST | /settings | ADMIN.SYSTEM_SETTINGS | Create a setting |
| GET | /settings/branding | public | Get branding |
| PUT | /settings/branding | ADMIN.BRANDING | Replace branding |
| GET | /settings/branding/full | selfservice | Get full |
| PATCH | /settings/bulk_update | ADMIN.SYSTEM_SETTINGS | Bulk update |
| POST | /settings/bulk_update | ADMIN.SYSTEM_SETTINGS | Bulk update |
| GET | /settings/by_category | selfservice | Get by category |
| GET | /settings/by_key | selfservice | Get by key |
| PATCH | /settings/by_key | ADMIN.SYSTEM_SETTINGS | Update by key |
| POST | /settings/recompute-statuses | ADMIN.SYSTEM_SETTINGS | Recompute statuses |
| GET | /settings/{id} | selfservice | Get one setting |
| PATCH | /settings/{id} | ADMIN.SYSTEM_SETTINGS | Update a setting |
| PUT | /settings/{key} | ADMIN.SYSTEM_SETTINGS | Replace a setting |
| GET | /state-provinces/dropdown | selfservice | List state/province options for a picker |
| GET | /ui-text/catalog | ADMIN.UI_TEXT | The shipped UI strings, per locale |
| GET | /ui-text/draft | ADMIN.UI_TEXT | The unpublished overrides |
| PUT | /ui-text/draft | ADMIN.UI_TEXT | Save overrides to the draft |
| POST | /ui-text/draft/discard | ADMIN.UI_TEXT | Discard the draft |
| GET | /ui-text/export | ADMIN.UI_TEXT | Export the overrides |
| GET | /ui-text/live | public | The published overrides (the login page renders them before any session) |
| POST | /ui-text/publish | ADMIN.UI_TEXT | Publish the draft |