API
The web app is built entirely on Plinthio's own REST API, so anything it does, a script can do. Everything is JSON under /api.
Authentication
| Method | How | Use it for |
|---|---|---|
| API key | X-API-Key: plinthio_… | Scripts, dashboards, home automation |
| API key over HTTP Basic | Authorization: Basic base64(username:key) | Apps that only do username and password: OPDS readers, Mihon |
| Session token | POST /api/auth/login, then Authorization: Bearer <token> | The web app. Lasts 7 days, renewed by POST /api/auth/refresh |
| Media token | POST /api/auth/media-token, then ?token=<mediaToken> | Links that can't carry headers (images, audio, video). Only accepted on /api/media/*, valid 24 hours |
| Komga session cookie | Set by /api/v1 and /api/v2 after a key sign-in | Mihon's tracker. Only honoured on those paths |
| kosync headers | x-auth-user: <username>, x-auth-key: md5(key) | KOReader. Only on /api/kosync |
Make API keys in Settings → Apps & API keys. A key acts as its owner, with their role, content limit and Kids Mode. A session token in a URL is refused, as is a media token in a header (P102).
With two-factor on, POST /api/auth/login returns { twoFactorRequired, challenge } instead of a token. Finish with POST /api/auth/login/2fa and { challenge, code } or { challenge, recoveryCode }.
KEY=plinthio_...
curl -H "X-API-Key: $KEY" "http://plinthio:8088/api/items?mediaType=manga&progress=in_progress"Errors
Every error is JSON with a code:
{ "error": "The media file is missing from disk", "code": "P301" }Admins also get a detail field with the underlying cause. GET /api/errors returns the whole list of codes, with no sign-in needed.
Rate Limits
| Scope | Limit |
|---|---|
/api/auth/login, /api/auth/login/2fa | 10 failed attempts per 15 minutes per address |
The rest of /api/auth | 60 per 15 minutes |
| General API | 600 per minute |
/api/media, /api/opds, /api/v1, /api/v2, /api/kosync | 1200 per minute |
| Making, downloading and copying backups | 12 per 15 minutes |
A limited request gets HTTP 429 with P005 (or P108 for sign-in). Behind a reverse proxy, set TRUST_PROXY so limits apply per client rather than to everyone at once.
Routes
Paths below are relative to /api. V means any signed-in user, E editor or admin, A admin. Responses respect hidden titles, missing files, content limits and Kids Mode for the caller.
Sign-In and Account
| Route | Who | What it does |
|---|---|---|
GET /auth/setup-status, POST /auth/setup | public | The first-run wizard. Only while no users exist, and only from the home network |
POST /auth/login, GET /auth/me, POST /auth/refresh | public / V | Sign in, who am I, renew |
POST /auth/login/2fa | public | Second step of a two-factor sign-in. Five tries per challenge |
POST /auth/media-token | V | Media token for links |
POST /auth/sign-out-everywhere | V | Revoke every token for your account |
GET /auth/access | public | Whether this device is home, Tailscale or outside, and whether it may sign in from there |
GET /auth/2fa, POST /auth/2fa/setup, /2fa/enable, /2fa/disable, /2fa/recovery-codes | V (session only, not API keys) | Two-factor for your own account |
PATCH /users/preferences, PATCH /users/password | V | Your preferences (merged, not replaced) and password |
POST /users/avatar, DELETE /users/avatar, GET /users/:id/avatar | V | Profile picture |
GET /keys, POST /keys, DELETE /keys/:id | V | Your API keys. A new key is returned once |
Library and Titles
| Route | Who | What it does |
|---|---|---|
GET /libraries | V | Libraries with visible item counts |
POST /libraries, DELETE /libraries/:id, POST /libraries/:id/scan, GET /libraries/browse | A | Manage libraries and scans |
PATCH /libraries/:id with { kidsAllowed } | E | Kids Mode for a whole library |
GET /items | V | Titles. Filters: libraryId, mediaType, author, series, search, progress (unread, in_progress, finished, skipped), genre, addedWithinDays, sort, limit, offset |
GET /items/:id, /items/:id/extras, /credits, /collection, /chapters | V | One title, its extras, TMDB credits and facts, its collection, chapters |
GET /items/series, /items/series/:name?library=&type= | V | Series list and one series |
POST /items/series/:name/mark-read, /mark-unread | V | A whole series |
GET /items/genres, /items/authors, /items/folders | V | Filter data and disk folders |
POST /items/:id/hide, /unhide | V / A | Hide for yourself, or for everyone with { "global": true } (admins) |
GET /items/hidden | V | What you've hidden |
GET /series/:libraryId/:name/settings, PUT | V / E | Series name, reading direction, age rating |
GET /kids, GET /kids/status, POST /kids/titles, DELETE /kids/titles/:id | E | Kids Mode series and titles |
Progress, Bookmarks and Ratings
| Route | Who | What it does |
|---|---|---|
GET /progress/continue | V | The Continue row |
GET /progress/:itemId, POST /progress/:itemId | V | Read or save a position (currentTime/duration, currentPage/totalPages, isFinished, cfi for EPUB, playbackRate for audio) |
POST /progress/finish with { itemIds, finished } | V | Mark several finished or not started |
POST /progress/skip with { itemIds, skipped } | V | Skip or unskip volumes |
GET /bookmarks/:itemId, POST /bookmarks, PATCH and DELETE /bookmarks/:id | V | Bookmarks and notes |
GET /highlights/:itemId, POST /highlights, PATCH and DELETE /highlights/:id | V | EPUB highlights: cfiRange, text, color (yellow, green, blue, pink, orange), optional note |
GET /ratings/:itemId, PUT with { rating }, DELETE | V | Your stars, the community average and TMDB's score (as enabled) |
POST /activity/start, /end, GET /activity/me, GET /activity/admin | V / A | Reading and watching sessions, sign-in history |
GET /stats/me, GET /stats/admin | V / A | Statistics |
Media
These accept a media token as ?token=.
| Route | What it does |
|---|---|
GET /media/cover/:id?w=, /media/still/:id | Cover thumbnails (WebP) or the original (raw=true), and episode stills |
GET /media/stream/:id | Audio, with byte ranges |
GET /media/manga/:id/pages, /media/manga/:id/page/:index | Page list and page images (0-based) for comics, manga and PDFs |
GET /media/book/:id/file | The EPUB or PDF file |
GET /media/video/:id/playback-info?clientHevc= | Playback mode, tracks and qualities |
GET /media/video/:id/stream | Direct play, with byte ranges |
GET /media/video/:id/hls/master.m3u8?audio=, POST …/hls/stop | HLS streaming |
GET /media/video/:id/subtitles/:track.vtt | Subtitles as WebVTT |
GET /media/video/:id/trickplay/index.json, sheet_N.jpg | Scrub previews |
GET /media/video/:id/markers, PUT (E) | Intro and credits ranges |
Lists, Requests and Parties
| Route | Who | What it does |
|---|---|---|
/collections… | V | Custom folders and lists: create, add library or outside titles, reorder, remove |
GET /requests, POST /requests, DELETE /requests/:id | V | Your requests |
GET /requests/pending-count, PATCH /requests/:id | E | The review queue |
POST /party, GET /party/:code, GET /party/:code/events, POST /party/:code/{action,buffering,item,settings,chat}, DELETE /party/:code | V | Watch parties (when turned on). Events are server-sent events |
Metadata
| Route | Who | What it does |
|---|---|---|
GET /metadata/search?mediaType=&query=&year= | V | Search providers |
POST /metadata/apply/:itemId, /metadata/cover/:itemId, /metadata/series-cover | E | Apply a match, upload covers |
GET /metadata/admin/items, POST /metadata/admin/match-single/:itemId, /batch-match, /batch-clean-titles | E | Bulk tools |
Server
| Route | Who | What it does |
|---|---|---|
GET /customization, PATCH | public / A | Branding, theme, layout, ratings, collections, parties, pause screen, opening sequence |
POST /customization/intro, DELETE | A | Upload (multipart field intro) or remove the opening sequence clip |
GET /media/intro | V | The opening sequence clip |
GET /settings/filters, PATCH | V / A | Which shelf views are offered |
/settings/transcoding, /transcoding/test, /metadata-providers, /metadata-providers/tmdb-key, /auto-scan, /backup/* | A | Server settings and backups |
GET /admin/health, POST /admin/health/remove-missing | A | Library Health, and removing missing titles |
POST /admin/health/offload, /unoffload | A | Keep titles as history ({ itemIds }), or undo it |
GET /admin/logs, DELETE | A | Server log |
GET /users, POST /users, PATCH /users/:id, /:id/role, /:id/password, /:id/extend, DELETE /users/:id | A | Accounts |
POST /users/:id/two-factor/reset | A | Turn off someone's two-factor and sign them out |
GET /admin/network, PATCH | A | Away-from-home settings and status |
GET /system/update | A | Update check status |
GET /health, GET /errors | public | Health check (with version) and the error code list |
Reading Apps
| Prefix | What it is |
|---|---|
/api/opds | OPDS 1.2 catalog with page streaming (HTTP Basic, key as password) |
/api/v1, /api/v2 | The Komga-compatible subset Mihon uses |
/api/kosync | KOReader progress sync |
Examples
Intro and Credits Markers
There's no screen for setting intro and credits ranges yet. Editors and admins set them per episode through the API. The request replaces all of that episode's markers. Types are intro and credits.
curl -X PUT -H "X-API-Key: $KEY" -H "Content-Type: application/json" \
-d '{"markers":[{"type":"intro","startSeconds":62,"endSeconds":152},{"type":"credits","startSeconds":2580,"endSeconds":2640}]}' \
http://plinthio:8088/api/media/video/<itemId>/markersHiding a Title From Everyone
Admins can hide a title for every account, and undo it:
curl -X POST -H "X-API-Key: $KEY" -H "Content-Type: application/json" \
-d '{"global": true}' http://plinthio:8088/api/items/<id>/hide
curl -X POST -H "X-API-Key: $KEY" -H "Content-Type: application/json" \
-d '{"global": true}' http://plinthio:8088/api/items/<id>/unhideHealth Check
curl http://<server>:8088/api/healthReturns {"status":"healthy","version":"…","app":"Plinthio"}.