The listen history.
Scope: read (since v0.39). Newest first. Filterable.
Parameters
limit query Page size. 50 by default, at most 500.
offset query Rows to skip.
artist query Exact artist, as stored.
album query Exact album, as stored.
track query Exact track title, as stored.
after query Unix seconds, inclusive.
before query Unix seconds, inclusive.
social query true for listening marked as heard in company, false for the rest.
Absent means no filter, which is the only sensible default: a listen nobody marked is unmarked, not proven solitary, and quietly excluding unmarked listening from a history page would hide most of it.
Responses
OK.
application/json
A page of the history, newest first.
scrobbles array required A stored listen. The quality columns are flat, not nested under a
quality object — the frontend reads them raw so its own qualityLabel()
produces the same tag everywhere, and a server-side copy of that logic
would drift.
The audio fields are null for most of any real history: they are only ever written by ingest and the session-shaped sources, so an imported listen carries none of them. Any share computed over them is a share of that population, not of the listening.
id integer · int64 required user_id integer · int64 required title string required artist string required The primary artist. Everyone credited lives in scrobble_artists and is
what the artist pages and charts aggregate over, so a listen crediting a
soloist appears under both names there while this field still names the
one the record is filed under.
album string | null timestamp integer · int64 required Unix seconds, UTC, start of play.
duration integer | null · int64 Seconds.
source_id string required Dedup key. Unique per user per play.
source_name string required Plex, Jellyfin, Navidrome, ingest:<client>, Import…
status string required pending is queued for forwarding, synced has been forwarded, and
imported is stored only and will never be forwarded. Both an import
and a skip land as imported — forwarding either would put a wrong
scrobble on a permanent public record.
There is no sent and no failed. A delivery that fails stays
pending and is retried, and which sinks it has already reached is in
delivered_sinks — that is what stops a partial failure
double-scrobbling.
mbid_recording string | null mbid_release string | null mbid_artist string | null A JSON-encoded array, not an array — the column stores what was written to it. Parse it before use.
mbid_release_group string | null The release group, which is what relates a reissue, a remaster and a
regional edition to one another. Filled from a submitting client's
additional_info, a ListenBrainz export's mbid_mapping, or a
Jellyfin/Emby session — never by a lookup.
mbid_release_track string | null The track on a particular release, as distinct from the recording: the same recording is a different release-track on an album, a single and a compilation. Only a tagging client ever knows it.
mbid_work string | null The composition rather than a performance of it — the identifier that relates every recording of one piece. Only a tagging client ever knows it.
mbid_album_artist string | null A JSON-encoded array, like mbid_artist. Whoever the record is
credited to, which on a compilation or a soundtrack is not the
performer of the track.
caa_id integer | null · int64 Cover Art Archive id. -1 is a tombstone meaning "asked, no artwork
exists" — treat any non-positive value as no artwork.
caa_release_mbid string | null isrc string | null format_type string | null pcm, dsd, mqa.
codec string | null bitrate integer | null · int32 kbps.
sample_rate integer | null · int32 Hz.
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 Hz.
dsd_multiplier integer | null · int32 dsd_to_pcm_converted boolean | null delivery_codec string | null Set only on a genuine transcode.
delivery_bitrate integer | null · int32 kbps.
delivery_sample_rate integer | null · int32 Hz.
delivery_bit_depth integer | null · int32 is_transcoded boolean | null transcode_reason string | null quality_score number | null · double 0–100, computed once at ingest and never recomputed. Null for anything imported, so an average over it has the same narrow population as the audio fields.
device_id integer | null · int64 chain_id integer | null · int64 The resolved signal chain.
chain_variant_id integer | null · int64 Which setup of that chain was fitted.
session_id integer | null · int64 listening_context string | null From the resolved chain.
submission_client string | null track_number integer | null · int32 artwork_url string | null Empty string is a tombstone, same idea as caa_id: -1.
source_ref integer | null · int64 The user_sources row this was polled from.
listened_ms integer | null · int64 Milliseconds actually heard, paused time excluded. Only a pushing
client can report this, so it is null for everything polled and
everything imported. What skipped cannot express — "cut short three
seconds in" and "cut short with thirty seconds left" are opposite
behaviours.
is_shuffle boolean | null Shuffle chose this, not the listener.
queue_source string | null What it played from, in the client's own words.
company string | null Who this was heard with, as names. Filled automatically for a listen
that happened inside a Shared Spool, and by hand via
POST /api/v1/scrobbles/bulk-company otherwise.
null means unmarked, not alone. Nobody recorded who was there —
which is true of every listen predating the field. Counting marked
listens is exact; reporting a share of listening that was solitary is
not, and would be a fact about how much has been marked.
joint_session_id string | null The Shared Spool this listen belonged to, as the spool's uuid. A joint listen is N rows across N decks sharing this — never one row with several owners. Only a sitting ever sets it.
delivered_sinks string | null JSON array of the sinks this has already reached, so a retry only hits the ones that have not. A string, not an array.
count integer required Rows in this page, not in the history.
limit integer · int64 required offset integer · int64 required 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
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."
}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
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"
}