Skip to content

Errors and status codes

Status Where Meaning What to do Exception
401 any data endpoint ✅ The session died, often before the expected ~24 h. The password is still fine. Log in again once and retry. Librus does this automatically - except in the kindergarten search, which never logs in again (a 401 from the kindergarten service counts there as "not a kindergarten account"; one on the main gateway is left to the next ordinary call). LibrusSessionExpiredError(status_code=401)
401 + Insufficient scopes SchoolInfo, Duties, WhatsNew, Reports, ... ✅ The account may not use this endpoint at all. Not a dead session - logging in again doesn't help. Only recognised on the data gateway (/gateway/api/2.0/); a 401 anywhere else, or one whose body can't be read, stays an expired session. Treat as not available; don't log in again. Librus remembers such a refusal for a day where it matters (child LID, new descriptive grading, GradingSystem). LibrusUnexpectedResponseError(status_code=401)
403 Timetables ✅ The school hasn't published this class's timetable yet (Synergia's own web UI says "nie został jeszcze opublikowany"), or it is a kindergarten account. Logging in again does not help. Treat as an empty timetable. LibrusSessionExpiredError(status_code=403)
403 other endpoints ✅ The module is not available to this account type (e.g. Substitutions, TeacherFreeDays, or Attendances/Types on a messages-only preschool login). Treat as empty. same
404 AttendanceTypes, some mailboxes Wrong path, or the mailbox doesn't exist for this account. — LibrusUnexpectedResponseError
503 any ✅ Maintenance. Seen on Me too. Windows observed so far were short: the next poll 20 minutes later worked. Retry later. LibrusServerMaintenanceError(status_code=503)
502, 504 any ❓ Not seen live yet: a gateway in front of Librus failed or timed out. Retry later. LibrusConnectionError(status_code=502 / 504)
429 any ❓ Not seen live yet: too many requests. Slow down, retry later. LibrusConnectionError(status_code=429)
— any A network error, a DNS failure or a timeout (by default 30 s for a whole data or login request, 10 s to connect). Retry later. LibrusConnectionError (status_code=None)
200 + HTML any The response isn't JSON (often a login page). — LibrusUnexpectedResponseError

The client reads small error bodies (up to 64 KB) before raising, so the connection can be reused. 429, 502, 503 and 504 are never treated as an expired session: Librus doesn't log in again for them.

File downloads (homework attachments, school documents, message attachments):

Case Meaning Exception
The Synergia page answers 200 instead of redirecting ✅ Seen live as the logged-out page ("Brak dostępu"): the web session died while the API session still works. A fresh login fixes it, so any such page counts as an expired session - a download is started by a person, so a wrong guess costs one login. LibrusSessionExpiredError(status_code=200)
The page answers 200 and plainly says the file isn't there ("nie znaleziono", "nie istnieje", "not found") and isn't the logged-out page Not a session problem. The page is read in the charset it names. LibrusUnexpectedResponseError
The page answers anything else without a redirect Not a session problem. LibrusUnexpectedResponseError
The page redirects anywhere but sandbox.librus.pl Synergia sends a dead session to its login page. LibrusSessionExpiredError
The page redirects to sandbox.librus.pl over plain http:// Not followed (the cookies would travel unencrypted). LibrusUnexpectedResponseError
The redirect (or a message attachment's downloadLink) isn't a valid URL Not followed. LibrusUnexpectedResponseError
A message attachment's downloadLink isn't https://sandbox.librus.pl/... Not followed. LibrusUnexpectedResponseError
A school document path that isn't on https://synergia.librus.pl (or isn't a valid URL) Refused before any request, so the session cookies never go elsewhere. LibrusUnexpectedResponseError
The whole download took longer than 150 s (DOWNLOAD_TIMEOUT_SECONDS) The sandbox is too slow; try again later. With Librus, the deadline covers the whole call, logins and a retry included. LibrusConnectionError ("wasn't downloaded within ...")
aiohttp's own timeout fired first The connection stalled. LibrusConnectionError ("the connection timed out")

Librus logs in again (once) when a homework or school-file download meets an expired session, and when a message attachment's Wiadomości session is still rejected with a 401 after one extra bootstrap - never when a download times out or gets an odd answer (a failed download key, a link outside the sandbox, a body that isn't JSON). At most two downloads run at once (MAX_CONCURRENT_DOWNLOADS), separately from the max_concurrent_requests limit.

Login failures:

Case Exception
The credentials POST returned no goTo (❓ assumed wrong password) LibrusInvalidCredentialsError
A captcha marker appeared in the login flow LibrusCaptchaRequiredError
Network, DNS or timeout error, or 429/502/503/504 during the login LibrusConnectionError

A failed login leaves the client "not logged in" (the next call logs in again rather than trusting the half-replaced cookies). Librus remembers a failed login for 60 s: calls in that time that would need the password get the same error without trying it again, so a wrong password isn't sent over and over. LibrusAccountActionRequiredError exists for an account that needs some action on Librus's own site first, but no such answer has been seen yet, so nothing raises it today.

Everything the library raises for a problem with Librus - an HTTP status, an odd answer, a network error or a timeout - is a LibrusError. Authentication problems inherit from LibrusAuthError, "try again later" from LibrusConnectionError. Only a programming error (for example an unsupported HTTP method passed to the low-level client) is something else.

Be gentle

These are real children's school accounts. Do not retry in a loop, do not fire dozens of parallel requests, and cache lookups for a day. Librus may have abuse heuristics, and nobody has tested where their limits are.

Librus.fetch_all() is a full snapshot: about 35 requests. Librus runs at most 6 at a time (max_concurrent_requests), remembers modules the account was refused for a day, skips lookups nothing refers to (comment lists, point-grade categories when there are no point grades), keeps comment texts and the descriptive-grade skill list (fetched again only for an id they don't have, or after a day) and, with cache_reference_data=True, keeps subjects, teachers, categories, free days, the standing plan and similar for a day. For "what's new" polling use fetch_changes() (about 10 requests, no comment texts), and don't poll more often than every ~15 minutes.

✅ There is no way to skip unchanged data: data responses send no ETag and no Last-Modified and are marked Cache-Control: no-store (checked live 2026-10-12), so every poll downloads the full lists.