← API reference

Patch

Patch — following people, on this instance and others. The ActivityPub surface (/users/…, /.well-known/webfinger) is unauthenticated and lets another server find out who somebody here is and say something to them; the rest is the session-only local half: the Reel, decks, Dubs and Shared Spools.

Every federated endpoint is off until a user switches federation on, and an account that has not is indistinguishable from one that does not exist — the same rule the public now-playing widget and device pairing follow.

46 of 46 · v0.120.0
GET /users/{name} #
Public

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.

Parameters

name path required
string

Responses

200

The actor document, or the SPA shell for a browser.

application/activity+json

text/html

string
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

Mirrors the HTTP status.

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.

Parameters

name path required
string

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

Mirrors the HTTP status.

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.

Parameters

name path required
string

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

Mirrors the HTTP status.

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

Mirrors the HTTP status.

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

Mirrors the HTTP status.

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.

Parameters

name path required
string

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

Mirrors the HTTP status.

error string required

Human-readable. Not a stable identifier — do not branch on it.

GET /users/{name}/following #
Public

How many decks this one has patched into.

The mirror of /followers, and the same rule: totalItems only.

Parameters

name path required
string

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

Mirrors the HTTP status.

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.

string

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

Mirrors the HTTP status.

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

Mirrors the HTTP status.

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.

enabled boolean required
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

Mirrors the HTTP status.

error string required

Human-readable. Not a stable identifier — do not branch on it.

{
  "code": 401,
  "error": "Authentication required. Log in to access this endpoint."
}
PUT /api/v1/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.

enabled boolean required
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.

400

enabled was absent.

application/json

object

Every error body in the API has this shape.

code integer · int32 required

Mirrors the HTTP status.

error string required

Human-readable. Not a stable identifier — do not branch on it.

401

No valid session cookie or token. Also returned when a token is presented to a session-only endpoint — the endpoint does not accept tokens at all, so the scope is irrelevant.

application/json

object

Every error body in the API has this shape.

code integer · int32 required

Mirrors the HTTP status.

error string required

Human-readable. Not a stable identifier — do not branch on it.

{
  "code": 401,
  "error": "Authentication required. Log in to access this endpoint."
}
GET /api/v1/profile/share-all #
Session

Whether patched decks see all your listens.

Session-only.

Responses

200

OK.

application/json

object
enabled boolean required
401

No valid session cookie or token. Also returned when a token is presented to a session-only endpoint — the endpoint does not accept tokens at all, so the scope is irrelevant.

application/json

object

Every error body in the API has this shape.

code integer · int32 required

Mirrors the HTTP status.

error string required

Human-readable. Not a stable identifier — do not branch on it.

{
  "code": 401,
  "error": "Authentication required. Log in to access this endpoint."
}
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

200

Saved.

application/json

object
enabled boolean required
400

enabled was absent.

application/json

object

Every error body in the API has this shape.

code integer · int32 required

Mirrors the HTTP status.

error string required

Human-readable. Not a stable identifier — do not branch on it.

401

No valid session cookie or token. Also returned when a token is presented to a session-only endpoint — the endpoint does not accept tokens at all, so the scope is irrelevant.

application/json

object

Every error body in the API has this shape.

code integer · int32 required

Mirrors the HTTP status.

error string required

Human-readable. Not a stable identifier — do not branch on it.

{
  "code": 401,
  "error": "Authentication required. Log in to access this endpoint."
}
GET /api/v1/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

200

The three lists.

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
username string required
display_name string | null
bio string | null
avatar_url 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
username string required
display_name string | null
bio string | null
avatar_url 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
username string required
display_name string | null
bio string | null
avatar_url 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

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/patch/reel #
Session

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

limit query

Default 40, at most 200.

integer · int64
before query

Keyset paging: the next from the previous page. Not an offset — the Reel grows from the top while it is being read.

integer · int64
q query

Narrows on track, artist, album, chain or deck, server-side.

string
deck query

One deck's username, for the filter chips.

string

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
title string required
artist string required
album string | null
artwork_url string | null
caa_id integer | null · int64
caa_release_mbid string | null
format_type string | null
codec 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
username string required
display_name string | null
avatar_url 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_icon string | null
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

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/patch #
Session

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
uuid string required
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

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 deck on this instance.

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

Parameters

uuid path required
string

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

object
state string required

accepted or rejected.

400

accept was absent.

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 patch request waiting on you with that id.

application/json

object

Every error body in the API has this shape.

code integer · int32 required

Mirrors the HTTP status.

error string required

Human-readable. Not a stable identifier — do not branch on it.

PUT /api/v1/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.

Parameters

id path required
integer · int64

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

object
shared boolean required
400

shared was absent.

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 chain of yours.

application/json

object

Every error body in the API has this shape.

code integer · int32 required

Mirrors the HTTP status.

error string required

Human-readable. Not a stable identifier — do not branch on it.

GET /api/v1/patch/deck/{username} #
Session

One deck, through an accepted patch.

Session-only. The deck's profile, how much it has played, its shared chains with their hours, and the artists you both play. A deck that does not exist and one you are not patched into are the same 404.

Parameters

username path required
string

Responses

200

The deck.

application/json

object

Somebody else's deck, as the person patched into it may see it.

user_id integer · int64 required
username string required
display_name string | null
bio string | null
avatar_url string | null
created_at integer · int64 required
listens integer · int64 required

Listens a patch into this deck can see — everything they play when they have opened up, and otherwise only what ran through a chain they publish. Not, either way, their whole history: skips never appear.

chains array required
each item
object

One chain a deck publishes.

name string required
icon string | null
components string required
hours number · double required
both_play array required

Overlap only — no score.

each item
string
actor string | null

Their actor URI, when they have an identity at all. None is a normal state, because federation is opt-in, and the page says so rather than printing endpoints that would 404.

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 deck, or you are not patched into it.

application/json

object

Every error body in the API has this shape.

code integer · int32 required

Mirrors the HTTP status.

error string required

Human-readable. Not a stable identifier — do not branch on it.

DELETE /api/v1/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.

Parameters

username path required
string

Responses

200

Unpatched.

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

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 deck, or you were not patched into it.

application/json

object

Every error body in the API has this shape.

code integer · int32 required

Mirrors the HTTP status.

error string required

Human-readable. Not a stable identifier — do not branch on it.

GET /api/v1/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.

Parameters

days query

Days back.

integer · int64

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.

name string required
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

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/patch/discover #
Session

Everyone on the instance, and what you have in common.

overlap is artists you both play, unit-separated (\x1f), and it is computed against their shared chains only — so this cannot become a way to learn what somebody plays privately by watching the list change.

state is your patch with them (pending / accepted / rejected) or null if you have never asked.

Responses

200

Decks, newest first.

application/json

object
decks array required
each item
object

A deck on the instance, with what the viewer has in common with it.

user_id integer · int64 required
username string required
display_name string | null
bio string | null
avatar_url string | null
created_at integer · int64 required
shared_chains integer · int64 required
state string | null

pending | accepted | rejected, or None if never asked.

overlap string required

Artists you both play, unit-separated. Computed against their shared chains only, so this cannot become a way to learn what somebody plays privately by watching the list change.

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

Parameters

days query

Days back.

integer · int64

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.

name string required
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
name string required
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

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/patch/ops #
Session

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.

verb string required
object string required
namespace string required
frozen boolean 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

Mirrors the HTTP status.

error string required

Human-readable. Not a stable identifier — do not branch on it.

{
  "code": 401,
  "error": "Authentication required. Log in to access this endpoint."
}
403

Not an admin.

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/patch/dub #
Session

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.

to string required

Their username.

kind string required

artist, album or track.

artist string required
name string | null

The album or track title.

mbid string | null
reason string | null
because string required

Required — what in their listening prompted this.

Responses

200

Filed in their crate.

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

Mirrors the HTTP status.

error string required

Human-readable. Not a stable identifier — do not branch on it.

401

No valid session cookie or token. Also returned when a token is presented to a session-only endpoint — the endpoint does not accept tokens at all, so the scope is irrelevant.

application/json

object

Every error body in the API has this shape.

code integer · int32 required

Mirrors the HTTP status.

error string required

Human-readable. Not a stable identifier — do not branch on it.

{
  "code": 401,
  "error": "Authentication required. Log in to access this endpoint."
}
403

You are not patched with that deck.

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.

404

No such deck on this instance.

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.

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

Mirrors the HTTP status.

error string required

Human-readable. Not a stable identifier — do not branch on it.

GET /api/v1/patch/dubs #
Session

Records people handed you.

Crate entries whose origin is a person rather than a model — from_user_id IS NOT NULL is the whole filter, on a page that deliberately holds both kinds.

Responses

200

Dubs, open ones first.

application/json

object
dubs array required
each item
object

A record somebody handed you.

id integer · int64 required
kind string required
artist string required
name string | null
reason string | null
because string | null
status string required
created_at integer · int64 required
from_username string required
from_display_name 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

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

uuid string required
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.

title string | null
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
hosting boolean required
my_state string required

The caller's membership: invited until they join.

members array required
each item
object
user_id integer · int64 required
username string required
display_name string | null
state string required

invited | joined | left.

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.

each item
string
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
title string required
artist string required
album string | null
duration integer | null · int64
played_at integer · int64 required
state string 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

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/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
title string | null
with array | null

Usernames to invite.

each item
string
guests array | null

Names of people in the room with no account on this instance.

each item
string

Responses

200

The sitting's uuid.

application/json

object
uuid string required
401

No valid session cookie or token. Also returned when a token is presented to a session-only endpoint — the endpoint does not accept tokens at all, so the scope is irrelevant.

application/json

object

Every error body in the API has this shape.

code integer · int32 required

Mirrors the HTTP status.

error string required

Human-readable. Not a stable identifier — do not branch on it.

{
  "code": 401,
  "error": "Authentication required. Log in to access this endpoint."
}
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

Mirrors the HTTP status.

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

Mirrors the HTTP status.

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

Mirrors the HTTP status.

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.

Parameters

uuid path required
string

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.

400

accept was absent.

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 sitting you were invited to with that id.

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

Parameters

uuid path required
string

Responses

200

Ended.

application/json

object
ended boolean required
401

No valid session cookie or token. Also returned when a token is presented to a session-only endpoint — the endpoint does not accept tokens at all, so the scope is irrelevant.

application/json

object

Every error body in the API has this shape.

code integer · int32 required

Mirrors the HTTP status.

error string required

Human-readable. Not a stable identifier — do not branch on it.

{
  "code": 401,
  "error": "Authentication required. Log in to access this endpoint."
}
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

Mirrors the HTTP status.

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.

Parameters

limit query

Default 50, at most 200.

integer · int64

Responses

200

The sittings.

application/json

object
sittings array required

Newest first.

each item
object

One sitting on the history list.

uuid string required
title string | null
notes string | null
started_at integer · int64 required
ended_at integer | null · int64
hosting boolean required
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.

each item
string
guests array required

Names with no account on this instance.

each item
string
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

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

Parameters

uuid path required
string

Responses

200

The sitting.

application/json

object

A sitting, in full.

uuid string required
viewer_id integer · int64 required
title string | null
notes string | null
started_at integer · int64 required
ended_at integer | null · int64
host_user_id integer · int64 required
hosting boolean required
my_state string required
members array required
each item
object
user_id integer · int64 required
username string required
display_name string | null
state string required

invited | joined | left.

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
each item
string
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.

title string required
artist string required
album string | null
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

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

Mirrors the HTTP status.

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.

Parameters

uuid path required
string

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.

title string | null
notes string | null
guests array | null

Names of people in the room with no account here. Replaces the list.

each item
string

Responses

200

Saved.

application/json

object
saved boolean required
400

No field named.

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 sitting of yours with that id.

application/json

object

Every error body in the API has this shape.

code integer · int32 required

Mirrors the HTTP status.

error string required

Human-readable. Not a stable identifier — do not branch on it.

GET /api/v1/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

uuid path required
string
size query

story (1080×1920), square or wide.

string
theme query

light or dark. Anything else is the default rather than a 400.

string
format query

png (the default) or svg.

string

Responses

200

The card. X-Tapedeck-Glyph-Coverage is partial when a glyph had to be substituted.

image/png

string · binary

image/svg+xml

string
400

An unknown size or format.

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 sitting of yours with that id.

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

Parameters

uuid path required
string

Request body required

application/json

object
with array | null

Usernames to add. Required.

each item
string
present_from integer | null · int64

When they arrived, or absent for "they were here all along".

Responses

200

How many were added.

application/json

object
added integer · int64 required
400

No decks named.

application/json

object

Every error body in the API has this shape.

code integer · int32 required

Mirrors the HTTP status.

error string required

Human-readable. Not a stable identifier — do not branch on it.

401

No valid session cookie or token. Also returned when a token is presented to a session-only endpoint — the endpoint does not accept tokens at all, so the scope is irrelevant.

application/json

object

Every error body in the API has this shape.

code integer · int32 required

Mirrors the HTTP status.

error string required

Human-readable. Not a stable identifier — do not branch on it.

{
  "code": 401,
  "error": "Authentication required. Log in to access this endpoint."
}
403

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

Mirrors the HTTP status.

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

Mirrors the HTTP status.

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.

Parameters

uuid path required
string

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.

400

No field named.

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 sitting of yours with that id.

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/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
ids array required

The captures. Required.

each item
integer · int64
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

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

Parameters

id path required
integer · int64

Request body required

application/json

object
played_at integer · int64 required

Unix seconds. Required.

Responses

200

Saved.

application/json

object
saved boolean required
400

No played_at.

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 offer of yours with that id still waiting.

application/json

object

Every error body in the API has this shape.

code integer · int32 required

Mirrors the HTTP status.

error string required

Human-readable. Not a stable identifier — do not branch on it.

GET /api/v1/profile/spool-chain #
Session

The chain an accepted Shared Spool play is filed under.

Responses

200

chain_id, or null.

application/json

object
chain_id integer | null · int64

Null files accepted plays under no chain.

401

No valid session cookie or token. Also returned when a token is presented to a session-only endpoint — the endpoint does not accept tokens at all, so the scope is irrelevant.

application/json

object

Every error body in the API has this shape.

code integer · int32 required

Mirrors the HTTP status.

error string required

Human-readable. Not a stable identifier — do not branch on it.

{
  "code": 401,
  "error": "Authentication required. Log in to access this endpoint."
}
PUT /api/v1/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

200

Saved.

application/json

object
saved boolean required
400

chain_id absent.

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 chain of yours with that id.

application/json

object

Every error body in the API has this shape.

code integer · int32 required

Mirrors the HTTP status.

error string required

Human-readable. Not a stable identifier — do not branch on it.

GET /api/v1/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.

Parameters

name query required

The artist. Required.

string

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
username string required
display_name string | null
avatar_url string | null
actor_uri 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
name string required
plays integer · int64 required
400

name was missing.

application/json

object

Every error body in the API has this shape.

code integer · int32 required

Mirrors the HTTP status.

error string required

Human-readable. Not a stable identifier — do not branch on it.

401

No valid session cookie or token. Also returned when a token is presented to a session-only endpoint — the endpoint does not accept tokens at all, so the scope is irrelevant.

application/json

object

Every error body in the API has this shape.

code integer · int32 required

Mirrors the HTTP status.

error string required

Human-readable. Not a stable identifier — do not branch on it.

{
  "code": 401,
  "error": "Authentication required. Log in to access this endpoint."
}
GET /api/v1/patch/live #
Session

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
username string required
display_name string | null
avatar_url string | null
title string required
artist string required
album string | null
duration integer | null · int64
position integer · int64 required

Seconds into the track.

position_known boolean required

The position was measured rather than extrapolated.

paused boolean required
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
codec 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

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

200

The count.

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

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/patch/sittings #
Session

Sittings other people have handed you.

Responses

200

Newest first. seen_at is null for ones you have not opened.

application/json

object
sittings array required

Newest first. seen_at is null for ones you have not opened.

each item
object

A sitting somebody handed you, as it appears in the inbox.

uuid string required
session_uuid string required
note string | null
created_at integer · int64 required
seen_at integer | null · int64
from_username string required
from_display_name string | null
title string | null

The sitting's own name, and when it ran. Null when it has since been regrouped out of existence — a real state rather than an error.

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

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

note string | null

At most 500 characters.

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

Mirrors the HTTP status.

error string required

Human-readable. Not a stable identifier — do not branch on it.

401

No valid session cookie or token. Also returned when a token is presented to a session-only endpoint — the endpoint does not accept tokens at all, so the scope is irrelevant.

application/json

object

Every error body in the API has this shape.

code integer · int32 required

Mirrors the HTTP status.

error string required

Human-readable. Not a stable identifier — do not branch on it.

{
  "code": 401,
  "error": "Authentication required. Log in to access this endpoint."
}
403

That deck is not patched with you.

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.

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

Mirrors the HTTP status.

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

200

OK.

application/json

object
sittings array required
each item
object

One sitting you passed on, for the sender's own list.

uuid string required
session_uuid string required
note string | null
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
title 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

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

Parameters

uuid path required
string

Responses

200

The document.

application/json

object

The document a hand-over renders to.

uuid string required
note string | null
from_username string required
from_display_name string | null
title 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.

title string required
artist string required
album string | null
timestamp integer · int64 required
duration integer | null · int64
artwork_url string | null
caa_id integer | null · int64
caa_release_mbid string | null
chain_name 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

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

Mirrors the HTTP status.

error string required

Human-readable. Not a stable identifier — do not branch on it.

DELETE /api/v1/patch/sittings/{uuid} #
Session

Withdraw a sitting you handed over.

A real withdrawal, unlike un-patching: nothing was ever copied, so removing the row removes the access. That stops being true once this federates.

Parameters

uuid path required
string

Responses

200

Withdrawn.

application/json

object
withdrawn boolean required
401

No valid session cookie or token. Also returned when a token is presented to a session-only endpoint — the endpoint does not accept tokens at all, so the scope is irrelevant.

application/json

object

Every error body in the API has this shape.

code integer · int32 required

Mirrors the HTTP status.

error string required

Human-readable. Not a stable identifier — do not branch on it.

{
  "code": 401,
  "error": "Authentication required. Log in to access this endpoint."
}
404

No sitting of yours with that id.

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

Parameters

uuid path required
string

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

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