Skip to content

API Internals

jsem-nerad edited this page Sep 1, 2026 · 3 revisions

API Internals

How the Strava.cz web app talks to its backend, reverse engineered from browser traffic and verified against a live account. The Czech-language original of these notes lives in notes/README.md, and complete recorded responses are in tests/fixtures/.

This is unofficial. Nothing here is documented or guaranteed by Strava.cz.

Conventions

  • Base URL https://app.strava.cz, endpoints under /api/<name>.
  • Everything is a POST with a JSON body.
  • The web app sends Content-Type: text/plain;charset=UTF-8 even though the body is JSON. The server accepts it, and this library sends the same thing.
  • The sid from login must be included in every authenticated request afterwards.
  • Money and prices come back as strings ("512.50"), not numbers.
  • ignoreCert is sent as the string "false".
  • Field naming is inconsistent between endpoints: objednavky wants s5url, while pridejJidloS5, saveOrders and logOut want the same value as url.

Errors

Failure is signalled with a non-standard HTTP 555 and this envelope:

{
    "state": "error",
    "number": 20,
    "message": "The client was not found in the client register or the wrong nickname or password was entered...",
    "backend": { "...": "..." }
}

The status alone is not enough — check state. Known numbers:

number meaning library exception
20 Unknown user, wrong password, or blocked account AuthenticationError
35 Not enough money on the account InsufficientBalanceError
43 Server overloaded, try again in a few minutes StravaAPIError

Code 43 turned up while logging in repeatedly from a script — "The service is currently overloaded, please try again in a few minutes" — so it looks more like rate limiting than genuine load. It is not mapped to its own exception yet; it arrives as a plain StravaAPIError with .code == 43.

POST /api/login

{
    "cislo": "3753",
    "jmeno": "vojtech.nerad",
    "heslo": "...",
    "zustatPrihlasen": false,
    "environment": "W",
    "lang": "EN"
}
key meaning type
cislo canteen number string
jmeno username string
heslo plaintext password string
zustatPrihlasen stay logged in boolean
environment the web app sends W string
lang interface language string

Response — the interesting parts:

key meaning
sid session identifier, required by every later request
s5url the Stravné 5 web service endpoint for this canteen
uzivatel.konto account balance, as a string
uzivatel.jmeno the person's full name
uzivatel.email e-mail address
uzivatel.mena currency
uzivatel.nazevJidelny canteen name

POST /api/objednavky

Fetches the menu.

{
    "cislo": "3753",
    "sid": "01C6...",
    "s5url": "https://wss5.strava.cz/WSStravne5_15/WSStravne5.svc",
    "lang": "EN",
    "konto": 0,
    "podminka": "",
    "ignoreCert": "false"
}

The response is an object whose keys are table0, table1, … — one table per day, each a list of that day's meals.

{
    "table0": [ { "...one meal..." }, { "...another..." } ],
    "table1": [ ... ]
}

Fields that matter on a meal:

key meaning example
datum date, dd.mm.yyyy "15.09.2026"
druh short type code, stable across languages "PO", "O1"
druh_popis display label for the type "Polévka", "Oběd 1"
nazev the meal's name "Vývar s kuskusem"
cena price, as a string "50.00"
alergeny [code, name] pairs [["04","Ryby"]]
pocet 0 not ordered, 1 ordered 0
veta the id used for ordering "7"
casKonec when ordering closes "2026-09-14T15:00:00"
omezeniObj what you may do with it — see below

Older builds of the app used a different date separator ("15-09.2025"), so parse tolerantly.

omezeniObj — the important part

This object decides what you may do, and it is easy to read wrongly. It has four fields:

field scope meaning
obj this meal may it be ordered
zm this meal may a saved order be changed or cancelled
bur this meal is it available on the canteen's exchange (burza)
den the whole day a day-level code

An empty string in obj or zm means "allowed". Any other value is a single letter naming the reason:

value meaning
"" no restriction
"C" the ordering deadline has passed (casKonec is in the past)
"I" cannot be ordered on its own — typically soup, included with the main dish
"V" no cooking that day

Verified against a live account (32 days, 118 meals), and it lines up exactly with casKonec:

situation obj zm bur den
deadline still open "" "" "C" ""
deadline passed "C" "C" "!" "" or "CO"
not ordered automatically "" "" "C" "T"
holiday / no cooking "V" "" "V" "VP"
soup (always) "I" "I" "I" —

The den field is not about orderability

den describes the day, not the meal, and cannot be used to decide whether something can be ordered:

value meaning DayStatus
"" ordinary day NORMAL
"T" the canteen does not order this day automatically NOT_AUTO_ORDERED
"VP" holiday or closure NO_SCHOOL
"CO" the day's deadline has passed CLOSED
  • "VP" — holiday or closure. That day's rows are placeholders named "Nevaří se" or "Státní svátek", not real meals.
  • "T" — the canteen's standing order does not cover this day. Its meals carry obj: "" and can be ordered by hand like any other; they simply never appear on their own. In practice this is the weekday the school does not teach.
  • "CO" — a day past its deadline, whose meals carry obj: "C". Builds differ: 5.13 and some 5.15 canteens send "CO", others leave den empty and say it only in obj, so obj is the only reliable source.

Evidence for "T", from a live account over 32 days and 118 meals:

  • "T" appeared on 6 days and every one of them was a Friday; never on any other weekday.
  • Every one of those days had mains with obj: "" — orderable.
  • Across the window where the standing order had run, each Monday-to-Thursday day had exactly one meal ordered and every "T" Friday had none.
  • A "T" row differs from an ordinary row in omezeniObj.den and in nothing else — same price, same deadline, same fields.

Versions of this library up to 0.2.0 classified meals from den alone. That hid orderable days carrying "T", and on a 5.13-style menu it returned an empty menu. Since 0.3.0 orderability comes from obj and zm, while den is parsed into Day.status (and kept raw in Day.day_code).

Unpublished slots

Canteens publish the menu in stages. Until a meal is announced its row carries a generic label, recognisable because nazev equals druh_popis (both "Oběd 2"), or because the name is itself a generic label such as "Oběd 3" or "Oběd 2 Rad 1". The library skips those rows.

Testing for delsiPopis == "" && alergeny == [] — which this library used to do — is unreliable: real meals exist with neither a long description nor declared allergens.

Ordering

Ordering takes two steps, with the change only provisional in between:

  1. pridejJidloS5 — tick or untick one meal
  2. saveOrders — commit everything ticked

nactiVlastnostiPA reloads state from the server and discards anything provisional, which is what this library uses to roll back a failed transaction.

POST /api/pridejJidloS5

{
    "cislo": "3753",
    "sid": "01C6...",
    "url": "https://wss5.strava.cz/WSStravne5_15/WSStravne5.svc",
    "veta": "7",
    "pocet": "1",
    "lang": "EN",
    "ignoreCert": "false"
}

pocet is "1" to order and "0" to cancel. The response reports the state of every meal that day plus the new balance in konto.

POST /api/saveOrders

{
    "cislo": "3753",
    "sid": "01C6...",
    "url": "https://wss5.strava.cz/WSStravne5_15/WSStravne5.svc",
    "xml": null,
    "lang": "EN",
    "ignoreCert": "false"
}

Returns the current orders and the balance. It is not proof the order stuck — this library re-fetches the menu afterwards and checks pocet.

POST /api/nactiVlastnostiPA

{
    "sid": "01C6...",
    "url": "https://wss5.strava.cz/WSStravne5_15/WSStravne5.svc",
    "cislo": "3753",
    "ignoreCert": "false",
    "lang": "EN",
    "getText": true,
    "checkVersion": true,
    "resetTables": true,
    "frontendFunction": "refreshInformations"
}

POST /api/logOut

{
    "sid": "01C6...",
    "cislo": "3753",
    "url": "https://wss5.strava.cz/WSStravne5_15/WSStravne5.svc",
    "lang": "EN",
    "ignoreCert": "false"
}

The response body is the bare JSON literal true — not an object. Code that does response.json()["something"] breaks here.

Still unknown

  • The burza fields and the whole meal-exchange feature.
  • environment, podminka, ignoreCert and xml — accepted, but effect unknown.
  • Ordering more than one portion (pocet greater than 1).

Contributions welcome — see Contributing. If you can record a menu from a canteen that behaves differently from the ones above, that is genuinely useful.

Clone this wiki locally