Skip to content

Public API (v1)

Narratorr exposes a supported, versioned REST API under /api/v1/. It’s the contract third-party tools build against — search providers, add books by ASIN, list your library, and queue downloads from your own scripts or companion apps.

Narratorr is single-user and self-hosted: the authenticated user is the server operator, and there’s exactly one library. The v1 API reflects that — pagination is offset/limit (not cursor), there’s no per-key scoping or multi-tenant model, and the surface is deliberately small.

Narratorr serves a live OpenAPI spec and browsable Swagger UI at:

http://your-narratorr-host:3000/api/v1/docs

The docs subtree is reachable without an API key so consumers can read the contract before authenticating. Every native v1 endpoint, request schema, and response shape is generated directly from the running server’s code, so it’s always in sync with your version. The /json and /yaml sub-paths serve the raw OpenAPI document.

If you run behind a reverse proxy with a URL_BASE, the docs live under {URL_BASE}/api/v1/docs.

All v1 data endpoints require an API key. The key is auto-generated on first run — find it in Settings > Security.

Pass it one of two ways:

MethodExampleNotes
HeaderX-Api-Key: your-api-keyRecommended.
Query parameter?apikey=your-api-keyConvenient for webhook URLs, but query strings can appear in server logs and referrer headers.
Terminal window
curl -H "X-Api-Key: your-api-key" \
"http://your-narratorr-host:3000/api/v1/books"

A rejected key on a native v1 route returns the standard v1 error envelope:

{ "error": { "code": "INVALID_API_KEY", "message": "Invalid API key" } }

These hold across every native v1 endpoint.

List responses are always an object, never a bare array:

{ "data": [ /* ...items... */ ], "total": 42 }

total is the count of all matching rows, not just the page returned.

Pagination is offset/limit. Both are optional query params on every list endpoint:

ParamTypeBoundsDefault
limitinteger1500Endpoint-specific (omit for the server default)
offsetinteger≥ 00

Identifiers are opaque, non-enumerable strings — never database row IDs. Each entity type carries a prefix so the kind is obvious at a glance:

EntityID prefixExample
Bookbk_bk_3Qp9z...
Authorau_au_7Kx2m...
Narratornr_nr_Lp4Vn...
Seriessr_sr_9Wd1c...
Downloaddl_dl_2Hq8t...

Dates are ISO 8601 strings (e.g. 2026-06-22T14:30:00.000Z).

Unknown query params and body fields are rejected with a 400, not silently ignored — a misspelled filter is an error, so typos surface immediately.

Errors use a stable envelope with a machine-readable code and a human-readable message:

{ "error": { "code": "NOT_FOUND", "message": "Resource not found" } }
StatusWhen
400Validation failure — bad/unknown query param or body field.
401Missing or invalid API key.
404The requested ID doesn’t resolve to a row.
409Conflict — the resource already exists, or the book already has an active download.
422The request was well-formed but couldn’t be fulfilled (e.g. ASIN didn’t resolve).
429Upstream metadata provider rate-limited the lookup. A Retry-After header is included.
500Internal server error.
502 / 504A download client or metadata provider was unreachable or timed out.

Search audiobook metadata providers (Audible/Audnexus). These are pre-library results — they carry an asin, not a library id. Use this to find a book’s ASIN before adding it.

Query paramTypeRequiredNotes
qstringYesSearch query. Trimmed; 1–500 characters.

Each result book exposes:

FieldTypeNotes
asinstring?Audible ASIN. Optional — not every provider record has one.
titlestring
authorsarray{ name, asin? } per author.
narratorsarray{ name } per narrator.
seriesobject?{ name, position? } — present only when the result belongs to a series.
coverstring?Cover image URL.
publishedDatestring?
libraryobject?Cross-reference: present only if this ASIN is already in your library. { bookId, status } — the bk_ id and its current status.

The library field is the quick way to tell, in a search UI, which results you already own. A no-match search returns { "data": [], "total": 0 } with a 200, never a 404.

Terminal window
curl -H "X-Api-Key: your-api-key" \
"http://your-narratorr-host:3000/api/v1/metadata/search?q=project+hail+mary"
{
"data": [
{
"asin": "B08G9PRS1K",
"title": "Project Hail Mary",
"authors": [{ "name": "Andy Weir", "asin": "B00G0WYW92" }],
"narrators": [{ "name": "Ray Porter" }],
"cover": "https://m.media-amazon.com/images/I/...jpg",
"publishedDate": "2021-05-04",
"library": { "bookId": "bk_3Qp9z...", "status": "imported" }
}
],
"total": 1
}

Your library. List and inspect books, or add a new one by ASIN.

List books in the library.

Query paramTypeNotes
limit, offsetintegerPagination (see Conventions).
statusstringFilter by exact status. Matches the canonical state exactly — downloading returns only books that are exactly downloading.
authorstringFilter by author name.
seriesstringFilter by series name.
narratorstringFilter by narrator name.
sortFieldstringOne of createdAt, title, author, narrator, series, quality, size, format.
sortDirectionstringasc or desc.

Fetch one book by its bk_ id. Returns 404 if the id doesn’t resolve.

The book DTO (returned by both endpoints, and by the add-by-ASIN endpoint below):

FieldTypeNotes
idstringOpaque bk_ id.
titlestring
authorsarray{ id, name } per author — id is an au_ reference. Ordered primary-first.
narratorsarray{ id, name } per narrator — id is an nr_ reference.
seriesobject?{ name, position } (position may be null), or null when the book has no series.
statusstringCurrent lifecycle status.
{
"id": "bk_3Qp9z...",
"title": "Project Hail Mary",
"authors": [{ "id": "au_7Kx2m...", "name": "Andy Weir" }],
"narrators": [{ "id": "nr_Lp4Vn...", "name": "Ray Porter" }],
"series": null,
"status": "imported"
}
StatusMeaning
wantedTracked, awaiting a search.
searchingIndexer search in progress.
downloadingA release is downloading.
importingDownload complete; importing into the library.
importedIn the library.
missingWanted but no release found.
failedA search, download, or import failed.

Add a book to the library by ASIN. The request body is ASIN-only — Narratorr hydrates the full record itself from the metadata provider; you don’t supply title, author, or any other field.

{ "asin": "B08G9PRS1K" }

On success, returns 201 with the newly created book DTO. If your General settings have search immediately enabled and the book lands in the wanted state, Narratorr fires a release search in the background (the response returns right away regardless).

StatusBodyMeaning
201Book DTOCreated.
400Error envelopeMissing/blank asin or an extra body field.
409{ error, existingId }A book with this ASIN is already in the library. existingId is its bk_ id — a lost-response retry can re-resolve to it.
422Error envelopeasin_not_resolved (provider miss), invalid_record (incomplete provider record), or edition_rejected (the edition matches your reject-words filter).
429Error envelopeProvider rate-limited. Includes a Retry-After header.
502Error envelopeProvider lookup failed (transient).
Terminal window
curl -X POST -H "X-Api-Key: your-api-key" -H "Content-Type: application/json" \
-d '{"asin":"B08G9PRS1K"}' \
"http://your-narratorr-host:3000/api/v1/books"

The 409 conflict body:

{
"error": { "code": "book_exists", "message": "A book with this ASIN already exists" },
"existingId": "bk_3Qp9z..."
}

Three read-only reference resources with an identical shape. Each is listable regardless of whether it’s linked to a book in your library.

EndpointReturns
GET /api/v1/authors · GET /api/v1/authors/:idAuthors (au_ ids).
GET /api/v1/narrators · GET /api/v1/narrators/:idNarrators (nr_ ids).
GET /api/v1/series · GET /api/v1/series/:idSeries (sr_ ids).

List endpoints accept limit and offset only — no filter or sort params; the order is fixed. The DTO for all three is the same:

FieldType
idstring (entity-prefixed opaque id)
namestring
{
"data": [
{ "id": "au_7Kx2m...", "name": "Andy Weir" },
{ "id": "au_5Tn8p...", "name": "Brandon Sanderson" }
],
"total": 2
}

Read your download/import activity.

List downloads. Accepts limit and offset only — no filters or sort.

Fetch one download by its dl_ id.

The download DTO:

FieldTypeNotes
idstringOpaque dl_ id.
titlestringRelease title.
statusstringSingle derived display status — see below.
clientStatusstringRaw download-client state: queued, downloading, paused, completed, failed.
pipelineStagestringNarratorr’s processing overlay: idle, checking, pending_review, importing, imported.
bookobject?{ id } — the linked book’s bk_ id, or null if the download has no associated book.
protocolstringtorrent or usenet.
progressnumberDownload progress (0–100).
addedAtstringISO 8601.
completedAtstring?ISO 8601, or null if not yet complete.
errorMessagestring?Error detail, or null.

status is a single value derived from the (clientStatus, pipelineStage) pair so simple consumers don’t have to combine the two axes themselves. Both raw axes are also exposed if you need them. Derived status is one of: queued, downloading, paused, completed, checking, pending_review, importing, imported, failed.

{
"id": "dl_2Hq8t...",
"title": "Project Hail Mary [B08G9PRS1K]",
"status": "importing",
"clientStatus": "completed",
"pipelineStage": "importing",
"book": { "id": "bk_3Qp9z..." },
"protocol": "usenet",
"progress": 100,
"addedAt": "2026-06-22T14:30:00.000Z",
"completedAt": "2026-06-22T14:52:11.000Z",
"errorMessage": null
}

The write path is two steps: search a book’s indexers for candidate releases, then grab one. This mirrors how the web UI queues a download — it’s not an entity POST.

Each search result carries an opaque, signed releaseId token. You pass that exact token back to grab. The token encodes everything Narratorr needs to fetch the release (download URL, protocol, indexer) without exposing those raw fields in the response, and it’s HMAC-signed server-side so it can’t be forged.

Search indexers for releases of the given book (by its bk_ id). No request body.

Returns a list of release candidates:

FieldTypeNotes
releaseIdstringOpaque signed token — pass back to grab.
titlestringRelease title.
authorstring?null if unknown.
narratorstring?null if unknown.
protocolstringtorrent or usenet.
sizenumber?Bytes, or null.
seedersnumber?Torrents only; null otherwise.
indexerstringSource indexer name.
isFreeleechboolean
matchScorenumber?Relevance score, or null.
StatusMeaning
200Results (possibly an empty list).
400The book’s derived search query is empty after normalization.
404No book with that id.

Grab a release from the search results. The body carries the opaque releaseId from a search result:

{ "releaseId": "eyJkb3dubG9hZFVybCI6..." }

Returns the resulting download DTO.

This endpoint is idempotent per release: if you retry after a timeout (the response was lost but the grab succeeded), Narratorr recognizes the same release for the same book and returns the existing download instead of starting a second one.

StatusBodyMeaning
201Download DTONew download queued.
200Download DTOA matching download already exists — returned instead of creating a duplicate (idempotent retry).
400Error envelopeInvalid or malformed releaseId.
401Error envelopeDownload client authentication failed.
404Error envelopeNo book with that id.
409Error envelopeThe book already has an active download.
502Error envelopeDownload client error.
504Error envelopeDownload client request timed out.
Terminal window
# 1. Find candidates
curl -X POST -H "X-Api-Key: your-api-key" \
"http://your-narratorr-host:3000/api/v1/books/bk_3Qp9z.../search"
# 2. Grab one using its releaseId
curl -X POST -H "X-Api-Key: your-api-key" -H "Content-Type: application/json" \
-d '{"releaseId":"eyJkb3dubG9hZFVybCI6..."}' \
"http://your-narratorr-host:3000/api/v1/books/bk_3Qp9z.../grab"

Version and build information for the running server — handy as a health probe or a “what version am I on?” check from a companion app. Like every v1 endpoint, it requires an API key. Returns a single object (not a list):

FieldTypeNotes
versionstringNarratorr version (semver, or dev for an unversioned build).
commitstringGit commit the build was cut from (unknown if not stamped).
buildTimestringISO 8601 build timestamp (unknown if not stamped).
nodeVersionstringNode.js runtime version, e.g. v24.0.0.
osstringHost OS type and release, e.g. Linux 5.10.0.
Terminal window
curl -H "X-Api-Key: your-api-key" \
"http://your-narratorr-host:3000/api/v1/system"
{
"version": "1.0.0",
"commit": "1a2b3c4",
"buildTime": "2026-06-30T12:00:00.000Z",
"nodeVersion": "v24.0.0",
"os": "Linux 5.10.0"
}

The response is limited to exactly these fields — operational details like the library path, disk usage, or database size are deliberately not exposed here.

/api/v1/ is the supported, stable contract. The internal /api/* routes are unversioned and may change at any time — don’t build against them. When a breaking change to the public API is needed, it ships under a new version prefix (/api/v2/), leaving /api/v1/ intact.