Unofficial TypeScript SDK for the Chess.com Published-Data API. Not affiliated with, endorsed by, or sponsored by Chess.com.
A clean, typed, isomorphic client for the public Chess.com API — with built-in rate limiting, ETag caching, runtime response validation, and lazy pagination.
- ✅ Typed responses, validated at runtime with zod
- ✅ Serial rate limiting + backoff (respects the Chess.com "be serial" rule)
- ✅ Transparent ETag caching (
304 Not Modifiedaware) - ✅ Lazy async iteration over monthly game archives
- ✅ Isomorphic — native
fetch, runs in Node 22+, Deno, Bun, the browser - ✅ One dependency (
zod)
✅ Stable since
1.0.0. The public API follows semver — breaking changes ship only in a new major version.
npm install chesscom-sdkimport { ChessComClient } from "chesscom-sdk";
const client = new ChessComClient({
// Required by Chess.com — include an app name and a contact.
userAgent: "myapp/1.0 (me@example.com)",
});
const profile = await client.getPlayer("hikaru");
const stats = await client.getPlayerStats("hikaru");
// Lazily stream a player's games across monthly archives:
for await (const game of client.streamPlayerGames("hikaru", {
since: "2024-01",
})) {
console.log(game.url, game.pgn);
}Chess.com rejects requests without a descriptive User-Agent (HTTP 403) and asks
that you include a way to contact you. The client therefore requires it at
construction. Use "<app>/<version> (<contact>)", e.g.
"my-bot/1.0 (me@example.com)".
Note: in browsers,
fetchignores a customUser-Agent(it is a forbidden header). The option only takes effect on Node, Deno, and Bun.
All methods return validated, fully typed results.
| Method | Returns | Endpoint |
|---|---|---|
getPlayer(username) |
PlayerProfile |
/player/{username} |
getPlayerStats(username) |
PlayerStats |
/player/{username}/stats |
getPlayerArchives(username) |
string[] |
/player/{username}/games/archives |
getPlayerGames(username, year, month) |
Game[] |
/player/{username}/games/{YYYY}/{MM} |
getPlayerGamesPgn(user, year, month) |
string (raw PGN) |
/player/{username}/games/{YYYY}/{MM}/pgn |
streamPlayerGames(username, options?) |
AsyncGenerator<Game> |
iterates over the monthly archives |
getPlayerGamesToMove(username) |
ToMoveGame[] |
/player/{username}/games/to-move |
getPlayerClubs(username) |
PlayerClub[] |
/player/{username}/clubs |
getPlayerTournaments(username) |
PlayerTournaments |
/player/{username}/tournaments |
getClub(urlId) |
ClubProfile |
/club/{url-id} |
getClubMembers(urlId) |
ClubMembers |
/club/{url-id}/members |
getTournament(urlId) |
Tournament |
/tournament/{url-id} |
getTournamentRound(urlId, round) |
TournamentRound |
/tournament/{url-id}/{round} |
getTournamentRoundGroup(urlId, r, g) |
TournamentRoundGroup |
/tournament/{url-id}/{round}/{group} |
getLeaderboards() |
Leaderboards |
/leaderboards |
getStreamers() |
Streamer[] |
/streamers |
getDailyPuzzle() |
Puzzle |
/puzzle |
getRandomPuzzle() |
Puzzle |
/puzzle/random |
getCountry(iso) |
Country |
/country/{iso} |
getCountryPlayers(iso) |
string[] |
/country/{iso}/players |
getCountryClubs(iso) |
string[] |
/country/{iso}/clubs |
getPlayerMatches(username) |
PlayerMatches |
/player/{username}/matches |
getClubMatches(urlId) |
ClubMatches |
/club/{url-id}/matches |
getMatch(id) |
Match |
/match/{id} |
getMatchBoard(id, board) |
MatchBoard |
/match/{id}/{board} |
getTitledPlayers(title) |
string[] |
/titled/{title} |
Each method also accepts a final options object with an AbortSignal:
const controller = new AbortController();
const profile = await client.getPlayer("hikaru", { signal: controller.signal });The API wraps most collections in an envelope. Two rules keep returns predictable:
- Single-key array envelopes are unwrapped. Endpoints whose payload is just
{ games: [...] },{ players: [...] },{ clubs: [...] }, etc. return the array directly (Game[],string[], …) — the wrapper carries no extra meaning. - Multi-key structures are returned as-is. When the grouping is the data —
{ finished, in_progress, registered }(matches, tournaments),{ weekly, monthly, all_time }(club members), the leaderboard categories — the object is returned whole (PlayerMatches,ClubMembers, …).
year/month are typed number (they are validated — month must be 1–12).
Opaque path ids — a match id, a board, a tournament round/group — accept
number | string, since they are passed through verbatim and can exceed
Number.MAX_SAFE_INTEGER.
Hides the monthly pagination: it lists the archives, then fetches one month at a time (lazily) and yields game by game. The rate limiter and cache apply per month, so re-runs are fast and polite.
for await (const game of client.streamPlayerGames("hikaru", {
since: "2024-01", // YYYY-MM, inclusive
until: "2024-12", // YYYY-MM, inclusive
order: "newest-first", // or "oldest-first" (default: newest-first)
timeClass: "blitz", // keep only blitz games
rated: true, // keep only rated games
})) {
// …
}Months outside the since/until window are never requested.
Games expose their raw PGN as a string — this SDK does not parse moves, so you can pair it with whatever chess library you prefer. For example, with chess.js:
import { Chess } from "chess.js";
const games = await client.getPlayerGames("hikaru", 2024, 1);
const game = games[0];
if (game?.pgn) {
const chess = new Chess();
chess.loadPgn(game.pgn);
console.log(chess.history()); // ["e4", "c5", "Nf3", …]
console.log(chess.header()); // { White, Black, Result, ECO, … }
}(Header fields like white, black, time_control, eco, and end_time are
also available as structured fields on the Game object, no parsing needed.)
new ChessComClient({
userAgent: "myapp/1.0 (me@example.com)", // required
fetch, // custom fetch (default: global fetch)
cache, // custom CacheStore (default: in-memory Map)
timeout: 10_000, // per-request timeout in ms (default: none)
baseUrl: "https://api.chess.com/pub", // default
onValidationError: "throw", // "throw" | "warn" | "ignore" (default: "throw")
onRateLimit: (info) => console.warn("rate limited", info),
});Every error thrown by the SDK extends ChessComError and carries a discriminant
kind. Branch with instanceof or switch (err.kind).
import { ChessComError, NotFoundError } from "chesscom-sdk";
try {
await client.getPlayer("does-not-exist");
} catch (err) {
if (err instanceof NotFoundError) {
// …
} else if (err instanceof ChessComError) {
console.error(err.kind, err.status, err.url);
}
}| Error | kind |
When |
|---|---|---|
NotFoundError |
not_found |
HTTP 404 / 410 |
RateLimitError |
rate_limit |
HTTP 429 after retries are exhausted |
ForbiddenError |
forbidden |
HTTP 403 (often a missing/rejected User-Agent) |
ServerError |
server |
HTTP 5xx or an unexpected status |
ValidationError |
validation |
A response did not match its schema |
NetworkError |
network |
The request never produced a response |
Responses are validated against zod schemas. If the API drifts from the expected
shape, onValidationError decides what happens:
"throw"(default) — throw aValidationError."warn"— log a warning and return the raw data."ignore"— return the raw data silently.
Chess.com asks clients to make requests serially (parallel requests get a 429).
By default the client funnels all requests through a serial queue and retries a
429 with exponential backoff, honoring the server's Retry-After. This is
per-client instance; share one client to share the queue.
The client revalidates with ETags (If-None-Match) and serves the cached body on
304 Not Modified. The default store is an in-memory Map. Plug in your own by
implementing CacheStore:
import type { CacheStore, CacheEntry } from "chesscom-sdk";
class RedisCacheStore implements CacheStore {
constructor(private redis: import("ioredis").Redis) {}
async get(key: string): Promise<CacheEntry | undefined> {
const raw = await this.redis.get(key);
return raw ? (JSON.parse(raw) as CacheEntry) : undefined;
}
async set(key: string, value: CacheEntry): Promise<void> {
await this.redis.set(key, JSON.stringify(value));
}
}
const client = new ChessComClient({
userAgent: "myapp/1.0 (me@example.com)",
cache: new RedisCacheStore(redis),
});- The published library runs on Node 22+, Deno, Bun, and browsers (anything
with a global
fetch). - Contributing to this repo requires Node 22+ (the dev toolchain).
Contributions are welcome — see CONTRIBUTING.md.
SPEC.md— technical design and architectureSTYLE.md— code conventionsCONTRIBUTING.md— how to contributeRELEASING.md— release & publish process