← API reference

Playlists

Playlists that live in Tapedeck, reviewed before they reach a media server, plus the direct push that has always existed.

10 of 10 · v0.120.0
GET /api/v1/playlist-lab #
Session

The vector space the Playlist Lab generates from.

Session-only. Returns the candidate pool with a position for each track, plus what every axis means.

Read granularity and note before using this. The space is ten real dimensions, and they come from two different places:

  • Six are about the music (group: sound), from the Every Noise genre map — x (dense/atmospheric ↔ spiky/bouncy), y (organic ↔ mechanical/electric), the colour's three channels, and how far apart a track's own genres sit.
  • Four are about the listener (group: habit) — era, familiarity, hour of day, recency — from their own history.

These are not per-recording audio features. Genres attach to artists, so granularity is artist and every track by one artist shares its six sound coordinates exactly; the habit axes are the only thing separating them. Anything describing this as track-level audio similarity is wrong. A client mixing the two groups without weighting will find "similar" quietly means "played at the same hour".

Values are normalised 0–1 against the pool's own extremes, so "newer" means newer than the rest of this collection. A track whose genres have no place on the map is dropped, not centred — placing it in the middle would invent a similarity it does not have — and the count comes back as unplaced.

tracks is compact — [title, artist, album, plays, [v0…v9]] — because on a 3,000-track pool the repeated key names would be most of the payload. Answers 409 when the genre map has not been fetched, when there is no history, or when nothing has been placed yet.

Parameters

min_plays query

Plays a track needs to be in the pool. Lower it to reach further into the tail of the history.

integer · int64
cap query

The most tracks to return, most-played first.

integer

Responses

200

OK.

application/json

object

The pool, where each track sits, and what the axes mean.

dims array required

The ten axes, in the order every track's vector lists them.

each item
object

One axis.

key string required
label string required
low string required

What the low end means.

high string required

What the high end means.

group string required

Which half of the space an axis belongs to: sound is about the music (from the genre map), habit about the listener (from their history).

soundhabit
tracks array required

One array per track rather than an object, to keep a large pool small: [title, artist, album, plays, vector], the vector's coordinates rounded to three places.

each item
array
each item
object
loved array required

Indexes into tracks of the ones you love.

each item
integer
pool integer required

Tracks in the pool.

considered integer required

Tracks considered before the cut.

unplaced integer required

Tracks left out for having no genre the map places.

min_plays integer · int64 required
cap integer required
granularity string required

Always artist: the Sound axes are reached through each track's artist, so every track by one artist shares them. Present so no client can present this as per-recording analysis.

note string required
401

No valid session cookie or token. Also returned when a token is presented to a session-only endpoint — the endpoint does not accept tokens at all, so the scope is irrelevant.

application/json

object

Every error body in the API has this shape.

code integer · int32 required

Mirrors the HTTP status.

error string required

Human-readable. Not a stable identifier — do not branch on it.

{
  "code": 401,
  "error": "Authentication required. Log in to access this endpoint."
}
409

No genre map yet, no history, or nothing placed yet — the message says which.

application/json

object

Every error body in the API has this shape.

code integer · int32 required

Mirrors the HTTP status.

error string required

Human-readable. Not a stable identifier — do not branch on it.

GET /api/v1/playlist-targets #
Session

Servers a generated list can be sent to.

Session-only, and permanently so: pushing spends the credential of a configured source, which puts it on the same side of the line as /api/v1/sources — the line API tokens do not cross.

The targets are the caller's enabled sources, filtered to the kinds that can receive a playlist. protocol names the wire protocol rather than the product, so a Navidrome source reads as Subsonic: the calls are search3 and createPlaylist, which any OpenSubsonic server answers. A kind missing from the list is a source that polls and cannot be written to, not a bug.

Responses

200

OK.

application/json

object
targets array required

Enabled, writable sources, and TIDAL when it is connected. Empty is a normal answer.

each item
object

A server this user can push a playlist to.

source_id integer · int64 required

What source_id takes. -1 is TIDAL, which is a connection rather than a source.

kind string required

plex, jellyfin, navidrome or tidal.

protocol string required

Plex, Jellyfin, Subsonic or TIDAL.

label string required

Falls back to the protocol name when the source has none.

account string | null
401

No valid session cookie or token. Also returned when a token is presented to a session-only endpoint — the endpoint does not accept tokens at all, so the scope is irrelevant.

application/json

object

Every error body in the API has this shape.

code integer · int32 required

Mirrors the HTTP status.

error string required

Human-readable. Not a stable identifier — do not branch on it.

{
  "code": 401,
  "error": "Authentication required. Log in to access this endpoint."
}
POST /api/v1/playlists #
Session

Create a playlist on one of your servers from a list of tracks.

Session-only — see /api/v1/playlist-targets.

Tapedeck stores names, not the server's item ids, for a generated list, so every track is resolved by searching the target and every one of those searches is fuzzy. A hit is checked on artist and title before it is used; an exact title beats a (Remastered) one, and a hit for a different artist is discarded rather than taken.

A partial push is a success, not an error. A listening history is not the same set as any one library, so matched below requested is the normal case and missing names every track that did not land. The push only fails outright when nothing matched (404) or the server refused or could not be reached (502) — the two need completely different things from the user, so they are not collapsed.

Order is preserved; misses drop out. At most 100 tracks, which is a wall-clock bound as much as a politeness one — the searches run in turn against what is often a Raspberry Pi.

Request body required

application/json

object
source_id integer · int64 required

From /api/v1/playlist-targets.

name string required

Trimmed, and truncated to 120 characters.

tracks array required

In playlist order.

each item
object

One track Tapedeck wants on the far end.

Names only, because names are all the generating queries have. album is a tiebreaker rather than a requirement: a listen's album is whichever release the tagger claimed, which for a compilation track is not the album the server files it under.

title string required
artist string required
album string | null

A tiebreaker between pressings, never a requirement.

isrc string | null

The recording's ISRC. Filled from the history when absent, and where a target can look one up no name matching is needed at all.

Responses

200

Created — possibly with fewer tracks than asked for.

application/json

object

What was made, and every track that did not land, with why.

playlist_id string required

The server's own id. Empty when a Subsonic server answered ok without one.

name string required
protocol string required
target object required

A server this user can push a playlist to.

source_id integer · int64 required

What source_id takes. -1 is TIDAL, which is a connection rather than a source.

kind string required

plex, jellyfin, navidrome or tidal.

protocol string required

Plex, Jellyfin, Subsonic or TIDAL.

label string required

Falls back to the protocol name when the source has none.

account string | null
requested integer required
matched integer required
from_cache integer required

How many were answered from remembered matches without asking the far end. Nearly everything on a re-push.

missing array required

A partial match is the normal outcome — a listening history is not the same set as any one library.

each item
object

Why a track did not land — which distinguishes a search problem from a matching one. A mismatch carries what the server actually offered, at most five, which is the fact that settles it at a glance.

one of
option 1 object

The search itself failed — the server is unwell, not unhelpful.

reason string required
search_failed
option 2 object

The server answered, with nothing.

reason string required
no_candidates
option 3 object

It offered tracks, none by this artist.

offered array required
each item
object

One track the far end offered. Carries its id, so a candidate can be chosen rather than only read: POST it to /api/v1/playlist-matches and every later push of that track uses it.

id string required

The far end's own id, which is what POST /api/v1/playlist-matches takes. Opaque here; it only ever goes back to the server that made it.

label string required

What to show. The artist for an artist mismatch, the title for a title mismatch — whichever half is the thing that disagreed.

reason string required
artist_mismatch
option 4 object

The artist agreed and no title did.

offered array required
each item
object

One track the far end offered. Carries its id, so a candidate can be chosen rather than only read: POST it to /api/v1/playlist-matches and every later push of that track uses it.

id string required

The far end's own id, which is what POST /api/v1/playlist-matches takes. Opaque here; it only ever goes back to the server that made it.

label string required

What to show. The artist for an artist mismatch, the title for a title mismatch — whichever half is the thing that disagreed.

reason string required
title_mismatch
title string required
artist string required
album string | null
updated_existing boolean required

A playlist of this name was pushed here before and was rewritten in place rather than made again.

400

Malformed or rejected input.

application/json

object

Every error body in the API has this shape.

code integer · int32 required

Mirrors the HTTP status.

error string required

Human-readable. Not a stable identifier — do not branch on it.

401

No valid session cookie or token. Also returned when a token is presented to a session-only endpoint — the endpoint does not accept tokens at all, so the scope is irrelevant.

application/json

object

Every error body in the API has this shape.

code integer · int32 required

Mirrors the HTTP status.

error string required

Human-readable. Not a stable identifier — do not branch on it.

{
  "code": 401,
  "error": "Authentication required. Log in to access this endpoint."
}
404

No such source, or none of those tracks are on it.

application/json

object

Every error body in the API has this shape.

code integer · int32 required

Mirrors the HTTP status.

error string required

Human-readable. Not a stable identifier — do not branch on it.

501

TIDAL was named and is not configured on this instance.

application/json

object

Every error body in the API has this shape.

code integer · int32 required

Mirrors the HTTP status.

error string required

Human-readable. Not a stable identifier — do not branch on it.

502

The server refused the playlist or could not be reached.

application/json

object

Every error body in the API has this shape.

code integer · int32 required

Mirrors the HTTP status.

error string required

Human-readable. Not a stable identifier — do not branch on it.

GET /api/v1/saved-playlists #
Token reador Session

Playlists kept in Tapedeck.

Scope: read (since v0.120). origin is user for one you built, or the name of the MCP connection that wrote it — a list an assistant produced stays distinguishable from one you made, the same provenance rule notes follow.

Responses

200

The playlists.

application/json

object
playlists array required
each item
object
id integer · int64 required
name string required
description string | null
origin string required

user, or mcp for one an assistant wrote — connection_name says which.

created_at integer · int64 required
updated_at integer · int64 required
pushed_at integer | null · int64
pushed_to string | null

The protocol it was last sent to — Plex, Jellyfin, Subsonic, TIDAL.

connection_name string | null
tracks integer · int64 required

How many tracks.

401

No valid session cookie or token. Also returned when a token is presented to a session-only endpoint — the endpoint does not accept tokens at all, so the scope is irrelevant.

application/json

object

Every error body in the API has this shape.

code integer · int32 required

Mirrors the HTTP status.

error string required

Human-readable. Not a stable identifier — do not branch on it.

{
  "code": 401,
  "error": "Authentication required. Log in to access this endpoint."
}
POST /api/v1/saved-playlists #
Session

Save a playlist.

Session-only. Tracks are names, not file paths — Tapedeck stores no server item ids for a generated list and resolves them against the real library at push time.

Request body required

application/json

object
name string required
description string | null
tracks array required

In playlist order. At least one.

each item
object

Names, not ids — the only shape that survives the user not owning the track on every server they have. Resolved against a real library at push time.

title string required
artist string required
album string | null

A tiebreaker at push time, never a requirement.

note string | null

Why this track is here, in the words of whatever chose it. Never generated by Tapedeck.

Responses

200

Saved.

application/json

object
id integer · int64 required
400

No name, or no tracks.

application/json

object

Every error body in the API has this shape.

code integer · int32 required

Mirrors the HTTP status.

error string required

Human-readable. Not a stable identifier — do not branch on it.

401

No valid session cookie or token. Also returned when a token is presented to a session-only endpoint — the endpoint does not accept tokens at all, so the scope is irrelevant.

application/json

object

Every error body in the API has this shape.

code integer · int32 required

Mirrors the HTTP status.

error string required

Human-readable. Not a stable identifier — do not branch on it.

{
  "code": 401,
  "error": "Authentication required. Log in to access this endpoint."
}
GET /api/v1/saved-playlists/{id} #
Token reador Session

One playlist and its tracks.

Scope: read (since v0.120).

Parameters

id path required
integer · int64

Responses

200

The playlist.

application/json

object
playlist object required
id integer · int64 required
name string required
description string | null
origin string required

user, or mcp for one an assistant wrote — connection_name says which.

created_at integer · int64 required
updated_at integer · int64 required
pushed_at integer | null · int64
pushed_to string | null

The protocol it was last sent to — Plex, Jellyfin, Subsonic, TIDAL.

connection_name string | null
tracks integer · int64 required

How many tracks.

tracks array required

With the plays and artwork the history already holds.

each item
object

A playlist track with what the listening history already knows about it.

title string required
artist string required
album string | null

A tiebreaker at push time, never a requirement.

note string | null

Why this track is here, in the words of whatever chose it. Never generated by Tapedeck.

plays integer · int64 required

Plays in this user's history. Zero for a track they have never played, which is a normal outcome rather than a problem.

artwork_url string | null
caa_id integer | null · int64
caa_release_mbid string | null
401

No valid session cookie or token. Also returned when a token is presented to a session-only endpoint — the endpoint does not accept tokens at all, so the scope is irrelevant.

application/json

object

Every error body in the API has this shape.

code integer · int32 required

Mirrors the HTTP status.

error string required

Human-readable. Not a stable identifier — do not branch on it.

{
  "code": 401,
  "error": "Authentication required. Log in to access this endpoint."
}
404

No such playlist.

application/json

object

Every error body in the API has this shape.

code integer · int32 required

Mirrors the HTTP status.

error string required

Human-readable. Not a stable identifier — do not branch on it.

DELETE /api/v1/saved-playlists/{id} #
Session

Delete a playlist.

Parameters

id path required
integer · int64

Responses

200

Deleted.

application/json

object
deleted boolean required
401

No valid session cookie or token. Also returned when a token is presented to a session-only endpoint — the endpoint does not accept tokens at all, so the scope is irrelevant.

application/json

object

Every error body in the API has this shape.

code integer · int32 required

Mirrors the HTTP status.

error string required

Human-readable. Not a stable identifier — do not branch on it.

{
  "code": 401,
  "error": "Authentication required. Log in to access this endpoint."
}
404

No such playlist.

application/json

object

Every error body in the API has this shape.

code integer · int32 required

Mirrors the HTTP status.

error string required

Human-readable. Not a stable identifier — do not branch on it.

POST /api/v1/saved-playlists/{id}/push #
Session

Send a stored playlist to a media server.

Session-only — pushing spends the credential of a configured source, which is the line WriteAuth deliberately does not cross.

This is the review step the MCP design turns on: a list an assistant built is saved first and reaches a media server only when the person whose server it is says so.

A partial match is the normal outcome — a listening history is not the same set as any one library — and every track that did not land is named. 404 means nothing matched; 502 means the server could not be reached. They need completely different things from the user.

Parameters

id path required
integer · int64

Request body required

application/json

object
source_id integer · int64 required

From /api/v1/playlist-targets.

Responses

200

Sent, wholly or partly.

application/json

object

What was made, and every track that did not land, with why.

playlist_id string required

The server's own id. Empty when a Subsonic server answered ok without one.

name string required
protocol string required
target object required

A server this user can push a playlist to.

source_id integer · int64 required

What source_id takes. -1 is TIDAL, which is a connection rather than a source.

kind string required

plex, jellyfin, navidrome or tidal.

protocol string required

Plex, Jellyfin, Subsonic or TIDAL.

label string required

Falls back to the protocol name when the source has none.

account string | null
requested integer required
matched integer required
from_cache integer required

How many were answered from remembered matches without asking the far end. Nearly everything on a re-push.

missing array required

A partial match is the normal outcome — a listening history is not the same set as any one library.

each item
object

Why a track did not land — which distinguishes a search problem from a matching one. A mismatch carries what the server actually offered, at most five, which is the fact that settles it at a glance.

one of
option 1 object

The search itself failed — the server is unwell, not unhelpful.

reason string required
search_failed
option 2 object

The server answered, with nothing.

reason string required
no_candidates
option 3 object

It offered tracks, none by this artist.

offered array required
each item
object

One track the far end offered. Carries its id, so a candidate can be chosen rather than only read: POST it to /api/v1/playlist-matches and every later push of that track uses it.

id string required

The far end's own id, which is what POST /api/v1/playlist-matches takes. Opaque here; it only ever goes back to the server that made it.

label string required

What to show. The artist for an artist mismatch, the title for a title mismatch — whichever half is the thing that disagreed.

reason string required
artist_mismatch
option 4 object

The artist agreed and no title did.

offered array required
each item
object

One track the far end offered. Carries its id, so a candidate can be chosen rather than only read: POST it to /api/v1/playlist-matches and every later push of that track uses it.

id string required

The far end's own id, which is what POST /api/v1/playlist-matches takes. Opaque here; it only ever goes back to the server that made it.

label string required

What to show. The artist for an artist mismatch, the title for a title mismatch — whichever half is the thing that disagreed.

reason string required
title_mismatch
title string required
artist string required
album string | null
updated_existing boolean required

A playlist of this name was pushed here before and was rewritten in place rather than made again.

400

The source is off, or cannot receive a playlist.

application/json

object

Every error body in the API has this shape.

code integer · int32 required

Mirrors the HTTP status.

error string required

Human-readable. Not a stable identifier — do not branch on it.

401

No valid session cookie or token. Also returned when a token is presented to a session-only endpoint — the endpoint does not accept tokens at all, so the scope is irrelevant.

application/json

object

Every error body in the API has this shape.

code integer · int32 required

Mirrors the HTTP status.

error string required

Human-readable. Not a stable identifier — do not branch on it.

{
  "code": 401,
  "error": "Authentication required. Log in to access this endpoint."
}
404

No such playlist, or none of the tracks are on that server.

application/json

object

Every error body in the API has this shape.

code integer · int32 required

Mirrors the HTTP status.

error string required

Human-readable. Not a stable identifier — do not branch on it.

501

TIDAL was named and is not configured on this instance.

application/json

object

Every error body in the API has this shape.

code integer · int32 required

Mirrors the HTTP status.

error string required

Human-readable. Not a stable identifier — do not branch on it.

502

The server could not be reached, or refused the playlist.

application/json

object

Every error body in the API has this shape.

code integer · int32 required

Mirrors the HTTP status.

error string required

Human-readable. Not a stable identifier — do not branch on it.

POST /api/v1/playlist-matches #
Session

Pin a track to one of the candidates a push offered.

Session-only, like the push it serves.

A push reports every miss with the candidates the far end offered. Until this existed that list was a dead end — plausible names you could read and not act on. Choosing one records a manual resolution, which never expires and is never overwritten by a later automatic guess: a person read the candidates and settled it, and a search that already got it wrong once does not get another vote.

This is also what makes the resolution cache self-improving. Every successful match is remembered, so a re-push costs almost no searches at all — which matters because the far end meters them.

Request body required

application/json

object

A track the search got wrong, and which of its candidates it really is.

source_id integer · int64 required

The target this resolution is for. -1 is TIDAL.

artist string required
title string required
remote_id string | null

An id from a miss's offered list. Required to record a match; ignored by the DELETE, which forgets the mapping instead.

remote_label string | null

What the far end calls it, so the mapping can be shown back as something a person recognises rather than an opaque id.

Responses

200

Recorded.

application/json

object
status string required

matched or cleared.

400

Malformed or rejected input.

application/json

object

Every error body in the API has this shape.

code integer · int32 required

Mirrors the HTTP status.

error string required

Human-readable. Not a stable identifier — do not branch on it.

401

No valid session cookie or token. Also returned when a token is presented to a session-only endpoint — the endpoint does not accept tokens at all, so the scope is irrelevant.

application/json

object

Every error body in the API has this shape.

code integer · int32 required

Mirrors the HTTP status.

error string required

Human-readable. Not a stable identifier — do not branch on it.

{
  "code": 401,
  "error": "Authentication required. Log in to access this endpoint."
}
DELETE /api/v1/playlist-matches #
Session

Forget a resolution.

Session-only. The pick was wrong, or the far end has changed what it holds; the next push searches for the track again from scratch. Takes the same body without remote_id.

Request body required

application/json

object

A track the search got wrong, and which of its candidates it really is.

source_id integer · int64 required

The target this resolution is for. -1 is TIDAL.

artist string required
title string required
remote_id string | null

An id from a miss's offered list. Required to record a match; ignored by the DELETE, which forgets the mapping instead.

remote_label string | null

What the far end calls it, so the mapping can be shown back as something a person recognises rather than an opaque id.

Responses

200

Forgotten.

application/json

object
status string required

matched or cleared.

401

No valid session cookie or token. Also returned when a token is presented to a session-only endpoint — the endpoint does not accept tokens at all, so the scope is irrelevant.

application/json

object

Every error body in the API has this shape.

code integer · int32 required

Mirrors the HTTP status.

error string required

Human-readable. Not a stable identifier — do not branch on it.

{
  "code": 401,
  "error": "Authentication required. Log in to access this endpoint."
}