← API reference

Sessions

Contiguous listening, grouped.

7 of 7 · v0.120.0
GET /api/v1/sessions #
Token reador Session

Listening sessions.

Scope: read (since v0.120). Contiguous listening with gaps under 30 minutes. A change of submitting token also breaks a session — one token per app is how people organise these, so "fooyin stopped, Pano started" is a different setup even a minute later.

Parameters

limit query

Page size. 50 by default, at most 200.

integer · int64
offset query
integer · int64

Responses

200

OK.

application/json

object

A page of sessions, newest first.

sessions array required
each item
object

A listening session: contiguous listening through one setup.

chain_name / device_name / token_name are joined so a list of sessions renders without a request per row. title is a user label; when it's null the UI derives one from the span.

id integer · int64 required
uuid string | null

Stable across merges and splits, and the id a shared session will carry.

title string | null

Your name for it; null means the screen derives one from the span.

notes string | null

A note about the sitting itself. Distinct from a liner note, which is about a record and hangs off an entity or a single listen.

started_at integer · int64 required

Unix seconds.

ended_at integer | null · int64

Unix seconds.

track_count integer · int64 required
total_duration integer · int64 required

Seconds.

avg_quality_score number | null · double
listening_context string | null
chain_id integer | null · int64
chain_name string | null
chain_variant_id integer | null · int64

Which variant of that chain the sitting's listens carry. Read off the listens rather than stored on the session, so it cannot disagree with them — the same rule recompute_session follows for the aggregates.

chain_variant_name string | null
device_id integer | null · int64
device_name string | null
token_id integer | null · int64

The API token the listens were submitted with. A change of token starts a new session.

token_name string | null
count integer required

Rows in this page.

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

One session and its listens.

Scope: read (since v0.120).

Parameters

id path required
integer · int64

Responses

200

OK.

application/json

object

One session and its listens, in play order.

session object required

A listening session: contiguous listening through one setup.

chain_name / device_name / token_name are joined so a list of sessions renders without a request per row. title is a user label; when it's null the UI derives one from the span.

id integer · int64 required
uuid string | null

Stable across merges and splits, and the id a shared session will carry.

title string | null

Your name for it; null means the screen derives one from the span.

notes string | null

A note about the sitting itself. Distinct from a liner note, which is about a record and hangs off an entity or a single listen.

started_at integer · int64 required

Unix seconds.

ended_at integer | null · int64

Unix seconds.

track_count integer · int64 required
total_duration integer · int64 required

Seconds.

avg_quality_score number | null · double
listening_context string | null
chain_id integer | null · int64
chain_name string | null
chain_variant_id integer | null · int64

Which variant of that chain the sitting's listens carry. Read off the listens rather than stored on the session, so it cannot disagree with them — the same rule recompute_session follows for the aggregates.

chain_variant_name string | null
device_id integer | null · int64
device_name string | null
token_id integer | null · int64

The API token the listens were submitted with. A change of token starts a new session.

token_name string | null
scrobbles array required
each item
object

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.

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

Retag, name or annotate a whole sitting.

Session-only. Setting chain_id rewrites the chain on every listen in the sitting.

Parameters

id path required
integer · int64

Request body required

application/json

object

Every field is optional and distinguishes absent ("leave alone") from an explicit null ("clear").

chain_id integer | null · int64

Retag every listen in the sitting with this chain. Present-and-null clears the chain; absent leaves it alone. Must be one of your chains.

title string | null

Name the sitting.

chain_variant_id integer | null · int64

Which variant of the chain — "with the microlinear", "on the Hexas". Only read alongside chain_id, and rewritten whenever it is, since a variant belongs to one chain.

notes string | null

A note about the sitting. Not a liner note about a record — that is /api/v1/notes, which hangs off an entity or a single listen.

Responses

200

Updated. The session as it now stands.

application/json

object

A session, as it stands after an edit.

session object required

A listening session: contiguous listening through one setup.

chain_name / device_name / token_name are joined so a list of sessions renders without a request per row. title is a user label; when it's null the UI derives one from the span.

id integer · int64 required
uuid string | null

Stable across merges and splits, and the id a shared session will carry.

title string | null

Your name for it; null means the screen derives one from the span.

notes string | null

A note about the sitting itself. Distinct from a liner note, which is about a record and hangs off an entity or a single listen.

started_at integer · int64 required

Unix seconds.

ended_at integer | null · int64

Unix seconds.

track_count integer · int64 required
total_duration integer · int64 required

Seconds.

avg_quality_score number | null · double
listening_context string | null
chain_id integer | null · int64
chain_name string | null
chain_variant_id integer | null · int64

Which variant of that chain the sitting's listens carry. Read off the listens rather than stored on the session, so it cannot disagree with them — the same rule recompute_session follows for the aggregates.

chain_variant_name string | null
device_id integer | null · int64
device_name string | null
token_id integer | null · int64

The API token the listens were submitted with. A change of token starts a new session.

token_name string | null
400

Not one of your chains, a variant of another chain, or a variant without a chain.

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/sessions/merge #
Session

Merge sessions.

Keeps the earliest session's identity — its id and uuid are what anything else already refers to. Ownership is checked for every id before anything moves, so a merge naming someone else's session changes nothing rather than half-completing.

Request body required

application/json

object
ids array required

Two or more session ids. The earliest survives.

each item
integer · int64

Responses

200

Merged. The surviving session.

application/json

A merge's outcome: the surviving session, or just its id.

one of
option 1 object

A session, as it stands after an edit.

session object required

A listening session: contiguous listening through one setup.

chain_name / device_name / token_name are joined so a list of sessions renders without a request per row. title is a user label; when it's null the UI derives one from the span.

id integer · int64 required
uuid string | null

Stable across merges and splits, and the id a shared session will carry.

title string | null

Your name for it; null means the screen derives one from the span.

notes string | null

A note about the sitting itself. Distinct from a liner note, which is about a record and hangs off an entity or a single listen.

started_at integer · int64 required

Unix seconds.

ended_at integer | null · int64

Unix seconds.

track_count integer · int64 required
total_duration integer · int64 required

Seconds.

avg_quality_score number | null · double
listening_context string | null
chain_id integer | null · int64
chain_name string | null
chain_variant_id integer | null · int64

Which variant of that chain the sitting's listens carry. Read off the listens rather than stored on the session, so it cannot disagree with them — the same rule recompute_session follows for the aggregates.

chain_variant_name string | null
device_id integer | null · int64
device_name string | null
token_id integer | null · int64

The API token the listens were submitted with. A change of token starts a new session.

token_name string | null
option 2 object

The fallback answer to a merge whose surviving session could not be read back.

id integer · int64 required

The surviving session.

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."
}
POST /api/v1/sessions/{id}/split #
Session

Split a session at a listen.

Splitting at the first listen is a no-op, not an error — it would leave one empty half, which is the session you already had. Aggregates are recomputed from the listens rather than adjusted arithmetically.

Parameters

id path required
integer · int64

Request body required

application/json

object
at_scrobble_id integer · int64 required

The listen that starts the new session.

Responses

200

Split.

application/json

object

Both halves of a split, as they stand now.

session object | null

The original session, now ending before the split.

id integer · int64 required
uuid string | null

Stable across merges and splits, and the id a shared session will carry.

title string | null

Your name for it; null means the screen derives one from the span.

notes string | null

A note about the sitting itself. Distinct from a liner note, which is about a record and hangs off an entity or a single listen.

started_at integer · int64 required

Unix seconds.

ended_at integer | null · int64

Unix seconds.

track_count integer · int64 required
total_duration integer · int64 required

Seconds.

avg_quality_score number | null · double
listening_context string | null
chain_id integer | null · int64
chain_name string | null
chain_variant_id integer | null · int64

Which variant of that chain the sitting's listens carry. Read off the listens rather than stored on the session, so it cannot disagree with them — the same rule recompute_session follows for the aggregates.

chain_variant_name string | null
device_id integer | null · int64
device_name string | null
token_id integer | null · int64

The API token the listens were submitted with. A change of token starts a new session.

token_name string | null
new_session object | null

The new session, starting at the listen you split at.

id integer · int64 required
uuid string | null

Stable across merges and splits, and the id a shared session will carry.

title string | null

Your name for it; null means the screen derives one from the span.

notes string | null

A note about the sitting itself. Distinct from a liner note, which is about a record and hangs off an entity or a single listen.

started_at integer · int64 required

Unix seconds.

ended_at integer | null · int64

Unix seconds.

track_count integer · int64 required
total_duration integer · int64 required

Seconds.

avg_quality_score number | null · double
listening_context string | null
chain_id integer | null · int64
chain_name string | null
chain_variant_id integer | null · int64

Which variant of that chain the sitting's listens carry. Read off the listens rather than stored on the session, so it cannot disagree with them — the same rule recompute_session follows for the aggregates.

chain_variant_name string | null
device_id integer | null · int64
device_name string | null
token_id integer | null · int64

The API token the listens were submitted with. A change of token starts a new session.

token_name string | null
400

That listen is not in this session, or is already its first.

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/sessions/rebuild #
Session

Start a regroup of every listen into sessions.

Session-only. Destructive — it rewrites the column that manual merges and splits live in, discarding them. Exists for histories that predate session linking, and for imports, which bypass the ingest path entirely.

(since v0.70.2) Runs in the background and answers 202 with a job_id; poll /api/v1/sessions/rebuild/{job_id}. It used to do the work inline, and on a real history that ran past a reverse proxy's origin timeout — the browser got a 524 while the rebuild carried on unseen, which is worse than failing outright for something destructive.

One rebuild per user at a time. A second request while one is running is a 409 carrying the running job_id, so a client can attach to it rather than starting a second pass that would clear the first's work halfway through.

Request body required

application/json

object
gap_seconds integer | null · int64

Gap in seconds that starts a new session: 1800 (the 30 minutes the live assigner uses) by default, clamped to between a minute and a day.

Responses

202

Started.

application/json

object

A rebuild, begun.

job_id string required

Poll /api/v1/sessions/rebuild/{job_id} with this.

state string required

Always running.

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

A rebuild is already running; the body carries its job_id.

application/json

object

A rebuild is already running; attach to it.

code integer · int32 required

Always 409.

error string required
job_id string required

The running rebuild.

GET /api/v1/sessions/rebuild/{job_id} #
Session

How a running regroup is going.

Session-only, and scoped to the caller — an unknown id and someone else's are the same 404, so polling cannot be used to learn that a job exists.

The registry is in memory, so a job is lost if the process restarts. That is safe rather than merely tolerable: the rebuild is one transaction, so a process that dies mid-pass rolls back instead of leaving half a regrouping behind — only the progress report is lost.

Parameters

job_id path required

From the 202 that started it.

string

Responses

200

OK.

application/json

object

One rebuild, as it runs.

job_id string required
state string required

running, done or error.

sessions integer · int64 required

Sessions written. Only meaningful once state is done.

listens integer · int64 required

Listens regrouped. Only meaningful once state is done.

status string required

A sentence saying how it went — the error, when state is error.

started_at integer · int64 required

Unix seconds.

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.