litchee
litchee is an asynchronous, builder-pattern Rust client for the
Lichess API, covering every documented operation (184/184 of the official
OpenAPI spec) — from looking up players to playing games, running tournaments,
streaming live broadcasts, and "Log in with Lichess" via OAuth2 + PKCE.
It is open source (MIT) and aims at feature parity with the official API,
with an ergonomic, strongly-typed surface: every DTO is prefixed Lichess*,
every failure maps to a specific LichessError variant, and every endpoint is
reached through a small accessor on the client (client.account(),
client.broadcasts(), …).
use StreamExt;
use LichessClient;
async
Why litchee?
The Lichess API is large (24 tags, ~184 operations, four hosts, JSON + NDJSON +
PGN). litchee wraps all of it behind one cohesive, async-first client so Rust
applications — bots, analysis tools, "Log in with Lichess" web apps, dataset
exporters — can talk to Lichess without hand-rolling HTTP, streaming, and OAuth.
- Complete. All 184 documented operations are implemented and covered by tests.
- Async & streaming-native. Built on
tokio+reqwest; NDJSON endpoints (event streams, board/bot game state, game exports) return aStreamyou can consume directly. - Typed end to end.
Lichess*DTOs and an exhaustive, matchable error type. - "Log in with Lichess". First-class OAuth2 Authorization Code flow with PKCE, plus plain personal access tokens.
- Organized by business concern. The module tree mirrors the API's own
structure, grouped into categories under
litchee::api.
Installation
[]
= "0.1"
= { = "1", = ["full"] }
= "0.3" # to consume streams with `.next()`
The minimum supported Rust version is 1.95 (edition 2024).
Examples
1. Log in with Lichess (OAuth2 + PKCE)
New to OAuth or PKCE? The PKCE flow guide walks through the whole "Log in with Lichess" flow step by step, for beginners — with a glossary of OAuth terms at the end.
use LichessClient;
use ;
# async
2. Export an authenticated user's games (NDJSON stream)
use StreamExt;
use LichessClient;
# async
3. A user's played puzzles
use StreamExt;
use LichessClient;
# async
4. Studies (list + export PGN)
use StreamExt;
use LichessClient;
# async
5. Broadcasting (browse + round PGN)
use StreamExt;
use LichessClient;
# async
The examples/ directory contains runnable programs:
oauth_flow— the full "Log in with Lichess" flow end to end: PKCE authorization, opening the browser, catching the redirect on a tiny local listener, exchanging the code, then listing the signed-in user's recent games, puzzle attempts, and studies (cargo run --example oauth_flow).profile— print the authenticated user's profile (LICHESS_TOKEN=lip_xxx cargo run --example profile).tv_feed— stream the Lichess TV feed (cargo run --example tv_feed).
API coverage
Every documented Lichess operation is implemented. Endpoints are reached through
an accessor method on LichessClient and live in a module under litchee::api,
grouped by category. The tables below map each concern's endpoints to its module.
client.oauth() — litchee::api::auth::oauth (4 endpoints)
GET /oauth, POST /api/token, DELETE /api/token, POST /api/token/test
client.account() — litchee::api::users::account (6 endpoints)
GET /api/account, GET /api/account/email, GET /api/account/preferences, GET /api/account/kid, POST /api/account/kid, GET /api/timeline
client.users() — litchee::api::users::players (13 endpoints)
GET /api/user/{username}, POST /api/users, GET /api/users/status, GET /api/crosstable/{u1}/{u2}, GET /api/player/autocomplete, GET /api/user/{username}/rating-history, GET /api/user/{username}/perf/{perf}, GET /api/user/{username}/activity, GET /api/player, GET /api/player/top/{nb}/{perfType}, GET /api/streamer/live, GET /api/user/{username}/note, POST /api/user/{username}/note
client.fide() — litchee::api::users::fide (3 endpoints)
GET /api/fide/player/{playerId}, GET /api/fide/player/{playerId}/ratings, GET /api/fide/player
client.relations() — litchee::api::social::relations (5 endpoints)
GET /api/rel/following, POST /api/rel/follow/{username}, POST /api/rel/unfollow/{username}, POST /api/rel/block/{username}, POST /api/rel/unblock/{username}
client.messaging() — litchee::api::social::messaging (1 endpoint)
POST /inbox/{username}
client.teams() — litchee::api::social::teams (14 endpoints)
GET /api/team/{teamId}, GET /api/team/all, GET /api/team/of/{username}, GET /api/team/search, GET /api/team/{teamId}/users, GET /api/team/{teamId}/requests, POST /api/team/{teamId}/request/{userId}/accept, POST /api/team/{teamId}/request/{userId}/decline, POST /api/team/{teamId}/kick/{userId}, POST /team/{teamId}/join, POST /team/{teamId}/quit, POST /team/{teamId}/pm-all, GET /api/team/{teamId}/arena, GET /api/team/{teamId}/swiss
client.arena() — litchee::api::tournaments::arena (13 endpoints)
GET /api/tournament, GET /api/tournament/{id}, POST /api/tournament, POST /api/tournament/{id}, POST /api/tournament/team-battle/{id}, GET /api/tournament/{id}/teams, POST /api/tournament/{id}/join, POST /api/tournament/{id}/withdraw, POST /api/tournament/{id}/terminate, GET /api/tournament/{id}/results, GET /api/tournament/{id}/games, GET /api/user/{username}/tournament/created, GET /api/user/{username}/tournament/played
client.swiss() — litchee::api::tournaments::swiss (10 endpoints)
GET /api/swiss/{id}, POST /api/swiss/new/{teamId}, POST /api/swiss/{id}/edit, POST /api/swiss/{id}/join, POST /api/swiss/{id}/withdraw, POST /api/swiss/{id}/terminate, POST /api/swiss/{id}/schedule-next-round, GET /swiss/{id}.trf, GET /api/swiss/{id}/results, GET /api/swiss/{id}/games
client.simuls() — litchee::api::tournaments::simuls (1 endpoint)
GET /api/simul
client.puzzles() — litchee::api::training::puzzles (11 endpoints)
GET /api/puzzle/daily, GET /api/puzzle/{id}, GET /api/puzzle/next, GET /api/puzzle/activity, GET /api/puzzle/batch/{angle}, POST /api/puzzle/batch/{angle}, GET /api/puzzle/dashboard/{days}, GET /api/puzzle/replay/{days}/{theme}, GET /api/storm/dashboard/{username}, GET /api/racer/{id}, POST /api/racer
client.studies() — litchee::api::training::studies (9 endpoints)
GET /api/study/{studyId}/{chapterId}.pgn, GET /api/study/{studyId}.pgn, GET /api/study/by/{username}/export.pgn, GET /api/study/by/{username}, POST /api/study, POST /api/study/{studyId}/import-pgn, POST /api/study/{studyId}/{chapterId}/moves, POST /api/study/{studyId}/{chapterId}/tags, DELETE /api/study/{studyId}/{chapterId}
client.broadcasts() — litchee::api::broadcasting::broadcasts (19 endpoints)
GET /api/broadcast, GET /api/broadcast/top, GET /api/broadcast/search, GET /api/broadcast/by/{username}, GET /api/broadcast/my-rounds, GET /api/broadcast/{id}, GET /api/broadcast/{tourSlug}/{roundSlug}/{roundId}, GET /api/broadcast/round/{roundId}.pgn, GET /api/broadcast/{id}.pgn, GET /api/stream/broadcast/round/{roundId}.pgn, POST /api/broadcast/round/{roundId}/push, POST /api/broadcast/round/{roundId}/reset, GET /broadcast/{id}/players, GET /broadcast/{id}/players/{playerId}, GET /broadcast/{id}/teams/standings, POST /broadcast/new, POST /broadcast/{id}/edit, POST /broadcast/{id}/new, POST /broadcast/round/{roundId}/edit
client.tv() — litchee::api::broadcasting::tv (4 endpoints)
GET /api/tv/channels, GET /api/tv/feed, GET /api/tv/{channel}, GET /api/tv/{channel}/feed
client.opening_explorer() — litchee::api::database::opening_explorer (4 endpoints, explorer.lichess.org)
GET /masters, GET /lichess, GET /player, GET /masters/pgn/{gameId}
client.tablebase() — litchee::api::database::tablebase (3 endpoints, tablebase.lichess.org)
GET /standard, GET /atomic, GET /antichess
client.analysis() — litchee::api::database::analysis (1 endpoint)
GET /api/cloud-eval
client.board() — litchee::api::gameplay::board (13 endpoints)
GET /api/stream/event, GET /api/board/game/stream/{gameId}, POST /api/board/game/{gameId}/move/{move}, POST /api/board/game/{gameId}/abort, POST /api/board/game/{gameId}/resign, POST /api/board/game/{gameId}/draw/{accept}, POST /api/board/game/{gameId}/takeback/{accept}, POST /api/board/game/{gameId}/claim-victory, POST /api/board/game/{gameId}/claim-draw, POST /api/board/game/{gameId}/berserk, GET /api/board/game/{gameId}/chat, POST /api/board/game/{gameId}/chat, POST /api/board/seek
client.bot() — litchee::api::gameplay::bot (13 endpoints)
POST /api/bot/account/upgrade, GET /api/bot/online, GET /api/stream/event, GET /api/bot/game/stream/{gameId}, POST /api/bot/game/{gameId}/move/{move}, POST /api/bot/game/{gameId}/abort, POST /api/bot/game/{gameId}/resign, POST /api/bot/game/{gameId}/draw/{accept}, POST /api/bot/game/{gameId}/takeback/{accept}, POST /api/bot/game/{gameId}/claim-victory, POST /api/bot/game/{gameId}/claim-draw, GET /api/bot/game/{gameId}/chat, POST /api/bot/game/{gameId}/chat
client.challenges() — litchee::api::gameplay::challenges (11 endpoints)
GET /api/challenge, GET /api/challenge/{challengeId}/show, POST /api/challenge/{username}, POST /api/challenge/ai, POST /api/challenge/open, POST /api/challenge/{challengeId}/accept, POST /api/challenge/{challengeId}/decline, POST /api/challenge/{challengeId}/cancel, POST /api/challenge/{gameId}/start-clocks, POST /api/round/{gameId}/add-time/{seconds}, POST /api/token/admin-challenge
client.bulk_pairing() — litchee::api::gameplay::bulk_pairing (6 endpoints)
GET /api/bulk-pairing, POST /api/bulk-pairing, GET /api/bulk-pairing/{id}, DELETE /api/bulk-pairing/{id}, POST /api/bulk-pairing/{id}/start-clocks, GET /api/bulk-pairing/{id}/games
client.games() — litchee::api::gameplay::games (13 endpoints)
GET /game/export/{gameId}, GET /api/games/user/{username}, POST /api/games/export/_ids, GET /api/games/export/bookmarks, GET /api/games/export/imports, GET /api/account/playing, GET /api/user/{username}/current-game, GET /game/{gameId}/chat, POST /api/import, GET /api/stream/game/{id}, POST /api/stream/games-by-users, POST /api/stream/games/{streamId}, POST /api/stream/games/{streamId}/add
client.external_engine() — litchee::api::engine::external_engine (8 endpoints)
GET /api/external-engine, POST /api/external-engine, GET /api/external-engine/{id}, PUT /api/external-engine/{id}, DELETE /api/external-engine/{id}, POST /api/external-engine/{id}/analyse, POST /api/external-engine/work, POST /api/external-engine/work/{id}
The OAuth
GET /oauthendpoint is not a request the client makes — it's the URL you redirect the user's browser to.client.oauth().authorization_url(…)builds it.GET /api/stream/eventis shared by the Board and Bot APIs.
Design & technical choices
- Async-first on
tokio+reqwest(rustls). Async is required because many Lichess endpoints stream newline-delimited JSON (application/x-ndjson): event streams, board/bot game state, game/tournament exports, TV feeds. - Ergonomic streaming. Streaming endpoints return
BoxStream<'static, Result<T>>—Unpin,Send, and consumable directly withStreamExt::next(). Lines are buffered across network chunks and keep-alive blanks are skipped. - Exhaustive, matchable errors. Every failure maps to a specific
LichessErrorvariant: a structuredApiError(status → typed kind + body message +Retry-After), a typedOAuthError, transport/decode/stream failures, and PKCE validation errors. - Resilient by configuration. Connect/read timeouts are tunable on the
builder (the read timeout defaults to 5 minutes so long-lived NDJSON streams
aren't killed), the NDJSON line buffer is bounded as a DoS guard
(
max_line_bytes), and rate-limited (429) requests can be retried opt-in viaRetryPolicy— it waits the response'sRetry-Afterwhen present, otherwise exponential backoff clamped to a ceiling. Tokens are held in aSecretwrapper that redacts them fromDebugoutput. - Builder pattern. The client (
LichessClient::builder()) and every request with optional parameters (game export, challenges, tournaments, …) use builders rather than wide function signatures. - Four hosts, one client.
lichess.org,explorer.lichess.org,tablebase.lichess.org, andengine.lichess.ovhare routed internally; each is overridable on the builder (for self-hosted lila,localhost, or mocks). - Strong, forward-compatible types. Every DTO is prefixed
Lichess*and is#[non_exhaustive]. Where the API returns very large or evolving aggregates (e.g. perf stats, activity feeds, broadcast nested payloads), the documented fields are typed and the remainder is preserved losslessly inserde_jsonvalues — nothing is dropped. - Organized by business concern. The module tree mirrors the API's own
organization, grouped into categories under
src/api/(see below). Core plumbing (client,config,error,http,model,stream) lives at the crate root. - Tested deterministically. Each endpoint has an integration test that runs
against a
wiremockmock server with fixtures derived from the spec's own examples; pure logic (PKCE derivation, NDJSON splitting, error mapping, serde round-trips) has unit tests. CI runsfmt,clippy -D warnings, the test suite, the doc build, and an MSRV check. - Safety & quality gates.
#![forbid(unsafe_code)], clippypedantic,missing_docs, and a self-imposed ≤900 LOC/file and ≤20 LOC/method limit.
Project layout
src/
lib.rs
client/ config/ error/ http/ model/ stream/ # core plumbing
api/
auth/ oauth
users/ account, players, fide
social/ relations, messaging, teams
tournaments/ arena, swiss, simuls
training/ puzzles, studies
broadcasting/ broadcasts, tv
database/ opening_explorer, tablebase, analysis
gameplay/ board, bot, challenges, bulk_pairing, games
engine/ external_engine
The vendored OpenAPI spec (reference/ submodule)
This repository includes the official Lichess OpenAPI specification as a git
submodule at reference/lichess-api/ (source:
lichess-org/api). It is the source of
truth for the client and is used during development to:
- Model DTOs faithfully — field names, optionality, and enums are taken directly from the spec's schemas.
- Drive deterministic tests — integration-test fixtures are derived from the spec's documented examples, so tests need no network or credentials.
- Verify coverage — implemented endpoints are diffed against the spec to guarantee full (184/184) coverage as the API evolves.
The submodule is development-only: it is excluded from the published crate. Clone it with:
# or, in an existing clone:
Development
Contributing
Contributions are welcome. Please keep the project conventions: one concern per
module under the right src/api/ category, Lichess*-prefixed DTOs, an
integration test per endpoint plus unit tests for pure functions, and a clean
cargo fmt / cargo clippy -D warnings. The vendored spec under reference/ is
the source of truth for endpoints and types.
License
Licensed under the MIT License.