API reference
Collectoh API v1: 576 endpoints in 57 areas, drawn from the OpenAPI document your own server publishes.
On this page
The Collectoh web app does everything through this API, so a script can do anything the app can. It is JSON over HTTP, versioned, and described by an OpenAPI 3.1 document that your own server publishes. This page covers what holds for every endpoint; the reference for each one follows.
Base URL
Every path in the reference is relative to /api/v1 on your own server, at the same address and port as the web app:
curl https://media.example.com/api/v1/server/infoGET /server/info and GET /server/health need no sign-in, so they make a good first call. Requests and answers are JSON, except streams, images and files.
Authentication
- Session cookies. The web app signs in with
POST /auth/loginand keeps its session in HttpOnly cookies. A request signed in by cookie needs anX-Client-Idheader to change anything, and a page on another site cannot set one. - Tokens. An app that signs people in posts a username and password to
POST /auth/login. It gets an access token, good for 15 minutes, and a refresh token, good for 30 days.- Send the access token as
Authorization: Bearer …. - Trade the refresh token for a new pair at
POST /auth/refresh. Each refresh token works once; presenting one twice ends that sign-in. - An account with two-factor authentication answers
AUTH_2FA_REQUIREDwith achallenge, to complete atPOST /auth/2fa/verify.
- Send the access token as
- API keys. For scripts and integrations: a long-lived key that belongs to an account. It can do at most what that account can, and only within the scopes it was made with.
To create a key:
- Open your Account page (the avatar menu, then Settings), find API keys and choose New key.
- Name it after what will use it, tick its scopes, and choose when it expires and how many requests a minute it may make.
- Choose Create key and copy the key, which starts
co_. It is shown only once.
Send it as a bearer token:
curl -H "Authorization: Bearer co_…" https://media.example.com/api/v1/librariesA key is checked on every request, so revoking it works from the next one (401 AUTH_TOKEN_INVALID); an expired key answers AUTH_TOKEN_EXPIRED. Keys are made by a signed-in person, in the app or with POST /api-keys, never by another key. Child and guest accounts cannot hold them.
Whatever the credential, send X-Client-Id (a stable id for your installation) and X-Client-Name. They are how the server tells devices apart; a play queue, for one, belongs to a device.
Permissions and scopes
Each operation in the reference names the permission it needs (x-permission in the OpenAPI document). A role decides which permissions an account has, and a key's scopes narrow them further.
| Scope | Allows |
|---|---|
library:view | Browsing and playing everything the account can see |
library:manage | Creating, changing and deleting libraries and storages |
item:edit | Editing metadata, artwork, markers and matches |
user:manage | Managing accounts, invitations and access |
settings:manage | Changing server settings, themes and webhooks |
scan:trigger | Starting and cancelling scans |
session:manage | Seeing and ending anyone's playback sessions |
job:manage | Retrying and cancelling background jobs |
download:own | Making and fetching offline copies |
playlist:own | Creating and sharing playlists and collections |
photo:upload | Uploading photos |
A key cannot hold a scope its account lacks (403, listed in details.scopes). Library access, age limits, labels and viewing hours apply to every request as they do to the person. Anything the caller may not see answers 404, as if it did not exist.
Pagination and filters
Lists page with a cursor, not an offset, so a scan adding items while you page cannot shift the results:
curl -H "Authorization: Bearer co_…" \
"https://media.example.com/api/v1/libraries/$LIBRARY_ID/items?limit=50&sort=-added_at&filter=year:gte:2000;genre:in:horror,thriller;unwatched:eq:true"{
"items": [{ "id": "…", "type": "movie", "title": "The Thing", "year": 1982 }],
"page": { "cursor": "eyJzIjoiVGhlIFRoaW5nIiwiaSI6ImFiYyJ9", "hasMore": true, "total": 1284 }
}Each item here is trimmed to a few fields. Send page.cursor back as cursor until hasMore is false. total comes with the first page. It is exact up to 10,000 items and an estimate beyond that, flagged by totalIsEstimate.
Filters. A filter is a set of field:op:value conditions joined by ;, all of which must hold, with , between list values.
- Operators:
eq,ne,gt,gte,lt,lte,in,nin,contains,starts,between,exists. - Fields include:
type,title,year,added_at,rating,age_ratingandperson;- tags such as
genreandlabel; - the caller's own
progress,watched,unwatched,play_countandlast_played; - custom fields, as
custom.<key>.
- Dates may be relative, such as
now-30d. - Limits: a filter has at most 20 conditions, and a field not on the list is a
400, never a guess.
Sorting. sort takes up to three fields, with - for descending, or random for a shuffle that holds all day.
Smart collections and playlists store the same rules as JSON, nesting all and any groups up to three levels deep.
?include= adds related data to items, such as credits, artwork, progress or children; each endpoint lists what it accepts.
Errors
Every error has the same shape and a stable code to branch on:
{
"error": {
"code": "STREAM_LIMIT_REACHED",
"message": "This account can watch 2 streams at a time. Stop one and try again.",
"details": { "cap": 2, "active": 2 },
"requestId": "01J8XQ4TQ0N0M6W8F2B8V7Y3KZ"
}
}The message is for people and may change; the code will not. requestId matches the server's log lines for the request.
| Status | Common codes |
|---|---|
| 400 | VALIDATION_FAILED, with details.issues |
| 401 | UNAUTHENTICATED, AUTH_TOKEN_EXPIRED, AUTH_TOKEN_INVALID, AUTH_2FA_REQUIRED |
| 403 | FORBIDDEN, with details.permission; PERMISSION_TIME_RESTRICTED, with details.resumesAt |
| 404, 409, 413 | NOT_FOUND, CONFLICT, PAYLOAD_TOO_LARGE |
| 415 | MEDIA_NOT_PLAYABLE, MEDIA_UNSUPPORTED_CODEC, MEDIA_TRANSCODE_DISABLED |
| 423 | STORAGE_UNAVAILABLE |
| 429 | RATE_LIMITED, STREAM_LIMIT_REACHED |
| 502 | PROVIDER_UNAVAILABLE, PLUGIN_ERROR |
| 503 | SERVICE_STARTING |
Anything unexpected is 500 INTERNAL with a generic message, and the detail stays in the log.
Rate limits
Each API key has its own limit per minute: 600 unless it was made with another, up to 10,000. Every answer to a key's request carries three headers:
X-RateLimit-Limit: the key's limit per minute.X-RateLimit-Remaining: requests left this minute.X-RateLimit-Reset: seconds until the minute starts again.
Over the limit, the answer is 429 RATE_LIMITED with details.retryAfterSec. Sign-in has its own limit: ten attempts a minute per address and per username.
Live events
GET /events is a server-sent event stream of what happens on the server:
curl -N -H "Authorization: Bearer co_…" \
"https://media.example.com/api/v1/events?types=library.scan.completed,item.added"Each event arrives with its id and name, and an envelope as its data:
{
"id": "01J8XQ4TQ0N0M6W8F2B8V7Y3KZ",
"event": "playback.started",
"at": "2026-09-28T19:04:11.512Z",
"payload": { "sessionId": "…", "userId": "…", "itemId": "…", "mediaFileId": "…", "method": "direct_play", "isRemote": false, "reasons": [] }
}The events are:
library.scan.started,library.scan.progress,library.scan.completedandlibrary.scan.failed;item.added,item.updatedanditem.deleted;playback.started,playback.paused,playback.resumed,playback.progressandplayback.stopped;download.ready;user.created,invite.accepted,library.access.grantedandplaylist.shared;notification.createdandbug.reported;system.storage.unavailable,system.storage.availableandsettings.changed;watch.updated,watch.chatandwatch.ended, for Watch Together rooms;cast.commandandcast.device.updated, between devices;webhook.test.
types takes exact names, comma-separated.
A comment line every 20 seconds keeps proxies from closing the stream. After a drop, reconnect with the last id you saw in Last-Event-ID (or ?lastEventId=), and the server replays what you missed from its last thousand events.
The stream is scoped to the caller:
- Events about a library reach only those who can see it.
- Events about a person reach that person and those who manage sessions.
- Without
library:manage, an item event carries ids only. - No event carries a file path.
Webhooks
Webhooks push the same events to a URL. An admin creates one under Admin → Webhooks (see Backup, restore and upkeep) or with POST /webhooks. A webhook subscribes to exact event names, families such as playback.*, or *; casting's cast.* events are never sent. A webhook whose format is discord or slack posts a sentence into a chat room instead (see Backup, restore and upkeep); otherwise each delivery is a POST of the event envelope as JSON, with any headers you configured and these:
| Header | Value |
|---|---|
X-Collectoh-Event | The event's name |
X-Collectoh-Delivery | A unique id for the delivery |
X-Collectoh-Signature | sha256= followed by the hex HMAC-SHA256 of the raw body, keyed with the webhook's secret |
The secret is generated when the webhook is made and shown once, unless you supply your own of 16 characters or more; GET /webhooks/{id}?secret=true shows it again. Check the signature over the raw bytes, before parsing:
import { createHmac, timingSafeEqual } from 'node:crypto';
export function isFromCollectoh(rawBody: string, signature: string | undefined, secret: string): boolean {
if (signature === undefined) return false;
const expected = Buffer.from(`sha256=${createHmac('sha256', secret).update(rawBody, 'utf8').digest('hex')}`);
const actual = Buffer.from(signature);
return expected.length === actual.length && timingSafeEqual(expected, actual);
}Answer with any 2xx within ten seconds. Anything else is retried after 10 seconds, 1 minute, 10 minutes, 1 hour and 6 hours, and a webhook switches itself off after 20 deliveries in a row have failed. POST /webhooks/{id}/test sends a webhook.test delivery and reports the status, body and signature.
The OpenAPI document and the client
Your server publishes this API's description at /api/v1/openapi.json (OpenAPI 3.1) and a readable version at /api/v1/docs. Neither needs a sign-in, and both work offline. Each operation has an operationId, its schemas, its errors and its permission.
@collectoh/client is the typed TypeScript client the Collectoh apps use, generated from that document. It has no dependencies and runs in browsers, Node 18 or later, and React Native:
npm install @collectoh/clientimport { CollectohError, createClient } from '@collectoh/client';
const client = createClient({
baseUrl: 'https://media.example.com',
clientId: 'my-script-7f3a9c',
clientName: 'My script',
auth: { mode: 'api-key', key: process.env.COLLECTOH_KEY ?? '' },
});
try {
for (const library of await client.getLibraries()) {
const page = await client.getLibrariesByIdItems({
params: { id: library.id },
query: { limit: 50, filter: 'unwatched:eq:true', sort: '-added_at' },
});
console.log(library.name, page.items.map((item) => item.title));
}
} catch (error) {
if (error instanceof CollectohError) console.error(error.code, error.message, error.requestId);
else throw error;
}Methods are named by operationId. A failure is a CollectohError with its code, status, requestId and details. With a session, a 401 triggers one token refresh and one retry, never a loop; a key is never refreshed. onRateLimit in the options reports a key's remaining requests after each answer.
Versioning
/api/v1 changes only by adding: new endpoints, new optional parameters and new fields. A breaking change arrives as /api/v2. v1 is then kept for two minor releases, with Deprecation and Sunset headers on its answers in the meantime.
Endpoints by area
- Server6 endpointsIdentity and health. Public, so a client can find a server before it has a session.
- Setup5 endpointsThe first-run wizard. Only answers until an owner exists (
SETUP_ALREADY_COMPLETEafter). - Auth33 endpointsSessions, tokens, profiles, two-factor and password reset. docs/11 §1.
- API keys4 endpointsLong-lived scoped credentials for scripts (P6-02):
Authorization: Bearer co_…. A key does at most what its account does, within its scopes; each has its own per-minute limit, reported inX-RateLimit-*. Keys cannot manage keys. - Users17 endpointsAccounts, the permission matrix, devices and invites (
user:manage). - Libraries20 endpointsLibraries and their folders, ordering and trash.
- Storage15 endpointsStorage providers and host browsing (
library:manage). - Scanning5 endpointsStart, watch and cancel a scan.
- Items22 endpointsBrowse a library, read an item, its children and tree.
include=expands relations (docs/05 §1). - Editing13 endpointsChange what is known about an item, lock fields, match, refresh and delete (
item:edit). - Artwork16 endpointsPosters, backdrops and portraits: serve, list candidates, upload and choose.
- People9 endpointsCast and crew.
- Faces11 endpointsFaces found in photos by a local model, gathered to be named (P6-14).
- Studios2 endpointsStudios and networks, per library.
- Playback19 endpointsThe playback decision, progress, watched state, reading position and sessions. docs/08.
- Streaming12 endpointsHLS delivery for a transcode session, direct file streams and pre-roll.
- Subtitles22 endpointsSidecar and embedded subtitles, fonts, sync and search.
- Play queue9 endpointsThe ordered thing being played, per account (P1-40).
- Collections9 endpointsManual and smart collections (P3-08, P3-09).
- Playlists14 endpointsPlaylists, sharing and M3U import/export (P3-10).
- Labels6 endpointsLabels and label-based restrictions (P3-23).
- Saved filters3 endpointsNamed filter strings per library.
- Search5 endpointsSearch across items, people and transcripts; type-ahead suggestions.
- Courses11 endpointsCourse outlines, lesson completion, learning progress and resources (docs/07).
- Notes10 endpointsNotes and bookmarks on lessons and videos, and their export.
- Music10 endpointsAcoustic features and sonic similarity (P3-29); an artist’s popular tracks and similar artists.
- Discover6 endpointsWhat is in cinemas, new on DVD and Blu-ray and to stream, and on an account’s streaming services; where a title can be watched (P6-42, P6-43). TMDB’s lists, with JustWatch’s streaming data.
- Watchlist and friends12 endpointsA watchlist across every streaming service (P6-49), and ratings, reviews and friends’ activity on this server (P6-50). Friends are the people on the same server; each shares only what they choose (
sharingin their preferences), and nobody sees anything about an item they couldn’t open themselves. - Live TV45 endpointsThe owner’s own channels, from an HDHomeRun tuner on the home network or an M3U playlist from an IPTV provider, what is on them from an XMLTV guide, watching them, and the DVR (P6-44, P6-45). Watching takes
library:viewand an account without an age limit; sources, guides and channelssettings:manage; the DVRlibrary:manage. - Uploads4 endpointsChunked photo uploads into a library (P3-35).
- Downloads7 endpointsOffline copies: request, sync and remove (P3-14).
- Cast6 endpointsHand playback to a receiver (P3-15).
- Watch Together11 endpointsOne room, the server’s clock (P5-11).
- Home5 endpointsThe home screen and its layout (P3-19, P5-05).
- Notifications8 endpointsIn-app notifications, preferences and web push (P3-21).
- Events1 endpointServer-sent events for live updates.
- Webhooks7 endpointsOutbound webhooks and their deliveries (P3-17).
- Email7 endpointsEmail from the server: through its own mail server, else collectoh.com while claimed. Invitations, reset links, alerts and the weekly digest. docs/11 §12.
- Themes7 endpointsUI themes: list, read, validate and save (P5-02).
- Custom fields4 endpointsPer-type custom fields (P5-06).
- Library types5 endpointsCustom library types (P5-07).
- Support6 endpointsBug reports sent from the app: anyone signed in may send one; reading and resolving them takes
settings:manage, and every admin is notified of each. - Games18 endpointsGames libraries (2026-10-05): starting a game in the browser with the EmulatorJS the owner added to the server — Collectoh brings no emulator, BIOS or game — and the emulator’s status for the admin.
- Comics3 endpointsComics libraries (2026-10-13): a comic as the reader opens it — CBZ, CBR, CB7, CBT, PDF, an EPUB of page images or a folder of them — its pages one at a time, and a series’ comics in number order. Where a reader is goes through
PUT /items/:id/reading(page:<n>), as a book’s does. - Icons10 endpointsLibrary icons: the icon libraries that ship with the server (Lucide, Font Awesome Free) and the ones an owner makes by uploading icons or importing an Iconify JSON file. Choosing and managing them takes
library:manage; the pictures are for anyone who can see a library. - Branding5 endpointsYour logo (P6-17): the server’s own main logo, in place of Collectoh’s mark in the app and its tab, and its loading logo, each for dark themes and light. Reading them answers anyone, since the sign-in page and the loading screen show them; changing them takes
settings:manage. - Collectoh account6 endpointsWhich Collectoh account this server belongs to (P6-19, docs/14 §5), claimed the way a television signs in: the server shows a short code and its owner confirms it on collectoh.com. The first-run wizard does it under
/setup/claim, open while there is no owner; afterwards/claimtakessettings:manage. Only the owner has an account; everyone they invite signs in to this server alone. - Collectoh Plus4 endpointsCollectoh Plus on this server (P6-20, docs/14 §6): on while the server holds a licence that checks out offline, signed by collectoh.com and not run out. One comes with each daily check-in of a claimed server, good for 30 days without another; an account’s licence key can be pasted in instead, for a server that cannot sign in. Without Plus, what only it does is refused with
PLUS_REQUIRED, whose details name the feature; when it lapses nothing is deleted, and what only Plus shows is out of sight until it is back. - Settings7 endpointsServer-wide settings (
settings:manage). - Plugins14 endpointsOut-of-process metadata and storage providers (P6-03, docs/13-plugins.md). A plugin is installed off, enabled with exactly the permissions the admin grants at the prompt, and runs under Node’s permission model with no filesystem beyond its folder.
settings:manage. - Spine3 endpointsThe identity spine (P6-16, ADR-011): what the open world knows a film, a show, an artist, an album or a book to be, from Wikidata (CC0), loaded as a pack and asked before any catalogue so a known title is fetched by id and never searched.
- Diagnostics23 endpointsHardware, storage and remote-access checks, Collectoh Relay; the support bundle.
- Maintenance2 endpointsScheduled tasks and the maintenance window (P1-36).
- Backup12 endpointsBackups, restore and personal data export (P3-18).
- Import14 endpointsA household brought over from Plex, Jellyfin or Emby (P7-06): the other server connected with its owner’s token or an API key, kept sealed and never returned; its people offered as accounts here, invitations or nothing (never accounts made with passwords); its libraries’ folders offered, never mounted; and an import that reads — never writes — each person’s watched state, positions, ratings, favourites and playlists and the collections, matched here by the ids both sides hold. An admin sees counts of anyone’s state, never titles.
user:manage. - Jobs3 endpointsQueue status, retry and cancel (
job:manage). - Stats3 endpointsWatch history and server-wide statistics (P3-20).