API Reference
The API is built for importing a user's games into another system. It gives you lightweight JSON lists, full game details, PGN export, and an incremental sync feed. Every response is limited to the user behind the access token.
Base URL: https://api.chessscanner.com
Authenticate every request with the bearer token from authorization:
Authorization: Bearer ACCESS_TOKEN
Get the current user
GET /api/v1/me (scope user.read)
Returns the authenticated user's basic profile.
curl https://api.chessscanner.com/api/v1/me \
-H "Authorization: Bearer $TOKEN"
{
"data": {
"id": "5f8c…",
"name": "Magnus",
"email": "magnus@example.com",
"image": "https://cdn.chessscanner.com/…"
}
}
The id is a pairwise pseudonymous identifier. It's stable for your app but
different for every app, and it's never the user's real internal id. Use it as
the user key in your system.
List databases
GET /api/v1/databases (scope databases.read)
Returns the databases owned by the user.
curl https://api.chessscanner.com/api/v1/databases \
-H "Authorization: Bearer $TOKEN"
{
"data": [
{
"id": "9a1c…",
"name": "My Games",
"slug": "my-games",
"private": true,
"maxGames": 500,
"createdAt": "2026-06-14T10:00:00.000Z",
"updatedAt": "2026-06-14T10:00:00.000Z"
}
]
}
List games in a database
GET /api/v1/databases/{id}/games (scope games.read)
Returns the games in one database. Add format=pgn to download every game as a
single PGN file instead of JSON.
Parameters
- Name
- id
- Type
- Required
- Description
Database UUID or slug, in the path.
string- Name
- limit
- Type
- OptionalDefault: 50
- Description
Number of games per page (max 200).
integer- Name
- offset
- Type
- OptionalDefault: 0
- Description
How many games to skip.
integer- Name
- format
- Type
- Optional
- Description
Set to
pgnfor a bulk PGN export.
string
# JSON
curl "https://api.chessscanner.com/api/v1/databases/<id>/games?limit=25" \
-H "Authorization: Bearer $TOKEN"
# PGN
curl "https://api.chessscanner.com/api/v1/databases/<id>/games?format=pgn" \
-H "Authorization: Bearer $TOKEN"
List games
GET /api/v1/games (scope games.read)
Returns the user's games across all databases as a lightweight list (no moves
or PGN). For a single database, use GET /api/v1/databases/{id}/games.
Parameters
- Name
- limit
- Type
- OptionalDefault: 50
- Description
Number of games per page (max 200).
integer- Name
- offset
- Type
- OptionalDefault: 0
- Description
How many games to skip.
integer
{
"data": [
{
"id": "1b2d…",
"slug": "magnus-vs-hikaru",
"databaseId": "9a1c…",
"event": "Casual",
"date": "2026-05-01",
"result": "1-0",
"eco": "B90",
"white": {
"name": "Magnus",
"elo": 2830,
"fideId": "1503014",
"country": "NOR"
},
"black": {
"name": "Hikaru",
"elo": 2790,
"fideId": "2016192",
"country": "USA"
}
}
]
}
The white/black player refs include fideId and country in every game
list, the changes feed, and the game detail — use the FIDE ID to match players
against your own records.
Get a game
GET /api/v1/games/{id} (scope games.read)
Returns one game with full data, including the moves, PGN, and metadata — the
game's extra PGN tag pairs (e.g. a Site tag) as a JSON object. Add
format=pgn (or send Accept: application/x-chess-pgn) to get the PGN
directly; the PGN exports include those extra tags as well.
Parameters
- Name
- id
- Type
- Required
- Description
Game UUID, in the path.
string- Name
- format
- Type
- Optional
- Description
Set to
pgnto return the PGN instead of JSON.
string
curl "https://api.chessscanner.com/api/v1/games/<id>?format=pgn" \
-H "Authorization: Bearer $TOKEN"
{
"data": {
"id": "1b2d…",
"result": "1-0",
"white": { "name": "Magnus", "fideId": "1503014", "country": "NOR" },
"black": { "name": "Hikaru", "fideId": "2016192", "country": "USA" },
"fen": "…",
"moves": "e2e4 c7c5 …",
"pgn": "1. e4 c5 …",
"metadata": { "Site": "Oslo" }
}
}
Changes feed
GET /api/v1/changes (scope games.read)
The recommended way to keep an external system up to date. It returns games
ordered by when they last changed, including deleted ones (with deleted: true),
and pages through them with an opaque cursor.
Parameters
- Name
- cursor
- Type
- Optional
- Description
Opaque cursor from a previous response. Omit it on the first run.
string- Name
- since
- Type
- Optional
- Description
ISO timestamp to start from on the first run instead of a full backfill.
string- Name
- limit
- Type
- OptionalDefault: 100
- Description
Number of changes per page (max 500).
integer
# First run
curl "https://api.chessscanner.com/api/v1/changes?limit=200" \
-H "Authorization: Bearer $TOKEN"
# Later runs: pass the previous nextCursor
curl "https://api.chessscanner.com/api/v1/changes?cursor=<opaque>" \
-H "Authorization: Bearer $TOKEN"
{
"data": [
{
"id": "1b2d…",
"databaseId": "9a1c…",
"updatedAt": "2026-06-14T10:00:00.000Z",
"deleted": false,
"white": { "name": "Magnus" },
"black": { "name": "Hikaru" },
"result": "1-0"
}
],
"nextCursor": "<opaque>",
"hasMore": true
}
Treat the feed as a signal that something changed, then fetch the latest. For
full move data, follow up with GET /api/v1/games/{id} or ?format=pgn. For
instant updates, add a webhook instead of polling.
Rate limiting
Requests are rate limited per app and user. If you go over the limit you get
429 Too Many Requests with a Retry-After header and X-RateLimit-* headers.
Back off and retry after the time it indicates.
Errors
| Status | Meaning |
|---|---|
401 invalid_token | Missing, invalid, or expired access token. Re-authorize. |
403 insufficient_scope | The token lacks the required scope |
404 not_found | Resource not found, or not owned by the user |
429 rate_limited | Too many requests. See Retry-After. |
Errors use the shape { "error": "<code>", "error_description": "<message>" }.