← API reference

Shelf

Records, tapes and discs — a shelf you catalogue and sides you play.

29 of 29 · v0.120.0
GET /api/v1/physical/lookup #
Token writeor Session

Resolve a barcode to a pressing.

Scope: write (since v0.120), although it is a GET: it exists only to add a record to the shelf, and it spends rate-limited Discogs and MusicBrainz lookups doing it, so a token that can only read the shelf has no use for it. GET /api/v1/art/library makes the same call.

Discogs first when configured — it catalogues pressings, so a barcode usually resolves to the exact edition, often with real side letters that MusicBrainz lacks. Falls back to MusicBrainz.

Barcodes may be sent with or without the printed spacing; the unspaced form a scanner produces matches slightly more often.

Parameters

barcode query

Scanned off the sleeve or the spine.

string
catalog_number query

The label's catalogue number, which is on the spine when the barcode isn't — and is the only identifier on most pre-1980 pressings.

string
artist query

With release, when there is neither.

string
release query
string
kind query

vinyl, cassette, cd or sacd; narrows the format filter.

string

Responses

200

OK.

application/json

object

Candidate pressings for what you are holding.

candidates array required

Discogs first when a barcode and a credential are in hand, then MusicBrainz.

each item
object

A candidate pressing.

source string required

musicbrainz or discogs.

artist string required
title string required
year integer | null · int64
country string | null
format string | null
mbid_release string | null
discogs_id string | null
barcode string | null
catalog_number string | null
artwork_url string | null
tracks array required
each item
object

One track on a physical side.

side string required

"A", "B", "C"… Cassettes have two, a double LP four.

position integer · int64 required

Position within the side, from 1.

title string required
artist string | null
duration_secs integer | null · int64
disc integer | null · int64

Which physical disc of the set this track is on, from 1.

Null means nobody recorded which disc, not disc 1.

discogs_enabled boolean required
notes array required

A provider that failed, and why. The others still answered.

each item
string
400

Nothing to look up by.

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/physical/export #
Session

The whole shelf as a zip.

Session-only. shelf.json (pressings, purchase dates, tracklists and an image manifest), plays.csv (every side played, oldest first, gear named), gear.json (equipment and chains with purchase dates) and scans/ — the images the user uploaded, one directory per pressing.

Images that live at the Cover Art Archive or Discogs are listed with their URLs rather than copied in: they are not the user's to redistribute and they are still there to fetch. Their own scans exist nowhere else, which is why those are the ones in the file.

This exists separately from GET /api/v1/backup for a concrete reason: a backup is a VACUUM INTO snapshot of the database, and uploaded scans are files beside it rather than rows inside it. A backup and this export together are the whole shelf.

Responses

200

A zip archive.

application/zip

string · binary
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/physical/import #
Session

Put a shelf back from an export.

Session-only, multipart/form-data, 256 MB maximum. The counterpart of GET /api/v1/physical/export, and deliberately not a database restore: it brings back the objects — pressings, tracklists, gear, the sides played, and your own scans.

What it cannot bring back is the listening. A shelf export holds no scrobbles, so the listens a side generated are not in the file, and the chain hours derived from them do not return with the objects. The response says so rather than leaving it to be discovered.

Everything is matched before it is inserted, so importing the same archive twice changes nothing the second time. A pressing is matched on its barcode or release MBID when it has one, and otherwise on artist, title and medium together; a side played is matched on its side and timestamp, so re-importing cannot double the wear on a stylus.

Signal chains are matched by name, never created — the same rule [[gear:…]] follows. A chain is something you build step by step, and conjuring an empty one from a name in a file would silently attribute listens to a chain with no components. Gear is created, since a piece of equipment is fully described by the file.

Nothing is written until shelf.json parses, so an unrelated or truncated zip fails before it has half-populated a shelf.

Request body required

multipart/form-data

object

A multipart/form-data upload of one file.

The server takes the first file part whatever its field name; file is the name to use. Its declared Content-Type decides how the file is stored — a filename from the client never reaches a path on disk.

file string · binary required

Responses

200

Imported.

application/json

object
releases integer required

Pressings added.

already_on_the_shelf integer required

Matched an existing pressing and left alone.

skipped integer required

Entries that could not be read.

gear integer required
images integer required
missing_scans integer required

Listed in the manifest but absent from the archive.

plays integer required

Sides played, restored.

note string required

What a shelf export cannot carry.

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

Larger than 256 MB.

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/physical/discogs/{id} #
Token writeor Session

A Discogs release, with its tracklist positions.

Scope: write (since v0.120), for the reason the barcode lookup is: it is the second half of adding a record, and it spends a Discogs request.

Parameters

id path required
string

Responses

200

OK.

application/json

object
candidate object required

A candidate pressing.

source string required

musicbrainz or discogs.

artist string required
title string required
year integer | null · int64
country string | null
format string | null
mbid_release string | null
discogs_id string | null
barcode string | null
catalog_number string | null
artwork_url string | null
tracks array required
each item
object

One track on a physical side.

side string required

"A", "B", "C"… Cassettes have two, a double LP four.

position integer · int64 required

Position within the side, from 1.

title string required
artist string | null
duration_secs integer | null · int64
disc integer | null · int64

Which physical disc of the set this track is on, from 1.

Null means nobody recorded which disc, not disc 1.

400

Discogs is not configured on this 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.

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

502

Discogs failed to answer.

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/physical/equipment/{id}/wear #
Token reador Session

Stylus hours or head wear for a piece of gear.

Scope: read (since v0.120).

Derived from physical_plays. A duplicate submission is not counted as wear.

Parameters

id path required
integer · int64

Responses

200

OK.

application/json

object
hours number · double required

To one decimal place.

sides_played integer · int64 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."
}
GET /api/v1/physical/settings/discogs #
Session

Whether Discogs is configured.

Session-only. Never returns the credential itself.

Responses

200

OK.

application/json

object

Whether Discogs is configured, and never the credential.

configured boolean required
from_env boolean required
stored boolean required
kind string required

Which credential shape is in use: token, key_secret or none.

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

Admin only.

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.

PUT /api/v1/physical/settings/discogs #
Session

Store a Discogs credential.

Session-only. Accepts either a personal access token or a consumer key/secret pair — either is enough, since Tapedeck only reads the public database. The three-legged OAuth flow is deliberately not implemented: it exists so an app can act as a user, and registering an app hands you a key and secret that work directly. Stored encrypted.

Request body required

application/json

object

Each field is written only when present; an empty string clears it.

token string | null

A personal access token. Empty clears it.

key string | null

An application's consumer key and secret. Either empty clears the pair.

secret string | null

Responses

200

Saved.

application/json

object
configured 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."
}
403

Admin only.

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/physical/settings/discogs/test #
Session

Verify the stored Discogs credential.

Session-only, and necessary rather than a nicety: Discogs does not reject a bad credential. With a bogus key and secret, /releases/1 and /database/search both return a normal 200 with real data, and the rate-limit header reads the authenticated 60 — the limit is raised for the presence of an Authorization header without checking its contents. /users/{username} is the one endpoint that actually verifies, and is what this calls. Without it, a typo is indistinguishable from a working credential.

Responses

200

Result of the check.

application/json

object

A rejection is a 200 with ok: false: the request succeeded and the answer is no.

ok boolean required
message string required

Discogs' own words, which name the credential shape it rejected.

400

Discogs is not configured on this 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.

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

Admin only.

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/physical/now-playing #
Token writeor Session

Nothing is on the deck.

Scope: write (since v0.120). Its own route rather than a field on the push, because it is also what cancelling a side means and that has no track to name. Clears only entries this shelf reported — an idle turntable must not wipe a now-playing the user's phone is currently sending over ingest.

Responses

200

Cleared.

application/json

object
status string required

ok, deleted 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."
}
GET /api/v1/physical/spin #
Token reador Session

The side currently on the deck.

Scope: read (since v0.120). Returns { "spin": null } when nothing is on.

The shelf is the one source where the browser is the player, so the countdown runs in the page — but what is on and when it started live here. That is what lets a second device see the record, and what makes a side survive a browser crash.

A spin whose release has since been deleted is cleared and reported as nothing on, rather than returned as a dangling id.

Responses

200

OK.

application/json

object
spin object | null

Null when nothing is on the deck.

release_id integer · int64 required
kind string required
artist string required
title string required
artwork_url string | null
mbid_release string | null
side string required
started_at integer · int64 required
paused_at integer | null · int64

Null while running.

paused_total integer · int64 required

Seconds spent stopped.

outcomes array required

Per-track {position, started_at, skipped}, as the deck wrote them.

each item
object
chain_id integer | null · int64
equipment_id integer | null · int64
start_secs integer · int64 required

How far into the side this spin began — non-zero when the needle was dropped mid-side, or a tape resumed from where it was parked.

updated_at integer · int64 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."
}
PUT /api/v1/physical/spin #
Token writeor Session

Put a side on the deck, or update the one that is on.

Scope: write (since v0.120). One deck per user — the table's primary key is the user, because nobody listens to two records at once, so starting a second side replaces the first rather than erroring.

The whole state is sent every time rather than a patch per field: it is half a dozen small numbers, it changes only on real events (start, pause, skip, next track), and a partial update would let two devices interleave into a state neither of them meant.

Request body required

application/json

object

The whole state of the deck, every time.

release_id integer · int64 required

Must be yours — a 404 otherwise.

side string required
started_at integer · int64 required
paused_at integer | null · int64

Null while running.

paused_total integer · int64

Seconds spent stopped.

outcomes array

Per-track {position, started_at, skipped}, stored whole.

each item
object
chain_id integer | null · int64
equipment_id integer | null · int64
start_secs integer · int64

How far into the side this spin began — the needle dropped mid-side, or a tape resumed from where it was parked.

Responses

200

Stored.

application/json

object
status string required

ok, deleted 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."
}
404

No such record.

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/physical/spin #
Token writeor Session

Take the side off the deck.

Scope: write (since v0.120). Clears the now-playing entry as well, so the two cannot disagree. Recording a side (POST /api/v1/physical/{id}/play) clears it too — otherwise reopening the page would offer to resume a side that has already been logged, which is how one gets logged twice.

Responses

200

Cleared.

application/json

object
status string required

ok, deleted 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."
}
PATCH /api/v1/physical/images/{id} #
Token writeor Session

Re-file, caption or reorder a scan.

Scope: write (since v0.120).

Parameters

id path required
integer · int64

Request body required

application/json

object

Absent fields are left alone; a null caption clears it.

kind string | null
caption string | null
position integer | null · int64

Responses

200

Saved.

application/json

object
status string required

ok, deleted 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."
}
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/physical/images/{id} #
Session

Remove a scan.

Session-only, deliberately, for the reason removing a shelf item is: an uploaded file is deleted from disk with the row and cannot be recovered, and no backup holds it. A provider's image is only unlinked and can be fetched again.

Parameters

id path required
integer · int64

Responses

200

Deleted.

application/json

object
status string required

ok, deleted 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."
}
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/physical/images/{id}/file #
Token reador Session

An uploaded scan's bytes.

Scope: read (since v0.120), and scoped to the caller by (id, user_id) — these are photographs of someone's own possessions and ids must not be walkable, the same rule the artwork proxy keeps.

Cached hard, because a scan never changes once uploaded, but private: it is per-user, and a shared cache holding it would serve it to the next person through the proxy. A provider-hosted image 404s here — it has a URL of its own and this server is not in front of it.

Parameters

id path required
integer · int64

Responses

200

The image.

image/*

string · binary
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 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/physical #
Token reador Session

Your shelf.

Scope: read (since v0.120).

Parameters

kind query

vinyl, cassette, cd or sacd. Anything else is ignored rather than rejected.

string

Responses

200

OK.

application/json

object
releases array required
each item
object

A record, tape or disc you own — an object with a pressing, a barcode and a side order. Two pressings of the same album are two of these.

id integer · int64 required
kind string required

vinyl, cassette, cd or sacd.

artist string required
title string required
year integer | null · int64
mbid_release string | null
discogs_id string | null
barcode string | null
catalog_number string | null
artwork_url string | null
tracks string required

The tracklist, as a JSON-encoded string of an array of tracks (side, position, title, artist, duration_secs, disc).

notes string | null
added_at integer · int64 required
purchased_at integer | null · int64

When you bought it — how long it has been on the shelf, which nothing else here can answer. Absent for everything shelved before the column.

acquired_as string | null

How it came to be yours — new, used, gift, inherited. None means nobody said, never "new": an existing shelf must not be retroactively claimed as bought new.

acquired_from string | null

Where from — the shop, the fair, the person. Free text, because that is what makes a thrift find a story rather than a flag.

chain_id integer | null · int64

What this normally gets played on. The deck pre-fills from these and writes the answer back, so a record with one turntable is asked once.

equipment_id integer | null · int64
chain_variant_id integer | null · int64

Which variant of that chain this pressing is normally played on — the cartridge you keep fitted for it. Pre-fills the deck the same way chain_id does.

parked_side string | null

Where the tape was left. Cassettes only — a record has random access and no state to remember, so these stay null for every other medium.

parked_secs integer | null · int64
play_count integer · int64 required

How many sides have been played, and for how long. Derived in the query rather than kept as a counter, so they cannot drift from the plays they claim to count.

played_secs integer · int64 required
last_played integer | null · int64
image_count integer · int64 required

Sleeves, booklet pages and labels held for this pressing. Derived the same way, so a card can say "29 scans" without a second request.

cover_thumb string | null

The front sleeve from this pressing's own scans, when it has one. artwork_url is only what the lookup returned, and is often empty.

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/physical #
Token writeor Session

Add a release to the shelf.

Scope: write (since v0.120). Seed from a barcode lookup, or supply the tracklist yourself.

Request body required

application/json

object
kind string required

vinyl, cassette, cd or sacd. A disc has no sides: its tracklist is one numbered unit per disc.

artist string required
title string required
year integer | null · int64
mbid_release string | null
discogs_id string | null
barcode string | null
catalog_number string | null
artwork_url string | null
tracks array
each item
object

One track on a physical side.

side string required

"A", "B", "C"… Cassettes have two, a double LP four.

position integer · int64 required

Position within the side, from 1.

title string required
artist string | null
duration_secs integer | null · int64
disc integer | null · int64

Which physical disc of the set this track is on, from 1.

Null means nobody recorded which disc, not disc 1.

notes string | null
format string | null

The medium the release declares — "CD", "12\" Vinyl", "Hybrid SACD" — as the lookup returned it. When present it overrides kind, because kind is what was picked before anyone looked at the record and this is what the record says it is.

purchased_at integer | null · int64

When you bought it, unix seconds. Optional — most of a shelf is catalogued long after the fact and guessing would be worse than blank.

acquired_as string | null

How it came to be yours — new, used, gift or inherited. Optional, and absent means nobody said: a shelf catalogued before this existed must not be retroactively claimed as bought new.

acquired_from string | null

Where from — the shop, the record fair, the person who gave it to you. Free text, because that is the half that makes a thrift find a story rather than a flag.

fetch_images boolean

Pull the sleeve, labels and booklet from the Cover Art Archive as part of adding it. On by default. A failure does not fail the add, and it does nothing without mbid_release.

Responses

201

Added.

application/json

object
id integer · int64 required
images integer required

How many scans came back from the archive.

kind string required

The shelf it went on.

corrected_from string | null

Set when the release's own format overrode the kind asked for — a box of CDs looked up by barcode is filed as CDs whatever was picked.

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."
}
GET /api/v1/physical/{id} #
Token reador Session

One shelf item, with its sides.

Scope: read (since v0.120).

Parameters

id path required
integer · int64

Responses

200

OK.

application/json

object
release object required

A record, tape or disc you own — an object with a pressing, a barcode and a side order. Two pressings of the same album are two of these.

id integer · int64 required
kind string required

vinyl, cassette, cd or sacd.

artist string required
title string required
year integer | null · int64
mbid_release string | null
discogs_id string | null
barcode string | null
catalog_number string | null
artwork_url string | null
tracks string required

The tracklist, as a JSON-encoded string of an array of tracks (side, position, title, artist, duration_secs, disc).

notes string | null
added_at integer · int64 required
purchased_at integer | null · int64

When you bought it — how long it has been on the shelf, which nothing else here can answer. Absent for everything shelved before the column.

acquired_as string | null

How it came to be yours — new, used, gift, inherited. None means nobody said, never "new": an existing shelf must not be retroactively claimed as bought new.

acquired_from string | null

Where from — the shop, the fair, the person. Free text, because that is what makes a thrift find a story rather than a flag.

chain_id integer | null · int64

What this normally gets played on. The deck pre-fills from these and writes the answer back, so a record with one turntable is asked once.

equipment_id integer | null · int64
chain_variant_id integer | null · int64

Which variant of that chain this pressing is normally played on — the cartridge you keep fitted for it. Pre-fills the deck the same way chain_id does.

parked_side string | null

Where the tape was left. Cassettes only — a record has random access and no state to remember, so these stay null for every other medium.

parked_secs integer | null · int64
play_count integer · int64 required

How many sides have been played, and for how long. Derived in the query rather than kept as a counter, so they cannot drift from the plays they claim to count.

played_secs integer · int64 required
last_played integer | null · int64
image_count integer · int64 required

Sleeves, booklet pages and labels held for this pressing. Derived the same way, so a card can say "29 scans" without a second request.

cover_thumb string | null

The front sleeve from this pressing's own scans, when it has one. artwork_url is only what the lookup returned, and is often empty.

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

PATCH /api/v1/physical/{id} #
Token writeor Session

Edit the shelf-keeping fields.

Scope: write (since v0.120). When it was bought, what it is normally played through, and the note beside it.

Every field distinguishes absent from null: omitting one leaves the column alone, and an explicit null clears it. That is not decoration — a bare optional reads a misspelled key as an explicit null, so a typo would clear the chain it was trying to set and answer 200. Same shape and the same reason as default_chain_id on PATCH /admin/tokens/{id}.

chain_id and equipment_id are checked against the caller's own rows before they are stored.

kind is the exception to the absent/null rule — a format has no "cleared" state, so a plain optional is correct. Changing it rewrites the tracklist's sides, because what a side is depends on the medium: a record has a break assign_sides has to place by playing time, and a disc has none at all. The response reports sides_rewritten so a client knows the tracklist it is holding is stale.

Parameters

id path required
integer · int64

Request body required

application/json

object

The shelf-keeping fields. An absent field is left alone and an explicit null clears it — a misspelled key is never read as a clear.

purchased_at integer | null · int64

When you bought it, Unix seconds.

acquired_as string | null

new, used, gift or inherited.

acquired_from string | null

The shop, the fair, the person.

chain_id integer | null · int64

What it is normally played on. Must be yours — a 400 otherwise.

equipment_id integer | null · int64

Must be yours — a 400 otherwise.

notes string | null
chain_variant_id integer | null · int64

The variant of chain_id this pressing is normally played on — the cartridge you keep fitted for it.

kind string | null

Move it to another shelf: vinyl, cassette, cd or sacd. Not clearable. Changing it re-derives the tracklist's sides, because what a side is depends on the medium.

mbid_release string | null

The MusicBrainz release this pressing is, typed in by hand — also the cheapest way to ask for running times.

Responses

200

Saved.

application/json

object
status string required

ok.

sides_rewritten boolean required

The medium changed and the tracklist's sides were re-derived — a record's A/B break, or a disc's numbers.

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 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/physical/{id} #
Session

Remove a shelf item.

Session-only, deliberately, although the rest of the shelf takes a write token: its plays and its images go with it, and any scan files the user uploaded are removed from disk — nothing else knows those files exist, so leaving them would orphan them permanently. Scans are not in GET /api/v1/backup, so this is the one shelf operation a lost phone could make unrecoverable, which is the line DELETE /api/v1/scrobbles sits on.

Parameters

id path required
integer · int64

Responses

200

Deleted.

application/json

object
status string required

ok, deleted 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."
}
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.

PUT /api/v1/physical/{id}/tracks #
Token writeor Session

Replace the tracklist and side split.

Scope: write (since v0.120). Sides are not reliably available from any provider — MusicBrainz records a vinyl track number as "A1" only when an editor entered one. When there is no explicit side letter the split is guessed by playing time, not track count, because a mastering engineer balances sides and a nine-minute closer would otherwise land on the wrong one. It is still a guess, which is why this endpoint exists.

None of that applies to a disc. A CD or SACD plays start to finish, so its tracks are numbered one unit per disc with no guessing at all — splitting a CD by playing time would invent a break that is not there. The medium's own format string decides which rule applies, not what was searched for.

It is also how a running time is typed in, which for a pressing no provider carries is the only way one is ever known. That is not cosmetic: scrobbles.duration is what every hours readout sums and physical_plays.duration_secs is what the stylus meter reads, so this corrects both alongside the tracklist rather than leaving three answers free to disagree. Whole-side plays only — a partial records how many tracks ran and never which, so its wear cannot be reconstructed.

Unlike POST /durations, a value here overwrites a stored one. That is the whole difference between the two: there the source is another pressing and a hand-typed figure must survive it, here the source is the person, and refusing their correction would make the field pointless.

Parameters

id path required
integer · int64

Request body required

application/json

object
tracks array required

The whole tracklist. Not empty, no side and position twice, and no running time of zero or less.

each item
object

One track on a physical side.

side string required

"A", "B", "C"… Cassettes have two, a double LP four.

position integer · int64 required

Position within the side, from 1.

title string required
artist string | null
duration_secs integer | null · int64
disc integer | null · int64

Which physical disc of the set this track is on, from 1.

Null means nobody recorded which disc, not disc 1.

Responses

200

Saved.

application/json

object
status string required

ok.

changed integer required

Running times that differ from what was stored.

listens_updated integer · int64 required

Listens already recorded from these sides that were corrected.

plays_recomputed integer · int64 required

Whole-side plays whose recorded wear was brought back in line.

400

Empty tracklist, a duplicated side and position, or a running time of zero or less.

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 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/physical/{id}/durations #
Session

What another pressing says this record's tracks run.

Preview only — nothing is written. Session-only.

Exists because a record catalogued from Discogs often has no running times at all, which used to leave the deck showing three minutes a track as though it were a length. The lookup is a search (MusicBrainz id if the record has one, else barcode, else artist and title), so whatever comes back is a different pressing and may be ordered differently or carry bonus tracks — tracks are therefore matched by title, never by position.

complete: true means every track already has a duration and there is nothing to do. An empty durations with a null source means nothing with running times was found, which is a normal answer.

Parameters

id path required
integer · int64

Responses

200

OK.

application/json

object

What another pressing says the tracks run. Nothing is written.

complete boolean required

Every track already has a running time; there is nothing to ask.

durations array required
each item
object

One track's proposed running time, as previewed and as applied.

side string required
position integer · int64 required
title string required
current integer | null · int64

What the shelf holds now.

proposed integer | null · int64

What the looked-up pressing says. Null where nothing matched — never a guess.

source object | null

The pressing the running times came from.

title string required
artist string required
year integer | null · int64
format string | null
country string | null
mbid_release string | null
unavailable boolean | null

The lookup could not be made — MusicBrainz was busy, throttling, or did not answer inside this endpoint's own 20-second deadline. Still a 200, deliberately: an upstream being slow is a normal outcome of asking, and a 5xx here is indistinguishable from a reverse proxy's own. An empty durations without this means the lookup succeeded and found nothing.

note string | null
400

That record has no tracklist.

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

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/physical/{id}/durations #
Session

Write the running times that were previewed.

Session-only. Takes the proposals back rather than searching again, so what lands is what was on the screen — a preview that applies something else is worse than no preview.

Only fills a duration the record does not already have: one that was typed in, or that an earlier lookup got right, is never overwritten by a later fetch.

The listens already recorded from those sides are given the same figures, because every hours readout derives from scrobbles.duration. Their timestamps are not touched — physical_plays records when a side started and nothing about when it ended, so where the tracks fell is unrecoverable, and re-spacing them would assume the side ran uninterrupted.

Parameters

id path required
integer · int64

Request body required

application/json

object

The proposals exactly as the preview showed them.

durations array required
each item
object

One track's proposed running time, as previewed and as applied.

side string required
position integer · int64 required
title string required
current integer | null · int64

What the shelf holds now.

proposed integer | null · int64

What the looked-up pressing says. Null where nothing matched — never a guess.

Responses

200

OK.

application/json

object
filled integer required

Tracks that gained a duration. One the shelf already had is never overwritten.

listens_updated integer · int64 required

Listens already recorded that were corrected.

plays_recomputed integer · int64 required

Side plays whose recorded wear was brought back in line. Whole-side plays only — a partial play records how many tracks ran but never which, so its wear cannot be reconstructed and is left alone.

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

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/physical/{id}/play #
Token writeor Session

Record a side as played.

Scope: write (since v0.120). Writes one listen per track, not one per side — every counting query in Tapedeck is over listens, so a side-shaped row would be invisible to all of them.

Timestamps are the one inferred thing here: you observed when the side started, so tracks are spaced by their running times. A track of unknown length gets three minutes — wrong but bounded, where stacking them on one instant would break session grouping and the hour-of-day heatmap outright.

Per-track outcomes from the browser deck are taken as authoritative: a side laid out from a start time can only describe an uninterrupted play. A skipped track is stored, excluded from every count, and never forwarded — and does not accrue gear hours.

Parameters

id path required
integer · int64

Request body required

application/json

object
side string required
tracks array

Per-track outcomes from the deck. Authoritative where present — the browser ran the clock, so it is the only thing that saw the pauses and the skips. Without it the running order is laid out from started_at.

each item
object

What actually happened to one track on the side, as the deck saw it.

position integer · int64 required
started_at integer · int64 required

When this track actually started, unix seconds.

skipped boolean

Skipped rather than heard. Stored, but excluded from every count and never forwarded.

started_at integer | null · int64

When the side started, unix seconds. Defaults to now minus its running time, which is what you want when you press the button as it finishes.

equipment_id integer | null · int64

The turntable, cartridge or deck. Its playback hours are summed from these rows.

chain_id integer | null · int64
to_position integer | null · int64

The last track that played, when the side was not run to its end. With from_position this selects a range; the two equal is a single track. Tracks after it are not written at all — not as skips either. The play still counts as wear.

from_position integer | null · int64

The first track that actually played, when the side did not start at its beginning. Load-bearing: without it, starting at track 4 records tracks 1–3 as played. They are not written at all — and not as skips, since a track you never reached is not one you skipped. Wear is credited only for the part that played.

parked_secs integer | null · int64

Where a tape was left when it came off, in seconds into the side. Cassettes only, and clamped to the side's length. Absent means the side ran through and the tape is wound back.

chain_variant_id integer | null · int64

Which variant of the chain was fitted — the cartridge in the headshell. The shelf is the one write path that legitimately knows this, because you told it when you dropped the needle.

Responses

200

Logged.

application/json

object
listens_added integer required
skipped integer required
tracks integer required
duration_secs integer · int64 required

Running time credited as wear.

started_at integer · int64 required
next_side string | null

The side to reach for next — only when this one ran to its end.

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 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/physical/{id}/now-playing #
Token writeor Session

A side is on the deck.

Scope: write (since v0.120). The shelf is the one source where the browser is the player: the clock runs in front of the turntable, in localStorage, and nothing server-side can derive a playhead from a stack of rows. So the deck pushes here, where Plex, Jellyfin and Navidrome are polled.

Call it on every track change, on pause and resume, and on a heartbeat of about ten seconds. The heartbeat is not optional: the registry expires an entry by running past the end of the track it knows about, so a paused record would drop off the dashboard one track length after it was paused.

Now-playing is forwarded to the user's Last.fm and ListenBrainz only when the track actually changed, and never while paused — their now-playing is a one-shot that expires on its own, so re-sending it on every heartbeat is noise aimed at someone else's servers. The listens themselves are written by /play when the side comes off; this endpoint stores nothing.

Parameters

id path required
integer · int64

Request body required

application/json

object
side string required
position integer · int64 required

The track's position on the side.

elapsed integer · int64

Seconds into that track.

paused boolean

A paused record is not playing, but it is still on the deck. The entry stays and stops advancing, which is what the still platter shows.

Responses

200

OK.

application/json

object
status string required

ok.

changed boolean required

A different track from the last report.

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 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/physical/{id}/sides #
Token reador Session

How often each side has been played, and for how long.

Scope: read (since v0.120). A record has a side you reach for and a side you rarely turn over, and the release's own totals cannot say which is which.

Everything here is derived from the sides played on read — never a counter — so it cannot drift from the plays it counts, and deleting a play takes its wear back with it. secs is the running time of the tracks that actually played, which is the same figure the stylus is charged for: a side started at track four counts four tracks' worth. It is nominal rather than wall clock.

Only sides with plays are returned. A side that has never been played is not an entry with zeros — the caller holds the tracklist and fills those in. That also means a side may appear that is not on the tracklist at all: re-filing a release rewrites its sides (a vinyl A/B becomes a CD's 1) while its plays keep the label they were recorded with, and dropping those would leave the sides summing to less than the release total with nothing to say why.

Parameters

id path required
integer · int64

Responses

200

OK.

application/json

object
sides array required

Only sides that have been played, including any recorded under a label the tracklist no longer uses.

each item
object

One side of a pressing, and what it has been through.

Every field is derived from the plays on read.

side string required

As it was recorded: "A", "B", or a disc number.

plays integer · int64 required
secs integer · int64 required

Running time of what actually played, summed over every play.

last_played integer | null · int64
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 record.

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/physical/{id}/images #
Token reador Session

Every scan held for a pressing.

Scope: read (since v0.120). Sleeves, labels, obi strips and booklet pages — the parts of a record you actually hold, which is why they are rows rather than one more artwork_url column. The Cover Art Archive carries 29 for a typical CD, 19 of them booklet pages.

Each entry carries a single src and thumb whichever provenance it came from: a provider's image is its own URL, an uploaded scan is a Tapedeck URL that checks who is asking. The server's own file layout is never in the response.

Parameters

id path required
integer · int64

Responses

200

OK.

application/json

object
images array required
each item
object

One scan or photograph of a pressing.

id integer · int64 required
release_id integer · int64 required
kind string required

front, back, booklet, medium, spine, … Free text rather than an enum: the archive's vocabulary grows, and a scan of something it has no word for is still a scan.

provider string required

caa, discogs or upload.

src string required

What to render, whichever provider it came from.

thumb string required
caption string | null
position integer · int64 required
bytes integer | null · int64
mime string | null
added_at integer · int64 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."
}
POST /api/v1/physical/{id}/images #
Token writeor Session

Upload scans of your own copy.

Scope: write (since v0.120). multipart/form-data: any number of file fields, plus optional kind and caption text fields that apply to all of them — which is what makes uploading a booklet in one go work, since the files land in the order they are sent.

The declared content type decides the stored extension; a filename from the client never reaches the path. Max 24 MB per image.

Uploads are files on the server rather than blobs in the database, so GET /api/v1/backup does not carry them — GET /api/v1/physical/export is what does.

Parameters

id path required
integer · int64

Request body required

multipart/form-data

object

A scan of your own copy. Several file fields may be sent at once.

file string · binary required

JPEG, PNG, WebP, GIF, AVIF or TIFF, at most 24 MB.

kind string | null

Defaults to other.

caption string | null

Responses

201

Stored.

application/json

object
ids array required
each item
integer · int64
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 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.

413

Larger than 24 MB.

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.

415

Not an image type this server stores.

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/physical/{id}/images/fetch #
Token writeor Session

Pull the sleeve from the Cover Art Archive.

Scope: write (since v0.120). Runs when the user asks rather than on a clock: the archive is not going to change under a record that has been on a shelf for thirty years, and the one case that does — an editor uploading the booklet you were missing — is exactly the case where the user knows and Tapedeck cannot.

Idempotent on the provider's own image id, so re-running this adds what is new and leaves the rest alone rather than stacking a second copy of all nineteen booklet pages on top of the first.

Discogs runs second and only when a credential is configured; it says only "primary" or "secondary" about an image, so everything but the cover lands in other for the user to file. A pressing with no MusicBrainz id is not an error — it comes back with a note saying there is nothing to look it up by.

Parameters

id path required
integer · int64

Responses

200

OK.

application/json

object
stored integer required

Newly stored.

total integer required

Now held.

notes array required

A provider with nothing to offer or that failed, and why.

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