The Schulportal Hessen API is a FastAPI wrapper around the SchulportalHessenAPI class that provides HTTP endpoints to interact with the Schulportal Hessen (Hessian School Portal) system. It supports multiple concurrent users with session-based authentication and a token-based system.
Base URL: http://localhost:8000 (when running locally)
Run locally:
uvicorn api:app --reloadThe API uses token-based session management to support multiple concurrent users:
- Session Creation: On successful login, a unique session token is generated
- Session Duration: Sessions expire after 60 minutes of inactivity (TTL: 3600 seconds)
- Token Usage: Include the token in the
X-Session-Tokenheader for all authenticated requests - Session Cleanup: Sessions are automatically purged on expiration or can be manually terminated via logout
All authenticated requests require the following header:
X-Session-Token: {token}
Authenticate a user and create a new session.
Request Body:
{
"school_id": "1234",
"username": "john.doe",
"password": "your_password"
}Parameters:
| Parameter | Type | Description |
|---|---|---|
school_id |
string | Schul-ID (e.g., 1234) |
username |
string | Username without school prefix |
password |
string | User password |
Response:
{
"token": "abc123def456...",
"school_id": "1234",
"username": "john.doe",
"encryption_ready": true
}Response Fields:
| Field | Type | Description |
|---|---|---|
token |
string | Session token to use for subsequent requests |
school_id |
string | School ID of the logged-in user |
username |
string | Username of the logged-in user |
encryption_ready |
boolean | Indicates if encryption is ready for the session |
Status Codes:
200 OK- Login successful401 Unauthorized- Invalid credentials
Terminate the current session.
Headers:
X-Session-Token: {token}(required)
Response:
{
"status": "logged_out"
}Status Codes:
200 OK- Logout successful401 Unauthorized- Invalid or expired session token
These endpoints require a valid X-Session-Token from /login. DSBmobile uses separate
credentials, so you pass them in the request body.
Login to DSBmobile and establish a session for substitution plan access.
Headers:
X-Session-Token: {token}(required)
Request Body:
{
"username": "{dsb_username}",
"password": "{dsb_password}"
}Response:
{
"success": true,
"session_cookie": "{dsb_cookie}",
"session_id": "{aspnet_session_id}",
"response_url": "https://www.dsbmobile.de/default.aspx"
}Status Codes:
200 OK- Login attempt completed401 Unauthorized- Invalid or expired session token
Fetch available substitution plan iframe URLs after login.
Headers:
X-Session-Token: {token}(required)
Request Body:
{
"username": "{dsb_username}",
"password": "{dsb_password}"
}Response:
{
"success": true,
"plan_urls": ["{plan_url}"],
"count": 1
}Fetch and parse the substitution plan table from a plan URL.
Headers:
X-Session-Token: {token}(required)
Request Body:
{
"username": "{dsb_username}",
"password": "{dsb_password}",
"plan_index": 0,
"plan_url": "{plan_url}",
"include_raw": false
}Response:
{
"success": true,
"plan_url": "{plan_url}",
"title": "{plan_title}",
"last_updated": "{timestamp}",
"raw_html": null,
"tables": [
{
"caption": "{caption}",
"headers": ["{header}"],
"rows": [
{"{header}": "{value}"}
]
}
]
}Check the health status of the API.
Response:
{
"status": "ok"
}Status Codes:
200 OK- API is operational
The school list endpoints provide access to public data about schools in Hesse. These endpoints do not require authentication.
Retrieve all schools organized by district/region.
Response:
{
"success": true,
"districts": [
{
"id": "7",
"name": "{region_name}",
"schools": [
{
"id": "3354",
"name": "{school_name}",
"location": "{city_name}"
},
"..."
]
},
"..."
]
}Response Fields:
| Field | Type | Description |
|---|---|---|
success |
boolean | Indicates if the request was successful |
districts |
array | List of all districts with their schools |
districts[].id |
string | District ID |
districts[].name |
string | District name (e.g., "Bergstraße/Odenwaldkreis") |
districts[].schools |
array | List of schools in the district |
districts[].schools[].id |
string | School ID |
districts[].schools[].name |
string | School name |
districts[].schools[].location |
string | City/location of the school |
Status Codes:
200 OK- School list retrieved successfully500 Internal Server Error- Failed to fetch or parse school list
Retrieve schools for a specific district by ID.
Path Parameters:
| Parameter | Type | Description |
|---|---|---|
district_id |
string | The district ID (e.g., "7") |
Response:
{
"success": true,
"district": {
"id": "7",
"name": "{region_name}",
"schools": [
{
"id": "3354",
"name": "{school_name}",
"location": "{city_name}"
},
"..."
]
}
}Status Codes:
200 OK- District schools retrieved successfully500 Internal Server Error- Failed to fetch or parse school list
Search for schools by name across all districts (case-insensitive).
Query Parameters:
| Parameter | Type | Required | Description |
|---|---|---|---|
q |
string | Yes | School name or partial name to search for |
Response:
{
"success": true,
"query": "{search_term}",
"count": 3,
"results": [
{
"district_id": "7",
"district_name": "{region_name}",
"school": {
"id": "3351",
"name": "{school_name}",
"location": "{city_name}"
}
},
"..."
]
}Response Fields:
| Field | Type | Description |
|---|---|---|
success |
boolean | Indicates if the request was successful |
query |
string | The search term used |
count |
integer | Number of matching schools |
results |
array | List of matching schools with their district info |
Status Codes:
200 OK- Search completed successfully500 Internal Server Error- Failed to fetch or parse school list
Retrieve the currently logged-in user's profile information.
Headers:
X-Session-Token: {token}(required)
Response:
{
"success": true,
"data": {
"{username}": "...",
"{firstname}": "...",
"{lastname}": "...",
"{email}": "...",
"..."
}
}Status Codes:
200 OK- User data retrieved successfully401 Unauthorized- Invalid or expired session token
Retrieve all available modules for the current user.
Headers:
X-Session-Token: {token}(required)
Response:
{
"success": true,
"modules": [
{
"name": "{module_name}",
"url": "{module_url}",
"color": "{color_code}",
"logo": "{logo_class}",
"folders": ["{folder_name}"],
"target": "{target}",
"usable": true,
"usage": ["{method_name}"]
},
"..."
]
}Response Fields:
| Field | Type | Description |
|---|---|---|
success |
boolean | Indicates if the request was successful |
modules |
array | List of available modules |
modules[].name |
string | Name of the module |
modules[].url |
string | URL to access the module |
modules[].color |
string | Color code for the module |
modules[].logo |
string | CSS class for the module icon |
modules[].folders |
array | List of folders the module belongs to |
modules[].target |
string | Link target (_blank or _self) |
modules[].usable |
boolean | Whether the module is supported by this package |
modules[].usage |
array | List of API method names for this module |
Status Codes:
200 OK- Modules retrieved successfully401 Unauthorized- Invalid or expired session token
Retrieve all available apps for the current user.
Headers:
X-Session-Token: {token}(required)
Response:
{
"success": true,
"data": {
"error": "0",
"folders": [
{
"name": "{folder_name}",
"logo": "{logo_class}",
"farbe": "{color_code}"
},
"..."
],
"entrys": [
{
"Name": "{app_name}",
"Farbe": "{color_code}",
"Logo": "{logo_class}",
"Ordner": ["{folder_name}"],
"link": "{app_link}",
"target": "{target}"
},
"..."
],
"till": {timestamp}
}
}Response Fields:
| Field | Type | Description |
|---|---|---|
success |
boolean | Indicates if the request was successful |
data |
object | Container for app data |
data.error |
string | Error code (0 for success) |
data.folders |
array | List of available folders |
data.folders[].name |
string | Name of the folder |
data.folders[].logo |
string | CSS class for the folder icon |
data.folders[].farbe |
string | Color code for the folder |
data.entrys |
array | List of available apps/entries |
data.entrys[].Name |
string | Name of the app |
data.entrys[].Farbe |
string | Color code for the app |
data.entrys[].Logo |
string | CSS class for the app icon |
data.entrys[].Ordner |
array | List of folders the app belongs to |
data.entrys[].link |
string | Link to the app |
data.entrys[].target |
string | Link target (_blank or _self) |
data.till |
integer | Timestamp until the data is valid |
Status Codes:
200 OK- Apps retrieved successfully401 Unauthorized- Invalid or expired session token
Retrieve message headers/list of conversations.
Headers:
X-Session-Token: {token}(required)
Query Parameters:
| Parameter | Type | Default | Description |
|---|---|---|---|
get_type |
string | "All" | Filter type: "All", "Unread", "Sent", etc. |
last |
integer | 0 | ID of the last message to start pagination from |
Response:
{
"success": true,
"total": 40,
"conversations": [
{
"id": "{conversation_id}",
"sender": "{sender_username}",
"subject": "{subject}",
"date": "{date}",
"unread": 0,
"read": true,
"..."
},
"..."
]
}Notes:
- Each conversation includes
unreadfrom the portal (0/1). If missing, the API setsunreadto 0. - The API adds
readas a derived boolean (read = !unread).
Status Codes:
200 OK- Message headers retrieved successfully401 Unauthorized- Invalid or expired session token
Retrieve a specific conversation with all messages.
Headers:
X-Session-Token: {token}(required)
Path Parameters:
| Parameter | Type | Description |
|---|---|---|
conversation_id |
string | The ID of the conversation |
Query Parameters:
| Parameter | Type | Default | Description |
|---|---|---|---|
last |
integer | 0 | ID of the last message to start pagination from |
Response:
{
"success": true,
"conversation_id": "{conversation_id}",
"messages": [
{
"id": "{message_id}",
"sender": "{sender_username}",
"content": "{message_content}",
"date": "{date}",
"..."
},
"..."
]
}Status Codes:
200 OK- Conversation retrieved successfully401 Unauthorized- Invalid or expired session token404 Not Found- Conversation not found
Search for message recipients.
Headers:
X-Session-Token: {token}(required)
Query Parameters:
| Parameter | Type | Description |
|---|---|---|
q |
string | Search query (name, username, etc.) |
Response:
{
"success": true,
"results": [
{
"id": "{user_id}",
"name": "{full_name}",
"username": "{username}",
"type": "{user_type}",
"..."
},
"..."
]
}Status Codes:
200 OK- Search results retrieved successfully401 Unauthorized- Invalid or expired session token
Send a new message.
Headers:
X-Session-Token: {token}(required)
Request Body:
{
"recipients": ["{user_id_1}", "{user_id_2}"],
"subject": "{message_subject}",
"body": "{message_body}"
}Response:
{
"success": true,
"message_id": "{message_id}"
}Status Codes:
200 OK- Message sent successfully400 Bad Request- Invalid message format401 Unauthorized- Invalid or expired session token
Send a reply to an existing conversation.
Headers:
X-Session-Token: {token}(required)
Request Body:
{
"conversation_id": "{conversation_id}",
"body": "{reply_body}",
"to": "all"
}Response:
{
"success": true,
"details": {
"back": true,
"id": "{message_id}"
}
}Status Codes:
200 OK- Reply sent successfully400 Bad Request- Invalid reply payload401 Unauthorized- Invalid or expired session token
Retrieve an overview of all courses.
Headers:
X-Session-Token: {token}(required)
Response:
{
"success": true,
"courses": [
{
"id": "{course_id}",
"name": "{course_name}",
"teacher": "{teacher_name}",
"entries_count": 5,
"..."
},
"..."
]
}Status Codes:
200 OK- Course overview retrieved successfully401 Unauthorized- Invalid or expired session token
Retrieve detailed information about a specific course.
Headers:
X-Session-Token: {token}(required)
Path Parameters:
| Parameter | Type | Description |
|---|---|---|
course_id |
string | The ID of the course |
Response:
{
"success": true,
"course_id": "{course_id}",
"course_name": "{course_name}",
"semester": "{semester}",
"teacher_short": "{teacher_short}",
"teacher_full": "{teacher_full}",
"entry_count": 0,
"entries": [
{
"entry_id": "{entry_id}",
"date": "",
"hours": "{hours}",
"thema": "{thema}",
"homework": "{homework}",
"homework_done": false,
"attendance": "{attendance}",
"content": "{detail_content}",
"files": [
{
"name": "{file_name}",
"size": "{size}",
"url": "{file_url}",
"download_url": "{download_url}"
}
]
}
],
"exams": [],
"marks": [],
"attendance_summary": []
}Status Codes:
200 OK- Course details retrieved successfully401 Unauthorized- Invalid or expired session token404 Not Found- Course not found
Retrieve detailed information about a specific course entry.
Headers:
X-Session-Token: {token}(required)
Query Parameters:
| Parameter | Type | Description |
|---|---|---|
url |
string | The URL of the entry to fetch details for |
Response:
{
"success": true,
"entry": {
"id": "{entry_id}",
"title": "{entry_title}",
"content": "{entry_content}",
"date": "{date}",
"attachments": [
{
"name": "{file_name}",
"url": "{file_url}",
"..."
},
"..."
]
}
}Status Codes:
200 OK- Entry details retrieved successfully401 Unauthorized- Invalid or expired session token404 Not Found- Entry not found
Retrieve a weekly view of course entries.
Headers:
X-Session-Token: {token}(required)
Response:
{
"success": true,
"week": {
"start_date": "{date}",
"entries": [
{
"date": "{date}",
"course": "{course_name}",
"entry": "{entry_title}",
"url": "{entry_url}",
"..."
},
"..."
]
}
}Status Codes:
200 OK- Weekly view retrieved successfully401 Unauthorized- Invalid or expired session token
Retrieve all submissions/tasks that need attention.
Headers:
X-Session-Token: {token}(required)
Response:
{
"success": true,
"submissions": [
{
"id": "{submission_id}",
"title": "{submission_title}",
"course": "{course_name}",
"due_date": "{date}",
"status": "{status}",
"url": "{submission_url}",
"..."
},
"..."
]
}Status Codes:
200 OK- Submissions retrieved successfully401 Unauthorized- Invalid or expired session token
Retrieve the calendar overview page metadata for the current user.
Headers:
X-Session-Token: {token}(required)
Response:
{
"success": true,
"page_title": "Kalender",
"calendar": {
"first_id": "{calendar_view_id}",
"new_events_count": "{count}",
"can_write": false,
"key": "{calendar_key}",
"public_view": false,
"institution": "{school_id}",
"is_admin": false
},
"categories": [
{
"id": 20,
"name": "Sonstige Termine",
"color": "#2e2e2e",
"logo": ""
}
],
"groups": [],
"export_links": [
{
"label": "als PDF",
"url": "kalender.php?a=export..."
}
]
}Status Codes:
200 OK- Calendar overview retrieved successfully401 Unauthorized- Invalid or expired session token
Retrieve calendar events using the same filter contract as the web UI.
Headers:
X-Session-Token: {token}(required)
Query Parameters:
year-0for the current school year,1for the next school yearstart- Calendar start mode, defaultyearcategory- Filter by category idsearch- Search text for title, location, or descriptiontarget- Zielgruppe filterview_id- Selected calendar view id
Response:
{
"success": true,
"events": [
{
"id": "{event_id}",
"title": "{event_title}",
"category": 20,
"description": "{description}",
"start": {"date": "2026-04-29 08:00:00"},
"end": {"date": "2026-04-29 09:30:00"},
"all_day": false,
"new": "",
"editable": false,
"properties": {}
}
],
"count": 1,
"filters": {
"year": 0,
"start": "year",
"category": "",
"search": "",
"target": "",
"view_id": "{calendar_view_id}"
}
}Status Codes:
200 OK- Calendar events retrieved successfully401 Unauthorized- Invalid or expired session token
Retrieve the full payload for a single calendar event.
Headers:
X-Session-Token: {token}(required)
Query Parameters:
view_id- Selected calendar view id
Response:
{
"success": true,
"event": {
"id": "{event_id}",
"title": "{event_title}",
"...": "..."
},
"filters": {
"event_id": "{event_id}",
"view_id": "{calendar_view_id}"
}
}Status Codes:
200 OK- Calendar event retrieved successfully401 Unauthorized- Invalid or expired session token
Retrieve the substitution plan from Schulportal Hessen (vertretungsplan.php).
Headers:
X-Session-Token: {token}(required)
Query Parameters:
| Parameter | Type | Default | Description |
|---|---|---|---|
include_raw |
boolean | false | Include raw HTML in the response |
Response:
{
"success": true,
"mode": "ajax",
"last_updated": "{timestamp}",
"days": [
{
"date": "{date}",
"substitutions": [
{"fach": "{subject}", "klasse": "{class}"}
]
}
],
"count": 1
}Status Codes:
200 OK- Substitution plan retrieved successfully401 Unauthorized- Invalid or expired session token
Retrieve the timetable from stundenplan.php (all and personal views).
The response also includes exams, a list of dated exam objects from /lerngruppen (same schema). Exams are kept separate from recurring lesson templates; clients match their ISO date to the displayed day, including rolling views. The backend reuses the user’s Lerngruppen cache. If exam loading fails, lessons remain available with exams: [] and an exams_error message. Incomplete timetable responses and failed Lerngruppen responses are not cached, so a retry can recover.
Headers:
X-Session-Token: {token}(required)
Response:
{
"success": true,
"week_start": "2026-08-24",
"days": ["Montag", "Dienstag", "Mittwoch", "Donnerstag", "Freitag"],
"plan_for_all": [[{
"name": "{subject}",
"room": "{room}",
"course_id": "{matching_meinunterricht_book_id}",
"course_name": "{matching_course_name}",
"homework": [{
"entry_id": "{homework_entry_id}",
"text": "{homework_text}",
"done": false,
"assigned_date": "2026-08-11"
}]
}]],
"plan_for_own": [[{"name": "{subject}", "course_id": "{course_id}"}]],
"template_plan_for_all": [[{"name": "{subject}"}]],
"template_plan_for_own": [[{"name": "{subject}"}]],
"hours": [{"label": "1", "start_time": {"hour": 8, "minute": 0}}],
"week_badge": "{week}"
}Timetable lessons are matched to Mein Unterricht courses using the teacher
abbreviation and subject name together. The abbreviation is read from the
dedicated teacher field or a trailing parenthesized Kürzel in the full teacher
name. Short timetable subject codes are matched against aliases derived from
the words in each course name (prefixes and initials); there is no fixed subject
mapping table. Matching lessons include course_id and course_name. A
homework item is included only on the first active A/B-week lesson after the
Unterricht entry date; completed homework remains present with done: true.
Enrichment is automatic for all requests. If the course overview cannot be
loaded, the regular timetable is returned without the optional course and
homework fields.
week_start anchors week_badge to a calendar week. The two template_plan
fields contain the enriched recurring timetable before date-specific custom
lesson overrides are merged. Clients can use them to project the correct A/B
lessons onto rolling or future date ranges; the legacy plan_for_all and
plan_for_own fields continue to include overrides for the anchored week.
Status Codes:
200 OK- Timetable retrieved successfully401 Unauthorized- Invalid or expired session token
Retrieve files and folders from the dateispeicher.
Headers:
X-Session-Token: {token}(required)
Query Parameters:
| Parameter | Type | Default | Description |
|---|---|---|---|
folder_id |
integer | 0 | Folder id to fetch |
Response:
{
"success": true,
"folder_id": 0,
"files": [{"id": 1, "name": "{file}"}],
"folders": [{"id": 2, "name": "{folder}"}]
}Search files in the dateispeicher.
Headers:
X-Session-Token: {token}(required)
Query Parameters:
| Parameter | Type | Description |
|---|---|---|
q |
string | Search query |
Response:
{
"success": true,
"query": "{query}",
"results": []
}Download a Dateispeicher file through the authenticated backend session.
Headers:
X-Session-Token: {token}(required)
Path Parameters:
| Parameter | Type | Description |
|---|---|---|
file_id |
integer | File id returned by /dateispeicher or /dateispeicher/search |
Response: The file body is streamed with its upstream content type and a
Content-Disposition: attachment filename header.
Retrieve study groups and exam data from lerngruppen.php.
Headers:
X-Session-Token: {token}(required)
Response:
{
"success": true,
"groups": [{"id": "{group_id}", "course_name": "{course}"}],
"exams": [{"id": "{exam_id}", "date": "{date}"}]
}All error responses follow this format:
{
"detail": "Error message describing what went wrong"
}| Code | Description |
|---|---|
200 OK |
Request successful |
400 Bad Request |
Invalid request parameters or body |
401 Unauthorized |
Invalid, expired, or missing session token |
404 Not Found |
Requested resource not found |
500 Internal Server Error |
Server error |
# Step 1: Login
curl -X POST http://localhost:8000/login \
-H "Content-Type: application/json" \
-d '{
"school_id": "1234",
"username": "john.doe",
"password": "password123"
}'
# Response:
# {
# "token": "abc123def456...",
# "school_id": "1234",
# "username": "john.doe",
# "encryption_ready": true
# }
# Step 2: Fetch message headers using the token
curl -X GET "http://localhost:8000/nachrichten/headers?get_type=All" \
-H "X-Session-Token: abc123def456..."
# Step 3: Logout
curl -X POST http://localhost:8000/logout \
-H "X-Session-Token: abc123def456..."# Get all courses
curl -X GET http://localhost:8000/meinunterricht \
-H "X-Session-Token: {token}"
# Get a specific course
curl -X GET http://localhost:8000/meinunterricht/course/12345 \
-H "X-Session-Token: {token}"GET /cache/status requires X-Session-Token but does not contact
Schulportal. Its response has this shape:
{
"available": true,
"last_successful_fetch_at": "2026-09-13T08:15:00Z",
"snapshot_count": 7,
"retention_seconds": 86400
}Selected successful JSON read endpoints retain account-isolated snapshots for
up to 24 hours. A snapshot is returned only after the current request positively
observes an upstream timeout, connection failure, or HTTP 5xx response. Cached
fallback responses carry X-LANIS-Cache: stale and
X-LANIS-Fetched-At: <original UTC timestamp>; these headers are exposed over
CORS. fresh identifies a live upstream response and hit an ordinary API
cache hit. Authentication, authorization, and TLS failures are returned as-is.
Writes invalidate snapshots derived from the affected data, logout removes all
snapshots for the account, and a backend restart clears the in-memory store.
- Session tokens expire after 60 minutes of inactivity
- All timestamps are in UTC format
- Response data is anonymized in this documentation; placeholders like
{username},{course_name}represent actual values - Multiple concurrent users are supported through session isolation
- The API automatically cleans up expired sessions
GET /status requires no session token or administrator privileges. It reads the
existing Schulportal synthetic monitor's stored results; requests never run a
probe or log in. The sanitized response is shared for up to 30 seconds (less when
a current result is about to become stale). generated_at is the snapshot time.
current:status(up,degraded,down,unknown), UTCchecked_at(nullable),stale, andfeatureswith allowlistedlogin/modulesnames andup/down/unknownstatus. Results older than twice the configured check interval, absent checks, and a disabled/unconfigured monitor showunknown.stalespecifically describes the age or absence of the latest observation.summary: rollingperiod_days: 90,checks,available_checks,failed_checks(including degraded results),unknown_checks,uptime_percent(nullable), andcoverage_percent.daily: the same counts/percentages per UTCdaywith a dailystatus. Days without known observations remainunknown. The rolling 90×24-hour window intersects up to 91 calendar dates; the first and last dates are partial days.history: at most 100 recent observations (checked_at,status,features).incidents: at most 100 recent failed/degraded observations, with the same safe shape. These are individual failed checks, not inferred continuous outages.measurement:interval_seconds,stale_after_seconds, UTCperiod_startandperiod_end, and a human-readable description of the methodology.
Availability is successful checks divided by known checks. Missing or unknown checks are excluded from that percentage, not counted as successes. Coverage is the percentage of interval-sized slots containing at least one known result; duplicated/manual checks in the same slot do not increase coverage. Slots are anchored at the start of the requested period (or clipped UTC day). The currently configured interval is used as the expected cadence throughout the window, so changing it affects the coverage estimate. Partial coverage does not mean all unobserved time was healthy; show coverage alongside the availability percentage.
The existing monitor authenticates with one configured account and opens that
account's available modules. Its result does not establish availability for all
schools, users, or module operations. No credentials, account/module names, raw
errors, target URLs, or private diagnostics are published. Monitor credentials
and cadence retain their existing LANIS_UPTIME_* configuration; no additional
production setup is performed by this endpoint. /health reports only current
LANIS API reachability. LANIS's historical availability is not measured here.