← API reference

ListenBrainz

ListenBrainz-compatible surface under /1/. Shapes follow the LB Core API because clients branch on those exact fields. Two deliberate divergences: these require auth and are scoped to the caller (LB serves them publicly; a self-hosted instance must not), and an invalid token on validate-token is 200 {valid:false}, never 401, because clients read the field.

9 of 9 · v0.120.0
POST /1/submit-listens #
Token or Session

Submit listens.

Accepts a session cookie or any valid token — this is the one endpoint a submit-scoped token reaches.

listen_type decides forwarding, and getting it wrong rewrites someone's permanent public history. single and playing_now are live and will be forwarded.

import is stored only and not forwarded (since v0.26 — before that, an import was forwarded like a live listen) — unless the token used to submit it is marked imports_are_live, which exists because some players label every scrobble import and nothing on the wire distinguishes that from a backfill. Even then two gates apply and both must pass:

  • the listen is no more than 24h old, so a genuine history import from the same client still stays local; and
  • its timestamp is no more than 1h in the future, so ordinary clock skew is tolerated but a broken clock cannot park a scrobble years ahead of everything else.

The flag is per token and invisible from the client side, which is why GET /1/validate-token reports it — read it there rather than guessing from whether listens appear at the far end.

Dedup is on (user_id, source_id, source_name) plus a fuzzy window, so re-sending the same listen is safe. Max 1000 listens per request.

additional_info is free-form and no field in it can fail a submission: an unusable value costs that field and the listen is still stored. Unknown keys are ignored.

Parameters

dry_run query

Resolve everything, store nothing and forward nothing, and report what would have happened. 1, true, yes, on and a bare ?dry_run all count.

This is the endpoint to point a client at while it is being set up: it names the winning rung of the chain ladder, the quality score your fields produced, whether the listen would be forwarded or stored only, and whether dedup would swallow it — none of which is visible from a successful submit. The preview and the real path share one implementation, so it cannot promise something the real path would not do.

Note it is genuinely read-only: a dry run looks a device up rather than upserting it, because asking what would happen must not itself be a listen.

string

Request body required

application/json

object

A ListenBrainz-shaped submission.

listen_type string required

This decides forwarding. single and playing_now are live; import is stored only unless the token is marked imports_are_live — see POST /1/submit-listens.

singleplaying_nowimport
payload array required

At least one listen and at most 1000; exactly one for playing_now.

each item
object

One listen in a ListenBrainz-shaped submission.

listened_at integer | null · int64

Unix seconds, UTC, start of play. Required for single and import, and ignored for playing_now, which is always now.

track_metadata object required
artist_name string required

The artist credit as one string. A semicolon-separated value (Bach; Hilary Hahn) is read as several credited artists when additional_info.artist_names is absent — see there.

track_name string required
release_name string | null
additional_info object | null

ListenBrainz's free-form bag, and where Tapedeck's own extensions ride.

No field in here can fail a submission. A value of the wrong type costs that field — or, for a tapedeck_* object, that object — and the listen is still stored. Unknown keys are ignored.

submission_client string | null
submission_client_version string | null
duration_ms integer | string | null
isrc string | null

The recording's ISRC, when the client knows it — a tag read off the file usually does. It survives crossing between a library, a streaming catalogue and MusicBrainz, so a listen that arrives with one never needs a lookup to be identified.

track_number integer | string | null
skipped boolean | null

The track was skipped rather than played through. Stored, excluded from every count, and never forwarded — a forwarded skip is a wrong scrobble on a permanent record. Top level of additional_info; there is no tapedeck_skipped.

listened_ms integer | string | null
recording_mbid string | null

MBIDs as a submitting client sends them. A listen that arrives identified is never looked up again.

release_mbid string | null
release_group_mbid string | null

The release group, which is what relates a reissue, a remaster and a regional edition to one another.

release_track_mbid string | null

The track on a particular release, as distinct from the recording. Only a tagging client ever knows it.

work_mbid string | null

The composition rather than a performance of it. Only a tagging client ever knows it.

artist_mbids array | null
each item
string
album_artist_mbids array | null

Whoever the record is credited to. On a compilation or a soundtrack that is not the performer, which is why it is a field of its own rather than a fallback for artist_mbids.

each item
string
artist_names array | null

Everyone credited, where the client can separate them — the only structured way to say "Bach and Hilary Hahn". All of them are recorded and the artist pages and charts aggregate over every one, so a soloist appears under their own name rather than vanishing into the composer's.

Failing this, a semicolon-separated artist_name is split on ; and nothing else. & and , appear inside real names — "Simon & Garfunkel", "Earth, Wind & Fire" — so splitting on those would invent artists who do not exist.

each item
string
tapedeck_audio object | null

How the audio actually sounded, as a submitting client reports it in additional_info.tapedeck_audio. Every field is optional; a plugin that can see its decoder reports most, a bare scrobble client none.

This is the write-side shape. A stored listen carries the same information flat on the Listen object rather than nested.

format_type string | null

pcm, dsd, or mqa. Not the codec.

codec string | null

The codec as your decoder names it — flac, mp3, dsd_lsbf_planar. Tapedeck matches DSD and PCM by prefix, so ffmpeg's per-ordering and per-sample-format spellings all classify correctly.

bitrate integer | null · int32

kbps.

sample_rate integer | null · int32

Hz.

bit_depth integer | null · int32
channels integer | null · int32
container string | null

The file container, e.g. ogg, m4a.

is_lossless boolean | null
source_quality string | null

What the source claims to be, where that differs from what was measured.

dsd_rate integer | null · int64

Hz.

dsd_multiplier integer | null · int32

64, 128, 256… Derived from dsd_rate when absent, and left unset rather than guessed when the rate is not a real DSD multiple.

delivery_codec string | null

Set only when the audio was genuinely transcoded. Remuxing a container without touching the audio is not a transcode, nor is a video transcode that left the audio alone. Scoring docks points whenever this is set, so filling it on a direct play quietly penalises the best listens on the system.

delivery_bitrate integer | null · int32

kbps.

delivery_sample_rate integer | null · int32

Hz.

delivery_bit_depth integer | null · int32
dsd_to_pcm_converted boolean | null

Usually derived from the signal chain rather than reported. SACD's DSD layer cannot leave a player over coax or optical, so nothing downstream will ever tell you this directly.

is_transcoded boolean | null
transcode_reason string | null
tapedeck_device object | null

What was playing it. Resolves to a devices row, which is rung 4 of the chain ladder — without it a listen from a client with no token default and no output binding gets no signal chain at all.

player_name string | null
player_version string | null
platform string | null
machine_id string | null

The key. Stable per installation and never the display name, which the user edits — keying on a name re-registers the device as a new one the moment it is renamed. Nothing is recorded without this.

output_device string | null

What the audio came out of. Recorded whether or not it is mapped, so the UI can offer unmapped ones for one-tap assignment, and it drives rung 2 of the chain ladder.

output_type string | null
interface string | null

USB, Bluetooth, analog… Preferred over output_type.

tapedeck_chain object | null

Signal chain, rung 1 of the four-rung ladder: explicit name → output-device binding → the submitting token's default_chain_id → the source device's default. An unknown name resolves to no chain rather than erroring.

Note this is an object. It was documented as a bare string until v0.66.0 and never was one.

chain_id string | null

The chain's name, despite the field being called chain_id — a client knows what the user called it, not its row id. Use ?dry_run=1 to check it resolves.

components array | null

Accepted and not yet stored.

each item
string
tapedeck_session object | null

What the client was doing when the track came up — specifically, whether the listener chose it.

is_shuffle boolean | null

Whether shuffle chose this track rather than the listener.

queue_source string | null

What it was played from — a playlist name, an album, a queue, radio. Free text: it is your vocabulary for your own containers and there is no shared one to normalise against.

tapedeck_playback object | null

Where the playhead is, from a client that can actually see it.

The ListenBrainz submission format carries no position, so a playing_now without this is wall-clock arithmetic from the moment you spoke — right until the listener pauses or seeks, wrong from then on, and reported as an estimate (position_known: false) rather than dressed up as a measurement. Send this and the deck shows a real position.

A playhead sent here is believed outright, including a step backwards: a polled source's report may lag its player by a few seconds and is smoothed for it, but a client measuring at the instant it sends has no such lag, so a backward step is the listener seeking and you are the authority on that.

position_ms integer | string | null
state string | null

playing or paused. Anything other than paused is read as playing, which is the safe way to fall: a wrongly-paused deck freezes and reads as broken.

Send paused when the listener pauses. Without it the position counts forward through the pause while position_known asserts it is a measurement — a confident and completely wrong readout. A paused entry holds still and expires after ten minutes of silence, so a heartbeat keeps it alive and stopping altogether clears it.

mbid_mapping object | null

The shape ListenBrainz returns its MBIDs in. Read as a fallback; a submitting client should put them in additional_info.

recording_mbid string | null
release_mbid string | null
artist_mbids array | null
each item
string
caa_id integer | null · int64

Cover Art Archive id.

caa_release_mbid string | null

Responses

200

Accepted, or — with ?dry_run — resolved and discarded. Per-listen results are reported rather than the whole batch failing: one bad row must not lose the other 999.

application/json

A submit answers one of these: the outcome, or — with ?dry_run — the preview.

one of
option 1 object

Everything past status is a Tapedeck extension and purely additive — ListenBrainz clients branch on status alone, so the extra keys cannot break them. They exist because a batch submit was otherwise all-or-nothing from the client's side: "ok" said nothing about which listens were stored, which were already held, and which were malformed.

status string required

Always ok.

accepted integer · int32 required

Newly stored.

duplicate integer · int32 required

Singular. Recognised as already held, by source_id or by the fuzzy window. Not an error — a client resending after a dropped connection is the normal way this happens. But a client whose every listen comes back here has a bug, which is why it is counted apart from accepted rather than folded in.

rejected array required

Could not be stored and must not be retried — malformed client data, so resending changes nothing. Always present, empty when nothing was rejected; an absent key would be ambiguous.

each item
object
index integer required

Index into the submitted payload, so the client can map it back to what it sent.

reason string required
option 2 object

The reply to ?dry_run — what would have happened.

status string required

Always ok.

dry_run boolean required

Always true.

listens array required
each item
object

One listen's resolved state, computed and thrown away.

index integer required

Index into the submitted payload.

artist string required
title string required
album string | null
timestamp integer · int64 required
quality_score number | null · double

What your tapedeck_audio fields scored, 0–100 — the main thing worth getting right and otherwise invisible.

chain_id integer | null · int64
chain_name string | null
chain_source string required

Which rung of the ladder won: explicit, output_binding, token_default, device_default or none. The single most useful field here: a chain arriving from the wrong rung looks identical to one arriving from the right one, so without this a misconfiguration is invisible until someone reads their history months later.

device_id integer | null · int64
listening_context string | null
status_if_stored string required

pending will be forwarded onward; imported is stored only. The distinction a backfill has to get right, answered before it is too late to change.

skipped boolean required
duplicate boolean required

Whether dedup would have swallowed it, checked read-only against the real rules.

rejected array required
each item
object
index integer required

Index into the submitted payload, so the client can map it back to what it sent.

reason string required
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."
}
503

One or more listens could not be stored. Retryable — resend the batch; dedup makes that safe. Distinct from rejected, which is the do-not-retry list.

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 /1/validate-token #
Token

Check a token, and ask what it may do.

An invalid token is 200 {valid:false}, never 401 — LB clients read the field, and a 401 sends them into a refresh loop.

The token may arrive in the Authorization header or as ?token=.

Everything past user_name is Tapedeck introspection, and it is additive: real ListenBrainz clients branch on valid alone, so the extra keys cannot break them, and putting them here rather than on a second endpoint costs no extra round trip. They let a settings screen say what this token can do instead of choosing between hiding features that might work and offering features that 403 at the moment of use.

They appear only when valid is true. This endpoint answers whoever asks, so reporting the build beside valid: false would hand it to anyone guessing at tokens.

Note this reports the grants themselves, never any of the user's data. It widens nothing: submit still reaches only this endpoint and submit-listens.

Parameters

token query

The token, for a client that cannot set a header. The Authorization: Token … header wins when both are sent.

string

Responses

200

Valid or not — read valid.

application/json

object

ListenBrainz's answer, plus what this token may do when it is valid.

code integer · int32 required

Always 200.

message string required
valid boolean required
user_name string | null

Present only when valid.

scopes array | null

As granted, with all expanded: some of submit, read, write. Present only when valid. Matching is exact and none implies another — submit does not carry read, and write does not carry read.

each item
string
default_chain_id integer | null · int64

Rung 3 of the chain ladder. Show the user which chain their listens will be attributed to rather than leaving them to find out from their history. Present only when valid, and then null when the token has no default.

imports_are_live boolean | null

Whether this token's listen_type: "import" submissions are relayed onward — see POST /1/submit-listens for the two gates that still apply. Invisible from the client side and silent in its absence, which matters most for players that label every scrobble import. Present only when valid.

server_version string | null

So a client shipped against one version can tell which extensions this instance understands without parsing this document to find out. Present only when valid.

400

No token at all, in the header or the query.

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 /1/user/{user}/listens #
Token or Session

A user's listens, LB-shaped.

Scoped to the caller. LB serves this publicly; a self-hosted instance must not, so asking for another user's listens is refused unless the caller is an admin. Newest first.

Parameters

user path required

Your own username.

string
max_ts query

Only listens strictly before this, unix seconds. Not with min_ts.

integer · int64
min_ts query

Only listens strictly after this, unix seconds. Not with max_ts.

integer · int64
count query

How many, newest first. 25 by default, at most 1000.

integer · int64

Responses

200

OK.

application/json

object

ListenBrainz wraps every read in payload.

payload object required
count integer required
user_id string required

The username asked for, as given in the path.

listens array required
each item
object

A listen in ListenBrainz's shape. Tapedeck's audio-quality extensions ride along in additional_info.tapedeck_audio under the keys the ingest API accepts, so a listen submitted to Tapedeck reads back the way it went in.

listened_at integer | null · int64

Unix seconds. Absent on a now-playing entry — that is how a client tells it apart from history.

track_metadata object required
artist_name string required
track_name string required
release_name string | null
additional_info object required

Every key is omitted when unknown rather than sent as null.

duration_ms integer | null · int64
recording_mbid string | null
release_mbid string | null
artist_mbids array | null
each item
string
submission_client string | null
track_number integer | null · int32
tapedeck_audio object | null

Present when any audio detail is known.

format_type string | null
codec string | null
bitrate integer | null · int32
sample_rate integer | null · int32
bit_depth integer | null · int32
channels integer | null · int32
container string | null
source_quality string | null
is_lossless boolean | null
dsd_rate integer | null · int64
dsd_multiplier integer | null · int32
dsd_to_pcm_converted boolean | null
delivery_codec string | null
delivery_bitrate integer | null · int32
delivery_sample_rate integer | null · int32
delivery_bit_depth integer | null · int32
is_transcoded boolean | null
transcode_reason string | null
playing_now boolean | null

Only on a now-playing entry, and then true.

latest_listen_ts integer · int64 required

The newest listen's timestamp, or 0 with none.

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."
}
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 /1/user/{user}/playing-now #
Token or Session

What is on the deck, LB-shaped.

Parameters

user path required

Your own username.

string

Responses

200

OK. count is 0 when nothing is playing.

application/json

object
payload object required
count integer required

0 when nothing is playing, else 1.

user_id string required
listens array required
each item
object

A listen in ListenBrainz's shape. Tapedeck's audio-quality extensions ride along in additional_info.tapedeck_audio under the keys the ingest API accepts, so a listen submitted to Tapedeck reads back the way it went in.

listened_at integer | null · int64

Unix seconds. Absent on a now-playing entry — that is how a client tells it apart from history.

track_metadata object required
artist_name string required
track_name string required
release_name string | null
additional_info object required

Every key is omitted when unknown rather than sent as null.

duration_ms integer | null · int64
recording_mbid string | null
release_mbid string | null
artist_mbids array | null
each item
string
submission_client string | null
track_number integer | null · int32
tapedeck_audio object | null

Present when any audio detail is known.

format_type string | null
codec string | null
bitrate integer | null · int32
sample_rate integer | null · int32
bit_depth integer | null · int32
channels integer | null · int32
container string | null
source_quality string | null
is_lossless boolean | null
dsd_rate integer | null · int64
dsd_multiplier integer | null · int32
dsd_to_pcm_converted boolean | null
delivery_codec string | null
delivery_bitrate integer | null · int32
delivery_sample_rate integer | null · int32
delivery_bit_depth integer | null · int32
is_transcoded boolean | null
transcode_reason string | null
playing_now boolean | null

Only on a now-playing entry, and then true.

playing_now boolean required

Always true.

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 /1/user/{user}/listen-count #
Token or Session

Total listens. Excludes skips.

Parameters

user path required

Your own username.

string

Responses

200

OK.

application/json

object
payload object required
count integer · int64 required

Every listen, skips excluded.

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 /1/user/{user}/services #
Token or Session

Connected forwarding services.

Parameters

user path required

Your own username.

string

Responses

200

OK.

application/json

object
user_name string required
services array required

The forwarding services connected: lastfm, listenbrainz, librefm.

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."
}
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 /1/playing-now/delete #
Token or Session

Clear the caller's now-playing entry.

Responses

200

Cleared. status is ok.

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."
}
GET /1/latest-import #
Token or Session

Timestamp of the caller's most recent import.

Importers use this as a bookmark: read the timestamp, fetch everything newer, write the new high-water mark back.

Parameters

user_name query

Whose bookmark. Defaults to the caller; anyone else's is admin-only.

string

Responses

200

OK.

application/json

object
musicbrainz_id string required

The username asked about.

latest_import integer · int64 required

Unix seconds; 0 when none was ever recorded.

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 /1/latest-import #
Token or Session

Record an import high-water mark.

Always the caller's own.

Request body required

application/json

object
ts integer · int64 required

Unix seconds. Not negative.

Responses

200

Stored. status is ok.

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