Skip to content

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 whose Id isn'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).
  • ✅ Substitutions returns 403 for parent/student accounts. In a timetable week, substitutions only show up as IsSubstitutionClass on lessons.
  • ✅ Calendars/Substitutions (root Substitutions, 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 has Id, OrgDate, OrgLessonNo (a string), OrgTeacher/OrgSubject (refs with Id), OrgClassroomSymbol, ClassroomSymbol, IsCancelled, IsShifted. A cancelled lesson has only the Org* side (12 of 31); a substitution also has Date, LessonNo, Teacher, Subject - the lesson that took its place. IsShifted was never true there. Calendars/Substitutions/<Id> returns one entry under Substitution. ?dateFrom=&dateTo= is a 400 - the list can't be narrowed. Parsed by parse_plan_changes into PlanChangeData (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 with Id), plus SubstitutionNote (null so far) and SubstitutionClassUrl. Comparing Classroom with OrgClassroom shows a room change; OrgSubject/OrgTeacher say what and who was replaced. Parsed into LessonData.original / LessonData.room_changed.
  • ✅ TimetableEntries (root TimetableEntries) is the standing weekly plan, not dated weeks: Id, Lesson.Id, DayOfTheWeek (1 = Monday), LessonNo, DateFrom/DateTo, Classroom with Id/Symbol/Name inline (60 entries on a tested class). ✅ Each entry is valid only between its DateFrom and DateTo: 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). ✅ Every Lesson.Id was one of the student's own Lessons (no other groups' lessons), and once filtered by date the plan matched the real Timetables weeks 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=false answers {"data": [...]} (extracurricular activities); empty on the tested account.
  • ✅ Realizations = the lessons held, with their topics (168 entries a month into the year): Id (a t-prefixed string), Lesson.Id, LessonNo, Date, Topic, IsTrip, CountInStatistics, CountInRPN, AddedBy.Id. The JSON counterpart of the zrealizowane_lekcje web page. Also Realizations/TypesOfDays (Dzień powszedni, Święto), /TypesOfClasses, /ThematicTeaching, /FilledByTeacher.
  • ✅ PlannedLessons is reachable and empty on the tested account.
  • ✅ Timetables?day=YYYY-MM-DD returns 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.