← API reference

Notes

Liner notes — your own writing about records.

9 of 9 · v0.120.0
GET /api/v1/notes #
Session or Token read

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.

string
kind query

recording / release / artist / listen. Anything else is no filter — a stale bookmark should show everything, not nothing.

string
sort query

updated (default), created or title.

string

Responses

200

OK.

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.

name string required
artist_name string required

Empty for kind = 'artist', where the name is the artist.

mbid string | null
created_at integer · int64 required
listen object | null

A listen a note hangs off.

id integer · int64 required
title string required
artist string required
album string | null
timestamp integer · int64 required
period object | null

A period a note hangs off.

unit string required

week, month or year.

key string required

2026-W32, 2026-08, 2026.

line object | null

Set on a pin: the line it hangs off.

artist string required
title string required
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.

artist string required
title string required
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.

body_md string required
excerpt string required

The first 200 characters, as text.

updated_at integer · int64 required
loved boolean required
revisions integer required

Earlier versions kept.

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

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."
}
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

Mirrors the HTTP status.

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 #
Session or Token write

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.

name string | null
artist string | null
mbid string | null
body_md string required

Markdown. [[track:…]], [[album:…]], [[artist:…]], [[gear:…]] and [[line:12]] shortcodes resolve against your own history.

visibility string | null

private by default.

Responses

200

Saved.

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.

name string required
artist_name string required

Empty for kind = 'artist', where the name is the artist.

mbid string | null
created_at integer · int64 required
listen object | null

A listen a note hangs off.

id integer · int64 required
title string required
artist string required
album string | null
timestamp integer · int64 required
period object | null

A period a note hangs off.

unit string required

week, month or year.

key string required

2026-W32, 2026-08, 2026.

line object | null

Set on a pin: the line it hangs off.

artist string required
title string required
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.

artist string required
title string required
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.

body_md string required
excerpt string required

The first 200 characters, as text.

updated_at integer · int64 required
loved boolean required
revisions integer required

Earlier versions kept.

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

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."
}
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

Mirrors the HTTP status.

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"
}
GET /api/v1/notes/{id} #
Session or Token read

One note.

Scope: read (since v0.39).

Parameters

id path required

The note.

integer · int64

Responses

200

OK.

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.

name string required
artist_name string required

Empty for kind = 'artist', where the name is the artist.

mbid string | null
created_at integer · int64 required
listen object | null

A listen a note hangs off.

id integer · int64 required
title string required
artist string required
album string | null
timestamp integer · int64 required
period object | null

A period a note hangs off.

unit string required

week, month or year.

key string required

2026-W32, 2026-08, 2026.

line object | null

Set on a pin: the line it hangs off.

artist string required
title string required
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.

artist string required
title string required
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.

body_md string required
excerpt string required

The first 200 characters, as text.

updated_at integer · int64 required
loved boolean required
revisions integer required

Earlier versions kept.

body_html string | null

The rendered, sanitised body with every shortcode resolved. Absent in a listing.

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."
}
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

Mirrors the HTTP status.

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"
}
404

No such resource, or it belongs to another user.

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/notes/{id} #
Session or Token write

Delete a note.

Scope: write (since v0.39). Revisions go with it.

Parameters

id path required

The note.

integer · int64

Responses

200

Deleted, with its revisions. status is deleted.

application/json

object

An acknowledgement with nothing else to report.

status names what happened — ok, updated, deleted, cleared, started and so on; the operation says which it sends. A client needs only the HTTP status to know it worked.

status 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."
}
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

Mirrors the HTTP status.

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"
}
404

No such resource, or it belongs to another user.

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/notes/{id}/revisions #
Session or Token read

A note's history.

Scope: read (since v0.39). Opt-in — the read path serves only the current body. Notes are meant to be kept, and a rewrite five years on should not erase what you thought at the time.

Parameters

id path required

The note.

integer · int64

Responses

200

OK.

application/json

object

Earlier versions of a note, newest first.

revisions array required
each item
object
id integer · int64 required
body_md string required
excerpt string required
created_at integer · int64 required

When this version was replaced.

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."
}
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

Mirrors the HTTP status.

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"
}
404

No such resource, or it belongs to another user.

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/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.

name string | null
artist string | null
mbid string | null
body_md string required

Responses

200

Rendered.

application/json

object

A rendered body.

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

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."
}
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

Mirrors the HTTP status.

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/for #
Session or Token read

The note for a given target, if any.

Scope: read (since v0.39).

Parameters

entity_id query
integer · int64
scrobble_id query
integer · int64
period_unit query
string
period_key query
string
line query
integer
from_scrobble query
integer · int64
kind query
string
name query
string
artist query
string

Responses

200

OK. note is null when nothing is written yet.

application/json

object

The note for a target, or null — "no note here" is the normal case, and the caller still needs the resolved target to open an editor on it.

note object | null

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.

name string required
artist_name string required

Empty for kind = 'artist', where the name is the artist.

mbid string | null
created_at integer · int64 required
listen object | null

A listen a note hangs off.

id integer · int64 required
title string required
artist string required
album string | null
timestamp integer · int64 required
period object | null

A period a note hangs off.

unit string required

week, month or year.

key string required

2026-W32, 2026-08, 2026.

line object | null

Set on a pin: the line it hangs off.

artist string required
title string required
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.

artist string required
title string required
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.

body_md string required
excerpt string required

The first 200 characters, as text.

updated_at integer · int64 required
loved boolean required
revisions integer required

Earlier versions kept.

body_html string | null

The rendered, sanitised body with every shortcode resolved. Absent in a listing.

target required

What a target resolved to. One key, named for its kind.

one of
option 1 object
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.

name string required
artist_name string required

Empty for kind = 'artist', where the name is the artist.

mbid string | null
created_at integer · int64 required
option 2 object
listen object | null

A listen a note hangs off.

id integer · int64 required
title string required
artist string required
album string | null
timestamp integer · int64 required
option 3 object
period object required

A period a note hangs off.

unit string required

week, month or year.

key string required

2026-W32, 2026-08, 2026.

option 4 object
line object required

One numbered line of a lyric.

artist string required
title string required
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.

loved boolean required
400

The query names no target, or one that does not resolve.

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."
}
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

Mirrors the HTTP status.

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"
}
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.

string

Responses

200

An attachment: tapedeck-liner-notes.zip, or .md with format=md.

application/zip

string · binary

text/markdown

string
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/lines #
Session or Token read

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

200

OK.

application/json

object

Every pinned line.

lines array required
each item
object
note_id integer · int64 required
line object required

One numbered line of a lyric.

artist string required
title string required
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