The actor document for a deck.
Content-negotiated. A caller whose Accept names
application/activity+json or application/ld+json gets the actor
document, served as application/activity+json — a server handed plain
application/json is entitled to treat it as untyped JSON, with the
result that the actor "does not exist" while this returns 200. Anything
else that names text/html gets the SPA shell, because it is a person.
A bare */* gets the document.
The document carries publicKey, inbox, outbox,
preferredUsername, name, type: Person and
manuallyApprovesFollowers: true — private by default, stated to the
network rather than only enforced at home.
Three things are deliberately absent, because advertising an
endpoint that 404s is worse than not having it: endpoints.sharedInbox
(there is no POST /inbox), and the followers / following
collections (Step 2 serves those, when patches rows exist to fill
them).
The id is written once, when the keypair is minted, and never
re-derived from today's PUBLIC_URL — recomputing it would silently
rename an actor that remote servers have cached.
Responses
200 The actor document, or the SPA shell for a browser.
application/activity+json
text/html
404 No such user, or they have not switched federation on. Identical answers — anything that told them apart would make this a membership oracle for a private, invite-only instance.
application/json
object
Every error body in the API has this shape.
code integer · int32 required
error string required
Human-readable. Not a stable identifier — do not branch on it.
GET /users/{name}/outbox # Public
An actor's public activities.
An OrderedCollection, empty until outbound delivery exists and
served anyway: remote servers probe an outbox to decide whether an actor
is real, and a 404 here reads as a broken actor rather than a quiet one.
Only activities stored visibility = 'public' appear. That filter is in
the query rather than in the handler because this collection is fetched
with no credential at all — a private or patched-in activity reaching it
is published to the internet, and nothing downstream would notice.
No first / next paging yet. An empty collection has no page to point
at, and inventing one would advertise a third endpoint that does not
exist; paging arrives with delivery.
Responses
200 An OrderedCollection with totalItems and orderedItems.
application/activity+json
404 No such user, or not discoverable. Identical answers.
application/json
object
Every error body in the API has this shape.
code integer · int32 required
error string required
Human-readable. Not a stable identifier — do not branch on it.
POST /users/{name}/inbox # Public
Accept a signed delivery.
Verifies an HTTP signature and files the activity. Nothing acts on
what arrives yet — Step 2 is what turns a pending Follow into a
patch.
Requires a draft-cavage Signature: header, rsa-sha256. The signature
must cover (request-target), host and date, plus digest whenever
there is a body — checked before any cryptography, because a signature
that verifies over the wrong set of headers is worse than none: it looks
like it proved something. Date must be within five minutes.
The signing key is fetched from the actor the keyId names and cached.
A signature that fails against a cached key triggers exactly one
refetch, which is what makes key rotation survivable.
The signature says who sent this; the activity's actor says who
wrote it. A delivery where they disagree is refused rather than filed
under either — that disagreement is how an impersonated Follow or
Delete gets in.
An activity with no id is refused: it could not be deduplicated,
referred to by a later Undo, or checked against a redelivery.
GET and DELETE here are 405 — both belonged to older revisions of
the protocol.
Request body required
application/activity+json
Responses
202 Filed. Also the answer to a redelivery — senders retry, and reporting a duplicate as an error is what makes one retry harder.
400 Not JSON, or the activity names no actor or no id.
application/json
object
Every error body in the API has this shape.
code integer · int32 required
error string required
Human-readable. Not a stable identifier — do not branch on it.
401 Unsigned, stale, badly covered, the digest does not match the body, the signature does not verify, or the signing key does not belong to the actor the activity is attributed to. The message says which.
application/json
object
Every error body in the API has this shape.
code integer · int32 required
error string required
Human-readable. Not a stable identifier — do not branch on it.
404 No such user, or not discoverable. Identical answers.
application/json
object
Every error body in the API has this shape.
code integer · int32 required
error string required
Human-readable. Not a stable identifier — do not branch on it.
GET /users/{name}/followers # Public
How many decks have patched into this one.
The count, not the roster. ActivityStreams allows a collection to
state its size without enumerating it, and that is the right default
here: who somebody has accepted is a fact about other people, and this
endpoint answers to anyone at all. Remote software uses totalItems;
the list is what would leak the shape of a household.
Served — and therefore advertised in the actor document — as of Step 2.
It was deliberately absent before that, because advertising an endpoint
that 404s is worse than not having one.
Responses
200 An OrderedCollection carrying totalItems and no items.
application/activity+json
404 No such user, or not discoverable. Identical answers.
application/json
object
Every error body in the API has this shape.
code integer · int32 required
error string required
Human-readable. Not a stable identifier — do not branch on it.
GET /.well-known/webfinger # Public
Resolve a handle to an actor.
RFC 7033. The entry point for the whole protocol — it is what somebody
typing a handle into another server's search box hits first. Served as
application/jrd+json.
resource is accepted as acct:ash@host, a bare ash@host,
@ash@host, or the actor URI itself; clients send all of these, and
refusing the unfashionable ones makes the instance undiscoverable from
software that is otherwise fine.
A missing or unreadable resource is a 400 — the caller asked
wrong. A well-formed one naming a host that is not ours, or a person we
do not publish, is a 404. Only the second is a fact about our users,
which is why the two statuses are drawn apart here and never within the
404.
Parameters
resource query required
acct:name@host, or the actor URI.
Responses
200 A JRD with a self link to the actor document.
application/jrd+json
400 No resource, or one that names no actor.
application/json
object
Every error body in the API has this shape.
code integer · int32 required
error string required
Human-readable. Not a stable identifier — do not branch on it.
404 Another host, or somebody we do not publish. Identical answers.
application/json
object
Every error body in the API has this shape.
code integer · int32 required
error string required
Human-readable. Not a stable identifier — do not branch on it.
GET /api/v1/profile/federation # Session
Whether your deck answers to the fediverse.
Your own switch. Reports enabled, the handle other people would
type, the actor URI once a keypair has been minted (null before
that), the would_be URI it will get, and frozen_at — non-null once
the username is load-bearing for another server.
Reading this does not mint a keypair. Opening a settings page is not
a decision to federate.
Responses
200 The switch and the identity it publishes.
application/json
object
Whether this account is discoverable, and the identity it publishes.
handle string required
What somebody on another server would type, @name@host.
actor string | null
The actor URI. Null until an identity has been minted — the first time
federation is switched on.
would_be string required
The URI that would be minted, shown as a preview.
frozen_at integer | null · int64
Once set, the username is load-bearing for other servers and cannot be
renamed.
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
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/profile/federation # Session
Turn discoverability on or off.
Session-only, permanently. A token is held by a scrobble client, and
no scrobble client has business deciding that its owner is findable on
the fediverse — so a token gets 401 and scopes never enter into it, the
same line /api/v1/sources and the password endpoints sit on.
Switching on mints the RSA-2048 keypair then and there, so the screen
can show the real actor URI rather than a promise.
Switching off stops answering and deletes nothing. Destroying the
keypair would destroy the identity: anyone already patched in holds a
key that would never verify again, and switching back on would present
them with a stranger wearing the same name.
enabled is required. A body that merely misspelled the key is a 400,
never a default — this is the one switch where guessing wrong publishes
something.
Request body required
application/json
object
enabled boolean required
Required — an absent key is a 400, never a default.
Responses
200 The switch as it now stands.
application/json
object
Whether this account is discoverable, and the identity it publishes.
handle string required
What somebody on another server would type, @name@host.
actor string | null
The actor URI. Null until an identity has been minted — the first time
federation is switched on.
would_be string required
The URI that would be minted, shown as a preview.
frozen_at integer | null · int64
Once set, the username is load-bearing for other servers and cannot be
renamed.
application/json
object
Every error body in the API has this shape.
code integer · int32 required
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
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/profile/share-all # Session
Let patched decks see every listen, or only shared chains.
Session-only. The third sharing switch, and it answers a third
question. public_now_playing decides whether a stranger may read
the deck; /profile/federation decides whether this account can be
found; this one decides how much a person you have already let in can
see.
Off by default and off for every account that predates it. A patch
accepted under the old rule was accepted on the understanding that a
listen through an unshared chain published nothing, and nobody consents
to more by their operator upgrading a binary.
On, a patched deck sees every listen — including those through chains
you keep private and those with no chain at all — and the chain is
named only where you share it, rendering as "Private signal chain"
otherwise. It widens what a patched deck sees and nothing else: the
Instance tab still counts shared-chain listens only, because a patch is
consent to one person and there is no patch behind a figure the whole
instance reads; and a Shared Spool still captures only through shared
chains, because a capture enters somebody's history rather than merely
being visible.
enabled is required — a body that merely misspelled the key must not
be read as an answer.
Request body required
application/json
object
enabled boolean required
Required — an absent key is a 400, never a default.
Responses
application/json
object
Every error body in the API has this shape.
code integer · int32 required
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
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/patch/decks # Session
Everyone on the instance, and where you stand with them.
Returns decks (everyone but you, and not disabled), following (the
patches you have made or asked for) and followers (the ones pointed at
you, pending and settled).
One call rather than three, because the screen shows one list of people
with a different button each — patched, pending, or not asked. Splitting
it would have the client stitch three lists back together and invent the
states in between. A peer with a null uuid and state is one nobody
has asked about.
Each peer carries two fields describing what a patch into them would
actually show, and they have to be read together.
shares_everything is the gate: with it on, a patched deck sees
every listen they play, whatever chain it ran through or whether it ran
through one at all. shared_chains is how many chains they publish,
which is what decides visibility when that switch is off — and, either
way, whether the chain is named on a row. Zero chains and closed is
the empty patch that needs saying on screen rather than looking broken;
zero chains and open is a deck publishing everything it plays, unnamed.
Responses
application/json
object
Everyone on the instance, and where you stand with them.
decks array required
Every deck you could patch into.
each item object
The other end of a patch — or a deck that could become one.
One type for three lists (out, in, and everyone patchable) so the three
cannot drift into describing a person differently. uuid and state are
null for a deck nobody has asked about yet, which is what tells "not
patched" apart from "asked and waiting".
uuid string | null
The patch's own id, None when no edge exists yet.
state string | null
pending | accepted | rejected, or None for no edge.
created_at integer | null · int64
user_id integer · int64 required
display_name string | null
shared_chains integer · int64 required
How many chains this person publishes. Zero is no longer the
interesting case on its own — read it with shares_everything, which
is the gate. A deck sharing no chains and open to patched decks shows
everything it plays; one sharing no chains and closed is the empty
patch that needs saying on screen rather than looking broken.
shares_everything boolean required
Whether they let decks they have patched in see everything they
play, not only listens through a chain they publish.
Carried so the screen can say what a patch will actually show, which it
could not do from shared_chains alone — and it leaks nothing: this
only ever describes the far end of an edge that exists, and whoever
can read it can already read the listens it is about.
following array required
Decks you patched into, or asked to.
each item object
The other end of a patch — or a deck that could become one.
One type for three lists (out, in, and everyone patchable) so the three
cannot drift into describing a person differently. uuid and state are
null for a deck nobody has asked about yet, which is what tells "not
patched" apart from "asked and waiting".
uuid string | null
The patch's own id, None when no edge exists yet.
state string | null
pending | accepted | rejected, or None for no edge.
created_at integer | null · int64
user_id integer · int64 required
display_name string | null
shared_chains integer · int64 required
How many chains this person publishes. Zero is no longer the
interesting case on its own — read it with shares_everything, which
is the gate. A deck sharing no chains and open to patched decks shows
everything it plays; one sharing no chains and closed is the empty
patch that needs saying on screen rather than looking broken.
shares_everything boolean required
Whether they let decks they have patched in see everything they
play, not only listens through a chain they publish.
Carried so the screen can say what a patch will actually show, which it
could not do from shared_chains alone — and it leaks nothing: this
only ever describes the far end of an edge that exists, and whoever
can read it can already read the listens it is about.
followers array required
Everything pointed at you, pending and settled — the inbox filters to
pending itself.
each item object
The other end of a patch — or a deck that could become one.
One type for three lists (out, in, and everyone patchable) so the three
cannot drift into describing a person differently. uuid and state are
null for a deck nobody has asked about yet, which is what tells "not
patched" apart from "asked and waiting".
uuid string | null
The patch's own id, None when no edge exists yet.
state string | null
pending | accepted | rejected, or None for no edge.
created_at integer | null · int64
user_id integer · int64 required
display_name string | null
shared_chains integer · int64 required
How many chains this person publishes. Zero is no longer the
interesting case on its own — read it with shares_everything, which
is the gate. A deck sharing no chains and open to patched decks shows
everything it plays; one sharing no chains and closed is the empty
patch that needs saying on screen rather than looking broken.
shares_everything boolean required
Whether they let decks they have patched in see everything they
play, not only listens through a chain they publish.
Carried so the screen can say what a patch will actually show, which it
could not do from shared_chains alone — and it leaks nothing: this
only ever describes the far end of an edge that exists, and whoever
can read it can already read the listens it is about.
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
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."
}
What the decks you patched into have been playing.
The one read in this application that returns another user's
listens, and every narrowing is in the query rather than the handler:
the patch must be accepted, the listen's chain must be shared, the
listen must have a chain at all, and skips are excluded.
A listen with no chain is never shared — nobody switched anything on for
it, so the default is exclusion.
Paged by keyset on before, not by offset: the Reel grows from the top
while it is being read, and an offset would repeat or skip rows as it
shifted underneath. next is null when the page was short.
Rows carry the chain — its name, icon and components — because that is
what makes a Tapedeck listen a Tapedeck listen rather than a sentence.
They deliberately do not carry the source id, the delivery state or the
session, which are nobody else's business.
Parameters
before query
Keyset paging: the next from the previous page. Not an offset — the
Reel grows from the top while it is being read.
q query
Narrows on track, artist, album, chain or deck, server-side.
deck query
One deck's username, for the filter chips.
Responses
200 listens and a next cursor.
application/json
object
A page of the Reel, newest first.
listens array required
each item object
A listen on somebody else's deck, reaching this user through a patch.
id integer · int64 required
timestamp integer · int64 required
artwork_url string | null
caa_id integer | null · int64
caa_release_mbid string | null
format_type string | null
bitrate integer | null · int64
sample_rate integer | null · int64
bit_depth integer | null · int64
is_lossless boolean | null
submission_client string | null
user_id integer · int64 required
display_name string | null
actor_uri string | null
This deck's actor URI, or null when they have not switched federation
on.
chain_name string | null
The chain, when its owner shares it.
chain_components string | null
has_chain boolean required
Whether there was a chain, whatever its owner shares.
next integer | null · int64
Pass as before for the next page. Null when this page was the last.
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
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."
}
Ask to patch into a deck.
A patch is a request, not a subscription. It lands pending and
appears in the target's inbox; nothing is shared until they say yes.
Idempotent in the direction that matters: asking twice does not mint a
second edge, and asking again does not disturb a patch that has
already been accepted. Asking again after a rejection does reopen it —
a rejection is not a block, and this design has no block yet.
Not gated on the target having switched federation on: that switch is
about the wider fediverse, and members of a private invite-only instance
already know each other exist. What protects a deck here is that the
patch needs approval and that a chain publishes nothing until its owner
shares it.
Request body required
application/json
object
username string required
The deck to patch into, by username.
Responses
200 The patch's uuid and its state as it now stands.
application/json
object
state string required
As it now stands: pending, or accepted for a patch that already
was.
400 A deck cannot patch into itself.
application/json
object
Every error body in the API has this shape.
code integer · int32 required
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
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 deck on this instance.
application/json
object
Every error body in the API has this shape.
code integer · int32 required
error string required
Human-readable. Not a stable identifier — do not branch on it.
POST /api/v1/patch/requests/{uuid}/settle # Session
Say yes or no to a patch request.
Only the target of a patch may settle it, and that check lives in
the query's WHERE rather than in the handler — this is the one
operation that widens who may read somebody's listening, and an
authorisation check that lives in a handler is one a second handler can
forget.
A patch that is not yours, one that does not exist, and one already
settled are the same 404 on purpose: anything else would confirm that a
given patch id is real to somebody it does not concern.
Accepting freezes the target's username — from that point the
identity is load-bearing for another party, and a rename would be a
deletion and a stranger rather than a rename.
accept is required. A body that merely misspelled the key must not be
read as an answer, and here the two answers are "let them read my
listening" and "no".
Request body required
application/json
object
A yes or a no, said explicitly.
accept boolean required
Required — an absent key is a 400, never a default.
Responses
200 The new state, accepted or rejected.
application/json
application/json
object
Every error body in the API has this shape.
code integer · int32 required
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
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 patch request waiting on you with that id.
application/json
object
Every error body in the API has this shape.
code integer · int32 required
error string required
Human-readable. Not a stable identifier — do not branch on it.
PUT /api/v1/patch/chains/{id}/shared # Session
Publish a signal chain to the people patched into you, or stop.
The sharing unit is the chain, and this is the only switch there is.
A listen reaches the Reel because the chain it ran through was switched
on by its owner — there is no per-listen override and there should not
be one. It makes "what am I publishing?" answerable by looking at a list
of chains you already understand, and it makes the decision a standing
one about how you listen rather than a choice to make per play.
Off for every chain by default, including every chain that predates the
column.
The Signal Chains page calls this same endpoint, which is what makes
"one state, two places" true rather than aspirational.
shared is required; an absent field is a 400, never a default.
Request body required
application/json
object
shared boolean required
Required — an absent key is a 400.
Responses
200 The switch as it now stands.
application/json
application/json
object
Every error body in the API has this shape.
code integer · int32 required
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
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."
}
application/json
object
Every error body in the API has this shape.
code integer · int32 required
error string required
Human-readable. Not a stable identifier — do not branch on it.
DELETE /api/v1/patch/deck/{username} # Session
Stop patching into a deck.
The follower's own operation. Takes a username rather than the patch's
uuid, because that is what the row on the screen knows.
No sweep of anything already delivered, deliberately: those listens
were shared at the time under a switch their owner set, and a
retroactive sweep would promise an unpatching that the protocol cannot
actually deliver.
Responses
application/json
object
unpatched 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
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 deck, or you were not patched into it.
application/json
object
Every error body in the API has this shape.
code integer · int32 required
error string required
Human-readable. Not a stable identifier — do not branch on it.
GET /api/v1/patch/artists # Session
Artists across the decks you've patched into.
Narrowed exactly like the Reel — accepted patch, shared chain, the
listen has a chain, no skips — because Database::SHARED_LISTENS is one
fragment every Patch reader shares. A filter omitted from one of them is
the shape this codebase keeps finding.
mine is your own plays of that artist: the overlap is reported as
which artists, never as a similarity score. A score invites a ranking
of people, which is not what this is for.
days bounds the window the ranking is computed over; absent is all
time. Server-side on purpose: the query cuts to the top N by plays,
so narrowing in the client would rank a week among artists chosen on
their all-time totals — a list that looks right and is not.
genre is the artist's own first tag from the cache the genre map
fills, and is null for anyone MusicBrainz has not tagged. first_played
is when they first appeared in the shared history.
Responses
200 Artists with plays, decks, who, genre, and your own overlap.
application/json
object
artists array required
each item object
An artist seen across the decks a user is patched into.
plays integer · int64 required
decks integer · int64 required
How many decks play them.
who string | null
Comma-joined usernames, for the little avatar stack on the card.
last_played integer | null · int64
first_played integer | null · int64
When this artist first appeared in the shared history — what "newest
arrival" sorts on.
mine integer · int64 required
The reader's own plays of this artist — the overlap. Shown as
which artists rather than as a similarity score: a score invites a
ranking of people, which is not what this is for.
genre string | null
The artist's own tags. Absent for anyone MusicBrainz has not tagged,
which is a normal state.
days integer | null · int64
The window asked for; null is all time.
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
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/patch/instance # Session
What this instance has been playing.
Over shared chains only, across all decks — and deliberately not
narrowed by your own patches: the Reel answers "what have the decks I
follow played", this answers "what is this instance listening to", which
is a fact about the people who chose to publish a chain and reads the
same for everyone.
A considered reversal of the Reports rule that there is no cross-user
figure. That rule existed to stop the instance exposing how much other
members play without their consent; a per-chain opt-in is that
consent. It stays inside the instance.
daily is raw (timestamp, hours) pairs rather than pre-bucketed days,
because the day boundary belongs to the reader's timezone and the server
does not know it here.
Responses
200 Decks, listens, hours, top artists, chains and daily hours.
application/json
object
What this instance has been playing, through shared chains.
days integer · int64 required
pulse object required
What the instance has been playing, over shared chains only.
decks integer · int64 required
publishing integer · int64 required
How many of them publish at least one chain.
shared_chains integer · int64 required
listens integer · int64 required
hours number · double required
artists_heard integer · int64 required
Distinct artists heard across shared chains in the window — not the
length of artists, which is cut to the top eight.
artists array required
each item object
An artist seen across the decks a user is patched into.
plays integer · int64 required
decks integer · int64 required
How many decks play them.
who string | null
Comma-joined usernames, for the little avatar stack on the card.
last_played integer | null · int64
first_played integer | null · int64
When this artist first appeared in the shared history — what "newest
arrival" sorts on.
mine integer · int64 required
The reader's own plays of this artist — the overlap. Shown as
which artists rather than as a similarity score: a score invites a
ranking of people, which is not what this is for.
genre string | null
The artist's own tags. Absent for anyone MusicBrainz has not tagged,
which is a normal state.
chains array required
each item object
plays integer · int64 required
daily array required
each item object
One listen's contribution to a day's hours. Sent as raw timestamps rather
than pre-bucketed, because the day boundary belongs to the reader's
timezone and the server does not know it here.
at integer · int64 required
hours number · double 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
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."
}
The federation Operations panel. Admin only.
Reports only what is recorded: identities minted, accounts
published, usernames frozen, remote actors cached, inbox activities
received, deliveries attempted.
Signature refusals are logged through tracing and counted nowhere, so
there is no tile for them — an invented number would be worse than an
honest absence, and the screen says where to look instead.
vocabulary is read-only. The design offers a picker with a Freeze
control; three vocabularies would mean three things to maintain and a
migration path between them for a decision made once, so the verb is
compiled in and this only reports it.
Responses
200 Counters, the frozen vocabulary, and whether delivery is on.
application/json
object
The federation counters, and the vocabulary this instance speaks.
ops object required
The admin Operations panel. Only what is actually recorded.
identities integer · int64 required
published integer · int64 required
frozen integer · int64 required
remote_actors integer · int64 required
inbox_activities integer · int64 required
deliveries integer · int64 required
vocabulary object required
Compiled in and read-only.
namespace string required
delivery boolean required
Whether outbound delivery is on. Not yet.
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
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."
}
application/json
object
Every error body in the API has this shape.
code integer · int32 required
error string required
Human-readable. Not a stable identifier — do not branch on it.
Pass a record to someone.
A dub is a crate entry that came from a person — not a second table
and not a second page. It lands on the Crate, which is the page they
already visit for records they are still flipping through, and it is
scored the same way: first_played_at is derived from their listens, so
"you dubbed 12 and they played 5" falls out with nothing accumulated.
Three guards, two of them the Crate's own:
- You must be patched with them, in either direction. Dubbing at a
stranger would be a way to put text on somebody's screen without their
consent.
because is required — what in their listening prompted this. A
suggestion with no grounding is free association.
- A record already in their history is refused, with the play count.
"You already have this" is Rediscovery under another name, and the
count is the half that tells the sender something.
Request body required
application/json
object
A record passed to somebody you are patched with, filed in their crate.
name string | null
The album or track title.
because string required
Required — what in their listening prompted this.
Responses
application/json
object
id integer · int64 required
400 Missing because, a bad kind, or dubbing to yourself.
application/json
object
Every error body in the API has this shape.
code integer · int32 required
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
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 You are not patched with that deck.
application/json
object
Every error body in the API has this shape.
code integer · int32 required
error string required
Human-readable. Not a stable identifier — do not branch on it.
404 No such deck on this instance.
application/json
object
Every error body in the API has this shape.
code integer · int32 required
error string required
Human-readable. Not a stable identifier — do not branch on it.
409 They already have it. The message carries the play count.
application/json
object
Every error body in the API has this shape.
code integer · int32 required
error string required
Human-readable. Not a stable identifier — do not branch on it.
GET /api/v1/patch/spool # Session
The sitting you are in, and what it has offered you.
spool is null when you are not in one. captures are plays from other
people's decks waiting on you — they are not listens, and enter no
history, count or chart until accepted.
spool also carries an invitation you have not answered yet, even
when the sitting has ended — ended_at is non-null in that case. An
invitation is outstanding until it is answered, and the host closing
the room is not an answer. A running sitting wins over a stale
invitation.
Responses
200 The sitting and its captures.
application/json
object
The sitting you are in, and what sittings have offered you.
spool object | null
A sitting several people are in at once.
viewer_id integer · int64 required
Who is asking. The members list includes the caller, and a screen saying
"with Noor and Tobias" has to leave the reader out of it — hosting
cannot do that, since it only identifies one of the two cases.
started_at integer · int64 required
ended_at integer | null · int64
Null while it is running. An ended sitting still reaches this view
when the caller was invited and never answered — an invitation you can
no longer see is one you can no longer honour, and forgetting to press
Join before the host closed the room is the ordinary case rather than
the odd one.
host_user_id integer · int64 required
my_state string required
The caller's membership: invited until they join.
members array required
each item object
user_id integer · int64 required
display_name string | null
present_from integer | null · int64
When they were in the room from, or None for the whole sitting.
Not joined_at, which is when they pressed the button — somebody added
an hour in was usually there all along, and the host says which.
present_until integer | null · int64
When they left, or None for stayed to the end.
guests array required
Names with no account here, so the banner can say who else is in the
room rather than only who else has a deck.
captured integer · int64 required
How many offers this sitting has put in the caller's inbox.
captures array required
Plays waiting for you to accept or decline.
each item object
A play from somebody else's deck, waiting to be accepted.
id integer · int64 required
duration integer | null · int64
played_at integer · int64 required
from_username string required
from_display_name string | null
spool_uuid string required
spool_title string | null
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
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/patch/spool # Session
Open a Shared Spool.
A sitting several people are in at once. You are in it immediately;
everyone named in with is invited, and an invitation is not
attendance — nothing is captured onto their deck until they join.
One running sitting per host, enforced by a partial unique index rather
than by a check this handler could race, so a second start is a 409.
You may only invite a deck you are patched with, in either
direction — an invitation puts a banner on somebody's Patch page, so
being able to aim one at a stranger would be a way to reach them
without their consent. One stranger in with refuses the whole
sitting rather than opening it without them.
Request body required
application/json
object
guests array | null
Names of people in the room with no account on this instance.
Responses
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
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 One of the named decks is not patched with you. Nothing was started.
application/json
object
Every error body in the API has this shape.
code integer · int32 required
error string required
Human-readable. Not a stable identifier — do not branch on it.
404 One of the named decks does not exist.
application/json
object
Every error body in the API has this shape.
code integer · int32 required
error string required
Human-readable. Not a stable identifier — do not branch on it.
409 You already have a sitting running.
application/json
object
Every error body in the API has this shape.
code integer · int32 required
error string required
Human-readable. Not a stable identifier — do not branch on it.
POST /api/v1/patch/spool/{uuid}/join # Session
Accept an invitation to a sitting, or leave one.
accept: true joins, false leaves. Required — an absent field is a
400, never a default, because joining is what starts plays being
captured onto your deck.
A sitting the host has already ended can still be joined, and doing
so offers the plays it already held — the response's offered says how
many. Forgetting to press Join before the room closed is the ordinary
case, not the odd one, and refusing it there would throw away an
evening with no way to ask for it again. Nothing is weakened: joining
still decides nothing on its own, since every play arrives as a capture
you have to accept.
Only plays through the player's shared chains are offered, exactly
as for a live sitting, and nothing already offered to you in any
sitting is offered twice.
Request body required
application/json
object
A yes or a no, said explicitly.
accept boolean required
Required — an absent key is a 400, never a default.
Responses
200 Your membership as it now stands, and the backlog joining offered you.
application/json
object
state string required
joined, declined or left — the row knows which.
offered integer · int64 required
Plays joining has just put in your inbox. For a sitting that ended
before you answered, that is the whole of it.
application/json
object
Every error body in the API has this shape.
code integer · int32 required
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
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 sitting you were invited to with that id.
application/json
object
Every error body in the API has this shape.
code integer · int32 required
error string required
Human-readable. Not a stable identifier — do not branch on it.
POST /api/v1/patch/spool/{uuid}/end # Session
Close a sitting. The host's only.
Captures already made are untouched — they are offers nobody has
answered yet, and ending the room does not withdraw what was played in
it.
Responses
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
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 running sitting of yours with that id.
application/json
object
Every error body in the API has this shape.
code integer · int32 required
error string required
Human-readable. Not a stable identifier — do not branch on it.
GET /api/v1/patch/spools # Session
Every sitting you were in, newest first.
Membership is the gate, not the patch. A sitting is an evening you
were in the room for, and un-patching from the host afterwards has no
standing to un-happen it — the same argument as "no delete sweep on
un-patching". Your own accepted listens from that evening stay in your
history whatever happens to the patch, and a history row whose sitting
has vanished is a row with nothing to explain it.
That widening does not widen what any of it shows: a sitting still
only ever renders the plays the per-chain sharing switch allows, and
withheld on the detail below says how many it did not.
A sitting you declined does not appear. You were not there.
Responses
application/json
object
sittings array required
each item object
One sitting on the history list.
started_at integer · int64 required
ended_at integer | null · int64
host_username string required
host_display_name string | null
present array required
Who else was actually in the room — the caller is left out here.
guests array required
Names with no account on this instance.
plays integer · int64 required
Plays that reached this sitting's timeline, from every deck in it.
pending integer · int64 required
Offers still sitting in the caller's inbox from this evening.
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
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/patch/spools/{uuid} # Session
One sitting, in full.
The roster with each person's span, the joint timeline, and a count of
what is not on it.
The timeline is one row per play, not one per listen. A joint listen
is N rows sharing a joint_session_id, one per person in the room, so
the naive read shows an evening at twice its length as soon as somebody
accepts their half. Each row's standing is the caller's own relation
to that play — mine, accepted, declined, pending, or theirs
for a play from a deck in the room that was never offered to you.
Declined and never-answered are told apart deliberately: both mean "not
in my history" and they are not the same decision.
withheld counts plays that happened in this sitting and are not shown,
because they ran through chains their owner does not share. Stated
rather than dropped — a document with invisible holes misrepresents the
evening and the reader could never tell.
Responses
application/json
object
viewer_id integer · int64 required
started_at integer · int64 required
ended_at integer | null · int64
host_user_id integer · int64 required
members array required
each item object
user_id integer · int64 required
display_name string | null
present_from integer | null · int64
When they were in the room from, or None for the whole sitting.
Not joined_at, which is when they pressed the button — somebody added
an hour in was usually there all along, and the host says which.
present_until integer | null · int64
When they left, or None for stayed to the end.
plays array required
each item object
One play on a sitting's timeline, whoever's deck it came off.
This is the first row type in the application that mixes several people's
listening into one sequence. Everything else — the Reel included — keeps
one deck per row.
duration integer | null · int64
played_at integer · int64 required
from_username string required
from_display_name string | null
standing string required
mine | accepted | declined | pending | theirs.
Four of those are facts about the caller and the fifth is the absence of
one. Declined and never-answered are told apart deliberately: both mean
"not in my history" and they are not the same decision.
capture_id integer | null · int64
The capture this row came from, where there is one to act on.
artwork_url string | null
caa_id integer | null · int64
caa_release_mbid string | null
withheld integer · int64 required
Plays that happened in this sitting and are not on the timeline, because
they ran through chains their owner does not share.
figures object required
What the evening came to, folded out of plays.
plays integer · int64 required
Plays on the timeline. The same count the history list shows, so the two
screens cannot disagree about how long an evening was.
seconds integer · int64 required
Running time of the plays that reported one, and how many did.
duration_known integer · int64 required
artists integer · int64 required
albums integer · int64 required
top_artist array | null · [string, integer · int64]
The artist the evening kept coming back to, and how often.
top_album array | null · [string, string, integer · int64]
The record it kept coming back to, with its artist.
decks array required
Whose deck played what, biggest first. A sitting is several people's
listening in one sequence, and this is the only figure that says so.
each item tuple · [string, integer · 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
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 sitting of yours with that id. Same answer as one somebody else was in.
application/json
object
Every error body in the API has this shape.
code integer · int32 required
error string required
Human-readable. Not a stable identifier — do not branch on it.
PATCH /api/v1/patch/spools/{uuid} # Session
Name a sitting, write about it, or say who else was there.
The host's, and editable after the fact on purpose. title used to
be settable once, at the start — which is the moment you know least
about an evening.
title and notes distinguish absent from null: an absent field is
left alone and an explicit null clears it. A body naming none of the
three is a 400 rather than a no-op reported as success.
Changing guests does not rewrite the company on plays already
fanned out. Those snapshots were accurate when they were taken.
Request body required
application/json
object
Name a sitting, write about it, or change who else was there. An absent
field is left alone; an explicit null clears it.
guests array | null
Names of people in the room with no account here. Replaces the list.
Responses
application/json
object
Every error body in the API has this shape.
code integer · int32 required
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
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 sitting of yours with that id.
application/json
object
Every error body in the API has this shape.
code integer · int32 required
error string required
Human-readable. Not a stable identifier — do not branch on it.
GET /api/v1/patch/spools/{uuid}/image # Session
An evening as a card.
The ending a sitting never had. Ending one used to just stop it;
this is the thing you look at afterwards and hand to the people who
were in it.
A fourth composition rather than a report card with a different label. A
sitting has no period, so no rank, no per-day rate and no bucket chart —
what it has instead is who was in the room, which is the subject, and
which no report card has anywhere to put.
It computes nothing: every figure comes from
GET /api/v1/patch/spools/{uuid}, so a card and the page it was shared
from cannot disagree. And it makes no network call, so it needs none of
the deadline machinery every other outbound-from-a-handler path here has
had to grow.
X-Tapedeck-Glyph-Coverage is full or partial. Partial is not a
failure and the image is still returned: the embedded faces are Latin,
the host's own fonts sit behind them and may well cover the rest. It
means this server cannot promise the image is free of tofu, which is a
thing to say rather than let the reader discover from the picture.
Parameters
size query
story (1080×1920), square or wide.
theme query
light or dark. Anything else is the default rather than a 400.
format query
png (the default) or svg.
Responses
200 The card. X-Tapedeck-Glyph-Coverage is partial when a glyph had to be substituted.
image/png
image/svg+xml
400 An unknown size or format.
application/json
object
Every error body in the API has this shape.
code integer · int32 required
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
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 sitting of yours with that id.
application/json
object
Every error body in the API has this shape.
code integer · int32 required
error string required
Human-readable. Not a stable identifier — do not branch on it.
POST /api/v1/patch/spools/{uuid}/members # Session
Add people to a sitting that has already started, or ended.
The guest list used to be frozen at the moment the sitting opened, which
is wrong about how an evening goes: somebody arrives, somebody's deck
comes on, or the host simply forgot one of the people in front of them.
present_from answers "from when", and it is stated rather than
inferred from the clock:
- absent — they were here all along, and are offered the whole
evening. This is the common case by a distance.
- a timestamp — they arrived then, and the backlog is bounded by it.
Host only. The rule that you may only invite a deck you are
patched with is what stops a guest bringing somebody the host has never
patched with into a room where the host's plays are being published.
One stranger in with refuses the whole call.
Somebody already in the sitting is left exactly as they are — re-adding
must not walk a declined back to invited.
Request body required
application/json
object
with array | null
Usernames to add. Required.
present_from integer | null · int64
When they arrived, or absent for "they were here all along".
Responses
application/json
object
added integer · int64 required
application/json
object
Every error body in the API has this shape.
code integer · int32 required
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
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 One of the named decks is not patched with you. Nothing was added.
application/json
object
Every error body in the API has this shape.
code integer · int32 required
error string required
Human-readable. Not a stable identifier — do not branch on it.
404 No sitting of yours with that id, or no deck by that name.
application/json
object
Every error body in the API has this shape.
code integer · int32 required
error string required
Human-readable. Not a stable identifier — do not branch on it.
PUT /api/v1/patch/spools/{uuid}/presence # Session
Correct when you were in the room.
Yours to set, because you are the one who knows. Everything it decides
is about your own inbox — how much of the evening you are offered — so
there is nothing here to overreach with.
Both fields distinguish absent from null; clearing present_from back
to null means "I was here the whole time".
Widening your presence offers you the plays you have just said you
heard. Narrowing it withdraws no offer already made: an offer is a
question somebody asked you, and it stays yours to answer.
Request body required
application/json
object
When you were in the room. An absent field is left alone; null clears
it.
present_from integer | null · int64
present_until integer | null · int64
Responses
200 How many plays this has now put in your inbox.
application/json
object
offered integer · int64 required
How many plays this has now put in your inbox.
application/json
object
Every error body in the API has this shape.
code integer · int32 required
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
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 sitting of yours with that id.
application/json
object
Every error body in the API has this shape.
code integer · int32 required
error string required
Human-readable. Not a stable identifier — do not branch on it.
POST /api/v1/patch/captures # Session
Accept or decline plays a sitting captured for you.
The only path by which another deck can write a listen into your
history, which is why it is explicit and per capture rather than a
standing setting.
Accepted plays land carrying the sitting's joint_session_id — the
N-rows-one-per-listener shape decided on 2026-08-02.
Whether they are forwarded on to Last.fm and ListenBrainz is the
user's own setting — GET|PUT /api/v1/connections/spool-forwarding,
off by default — bounded by a freshness window so an evening
reconstructed out of years ago is stored and never relayed. forwarded
in the response says how many of the settled plays the flush loop will
actually pick up; it is not derivable from settled.
Both ids and accept are required.
Request body required
application/json
object
accept boolean required
Required. Accepted plays land as imported and are never forwarded
from here.
Responses
200 How many were settled, and how many of those will be relayed.
application/json
object
settled integer · int64 required
forwarded integer · int64 required
How many of those will be relayed to your connections — which turns
on a setting and on each play being recent enough.
400 ids or accept was absent.
application/json
object
Every error body in the API has this shape.
code integer · int32 required
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
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/patch/captures/{id} # Session
Fix when a capture was played, before it becomes a listen.
The cheapest correction in the application. A capture is an offer
and not a record, so nothing is in a history, nothing has been forwarded
to anybody's Last.fm, no session has been grouped around it and no dedup
window has been drawn against it. Every trap that makes editing a
listen's timestamp delicate arrives only once it has been accepted.
A capture carries the host's clock, which is the ordinary way one
comes to be wrong.
Pending only. A settled capture has either become a listen — where
POST /api/v1/scrobbles/bulk-time is the thing to reach for — or been
declined, and re-timing a refusal changes nothing.
Request body required
application/json
object
played_at integer · int64 required
Responses
application/json
object
Every error body in the API has this shape.
code integer · int32 required
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
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 offer of yours with that id still waiting.
application/json
object
Every error body in the API has this shape.
code integer · int32 required
error string required
Human-readable. Not a stable identifier — do not branch on it.
PUT /api/v1/profile/spool-chain # Session
Name the chain a room you sat in is filed under.
A capture used to land with no chain at all, so those listens earned no
gear hours, carried no fidelity and could never reach the Reel. That was
defensible — it was not your gear — and it was not the whole truth
either: you were in the room hearing real speakers, and if you have
built a chain that says so it should get the hours.
Unset is the default and reproduces the old behaviour exactly.
chain_id is required and an absent field is a 400: this decides
where every play you accept out of a room lands, so a misspelled key
must not read as "clear it". Explicit null clears it.
Session-only. A scrobble client has no business deciding this.
Request body required
application/json
object
chain_id integer | null · int64 required
A chain of yours, or null to file accepted plays under nothing.
Required — an absent key is a 400, never a clear.
Responses
application/json
object
Every error body in the API has this shape.
code integer · int32 required
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
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 chain of yours with that id.
application/json
object
Every error body in the API has this shape.
code integer · int32 required
error string required
Human-readable. Not a stable identifier — do not branch on it.
GET /api/v1/patch/artist # Session
Who else here plays an artist, and which of your chains you hear them on.
The Patch half of an artist page. Not a separate artist screen: the
design draws one inside Patch, but its statistics are the ones
/api/v1/artists/{id} already computes and a second copy would drift.
This is only the part that is genuinely about other decks.
decks is instance-wide rather than narrowed by your patches — the
same deliberate reversal the Instance tab makes, on the same reasoning:
the rule that stops a deck exposing somebody's listening without consent
is the per-chain switch, and switching it on is that consent. Shared
chains only, and it stays inside the instance. plays therefore counts
a deck's listens through shared chains and nothing else.
patched says whether an accepted patch already exists in either
direction, so a screen can offer to open the deck or to ask — rather
than a link that 404s, since reading a deck needs a patch.
chains is your own listening of that artist, grouped by chain, and
is deliberately not filtered on shared: what you heard them on is a
fact about your setup rather than a publication.
Responses
200 The decks, and your own chains.
application/json
object
decks array required
Decks on this instance that play them through a shared chain.
each item object
A deck on this instance that plays a given artist.
user_id integer · int64 required
display_name string | null
plays integer · int64 required
Their plays of this artist, through shared chains only.
patched boolean required
Whether you already have an accepted patch with them, either direction.
Decides whether their deck can be opened or has to be asked for.
chains array required
Your own chains you hear them on.
each item object
plays integer · int64 required
application/json
object
Every error body in the API has this shape.
code integer · int32 required
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
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."
}
Decks you're patched into that are playing something right now.
Gated by exactly the rule the Reel uses — an accepted patch, and the
listen running through a chain its owner switched to shared. A live
entry that could not be checked never appears, which is why chain_name
is always present on a row here.
That gate is what NowPlaying.chain_id exists for. A name could not do
it — a name is not a key, and a polled source sets no chain name at all,
because its chain is the device's default rather than a choice.
The alternative was relaxing the rule for live entries so an unshared
chain would still show a track and a playhead. Rejected for the reason
the Shared Spool's capture rule was: it would be a second way a listen
becomes visible while the settings page explains one, and somebody
could hand over a chain they had deliberately kept private just by
pressing play.
Read from the in-memory now-playing registry, so it costs no history
query — one chain lookup per live deck. Nothing is persisted, so a
restart correctly shows an empty strip until a source reports again.
position_known is false when the position is wall-clock arithmetic
rather than a reported playhead; draw it as an estimate, never as a
measurement.
Responses
200 The decks currently playing, and what they are on.
application/json
object
live array required
each item object
A deck you are patched into that is playing now, through a chain you may
see.
user_id integer · int64 required
display_name string | null
duration integer | null · int64
position integer · int64 required
position_known boolean required
The position was measured rather than extrapolated.
chain_name string | null
Null with has_chain true is a private chain; null with it false is a
listen through nothing anybody recorded.
has_chain boolean required
artwork_url string | null
format_type string | null
bit_depth integer | null · int32
sample_rate integer | null · int32
is_lossless boolean | null
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
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/patch/inbox-count # Session
How many things are waiting on you in Patch.
One number, for the sidebar badge: patches awaiting a yes, unsettled
Shared Spool captures, open dubs, and sittings passed to you but not
opened.
Its own endpoint rather than the sidebar assembling the four lists —
that would be four requests on every page load, for a number, against a
pool of five connections.
Answers 0 rather than an error if the count cannot be read. A
badge is decoration, and a page that fails to render its sidebar
because a count failed is worse than one with no badge on it.
Responses
application/json
object
waiting integer · int64 required
Zero, rather than an error, when it cannot be read.
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
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/patch/sittings # Session
Pass a past sitting to somebody you are patched with.
Two different gestures, and kind chooses between them. There is no
default — they write different things to another person's permanent
record, so an absent field is a 400 rather than a guess.
kind: "spool" — the Shared Spool neither of you started. They were
in the room; you just forgot to press the button. The sitting is
reconstructed, its plays are offered as ordinary spool_captures, and
they enter the recipient's history when accepted — arriving in the
same inbox section and on the same accept a live sitting uses. Returns
offered and withheld. Idempotent: doing it again offers nothing new,
and naming a second person adds them to the same reconstructed sitting
rather than minting a parallel one.
kind: "recommendation" — they were not there. They get a
document rendered from your listens, which enters no history, count or
chart of theirs. note applies to this kind only; a set of captures has
nowhere to carry a sentence. Handing the same sitting to the same person
again updates the note rather than duplicating it.
You must be patched with them in either direction, and the sitting must
be yours.
Both kinds pass on only listens through shared chains — a sitting
made late captures exactly what one made on time would have, and this
phase has one switch for publishing rather than two. What is withheld is
counted either way (withheld here, omitted on the document), never
silently dropped. Unlike a live sitting there is a recovery: share the
chain and do it again.
Request body required
application/json
object
session string required
The sitting's uuid, from GET /api/v1/sessions.
to string required
The username to hand it to.
kind string required
Which of the two gestures this is, and it is required.
spool — they were in the room and neither of you started a sitting.
The plays are offered as ordinary captures and enter their history when
they accept, exactly as a live Shared Spool would have.
recommendation — they were not there. They get a document of what you
played, and nothing enters their history.
Responses
200 For spool: the sitting's uuid plus offered and withheld. For recommendation: the document's uuid.
application/json
object
kind string required
spool or recommendation, as asked.
uuid string required
The sitting's uuid for spool; the document's for recommendation.
offered integer | null · int64
spool only: plays offered to them as captures…
withheld integer | null · int64
…and plays held back because they ran through a chain you do not
share.
400 session, to or kind was missing, kind was not one of the two, or the note was too long.
application/json
object
Every error body in the API has this shape.
code integer · int32 required
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
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 That deck is not patched with you.
application/json
object
Every error body in the API has this shape.
code integer · int32 required
error string required
Human-readable. Not a stable identifier — do not branch on it.
404 No such deck, or no sitting of yours with that id.
application/json
object
Every error body in the API has this shape.
code integer · int32 required
error string required
Human-readable. Not a stable identifier — do not branch on it.
GET /api/v1/patch/sittings/sent # Session
Sittings you have passed on, and whether they have been read.
Session-only. The counterpart of GET /api/v1/patch/sittings, which lists
only what was passed to you — without this a hand-over could be sent and
never seen again, so there was nowhere to withdraw one from.
Only documents appear here. A reconstruction (kind: "spool") writes
real captures into the other person's inbox and no shared_sessions row, so
there is nothing to take back — those are plays they accept or decline for
themselves.
Responses
application/json
object
sittings array required
each item object
One sitting you passed on, for the sender's own list.
session_uuid string required
created_at integer · int64 required
seen_at integer | null · int64
When they opened it. None means it is still unread — which is the
difference between withdrawing something nobody saw and taking back
something already read, and the screen says which.
to_username string required
to_display_name string | null
started_at integer | null · int64
ended_at 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
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/patch/sittings/{uuid} # Session
A handed-over sitting, as a document.
Readable by the recipient or by whoever handed it over, so you can
see what you actually passed on, holes included. Any other caller gets
the same 404 a nonexistent id does — the alternative would confirm a
uuid is real to somebody it does not concern.
Rendered live rather than snapshotted: un-sharing a chain, or
regrouping the sitting, really does change what the recipient sees.
That is correct while both ends are on one instance, where the document
is served from the owner's own database on every read. It has to become
a snapshot when this federates, because bytes already on somebody
else's server cannot be recalled.
omitted is how many listens in the sitting are not being shown
because they ran through chains the owner does not share. gone means
the sitting has since been regrouped out of existence — an honest empty
document rather than a broken link.
Responses
application/json
object
The document a hand-over renders to.
from_username string required
from_display_name string | null
started_at integer · int64 required
ended_at integer | null · int64
listens array required
each item object
One listen inside a handed-over sitting. Not a listen of the reader's —
this enters no history, count or chart.
timestamp integer · int64 required
duration integer | null · int64
artwork_url string | null
caa_id integer | null · int64
caa_release_mbid string | null
omitted integer · int64 required
Listens in the sitting that ran through chains the owner does not
share, and are therefore not shown. Stated rather than silently
dropped — a document with invisible holes misrepresents the evening.
gone boolean required
The sitting has been regrouped out of existence since it was handed
over. The document is empty and says so.
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
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 sitting with that id, or not yours to read.
application/json
object
Every error body in the API has this shape.
code integer · int32 required
error string required
Human-readable. Not a stable identifier — do not branch on it.
POST /api/v1/patch/sittings/{uuid}/seen # Session
Clear a handed-over sitting from your inbox.
Not an accept. A handed-over sitting writes nothing to a history,
so there is nothing to consent to — only a notification to stop
showing. Contrast /api/v1/patch/captures, where accepting really does
put a play into your listening.
Responses
200 Whether it had not already been seen.
application/json
object
seen boolean required
False when it had already been seen.
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
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."
}