Data endpoints¶
GET https://synergia.librus.pl/gateway/api/2.0/<Endpoint>
These requests are authenticated only by the session cookies from
the login flow. Send no Authorization header.
Responses are JSON with one root key named after the resource, often
alongside Resources/Url metadata that can be ignored.
Two things apply to every endpoint:
- Id types are inconsistent. Most
Ids are ints, but Timetable lesson ids are strings ("41999"), SchoolNotices ids are strings ("LID-NBOARD-NOTICE-...") and some Attendance ids are prefixed strings ("t41685"). Normalize before you compare them. - References are objects, not ids. A grade's subject arrives as
{"Id": 42, "Url": ".../Subjects/42"}. Fetch the lookup endpoints (Subjects, Users, Classrooms, categories) once a day and join on your side. A record whoseIdisn't a usable number (other than the known string ids above) is skipped by the parsers.
✅ No conditional requests: Grades, Attendances, Me, Timetables
and SchoolNotices send no ETag and no Last-Modified, and every data
response carries Cache-Control: no-store (checked live 2026-10-12). Each
poll downloads the full list.
Identity and school¶
| Endpoint | Root key | Notes |
|---|---|---|
Me |
Me |
✅ Me.Account is the login's owner (the parent, on a parent login); Me.User is the student. Use User for the child's name. |
Schools |
School |
✅ Name, town, street, BuildingNumber, PostCode, Email, PhoneNumber, NameHeadTeacher/SurnameHeadTeacher. |
Classes |
Class |
✅ Number+Symbol (e.g. 7+"d"), ClassTutor.Id, BeginSchoolYear, EndFirstSemester, EndSchoolYear. |
Units |
— | ✅ School configuration: GradesSettings.{Standard,Point,Descriptive}GradesEnabled, bell schedule (LessonsRange), behaviour-points settings. |
VirtualClasses |
VirtualClasses |
✅ Reachable, empty on tested accounts. |
Users/{Me.Account.UserId} |
User |
✅ The student's own user record (live 2026-10-07): Id, AccountId (a LID-AUTH-USER-...), AccountNumericIdentifier, FirstName, LastName, Class.Id/UUID, Unit.Id, ClassRegisterNumber (the class register number), IsEmployee, GroupId. Users/{Me.Account.Id} is 404 - Account.Id is the parent's login account. |
Auth/TokenInfo, Auth/UserInfo/<lid> |
— | Identity of the token's user; used to find the child on kindergarten accounts. ✅ On a school parent account (2026-10-09): TokenInfo = SchoolId, UserIdentifier (a LID), UserType, SchoolNodeName, Scopes (47 strings); UserInfo/<UserIdentifier> = UserIdentifier, UserNumericIdentifier, AccountNumericIdentifier, SchoolId, UserType, UserSchoolClassId, IdentifierOfStudentAssignedWithUser (the child's LID), IdentifierOfClassOfStudentAssignedWithUser, UserState, KindergartenGraduationYear, Login, Gender. |
Auth/Subjects |
data |
✅ Subjects keyed by LID (77 on the tested school): identifier (LID), numericIdentifier (the id Subjects uses), name, type, extracurricular, schemaNumber. Maps the LID subject ids of the new descriptive grading (below) to ordinary ones. |
POST Auth/DescriptiveGradingSystem/PartialGrades/Student/<child LID> (body {}) |
data |
✅ Reachable for a grade 7 parent account (2026-10-09): {"data": [], "pagination": {"limit", "page", "total"}}. The new descriptive grading some schools use for grade 1 from 2026 - separate from DescriptiveGrades; the grade page in Synergia stays empty and loads these. 📖 An item: gradeId, studentId, teacherId, addedBy (LIDs), area (id, name, color), subjectId (a LID, see Auth/Subjects), lessonAssignmentId, scaleValue (id, value), content, comments, implementedRequirements (id, name), date, semester, addDate. GET Auth/DescriptiveGradingSystem/PartialGrades is 405 and lists .../LessonAssignments. Parsed by parse_partial_grades into DescriptiveGradeData (source="partial", id "p<gradeId>", area.name as the skill, scaleValue.value as the grade - a 0 stays "0", content + comments as comments - one comment given on its own, not in a list, is read too; a subjectId that isn't a known LID but a number is taken as the ordinary subject id). Librus.partial_grades() remembers a 403/404/405 (or a 401 Insufficient scopes) as "module not available" for a day and only then POSTs again, and keeps the Auth/Subjects lookup for a day; Librus.student_identifier() keeps the child's LID (or a definite "none") once Librus answered, remembers a refusal of the lookup (403/404/405, 401 Insufficient scopes) for a day, and tries again after any other failure. A bool given as subjectId or scaleValue.value is ignored. 📖 Not seen on a real grade: whether comments are strings or objects, whether teacherId or addedBy is the teacher, and how pagination continues past the first page (only the first page is read). |
UserProfile |
UserProfile |
✅ ClassNumber (e.g. 7), AccountType ("parent"), Town, State, UnitType ("Szkoła podstawowa"). |
Root |
Resources |
✅ An index of every module, with its URL. On a tested parent account it listed (among others) Realizations, SchoolTrips, SchoolFiles, TimetableEntries, BaseTextGrades, Calendars, Colors, Surveys, SpecialAchievement, EbiblioLendings, StudentInsurances, PushChanges, PushDevices, SilentNight, NotificationCenterDeferrals - being listed doesn't mean readable (see the 403/404s below). ✅ The full list on a tested parent account (2026-10-09, 89 names) also has AdditionalModulesAccess, BehaviourDescriptiveGrades, ChangeRegister, DeclarationOfAvailability, Duties, Educators, Employees, ExamResult, GradingSystem, Indywidualni, Messages, Motions, PersonalDataAccesses, PlannedLessons, PushConfigurations, Reports, SchoolInfo, StudentDuty, SystemData, WhatsNew and a few app-only ones (BannerAds, PremiumAccount*, WcagSettings, ...). Me also carries Refresh: 900 and lists Me/PeriodicGradeAverages, Me/BehaviourDescriptiveGrades. |
web page synergia.librus.pl/informacja |
— | ✅ Not part of the API: an HTML page with the student's details, including a <th>Nr w dzienniku</th><td>25</td> row. Only a fallback for the class register number - the JSON Users/{Me.Account.UserId}.ClassRegisterNumber above is the source. Opens with the same cookie session as the API (confirmed live 2026-10-07; a dead session redirects to the login page). Parsed by parse_student_number. |
Other modules, checked 2026-10-09 (parent account)¶
| Endpoint | Result |
|---|---|
AdditionalModulesAccess |
✅ 200 {"Data": {"module", "access"}} |
SystemData |
✅ 200 {"Date", "Time"} - the server's date and time |
PushConfigurations |
✅ 200 {"version", "settings": {...}} - which changes the app pushes (grades, attendances, calendars, notes, homeWorks, notices, plannedLessons, realizations, ... each add/edit/delete) |
Auth/Photos |
✅ 200 {"data": {"status", "photo", "awaitingPhoto"}} (no photo on the tested account) |
Calendars/SchoolFreeDays, Calendars/ClassFreeDays |
✅ 200, the same data as SchoolFreeDays / ClassFreeDays (with AddDate) |
Educators |
✅ 405 - only lists Educators/EducatorCalendarTasks |
ChangeRegister |
✅ 400 InvalidRequest - needs parameters (not known) |
SchoolInfo, Duties, WhatsNew |
✅ 401 Insufficient scopes - not a dead session; the client raises LibrusUnexpectedResponseError(status_code=401) for it (only on the data gateway, and only when the body could be read), so nothing logs in again |
StudentDuty |
✅ 403 Account not have access to resource |
ExamResult, DescriptiveLessonGrades |
✅ 404 |
EbiblioLendings |
✅ 200 (2026-10-12) - books lent to the student by the school library: {"lastSynchronizationTimestamp": <epoch s>, "lendings": [...]}, each lending identifier (a UUID), type ("BOOK"), title, author, lendedAt, dueDate, isExtendable. On the tested account all 12 were the year's free textbooks, lent at the start of September with a dueDate two weeks later that had long passed and isExtendable: false - so for textbooks dueDate is not a real return date; don't treat a past one as overdue. Parsed by parse_library_lendings into LibraryLendingData (Librus.library_lendings(), most recently lent first). |
SpecialAchievement |
✅ 404, but it lists SpecialAchievement/Types: 200 {"data": [{"id", "name"}]} - the school's categories of special achievements (sports competitions, volunteering, subject olympiads, ...). .../Student, .../Students, .../Achievements are 404, so the student's own achievements weren't found in the API. |
Calendars/Stats |
✅ 200 {"Stats": [{"Id", "MinDate", "MaxDate"}]} - the class calendar's id and the school year's first and last day. Calendars/<that Id> lists ids (no content) of SchoolFreeDays, TeacherFreeDays, HomeWorks, ... for the whole year. |
Calendars/Substitutions |
✅ 200 - see Timetable. |
PointGrades/Comments, DescriptiveGrades/Text/Categories |
✅ 200, empty (Comments / Categories) on the tested account. |
PlannedLessons/ThematicTeaching |
✅ 200 {"Realizations": []} |
FileTransferRestrictions |
✅ 200 {"Restrictions": {"upload", "upload_3g"}} |
Regulations |
✅ 200 {"Data": [{"module", "acceptanceDate", "url"}]} - when the terms of use were accepted |
Help |
✅ 200 {"Help": [...]} - Synergia's help articles (HTML Description, visibility per role) |
DeclarationOfAvailability |
✅ 404 "Could not find any valid declaration to display." |
PushChanges |
✅ 400 FilterInvalidValue - needs a registered push device |
PushDevices |
✅ 400 InvalidRequest |
Employees, PersonalDataAccesses, ReceivingPush, UserAppActivity, Motions/PaymentDeclaration, Motions/File |
✅ 405 (Employees lists Employees/Substitutions, itself 404) |
StudentInsurances, NotificationCenterDeferrals, SilentNight, SchoolNicknames, Educators/EducatorCalendarTasks (+/Types), Motions (+/Activities, /Available, /Charges) |
✅ 403 |
SchoolReceiveMessage, SchoolAgreement, IdwUser, OsinInfo |
✅ 401 Insufficient scopes |
Surveys/Details, ClientVersion, Auth/AutoLoginToken |
✅ 404 |
OneRoster |
✅ 500 with an empty HTML body |
Lookups (cache ~24 h)¶
| Endpoint | Root key | Notes |
|---|---|---|
Subjects |
Subjects |
✅ Id, Name, Short. |
Users |
Users |
✅ The teachers lookup. ✅ FirstName can be null (present but null, e.g. for secretariat accounts), so dict.get("FirstName", "") is not enough. |
Classrooms |
Classrooms |
✅ Id, Name, Symbol. |
Lessons |
Lessons |
✅ lesson id → {Subject, Teacher, Class}. The only way to map Attendances[].Lesson.Id to a subject (Attendances carry no subject). |
Grades/Categories |
Categories |
✅ Name, Weight, CountToTheAverage. The parser reads Weight as a number or a numeric string ("2", "1,5"); anything else (or none) is weight 1. |
Grades/Types |
— | ✅ The complete list of values Grade can take (see below). |
Attendances/Types |
Types |
✅ Note: not top-level AttendanceTypes, which returns 404. |
HomeWorks/Categories |
Categories |
✅ Agenda categories: Sprawdzian, Kartkówka, Wycieczka, Zebranie z rodzicami, ... (per school). |
Notes/Categories |
Categories |
✅ Uses CategoryName, not Name. |
BehaviourGrades/Points/Categories |
Categories |
✅ |
Grades¶
| Endpoint | Root key | Notes |
|---|---|---|
Grades |
Grades |
✅ Grade (string; parse_grade_value reads only the 1-6 scale and ignores anything outside it, e.g. a stray "85"), Subject.Id, Category.Id, Semester, AddDate, IsSemesterProposition, IsFinalProposition, IsSemester, IsFinal, Comments (❓ the four semester/final flags have only ever been false: no proposed or final grade issued yet). ✅ AddedBy.Id is the teacher who added the grade (a Users id): on a real account all 15 grades resolved to the subject's own teacher. 📖 Improvement.Id on a correction ("poprawa") points at the earlier grade it improves; the earlier grade stays in the list. |
Grades/Comments |
Comments |
✅ [{"Id", "Text"}]. Grades[].Comments is a list of ids into this endpoint, not embedded text. ✅ Real teacher comments on a real account resolve this way. ✅ Each list item is an {"Id": <int>} object (checked live 2026-10-09 on 21 commented grades); the parser still accepts a bare id too. Librus.grades() fetches this list only when some grade has a comment id, then keeps it: it is fetched again only when a grade refers to a comment id it hasn't asked about, or after a day (the same goes for the behaviour and descriptive comment lists). fetch_changes() never fetches it. |
DescriptiveGrades |
Grades |
✅ Grades in skills-based subjects (seen live 2026-10-09: music in grade 1 of a primary school). The shown grade is Map ("6", same as RealGradeValue) - Grade holds something else (3 for that "6"), so don't read it as the grade. ❓ Grade is almost certainly the grade's range on the descriptive scale (GradeRange in DescriptiveGrades/Types: 1 for 1-2, 2 for 3-4, 3 for 5-6): the "6" with Grade: 3 matches that, but the scale was read from another school than the grade. Also Subject.Id, Lesson.Id, Student.Id, Skill.Id (into DescriptiveGrades/Skills), AddedBy.Id (the teacher), Date, AddDate, Semester, Comments (a list of {"Id"} into DescriptiveGrades/Comments). They don't count towards the average. Only when Units enables them. |
DescriptiveGrades/Skills |
Skills |
✅ Every skill of the whole school (~1100 entries, ~330 KB; ✅ an empty list at a school with descriptive grades switched on but none given): Id, Name ("Ekspresja muzyczna. Śpiew" - Synergia shows it as the grade's "Kategoria"), Subject.Id, Weight, CountToTheAverage (false), Color.Id, sometimes Teacher.Id. Fetch it rarely: Librus.descriptive_grades() keeps it and asks again only for a skill id it doesn't have, or after a day. |
DescriptiveGrades/Comments |
Comments |
✅ The teacher's comments (seen live 2026-10-09): Id, Text, AddedBy.Id, Grade.Id (the descriptive grade) - same shape as Grades/Comments. |
DescriptiveGrades/Types |
Types |
✅ The descriptive grade scale (seen live 2026-10-09, 29 entries): Value (the grade as shown: "6", "5+", "bz", ...), Name (a letter for 1-6 - W for 6, B 5, D 4, S 3, M 2, N 1 - or the full word for a status mark such as "brak zadania"), EditOnly (bool), GradeRange (1 for 1-2, 2 for 3-4, 3 for 5-6; null for +, -, 0 and the status marks). |
DescriptiveGrades/Phrases |
Phrases |
✅ The school's bank of ready-made comment phrases (6403 entries on the tested school): Id, Content, ClassLevel, GradeRange (1-3, as in Types), Skill.Id. Not tied to a student. |
DescriptiveGrades/SubjectCompetences |
SubjectCompetences |
✅ Id, Name (3 entries on the tested school). |
DescriptiveGrades/Text |
- | ✅ 404 NotFound, although DescriptiveGrades lists it in Resources. |
PointGrades |
Grades |
✅ Reachable. ✅ Seen with real data on a user's account (a school grading some subjects in points, reported 2026-10-13): points, maximum and percentage come out right (e.g. 9.5/10 → 95 %). Fields: Grade (the text shown), GradeValue (the points), Category.Id, Subject.Id, Semester, AddDate, AddedBy.Id. The maximum lives on the category. Parsed by parse_point_grades; point_grades_percentage gives the weighted earned/possible percentage. Librus.point_grades() doesn't ask at all when Units says PointGradesEnabled: false, and fetches the categories only when there are grades. |
PointGrades/Categories |
Categories |
✅ Name, Weight, CountToTheAverage, ValueFrom, ValueTo (the maximum points) - the maximum shows correctly on a user's account. |
TextGrades |
— | ✅ Reachable, empty on tested accounts. |
BaseTextGrades |
Grades |
✅ Text grades that Grades doesn't contain (one real entry, 2026-09-18): Grade (free text; ✅ can contain the teacher's line breaks and indentation, e.g. "...sesja I\n 80%" - parse_text_grades collapses the whitespace), Subject.Id, Lesson.Id, Category.Id (into TextGrades/Categories), AddedBy.Id, Student.Id, Date, AddDate, Semester, ShowInGradesView. szkolny-android reads its "descriptive grades" from here. |
TextGrades/Categories |
Categories |
✅ Name, Short, Color.Id (into Colors), Weight, CountToTheAverage, Standard, IsReadOnly, BlockAnyGrades, ObligationToPerform (71 on a tested school). |
Colors |
Colors |
✅ Id, RGB ("F0E68C"), Name ("khaki") - the colours category Color.Ids point at. |
DescriptiveTextGrades, DescriptiveTextGrades/Skills, Grades/Scales |
Grades / Skills / Scales |
✅ Reachable, empty on the tested account. |
Grades/CategoriesAverages |
— | ✅ HTTP 403 for a parent account. |
Grades/Averages, PointGrades/Averages |
— | ✅ Reachable, but on the tested school both answer only {"Status": "Disabled"} - the school has its averages switched off. ❓ The shape when enabled is unknown. |
BehaviourGrades |
Grades |
✅ Reachable, empty on the tested account (its behaviour grade is in BehaviourGrades/Points). |
BehaviourGrades/Types |
Types |
✅ Id (string "1".."6"), Name ("wzorowe".."naganne"), Shortcut ("wz".."ng") - the names behind BehaviourGrade.Id. |
BehaviourGrades/Points/Comments |
Comments |
✅ [{"Id", "Text"}], same shape as Grades/Comments; the live monthly grade's comment ("Ocena zachowania miesiąc za IX/26") resolved this way. A behaviour grade's Comments ids point here. |
BehaviourGrades/SystemProposal |
— | ✅ HTTP 403 for a parent account on the tested school (a proposed behaviour grade, per third-party OpenAPI notes). |
BehaviourGrades/Points |
Grades |
The formal behaviour grade ("ocena zachowania"). Points schools use Value/ShortName. ✅ A classic-scale grade comes with ShortName/Text empty and no Value (seen live 2026-10-05 on a monthly grade, "Ocena zachowania miesiąc za IX/26" in Comments); ✅ the grade itself is BehaviourGrade.Id (1 wz, 2 bdb, 3 db, 4 popr, 5 ndp, 6 ng; names in BehaviourGrades/Types) - the live monthly grade was a "bdb" read this way. Also Category, Semester, AddDate, Comments (ids into BehaviourGrades/Points/Comments). |
Grade values¶
✅ Grades/Types lists every possible value: the numbers 1–6, each
optionally with +/-, plus non-numeric marks that must be excluded
from averages: bz (brak zadania), np (nieprzygotowany), nk
(nieklasyfikowany), uł/nł, zl/nz, zw (zwolniony), uc/nu, and
a bare +/-.
✅ The API gives no numeric value for a modified grade, only the
string "4+". ✅ The + counts as +0.5 and the - as −0.25:
a real 4+ showed as 4.5, and a subject with a real 6 and 4- showed
an average of 4.88 ((6 + 3.75) / 2) in Librus's own app. See
parse_grade_value.
✅ These are the school's settings, readable from GradingSystem
(checked 2026-10-09): {"countZero": false, "plusValue": 0.5, "minusValue":
0.25} on the tested school - the same as the values above. Another school
can set them differently, and countZero says whether a 0 counts towards
the average. parse_grading_system reads it and parse_grade_value(value,
grading) uses it (0.5 / 0.25 without it). Librus.grading_system() reads
it once per instance; when the school refuses it (403, 404, 405 or a 401
Insufficient scopes) it returns those defaults and asks again only after
a day. A value sent as a string
("0.5") is read too, and both are taken as sizes (abs). "0+" and
"0-" are never a grade. 📖 A school with other values hasn't been seen
yet.
✅ Also: Me/PeriodicGradeAverages answers {"annual", "firstSemester",
"secondSemester"} (all null at a school with averages switched off) and
Me/BehaviourDescriptiveGrades {"data": []}; BehaviourDescriptiveGrades
is 404, Reports and Indywidualni 401 (Insufficient scopes - raised as
LibrusUnexpectedResponseError, not as an expired session),
PersonalDataAccesses 405.
✅ Values seen on a real account so far: 2, 4, 5, 6, 4+, 5+,
4-, 6-, and a bare + filed under an "aktywność" (class activity)
category. That bare + is a mark, not a grade: a subject whose only entry
is a + has no average.
Behaviour notes (uwagi)¶
| Endpoint | Root key | Notes |
|---|---|---|
Notes |
Notes |
📖 Text, Category.Id, Teacher.Id, Date, Positive: 0 = negative, 1 = positive, 2 = neutral. |
Attendance¶
| Endpoint | Root key | Notes |
|---|---|---|
Attendances |
Attendances |
✅ Lesson.Id, LessonNo, Date, Semester, Type.Id. ✅ Most records are ordinary presence marks, so do not count records as absences. ✅ Id can be a string like "t41685". ✅ On a real account, all 151 records' Lesson.Ids resolved against Lessons. |
✅ Attendances/Types on a real account:
| Id | Name | IsPresenceKind |
|---|---|---|
| 1 | Nieobecność | false |
| 2 | Spóźnienie | true (late, but present) |
| 3 | Nieobecność uspr. | false |
| 4 | Zwolnienie | true |
| 100 | Obecność | true |
| 1685 | Pobyt w sanatorium | true |
✅ Schools add their own types on top of the standard ones (the
sanatorium stay above has a school-specific id). Don't hard-code ids;
always read IsPresenceKind from this endpoint.
There is no "excused" flag. Excused absences can only be recognized by
uspr. in the type name.
| Endpoint | Root key | Notes |
|---|---|---|
Justifications |
data |
✅ The absence justifications the parent submitted, with a lowercase JSON envelope unlike the rest of the API: {"status": "OK", "message": ..., "data": [...]}. Each item: id (int), messageFromParent, postDate ("YYYY-MM-DD HH:MM:SS"), justificationStatus (✅ "accept" seen; other values not seen yet), dateFrom, dateTo, lessons ([{"number", "date"}], can be empty), justifiedAbsences (int), attachment (bool), notifiedTeachers ([{"name"}]). Parsed by parse_justifications (JustificationData, newest first); justified_dates lists the days covered by a justification that wasn't rejected. |
| (submitting a justification) | — | ❌ Not possible with a Synergia login (checked 2026-10-15, read-only): Synergia's own /eusprawiedliwienia page answers "Brak dostępu" with a "Loguj" button leading to /loguj/przenies/eusprawiedliwienia, which redirects (302) to portal.librus.pl - e-Usprawiedliwienia live in the parent's Konto LIBRUS (portal / Librus app), not in Synergia. The justifications listed by Justifications were sent from there. Submitting would need the Konto LIBRUS login (email + password) and its own API, never probed. |
✅ Attendance can be missing for whole subjects. On one real account a subject taught twice a week had only 2 records after a month, both absences, while every other subject had a record for each lesson. Some teachers apparently don't take attendance in Librus. A per-subject percentage computed from a handful of records says nothing useful.
Timetable¶
GET Timetables?weekStart=YYYY-MM-DD (a Monday)
✅ The shape is date → list of period slots → list of lessons. It is not a flat list per day:
{"Timetable": {
"2026-09-01": [
[],
[{"LessonNo": "2", "HourFrom": "08:55", "HourTo": "09:40",
"Subject": {"Id": "41999"}, "Teacher": {"Id": "1001"}, "Classroom": {"Id": "12"},
"IsCanceled": false, "IsSubstitutionClass": false}],
[{...group A...}, {...group B...}]
]
}}
- Every day has the same number of slots. Empty slots are
[]. - A slot can hold several lessons when a class is split into groups (for example two language groups at the same time).
- Ids inside lessons are strings.
- ✅ HTTP 403 here can mean the school hasn't published the timetable yet, which is not a session problem (see errors).
- ✅
Substitutionsreturns 403 for parent/student accounts. In a timetable week, substitutions only show up asIsSubstitutionClasson lessons. - ✅
Calendars/Substitutions(rootSubstitutions, checked live 2026-10-12 on a parent account) is readable, though: every change to the class's plan since the start of the school year, not just the two weeks a timetable fetch covers (31 entries a month and a half in, the oldest from mid-September). Each entry hasId,OrgDate,OrgLessonNo(a string),OrgTeacher/OrgSubject(refs withId),OrgClassroomSymbol,ClassroomSymbol,IsCancelled,IsShifted. A cancelled lesson has only theOrg*side (12 of 31); a substitution also hasDate,LessonNo,Teacher,Subject- the lesson that took its place.IsShiftedwas nevertruethere.Calendars/Substitutions/<Id>returns one entry underSubstitution.?dateFrom=&dateTo=is a 400 - the list can't be narrowed. Parsed byparse_plan_changesintoPlanChangeData(kind=cancelled/substitution/shifted, oldest first;Librus.plan_changes()). - ✅ A substitution lesson often has no classroom, even when the regular lesson in that slot has one.
- ✅ A substitution lesson also carries the original lesson:
OrgDate,OrgLessonNo,OrgHourFrom,OrgHourTo,OrgSubject,OrgTeacher,OrgClassroom(refs withId), plusSubstitutionNote(null so far) andSubstitutionClassUrl. ComparingClassroomwithOrgClassroomshows a room change;OrgSubject/OrgTeachersay what and who was replaced. Parsed intoLessonData.original/LessonData.room_changed. - ✅
TimetableEntries(rootTimetableEntries) is the standing weekly plan, not dated weeks:Id,Lesson.Id,DayOfTheWeek(1 = Monday),LessonNo,DateFrom/DateTo,ClassroomwithId/Symbol/Nameinline (60 entries on a tested class). ✅ Each entry is valid only between itsDateFromandDateTo: when the plan changes during the year, the old and the new version of a slot are both listed (on the tested class, ranges such as 1-2 Sept, 3-6 Sept, from 7 Sept, and from late June). ✅ EveryLesson.Idwas one of the student's ownLessons(no other groups' lessons), and once filtered by date the plan matched the realTimetablesweeks slot for slot, room included; the only differences were cancelled lessons.parse_timetable_entries/plan_differences(live check 2026-10-08). - ✅
Timetables/OtherActivitiesRegister?dateFrom=&dateTo=&hideOutdatedEntries=falseanswers{"data": [...]}(extracurricular activities); empty on the tested account. - ✅
Realizations= the lessons held, with their topics (168 entries a month into the year):Id(at-prefixed string),Lesson.Id,LessonNo,Date,Topic,IsTrip,CountInStatistics,CountInRPN,AddedBy.Id. The JSON counterpart of thezrealizowane_lekcjeweb page. AlsoRealizations/TypesOfDays(Dzień powszedni,Święto),/TypesOfClasses,/ThematicTeaching,/FilledByTeacher. - ✅
PlannedLessonsis reachable and empty on the tested account. - ✅
Timetables?day=YYYY-MM-DDreturns just that one day (weekStart=returns 7). Not used by the library. - ✅ Other lesson fields seen:
Lesson,Class,DateFrom,DateTo,DayNo,TimetableEntry,VirtualClass/VirtualClassName(on lessons for a virtual class / group).
Agenda, homework, free days¶
| Endpoint | Root key | Notes |
|---|---|---|
HomeWorks |
HomeWorks |
✅ The agenda (terminarz), despite the name: tests, trips, parent meetings. Category.Id, Subject.Id, Date, TimeFrom, Content. Some teachers file a quiz under the "Inne" category and say "kartkówka" only in Content, so match both fields. ✅ School-wide entries (parent meetings, assemblies) have no subject. |
HomeWorkAssignments |
HomeWorkAssignments |
✅ Real homework: Topic, Text, Teacher.Id, Date, DueDate, Lesson.Id, Category.Id (only sometimes), MustSendAttachFile, SendFilePossible, AddedFiles, HomeworkAssigmentFiles (sic, a list - empty on all 7 real assignments), StudentsWhoRead/StudentsWhoMarkedAsDone. The response lists a HomeWorkAssignments/Attachment resource. ✅ Each HomeworkAssigmentFiles item is {"Id", "Name", "Url"}; Url points at HomeWorkAssignments/Attachment/<homework id>-<file id> (✅ through the gateway it answers {"Id", "Name", "DownloadUrl"}, with the file id alone it's a 400). Parsed by parse_homework_attachments. ✅ Download (live 2026-10-09): https://synergia.librus.pl/homework/downloadFile/<file id> redirects (302) to sandbox.librus.pl/index.php?action=CSTryToDownload&singleUseKey=<key>, an HTML page whose script POSTs action=CSCheckKey with singleUseKey in the form body every 5-10 s (not_downloaded_yet, then ready, or download_failed), then GETs action=CSDownload&singleUseKey=<key>, which returns the file. download_homework_attachment does the same (a real JPEG came back in about 4 s). ✅ The first download of a file can stay not_downloaded_yet for more than 30 s (a retry minutes later was quick), so the library waits up to about two minutes, as Librus's own page does (it offers to give up only after 15 checks). ✅ Now and then a key gets download_failed (or an answer that isn't JSON) at once, while a fresh key for the same file a moment later works - seen when the same file was being fetched from another session at about the same time - so the library retries with up to three keys. No Subject field. ✅ The subject can be recovered from the teacher's lessons in Timetables when that teacher teaches only one subject (all 7 real assignments on a tested account resolved this way). ✅ The web session (DZIENNIKSID) behind homework/downloadFile can expire while the API session still works; the page then answers 200 without a redirect (seen live 2026-10-09, with the logged-out "Brak dostępu" page) - the library reports any such page as an expired session so a fresh login fixes it (a download is started by a person, so a wrong guess costs one login), unless the page plainly says the file isn't there ("nie znaleziono", "nie istnieje", "not found") and isn't the logged-out page: that one is LibrusUnexpectedResponseError. The page is read in the charset it names. Only an https:// redirect to sandbox.librus.pl itself (compared by scheme and host) is followed; a redirect that isn't a valid URL is LibrusUnexpectedResponseError. The whole download, every key included, gives up after 150 s (DOWNLOAD_TIMEOUT_SECONDS) with LibrusConnectionError, so the three keys only help when a key fails quickly. |
SchoolFreeDays |
SchoolFreeDays |
✅ Name, DateFrom, DateTo. |
Calendars |
Calendars |
✅ The class calendars ([{"Id": "<class id>"}]). |
Calendars/{classId}?year=&month= |
Calendar |
✅ One month's ids of HomeWorks, Substitutions, ParentTeacherConferences, SchoolFreeDays, ClassFreeDays, TeacherFreeDays (refs only, no content), with Pages.Prev/Next. A cheap way to narrow the agenda by month. |
Calendars/ClassFreeDays/Types |
Types |
✅ Id + Name of class free-day types - the names of ClassFreeDays[].Type (parse_free_day_types; Librus.free_days() asks for it only when an entry of the student's class has a type and no name). E.g. "Wycieczka", "Próbny egzamin ósmoklasisty."). |
Calendars/TeacherFreeDays |
— | ✅ HTTP 403 for a parent account (like TeacherFreeDays). |
HomeWorkAssignments/Categories |
Categories |
✅ CategoryName and Teacher.Id - each teacher's own homework categories (96 on a tested school). |
SchoolTrips |
Data |
✅ School trips of the class (camelCase): id, destination, route, locomotion, termFrom, termTo, creatorName/creatorLastName, coordinatorName. |
SchoolFiles |
Data |
✅ Documents the school shares with parents: id, displayName, addedOnDate, downloadUrl (a Synergia web path, /pliki_szkoly/pobierz/<id>), iconUrl, fileStatus. ✅ downloadUrl (/pliki_szkoly/pobierz/<id>) redirects to a sandbox.librus.pl/GetFile/... link with the Synergia session (checked 2026-10-09) - async_download_school_file; without a logged-in session it shows "Brak dostępu" (reported as an expired session). The path must resolve to https://synergia.librus.pl/... - anything else (or a path that isn't a valid URL) is LibrusUnexpectedResponseError before any request, so the session cookies never go to another host. |
Surveys |
— | ✅ HTTP 404 on the tested account (lists Surveys/Details). |
ClassFreeDays |
ClassFreeDays |
✅ The free days of every class in the school, not only the student's (seen on a user's account 2026-10-13: 48 entries, two of them the student's class). Each entry: Id, Class.Id, Type.Id (name in Calendars/ClassFreeDays/Types; no Name of its own), DateFrom, DateTo; some also LessonNoFrom/LessonNoTo (a part of the day). Keep only the entries whose Class.Id is the student's class (Classes → Class.Id, ClassData.id) - Librus.free_days() does, parse_free_days(..., class_id=) can. Such an entry has no Name: it's named after its type (FreeDayData.name, type_id). LessonNoFrom/LessonNoTo (values not seen yet, read as lesson numbers) free only those lessons: FreeDayData.lesson_no_from/to, whole_day False. |
ParentTeacherConferences |
ParentTeacherConferences |
✅ Id, Topic, Teacher.Id (the class tutor on the tested account), Date, Time ("17:00:00"). ✅ The same meeting also appears in HomeWorks, under a "Zebranie z rodzicami" category, with the same date and TimeFrom but different wording. Merging both sources gives duplicates, so match on date and time. |
✅ NotificationCenter (listed in third-party OpenAPI notes) answers with
no JSON at all on Gateway 2.0 - it most likely lives on the newer
api.librus.pl/3.0 API, which this library doesn't use.
Announcements and lucky number¶
| Endpoint | Root key | Notes |
|---|---|---|
SchoolNotices |
SchoolNotices |
✅ Subject, Content (full text, not truncated), StartDate, EndDate, CreationDate, WasRead. ✅ Id is a string. |
SchoolNotices/MarkAsRead/<id> |
— | 📖 POST marks one notice read (WasRead turns true), like opening it in Librus. The answer's body isn't used. ✅ For a notice that is already read it answers HTTP 400 {"Code": "InvalidRequest", "Message": "[212] Invalid request params"} (seen 2026-10-13) - the library counts that as done. Marking an unread one not yet seen live. async_mark_school_notice_read / Librus.mark_announcement_read. |
LuckyNumbers |
LuckyNumber |
✅ {"LuckyNumber": {"LuckyNumber": 13, "LuckyNumberDay": "2026-09-08"}}. ✅ The number for the next school day can appear a day early, so check LuckyNumberDay. ✅ ?forDay=YYYY-MM-DD answers 403 on a parent account. |
The student's own class-register number is in the JSON API: ✅
Users/{Me.Account.UserId}.ClassRegisterNumber (see
Identity and school). Librus.student_number() reads
it from there (the informacja web page is only a fallback), so "is it my
number?" needs no input from the user.