Skip to content

Questarr API Reference

Doezer edited this page May 16, 2026 · 2 revisions

Base URL (local dev): http://<your_ip_or_domain>:<your_port>
Content-Type: application/json for all request bodies unless noted otherwise. Corresponding Questarr Version:: v1.3.0


Authentication Model

Questarr uses JWT Bearer tokens.

  1. obtain a token via POST /api/auth/login.
  2. Include the token in the Authorization header of every protected request:
    Authorization: Bearer <token>
    

Most /api/* routes require authentication. Exceptions are noted below.

Token payload contains id and username. Tokens are signed with JWT_SECRET (auto-generated if unset).


Common Error Response

All error responses share this shape:

{ "error": "Human-readable message", "details": [...] }

details is only present for validation failures (express-validator or Zod). In production, 5xx messages are replaced with "Internal Server Error".


Rate Limits

Limiter Window Max Requests Applied To
authRateLimiter 15 min 20 Login endpoint
sensitiveEndpointLimiter 1 min 30 Write operations

1. Auth

GET /api/auth/status

Auth: None
Purpose: Check whether initial setup has been completed.

Response 200:

{ "hasUsers": true }

POST /api/auth/login

Auth: None
Rate limit: authRateLimiter
Purpose: Authenticate and receive a JWT.

Request body:

Field Type Required
username string Yes
password string Yes

Response 200:

{
  "token": "<jwt>",
  "user": { "id": "uuid", "username": "admin" }
}

Errors: 400 missing/wrong types, 401 invalid credentials.


GET /api/auth/me

Auth: JWT required
Purpose: Returns the currently authenticated user.

Response 200:

{ "id": "uuid", "username": "admin", "steamId64": null }

2. System / Health

GET /api/health

Auth: None
Purpose: Liveness probe. Always returns 200 if the server process is running.

Response 200: { "status": "ok" }


GET /api/ready

Auth: None
Purpose: Readiness probe. Checks database connectivity and IGDB API reachability.

Response 200: { "status": "ok" }
Response 503: { "status": "error" }


3. Games (Collection)

All game routes require JWT authentication.

GET /api/games

Auth: JWT required
Purpose: List the authenticated user's game collection.

Query params:

Param Type Required Description
search string No Filter by title substring
status string No Comma-separated statuses: wanted, owned, completed, downloading
includeHidden "true" No Include hidden games

Response 200: Array of Game objects.


GET /api/games/status/:status

Auth: JWT required
Purpose: List games by a single status value.

Path param: status — one of wanted, owned, completed, downloading.

Query params:

Param Type Required Description
includeHidden "true" No Include hidden games

Response 200: Array of Game objects.


GET /api/games/search

Auth: JWT required
Purpose: Dedicated title search over the authenticated user's collection. Prefer this over GET /api/games?search= when you need a focused search: q is required and validated (1–200 chars), results can be capped with limit, and the endpoint cannot be combined with status filtering. Use GET /api/games?search= instead when you want to combine a title filter with status or retrieve the full collection.

Query params:

Param Type Required Constraints
q string Yes 1–200 chars
includeHidden "true" No
limit integer No 1–100

Response 200: Array of Game objects.


POST /api/games

Auth: JWT required
Rate limit: sensitiveEndpointLimiter
Purpose: Add a game to the collection. In Questarr, we first search for the game on IGDB to validate its title and retrieve the other informations. Note that they cannot be set manually, so ideally they should all be provided at call time.

Request body:

Field Type Required Constraints
title string Yes 1–500 chars
igdbId integer No min 1
summary string No max 5000 chars
coverUrl string No valid URL
releaseDate string No YYYY-MM-DD
rating number No 0–10
platforms string[] No each max 100 chars
genres string[] No each max 100 chars
publishers string[] No each max 200 chars
developers string[] No each max 200 chars
status string No default "wanted"

Response 201: Game object.
Errors: 400 validation, 409 game already in collection (returns { "error": "...", "game": {...} }).


PATCH /api/games/:id/status

Auth: JWT required
Rate limit: sensitiveEndpointLimiter
Purpose: Update a game's tracking status.

Path param: id — UUID.

Request body:

Field Type Required Values
status string Yes wanted, owned, completed, downloading
completedAt date No ISO date

Response 200: Updated Game object.
Errors: 400 validation, 404 not found.


DELETE /api/games/:id

Auth: JWT required
Rate limit: sensitiveEndpointLimiter
Purpose: Remove a game from the collection.

Path param: id — UUID.

Response 204: No content.
Errors: 404 not found.


POST /api/games/match-and-add

Auth: JWT required
Purpose: Quick-add a game by title — looks up the best IGDB match and adds it to the collection in one step.

Request body:

Field Type Required
title string Yes

Response 201: Game object.
Errors: 400 no title, 404 no IGDB match, 409 already in collection.


Common Workflows

Initial Setup

POST /api/auth/setup       → get token
POST /api/indexers         → add indexer(s)
POST /api/downloaders      → add download client

Add a Game and Start Downloading

GET  /api/igdb/search?q=Elden+Ring          → find IGDB data
POST /api/games                              → add to collection (status: "wanted")
GET  /api/indexers/search?query=Elden+Ring   → find releases
POST /api/downloads        { url, title, gameId }  → send to best downloader
                                             → game status auto-set to "downloading"

Track Download Completion

GET  /api/downloads                          → poll all downloaders
GET  /api/games/:id/downloads                → check tracked downloads for a game
PATCH /api/games/:id/status { status: "owned" }   → mark as owned when done

Data Shapes

Game Object

{
  id: string;              // UUID
  userId: string | null;
  igdbId: number | null;
  steamAppId: number | null;
  title: string;
  summary: string | null;
  coverUrl: string | null;
  releaseDate: string | null;  // "YYYY-MM-DD"
  rating: number | null;       // IGDB aggregated rating, 0–100
  userRating: number | null;   // personal rating, 0.5–10
  platforms: string[] | null;
  genres: string[] | null;
  publishers: string[] | null;
  developers: string[] | null;
  screenshots: string[] | null;
  source: "manual" | "steam" | "api";
  igdbWebsites: Array<{ category: number; url: string }> | null;
  aggregatedRating: number | null;
  status: "wanted" | "owned" | "completed" | "downloading";
  releaseStatus: string | null;  // "upcoming" etc.
  earlyAccess: boolean;
  hidden: boolean;
  searchResultsAvailable: boolean;
  addedAt: number | null;       // timestamp ms
  completedAt: number | null;   // timestamp ms
}