Skip to content

Overview

The Kivo media server's public API.

The contract between the Kivo server and every client: the web app, the Android TV app and third-party tools. It is long-lived; changes are additive.

Conventions

  1. Paths. Everything is under /api/v1, except /healthz. Segments are lowercase plural nouns, kebab-case when multiword (/play-events). An ID in a path is {id}, or {artworkId} and similar when nested. Actions on a resource are POST /{resource}/{id}/{action}, and actions on a whole collection POST /{collection}/{action} (/downloads/check, /device-codes/poll); no other path holds a verb. A play’s media lives under /api/v1/plays/{id}/.

  2. Methods and status codes. GET reads. POST creates (201 with the resource) or runs an action (200 with its result, 202 with Accepted when it starts background work, 204 when there is nothing to return). PATCH edits with merge semantics, so only fields present change, and returns the updated resource, except a play’s heartbeat, which returns 204. DELETE returns 204. There is no PUT.

  3. JSON. Field names are camelCase. Every resource has id; references are <thing>Id (libraryId, workId). Kind fields (kind, type, state and the like) are open strings with documented values, never closed enums: clients skip values they don’t know. Kind-specific data is nested under the kind’s name (episode: {seasonNumber, number, airDate}). Absent optional fields are omitted, never null. Documented string values are camelCase (directPlay, inProgress), except task names, which are the server’s own, and quality names (4k, 1080p), which are names, not words.

  4. Units. Times are RFC 3339 UTC strings named …At. Durations and positions are integer milliseconds named …Ms, on the version’s timeline for playback. Bitrates are bits per second named …Bps. Sizes are bytes named …Bytes.

  5. Lists. A list is the envelope {items} of one item type, and a paged list’s page size is limit. A grid’s schema is named …Page, a feed’s …Feed and a whole list’s …List (WorkSummaryPage, WorkSummaryFeed, WorkSummaryList). Each kind of list has one paging style. A grid (a library’s works, collections, authors and book series, and Favorites) is paged by position: offset items from the start, with total on every page. A feed (the Unmatched and Missing files lists, activity, history, play events and the logs) is read in order: next is an opaque keyset cursor, passed back as after, and total, where a feed has one, is set on its first page only. A list that isn’t paged comes back whole, with neither: an item’s children and files, a collection’s root works, Search, Home, and the short lists the server holds whole (libraries, users, sessions, plays, problems, folders, artwork choices and matches). Filters are plain query parameters, repeated for several values, a range as its From and To.

  6. Errors. Every non-2xx response is application/problem+json (RFC 9457) with a stable code and the requestId. Clients translate codes; detail is for logs. Something the caller may not see is 404, the same as something that doesn’t exist. A request body over 1 MB (26 MB for an artwork upload) is 413 with invalidRequest.

  7. Naming. operationIds are verbNoun camelCase. Paths name the resource listed (/libraries/{id}/authors), and each operation does one job. An acronym is a word in a name (ApiKey, HdrFormat, ExternalId). A thing is called by one word everywhere: a movie, show, episode, video or book is a work, and a count of them is workCount. One schema per concept: WorkSummary is a work’s summary everywhere (a library’s works, children, search, Home), WorkDetail its detail page; a collection, an author and a book series have summaries of their own.

  8. Access. Every operation states its access level in x-kivo-access; nothing is implicit:

    • public: no credentials.
    • signedIn: a signed-in session or an API key.
    • viewer: a signed-in session or an API key; reads are filtered by its user’s Viewer.
    • admin: a viewer whose user is an admin.
    • cast: a viewer, or a cast token. A cast token reaches these routes only, as an ordinary user even when its user is an admin; on any other route it is 403 with code forbidden.
    • play: the play’s ID in the path is the credential. These routes take no cookie or token, answer cross-origin GET and HEAD and their preflights without credentials (Google Cast), and check the play’s owner’s current permissions on every request.

    Two more extensions state an operation’s policy:

    • x-kivo-receiver: true: the Cast receiver calls it, so it answers the receiver’s origin across origins, preflights included, without credentials. Every cast operation is one too.
    • x-kivo-upload: true: its body is a file, which may be up to 26 MB and take 10 minutes to send.

Client identity

Every request a Kivo app makes says what is asking, a header per field:

  • Kivo-App: the app, such as kivo-android, kivo-apple, kivo-web or kivo-tizen.
  • Kivo-App-Version: its version.
  • Kivo-Platform: what it runs on, a ClientPlatform value.
  • Kivo-OS-Version: the operating system’s version.
  • Kivo-Device-Model: the device’s model, coarse: Android’s Build.MODEL, an Apple machine identifier such as iPhone16,2, or the browser’s family and major version.

Each value is printable ASCII, at most 100 characters; the server leaves out one that isn’t. A session keeps the identity it signed in with, and takes a changed one from its next request; a play takes it from its start request. The server shows it to the account and to admins, and sends it nowhere else. The screen’s size and the device’s own name aren’t sent.

Authentication

Signing in makes a session. The web client’s is a browser session, which uses the HttpOnly, SameSite=Strict session cookie set at sign-in (named __Host-kivo_session over TLS). An app’s is an app session, whose token it sends as Authorization: Bearer <token>. An Authorization header with another scheme is ignored, and the session cookie counts. Tokens are never accepted in query strings. Answers are Cache-Control: no-store, except media, images and fonts. Unsafe methods on a cookie session need a same-origin Origin header. An app signs in with POST /api/v1/sessions and a password, or by linking: it creates a code with POST /api/v1/device-codes, shows the code and linkUrl, and polls POST /api/v1/device-codes/poll every interval seconds until a signed-in user approves or denies the code. A browser can link the same way, as a browser link. A 401, or the live-events socket closing with 4401, means the session was signed out. Scripts and other tools send an API key, which an admin creates under Admin, API keys (POST /api/v1/api-keys), the same way: Authorization: Bearer <key>. GET /api/v1/users/me is the user behind either.

Casting gives the Cast receiver a cast token: a signed-in session creates one with POST /api/v1/cast-tokens and passes it to the receiver in the Cast load request. The receiver sends it as Authorization: Bearer <token> to the cast routes, starting and running plays as the session’s user. It ends when that session does, 30 minutes after its last use, or 24 hours after it was made, whichever comes first; the receiver ends it with DELETE /api/v1/cast-tokens/current when it closes.

A user has one session per client. An app sends clientId, a random ID it makes once per install and keeps, when it signs in or creates a device code. A browser gets one from the server: the HttpOnly, SameSite=Lax client cookie (named __Host-kivo_client over TLS) set when it signs in, which lasts 400 days and outlives signing out. Signing in again from a client revokes that user’s session on it; another user’s session on the same client stays.

Finding the server

GET /api/v1/server names the server by its stable id, which apps key their sign-in by. On the local network, the server advertises itself by DNS-SD over multicast DNS as a _kivo._tcp service named by its serverName, on its own host name, kivo- and the last six hex digits of its ID. The service’s TXT record holds id (the server’s ID), v (its version), path (its base path, empty at the root) and tls (1 over HTTPS, else 0). An app browses for the service, resolves it to an address and port, and confirms the ID with GET /api/v1/server. The server needs host networking to be seen.

Live events

GET /api/v1/events upgrades to a WebSocket. The server sends EventStreamMessage frames as JSON text: a snapshot first, then event hints carrying only IDs and counts, and resync when the client fell behind. There are no sequence numbers; a client that reconnects applies the new snapshot and refetches what it keeps live.

Information

  • License: Proprietary
  • OpenAPI version: 3.1.0

The web client’s session cookie; __Host-kivo_session over TLS.

Security scheme type: apiKey

Cookie parameter name: kivo_session

An app session’s token or an API key, in the Authorization header of every request, the live-events WebSocket’s upgrade too, where it may instead be the kivo-bearer.<token> subprotocol; never in a URL.

Security scheme type: http