[ norboten ]

API reference

<!-- generated by python -m norboten_api.reference; do not edit by hand -->

The API is served at https://api.<domain> (http://localhost:8000 with make stack-up). Requests and responses are JSON unless noted. The interactive form of the same document is at /docs on any running API.

Signing requests. Endpoints marked token need Authorization: Bearer <token> — a web or cli token from signing in with GitHub (/auth/github/poll in a terminal, /auth/github/exchange on the site), which is the only way in: no password anywhere. On a server with NORBOTEN_REQUIRE_AUTH=false (tests, make stack-up) X-Debug-User: <id> stands in for one. Endpoints marked public need nothing.

Errors are {"detail": "…"} with a status: 401 no or bad token, 403 not yours (or denied on GitHub), 404 unknown, 409 conflict (a taken nick), 410 gone (a sign-in expired or already used), 422 invalid body, 428 still waiting for GitHub, 429 rate-limited, 502/503 a model provider, GitHub or Discord failed, or this deployment has none.

Health and metrics

GET /healthz

public

The process is up. Docker's health check and Caddy use this.

GET /readyz

public

The process can do its job: the database and Redis answer. A deploy waits on this and rolls back when it never turns ready.

Accounts and signing in

GET /auth/config

public

How to sign in on this server. The TUI and the site ask, so a self-hosted server needs no new release; discord says whether the account page offers linking Discord.

POST /auth/github/device

public

Start signing a terminal in. GitHub's device code stays here; the terminal gets the code a person types, the page to type it on, and a handle to poll with.

Body — DeviceIn:

  • label · string · optional · min 1 · max 120
  • remember · boolean · optional

POST /auth/github/poll

public

Has the person approved on GitHub yet? One call to GitHub per poll, so a terminal that polls faster than interval is told to slow down by GitHub itself.

Body — PollIn:

  • poll_id · string · min 8 · max 128
  • remember · boolean · optional

GET /auth/github/go

public

Send the browser to GitHub. The page's state comes back to it in the fragment; GitHub is given this server's own state, which names the flow.

Parameter In Type Required
state query string yes
remember query boolean no
return query string no

GET /auth/github/callback

public

GitHub sends the browser here. Whatever happens, the browser goes back to the page it came from, with a one-time code or an error in the fragment.

Parameter In Type Required
state query string no
code query string no
error query string no

POST /auth/github/exchange

public

The one-time code from the callback, for a web token. It works once, for sixty seconds.

Body — ExchangeIn:

  • once · string · min 8 · max 128

POST /auth/logout

token

Revoke the token this request was made with.

GET /auth/tokens

token

Where this account is signed in: the site and each terminal, with when each expires.

GET /auth/me

token

Who this account is on GitHub, and what it has linked and asked for — for its own account page. 404 for a debug identity, which has no credentials.

GET /auth/preferences

token

What this account has asked to be sent. Everything is off until the learner turns it on.

PUT /auth/preferences

token

Turn the weekly digest on or off. It is a Discord direct message, so it can only be turned on once Discord is linked (409 before that).

Body — PreferencesIn:

  • digest · boolean

POST /auth/discord/start

token

Where to send the browser to link Discord to this account.

Body — norboten_api__routers__discord__StartIn:

  • join · boolean · optional

GET /auth/discord/callback

public

Discord sends the browser here after the consent screen.

Parameter In Type Required
state query string no
code query string no
error query string no

DELETE /auth/discord

token

Forget the Discord user, and stop the digest that went to it.

The lab catalogue

GET /labs

public

Every lab the server knows: id, short id, title, track, difficulty, estimated minutes, base images and topics.

GET /labs/{lab_id}

public

One lab's manifest and briefing, by id or short id. Hints come with the lab package; the reference solution never leaves the server.

Parameter In Type Required
lab_id path string yes

Theory

GET /quiz/topics

public

The theory banks: every topic bank and every lab's own, with how many questions each has.

Rated labs and rated theory

GET /rated/labs

token

The rated labs this server grades: id, title, track, topics, difficulty, clock and images. Nothing about what is broken. Empty on a server with no rated content.

GET /rated/labs/{lab_id}

token

One rated lab's manifest and briefing — what a learner reads before starting. The faults, the collectors, the judges, the hints and the solution are not in it.

Parameter In Type Required
lab_id path string yes

POST /rated/attempts

token

Start a rated attempt: an id, a nonce, the key that signs this attempt's records, the clock, and the break bundle. Any attempt still open for this account is closed as a loss first — seeing the faults and walking away is not free.

Body — norboten_api__routers__rated__StartIn:

  • lab_id · string
  • image · string · optional

GET /rated/attempts/{attempt_id}

token

Where an attempt stands: open, or its outcome with each check's verdict and the rating change.

Parameter In Type Required
attempt_id path string yes

GET /rated/attempts/{attempt_id}/collect

token

The collect bundle for an open attempt: which facts to gather, never what they must be.

Parameter In Type Required
attempt_id path string yes

POST /rated/attempts/{attempt_id}/abandon

token

Give an attempt up. It is rated as a loss, like running out of time.

Parameter In Type Required
attempt_id path string yes

POST /rated/attempts/{attempt_id}/facts

token

A signed fact record from the guest, before or after the reboot. The last one the lab needs closes the attempt: judged here, rated, and answered pass or fail per check.

Parameter In Type Required
attempt_id path string yes

Body — FactsIn:

  • record · object
  • signature · string · min 64 · max 64

GET /rated/quiz/banks

token

The rated question banks: topic, title, description, topics and how many questions a run asks. Empty on a server with no rated content.

POST /rated/quiz/sessions

token

Start a rated run and serve its first question. A run still open for this account is closed first, on what it had answered — a question seen and not answered counts as wrong.

Body — StartQuiz:

  • topic · string

POST /rated/quiz/sessions/{session_id}/answers

token

Answer the question on screen. Graded here, against the time it was served: the verdict, the right answer, the explanation and the references. The last answer closes and rates the run. An empty selection is how a client reports that its clock ran out.

Parameter In Type Required
session_id path string yes

Body — AnswerIn:

  • question_id · string
  • selected · list of string · optional

POST /rated/quiz/sessions/{session_id}/next

token

Serve the next question; its clock starts now. Asked for when the learner has read the last explanation, so reading it costs nothing.

Parameter In Type Required
session_id path string yes

Anonymous progress

POST /progress

public

Store an anonymous, opt-in progress event under a random client id. No account involved.

Body — Attempt:

  • client_id · string · min 8 · max 64
  • lab_id · string
  • score_percent · integer · ≥ 0.0 · ≤ 100.0
  • passed · boolean
  • minutes · integer · optional
  • base_image · string · optional

GET /progress/{client_id}

public

Read back the progress events recorded under a client id.

Parameter In Type Required
client_id path string yes

Profiles, attempts and the board

POST /me

token

Make the profile: a nick, chosen once, and a country. After that the country is the one thing that changes — a different nick is 409 "the nick is chosen once".

Body — SignUp:

  • nick · string · optional
  • country · string

GET /me

token

The signed-in account's profile: nick, country, overall and per-topic ratings, a year of contributions and recent attempts. 404 until a nick is chosen.

DELETE /me

token

Delete this account and everything on it: the profile and its nick, every attempt and rating, open rated attempts and theory runs, the Play recordings it made, the GitHub and Discord links, and every token — so the call ends the session that made it. Nothing is kept and nothing can be restored; the nick becomes free for someone else. Backups age out on their own schedule (the privacy policy says how long). The account need not have a nick yet.

GET /geo/country

public

The country this request probably comes from, to pre-fill a form — {"country": "PL"}, or "" when the address is unknown or private. Looked up offline (geo.py, DB-IP Lite); nothing is sent anywhere and nothing is stored.

GET /profile/{nick}

public

Anyone's public profile by nick, in the same shape as /me, with the GitHub account it signs in with.

Parameter In Type Required
nick path string yes

POST /attempts

token

Record an attempt on the profile. It never moves a rating: only a rated lab or rated theory, graded on this server (/rated), does — an unrated lab's checks and answer are public, so a reported pass proves nothing. rated in the body is accepted from older clients and ignored. The server still decides the difficulty, topics and clock, which the profile shows.

Body — AttemptIn:

  • lab_id · string
  • kind · string · optional
  • started_at · number
  • duration_seconds · integer · ≥ 0.0
  • score_percent · integer · ≥ 0.0 · ≤ 100.0
  • passed · boolean
  • rated · boolean · optional
  • difficulty · integer · optional
  • topics · list of string · optional

GET /leaderboard

public

Sorted on the conservative rating, so a lucky first win does not top the board. Cached for thirty seconds: every profile page and TUI screen asks, and it changes one attempt at a time.

Parameter In Type Required
topic query string no
limit query integer no

Live and recorded sessions

POST /play/sessions

token

Open a session. Anyone who can see the site can watch it from here on, and the Telegram channel is told (announce.py).

Body — norboten_api__routers__play__StartIn:

  • lab_id · string
  • width · integer · ≥ 20.0 · ≤ 400.0
  • height · integer · ≥ 5.0 · ≤ 200.0

POST /play/sessions/{session_id}/frames

token

Append one batch of frames, commands and diffs to your own live session; it is stored and published to viewers at once. 409 once the session has ended.

Parameter In Type Required
session_id path string yes

Body — AppendIn:

  • seq · integer · ≥ 0.0
  • at · number
  • events · list of list of · optional
  • commands · list of object · optional
  • changes · list of object · optional

POST /play/sessions/{session_id}/end

token

End your own session, optionally with whether the lab was passed; viewers receive end.

Parameter In Type Required
session_id path string yes

Body — EndIn:

  • passed · boolean · optional

GET /play/live

public

Who is working right now, and what finished recently.

Parameter In Type Required
limit query integer no

GET /play/sessions/{session_id}

public

The whole recording: an asciicast header, its events, the commands and the diffs.

Parameter In Type Required
session_id path string yes

GET /play/sessions/{session_id}/stream

public

Server-sent events: what is stored, then every batch as it is published, then end.

Parameter In Type Required
session_id path string yes
after query integer no

GET /play/leaderboard-context/{nick}

public

What a viewer sees next to a live terminal: whose it is, and how they are rated.

Parameter In Type Required
nick path string yes

The consultant

GET /chat/faq

public

The chips under the input: what people actually arrive wanting to know.

POST /chat

public

Ask the consultant once and wait for the whole answer: the reply, the passages it used, and whether the guard blocked it. The streaming form is /chat/stream.

Body — Ask:

  • question · string · min 3 · max 500
  • lab_id · string · optional

GET /chat/stream

public

sources first — so a reader can start checking before the answer lands — then tokens.

Parameter In Type Required
question query string yes
lab_id query string no

Donations

POST /donations/checkout

public

Start a donation. Returns the Stripe page to send the donor to.

Body — Donation:

  • amount · integer · ≥ 2.0 · ≤ 5000.0
  • interval · string · optional

Telemetry

POST /telemetry

public

Record an opt-in stuck point: which check a learner was on and at which hint level. 404 when the server has telemetry turned off.

Body — StuckPoint:

  • lab_id · string
  • base_image · string · optional
  • check_id · string
  • failed_attempts · integer · ≥ 1.0 · ≤ 1000.0
  • hint_level · integer · optional · ≥ 0.0 · ≤ 4.0
  • minutes · integer · optional · ≥ 0.0 · ≤ 600.0

GET /telemetry/stuck-points

public

What the weekly digest reads: the checks people fail most.

OAuth for MCP clients

GET /.well-known/oauth-authorization-server

public

Authorization server metadata (RFC 8414): where to authorize and exchange codes, PKCE S256 only, public clients, and client ID metadata documents instead of registration.

GET /.well-known/oauth-protected-resource

public

Protected resource metadata for the MCP server (RFC 9728): its identifier, and that this API issues the tokens it accepts.

GET /.well-known/oauth-protected-resource/mcp

public

Protected resource metadata for the MCP server (RFC 9728): its identifier, and that this API issues the tokens it accepts.

GET /.well-known/oauth-protected-resource/mcp/account

public

The same, for the door that asks for a token on every request (/mcp/account).

GET /oauth/authorize

public

Start an authorization: check the client, its redirect URI and the PKCE challenge, then send the browser to the site, where the account approves it. Errors before the redirect URI is trusted are answered here; after, they go back to the client.

GET /oauth/requests/{request_id}

public

What the site shows before the account approves: who is asking, where the browser goes back to, and what the token will allow.

Parameter In Type Required
request_id path string yes

POST /oauth/approve

token

The signed-in account's answer. Returns where to send the browser: back to the client, with a one-time code (five minutes) or access_denied.

Body — Decision:

  • request · string · min 16 · max 64
  • allow · boolean

POST /oauth/token

public

Exchange a code and its PKCE verifier, or a refresh token, for an mcp access token (an hour) and a new refresh token (thirty days; the old one stops working). Form-encoded.

On this page
Health and metrics GET /healthz GET /readyz Accounts and signing in GET /auth/config POST /auth/github/device POST /auth/github/poll GET /auth/github/go GET /auth/github/callback POST /auth/github/exchange POST /auth/logout GET /auth/tokens GET /auth/me GET /auth/preferences PUT /auth/preferences POST /auth/discord/start GET /auth/discord/callback DELETE /auth/discord The lab catalogue GET /labs GET /labs/{lab_id} Theory GET /quiz/topics Rated labs and rated theory GET /rated/labs GET /rated/labs/{lab_id} POST /rated/attempts GET /rated/attempts/{attempt_id} GET /rated/attempts/{attempt_id}/collect POST /rated/attempts/{attempt_id}/abandon POST /rated/attempts/{attempt_id}/facts GET /rated/quiz/banks POST /rated/quiz/sessions POST /rated/quiz/sessions/{session_id}/answers POST /rated/quiz/sessions/{session_id}/next Anonymous progress POST /progress GET /progress/{client_id} Profiles, attempts and the board POST /me GET /me DELETE /me GET /geo/country GET /profile/{nick} POST /attempts GET /leaderboard Live and recorded sessions POST /play/sessions POST /play/sessions/{session_id}/frames POST /play/sessions/{session_id}/end GET /play/live GET /play/sessions/{session_id} GET /play/sessions/{session_id}/stream GET /play/leaderboard-context/{nick} The consultant GET /chat/faq POST /chat GET /chat/stream Donations POST /donations/checkout Telemetry POST /telemetry GET /telemetry/stuck-points OAuth for MCP clients GET /.well-known/oauth-authorization-server GET /.well-known/oauth-protected-resource GET /.well-known/oauth-protected-resource/mcp GET /.well-known/oauth-protected-resource/mcp/account GET /oauth/authorize GET /oauth/requests/{request_id} POST /oauth/approve POST /oauth/token