Your liner notes.
Scope: read (since v0.39). Self-only, whatever visibility says.
q matches the note's body and its subject both — someone looking
for what they wrote about Burial means either, and asking which is a
worse answer than searching both. Case-insensitivity is SQLite's LIKE,
which folds ASCII only: BURIAL finds Burial, CŒUR does not find
Cœur.
total is the count before filtering, so a search that finds nothing
reads as a search rather than as an empty account.
Parameters
q query
Free text, matched against the body and the subject.
kind query
recording / release / artist / listen. Anything else is no
filter — a stale bookmark should show everything, not nothing.
sort query
updated (default), created or title.
Responses
application/json
object
Your notes, and how many there are in all.
notes array required
Without body_html or pins.
each item object
A liner note, with everything a screen needs: what it hangs off, the
rendered body, whether the subject is loved, and how many earlier versions
exist. Exactly one of entity, listen, period and line is set.
id integer · int64 required
body_md string required
Markdown, with [[kind:name]] shortcodes.
excerpt string required
The first 140 characters, as text.
visibility string required
private or another of the allowed visibilities.
created_at integer · int64 required
updated_at integer · int64 required
entity object | null
Something a note or a love can point at: a recording, a release, an artist.
id integer · int64 required
kind string required
recording, release or artist.
artist_name string required
Empty for kind = 'artist', where the name is the artist.
created_at integer · int64 required
listen object | null
A listen a note hangs off.
id integer · int64 required
timestamp integer · int64 required
period object | null
A period a note hangs off.
line object | null
Set on a pin: the line it hangs off.
line integer required
1-based, blank lines included.
text string | null
The words, when this instance still holds the lyric. A pin outlives the
lyric it points at, so null is not an error.
lines_quoted integer required
How many distinct lyric lines the body quotes.
pins array required
The pins that roll up under this note as footnotes, in the order the
record plays. Empty in a listing.
each item object
A pin, as it reads under the note it rolls up into.
note_id integer · int64 required
line object required
One numbered line of a lyric.
line integer required
1-based, blank lines included.
text string | null
The words, when this instance still holds the lyric. A pin outlives the
lyric it points at, so null is not an error.
excerpt string required
The first 200 characters, as text.
updated_at integer · int64 required
revisions integer required
body_html string | null
The rendered, sanitised body with every shortcode resolved. Absent in a
listing.
total integer required
Every note you have, filtered or not — "12 of 240".
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
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."
}
403 Authenticated, but not permitted. Either the token lacks the required
scope, or the endpoint needs the admin role. Deliberately not a 401 —
re-authenticating will not help.
application/json
object
Every error body in the API has this shape.
code integer · int32 required
error string required
Human-readable. Not a stable identifier — do not branch on it.
{
"code": 403,
"error": "This token does not have the 'write' scope"
}
Create or update a note.
Scope: write (since v0.39).
A note hangs off exactly one target: entity_id, scrobble_id, or
the period_unit + period_key pair. Editing archives the previous
body into revisions — but only when the body actually changed, so
flipping visibility is not a rewrite.
A period target names a week, a month or a year exactly as the
Reports page does. The key is validated against the shapes
reports::period_of produces and anything else is a 400 — the key is
the note's identity, so one that page cannot reproduce files a note its
author can never find again.
The body is Markdown. [[track:…]], [[album:…]], [[artist:…]],
[[track:3]] and [[gear:…]] shortcodes resolve server-side; an
unresolved one is never an error.
Request body required
application/json
object
A note's target, flattened alongside its body.
entity_id integer | null · int64
scrobble_id integer | null · int64
period_unit string | null
week, month or year, alongside period_key. Both or neither.
period_key string | null
As Reports names it — 2026, 2026-08, 2026-W32.
line integer | null
A pin: the line's number, alongside artist and name (the
track's title). Blank lines are numbered.
from_scrobble integer | null · int64
With kind: track, prefer the album this listen was filed under.
kind string | null
track/recording, album/release or artist.
body_md string required
Markdown. [[track:…]], [[album:…]], [[artist:…]], [[gear:…]] and
[[line:12]] shortcodes resolve against your own history.
Responses
application/json
object
A liner note, with everything a screen needs: what it hangs off, the
rendered body, whether the subject is loved, and how many earlier versions
exist. Exactly one of entity, listen, period and line is set.
id integer · int64 required
body_md string required
Markdown, with [[kind:name]] shortcodes.
excerpt string required
The first 140 characters, as text.
visibility string required
private or another of the allowed visibilities.
created_at integer · int64 required
updated_at integer · int64 required
entity object | null
Something a note or a love can point at: a recording, a release, an artist.
id integer · int64 required
kind string required
recording, release or artist.
artist_name string required
Empty for kind = 'artist', where the name is the artist.
created_at integer · int64 required
listen object | null
A listen a note hangs off.
id integer · int64 required
timestamp integer · int64 required
period object | null
A period a note hangs off.
line object | null
Set on a pin: the line it hangs off.
line integer required
1-based, blank lines included.
text string | null
The words, when this instance still holds the lyric. A pin outlives the
lyric it points at, so null is not an error.
lines_quoted integer required
How many distinct lyric lines the body quotes.
pins array required
The pins that roll up under this note as footnotes, in the order the
record plays. Empty in a listing.
each item object
A pin, as it reads under the note it rolls up into.
note_id integer · int64 required
line object required
One numbered line of a lyric.
line integer required
1-based, blank lines included.
text string | null
The words, when this instance still holds the lyric. A pin outlives the
lyric it points at, so null is not an error.
excerpt string required
The first 200 characters, as text.
updated_at integer · int64 required
revisions integer required
body_html string | null
The rendered, sanitised body with every shortcode resolved. Absent in a
listing.
400 No target named, or a period key Reports would not produce.
application/json
object
Every error body in the API has this shape.
code integer · int32 required
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
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."
}
403 Authenticated, but not permitted. Either the token lacks the required
scope, or the endpoint needs the admin role. Deliberately not a 401 —
re-authenticating will not help.
application/json
object
Every error body in the API has this shape.
code integer · int32 required
error string required
Human-readable. Not a stable identifier — do not branch on it.
{
"code": 403,
"error": "This token does not have the 'write' scope"
}
POST /api/v1/notes/preview # Session or Token write
Render Markdown and resolve shortcodes, without saving.
Scope: write (since v0.39). A POST that does not save, but not a
pure read: resolving a shortcode can create the entity it points at, the
same write-on-read trade /api/v1/resolve makes.
Request body required
application/json
object
What a note (or a love) is about. Name it one way:
entity_id — a track, album or artist already known;
scrobble_id — one listen;
from_scrobble + kind — the track, album or artist of that listen,
created if new;
kind + name (+ artist for a track or album) — spelled out, created
if new;
period_unit + period_key — a week, a month or a year of listening;
line + artist + name — one numbered line of a lyric, a pin. A
pin is never an entity and cannot be loved.
entity_id integer | null · int64
scrobble_id integer | null · int64
period_unit string | null
week, month or year, alongside period_key. Both or neither.
period_key string | null
As Reports names it — 2026, 2026-08, 2026-W32.
line integer | null
A pin: the line's number, alongside artist and name (the
track's title). Blank lines are numbered.
from_scrobble integer | null · int64
With kind: track, prefer the album this listen was filed under.
kind string | null
track/recording, album/release or artist.
Responses
application/json
object
body_html 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
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."
}
403 Authenticated, but not permitted. Either the token lacks the required
scope, or the endpoint needs the admin role. Deliberately not a 401 —
re-authenticating will not help.
application/json
object
Every error body in the API has this shape.
code integer · int32 required
error string required
Human-readable. Not a stable identifier — do not branch on it.
{
"code": 403,
"error": "This token does not have the 'write' scope"
}
422 The body is not a note — body_md missing or not a string.
GET /api/v1/notes/export # Session
Every note as Markdown, for Obsidian / Logseq / a folder of files.
Session-only, unlike the rest of /notes. This is the whole of the
user's own writing in one request; read on a token is meant for an app
showing a note, not for draining the lot.
format=zip (default) gives one file per note with YAML frontmatter —
title, kind, name, artist, mbid, loved, visibility,
created, updated, tags — plus a README.md. format=md gives one
document with a section per note.
Shortcodes become wikilinks, and only where they resolve. [[…]] is
Tapedeck's reference syntax and the wikilink syntax of every tool this
is aimed at, so [[track:Xtal]] left as-is would point at a page called
track:Xtal that will never exist. A resolved reference becomes
[[Xtal — Aphex Twin]], matching that note's own filename by
construction. An unresolved one is left exactly as written: it is
what the writer typed, and the same judgement the HTML renderer makes.
Filenames are de-duplicated (Untitled (2)), so a wikilink can never
land on whichever of two files the vault happened to see first.
Parameters
format query
zip (default) — one file per note. md — everything in one document.
Responses
200 An attachment: tapedeck-liner-notes.zip, or .md with format=md.
application/zip
text/markdown
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
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."
}
Every lyric line this user has pinned.
Scope: read.
A pin is a note whose target is the line, rather than prose that
quotes one. It is the one form of lyric reference that can ever
federate, because nothing of the provider's text is written into the
note — a reader elsewhere sees your words and a stub.
Across everything rather than per record on purpose: that is what makes
it a way back into writing you had forgotten rather than a second copy
of the margin you are already looking at.
line.text carries the words when this instance still holds them and is
simply absent when it does not. A pin outlives the lyric it points at,
the same way a note outlives the entity that resolved it, and that is
not an error.
Responses
application/json
object
lines array required
each item object
note_id integer · int64 required
line object required
One numbered line of a lyric.
line integer required
1-based, blank lines included.
text string | null
The words, when this instance still holds the lyric. A pin outlives the
lyric it points at, so null is not an error.
excerpt string required
The first 120 characters, as text.
updated_at integer · int64 required