← API reference

Chains

Signal chains, gear, devices and bindings.

21 of 21 · v0.120.0
GET /api/v1/chains #
Session or Token read

Signal chains.

Scope: read (since v0.37). This is what lets a client offer a chain picker instead of taking unvalidatable free text for a field that has to match a name exactly.

Responses

200

OK.

application/json

object
chains array required
each item
object

A signal chain: the gear a listen ran through, in order.

There is no is_default. A chain is not default for anything by itself; being the default is a property of the thing pointing at it, and there are two: a device (PATCH /api/v1/devices/{id}) and an API token, whose default_chain_id is rung 3 of the chain ladder and is reported by GET /1/validate-token.

Absent is not null here. icon, and a component's equipment_id and since, are omitted entirely when unset rather than sent as null — a strict deserialiser wants those optional, not nullable. description, detail and status really are present-and-null.

id integer | null · int64
user_id integer · int64 required
name string required
description string | null
components array required
each item
object

One step of a chain.

type string required

What kind of box a step is.

sourcetransportdacamptransducernetworkbluetooth
name string required

Always written, so deleting the gear leaves the chain readable.

detail string | null

Free text about this step. Always present; null when there is none. Guessed from the gear type when a step is first picked and never re-applied, so an override sticks.

equipment_id integer | null · int64

The equipment row this step is, set when it was picked from your gear. Omitted for free text — a chain can name something you do not own (a friend's amp, a rental).

since integer | null · int64

Unix seconds from which this box was in the path. Omitted means "for as long as the chain has existed", which is what every step written before this field meant.

Chain and gear hours are derived from the listens, so without it a box added to a chain that already has history is credited with all of it. Stamped automatically on gear a chain did not name before; send one to correct the date, or null to say it was always there.

listening_context string required

How you listen through a chain. Attention, not company — the two are orthogonal, and who you were with is company on the listen.

activeactive-mobilepassivebackgroundunknown
icon string | null

A name from the UI's CHAIN_ICONS set. Omitted when unset, not null. Unvalidated on purpose — an unknown value falls back to a generic mark rather than erroring, and rejecting a save because the front end learned a new icon first would be the worse failure.

is_active boolean required
shared boolean

(since v0.76.1) Whether this chain may be named to a deck patched into yours, and whether its listens reach the parts of Patch that have no patch behind them — the Instance tab, Discover, and Shared Spool captures. Not the sharing unit since v0.93.0: what a patched deck sees is share_all_with_patched on the profile.

total_hours number · double required

Derived from the listens (SUM(duration) where chain_id matches and the listen is not a skip), never accumulated — so assigning a chain to an old listen moves the hours, and un-assigning takes them back. A source that reported no length contributes nothing, so this is a floor.

created_at integer · int64 required

Unix seconds.

401

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

application/json

object

Every error body in the API has this shape.

code integer · int32 required

Mirrors the HTTP status.

error string required

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

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

Authenticated, but not permitted. Either the token lacks the required scope, or the endpoint needs the admin role. Deliberately not a 401 — re-authenticating will not help.

application/json

object

Every error body in the API has this shape.

code integer · int32 required

Mirrors the HTTP status.

error string required

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

{
  "code": 403,
  "error": "This token does not have the 'write' scope"
}
POST /api/v1/chains #
Session

Create a chain.

Session-only.

There is no is_default, and until v0.115.1 this said there was — sending it got a 201 and no effect, because unknown fields are ignored. A chain is made the default by a device binding or an API token; see the Chain schema.

A new chain is never shared. Sharing is switched on deliberately, from the chain drawer or Patch → Decks, and never as a side effect of creating something.

Request body required

application/json

object
name string required
description string | null
components array required
each item
object

One step of a chain.

type string required

What kind of box a step is.

sourcetransportdacamptransducernetworkbluetooth
name string required

Always written, so deleting the gear leaves the chain readable.

detail string | null

Free text about this step. Always present; null when there is none. Guessed from the gear type when a step is first picked and never re-applied, so an override sticks.

equipment_id integer | null · int64

The equipment row this step is, set when it was picked from your gear. Omitted for free text — a chain can name something you do not own (a friend's amp, a rental).

since integer | null · int64

Unix seconds from which this box was in the path. Omitted means "for as long as the chain has existed", which is what every step written before this field meant.

Chain and gear hours are derived from the listens, so without it a box added to a chain that already has history is credited with all of it. Stamped automatically on gear a chain did not name before; send one to correct the date, or null to say it was always there.

listening_context string

How you listen through a chain. Attention, not company — the two are orthogonal, and who you were with is company on the listen.

activeactive-mobilepassivebackgroundunknown
icon string | null

A glyph name from the UI's set. Not validated here — an unknown value falls back to a generic mark on render, which is a better failure than rejecting a save because the front end learned a new icon first.

Responses

201

Created.

application/json

object

A row that was just created.

id integer · int64 required
status string required

created.

400

Malformed or rejected input.

application/json

object

Every error body in the API has this shape.

code integer · int32 required

Mirrors the HTTP status.

error string required

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

401

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

application/json

object

Every error body in the API has this shape.

code integer · int32 required

Mirrors the HTTP status.

error string required

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

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

A chain of that name already exists for this user.

application/json

object

Every error body in the API has this shape.

code integer · int32 required

Mirrors the HTTP status.

error string required

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

GET /api/v1/chains/{id} #
Session or Token read

One chain.

Scope: read (since v0.37).

Parameters

id path required

The chain.

integer · int64

Responses

200

OK.

application/json

object

A signal chain: the gear a listen ran through, in order.

There is no is_default. A chain is not default for anything by itself; being the default is a property of the thing pointing at it, and there are two: a device (PATCH /api/v1/devices/{id}) and an API token, whose default_chain_id is rung 3 of the chain ladder and is reported by GET /1/validate-token.

Absent is not null here. icon, and a component's equipment_id and since, are omitted entirely when unset rather than sent as null — a strict deserialiser wants those optional, not nullable. description, detail and status really are present-and-null.

id integer | null · int64
user_id integer · int64 required
name string required
description string | null
components array required
each item
object

One step of a chain.

type string required

What kind of box a step is.

sourcetransportdacamptransducernetworkbluetooth
name string required

Always written, so deleting the gear leaves the chain readable.

detail string | null

Free text about this step. Always present; null when there is none. Guessed from the gear type when a step is first picked and never re-applied, so an override sticks.

equipment_id integer | null · int64

The equipment row this step is, set when it was picked from your gear. Omitted for free text — a chain can name something you do not own (a friend's amp, a rental).

since integer | null · int64

Unix seconds from which this box was in the path. Omitted means "for as long as the chain has existed", which is what every step written before this field meant.

Chain and gear hours are derived from the listens, so without it a box added to a chain that already has history is credited with all of it. Stamped automatically on gear a chain did not name before; send one to correct the date, or null to say it was always there.

listening_context string required

How you listen through a chain. Attention, not company — the two are orthogonal, and who you were with is company on the listen.

activeactive-mobilepassivebackgroundunknown
icon string | null

A name from the UI's CHAIN_ICONS set. Omitted when unset, not null. Unvalidated on purpose — an unknown value falls back to a generic mark rather than erroring, and rejecting a save because the front end learned a new icon first would be the worse failure.

is_active boolean required
shared boolean

(since v0.76.1) Whether this chain may be named to a deck patched into yours, and whether its listens reach the parts of Patch that have no patch behind them — the Instance tab, Discover, and Shared Spool captures. Not the sharing unit since v0.93.0: what a patched deck sees is share_all_with_patched on the profile.

total_hours number · double required

Derived from the listens (SUM(duration) where chain_id matches and the listen is not a skip), never accumulated — so assigning a chain to an old listen moves the hours, and un-assigning takes them back. A source that reported no length contributes nothing, so this is a floor.

created_at integer · int64 required

Unix seconds.

401

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

application/json

object

Every error body in the API has this shape.

code integer · int32 required

Mirrors the HTTP status.

error string required

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

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

Authenticated, but not permitted. Either the token lacks the required scope, or the endpoint needs the admin role. Deliberately not a 401 — re-authenticating will not help.

application/json

object

Every error body in the API has this shape.

code integer · int32 required

Mirrors the HTTP status.

error string required

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

{
  "code": 403,
  "error": "This token does not have the 'write' scope"
}
404

No such resource, or it belongs to another user.

application/json

object

Every error body in the API has this shape.

code integer · int32 required

Mirrors the HTTP status.

error string required

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

PATCH /api/v1/chains/{id} #
Session

Update a chain.

Session-only, and a whole-record replace despite the verb — it takes the same body as the create. Every field the editor owns is written, so an omitted icon, description or listening_context is cleared. Send the full record back.

Two fields it deliberately does not touch: shared, which is switched from the chain drawer or Patch → Decks — writing it here would un-publish a chain as a side effect of renaming it — and total_hours, which is derived from the listens.

A component naming gear the chain did not name before is stamped with since at the moment of the save. That is what stops a box added to a chain with history being credited with all of it; send your own since to correct the date, which is usually a few days before anybody got round to recording it.

Parameters

id path required

The chain.

integer · int64

Request body required

application/json

object
name string required
description string | null
components array required
each item
object

One step of a chain.

type string required

What kind of box a step is.

sourcetransportdacamptransducernetworkbluetooth
name string required

Always written, so deleting the gear leaves the chain readable.

detail string | null

Free text about this step. Always present; null when there is none. Guessed from the gear type when a step is first picked and never re-applied, so an override sticks.

equipment_id integer | null · int64

The equipment row this step is, set when it was picked from your gear. Omitted for free text — a chain can name something you do not own (a friend's amp, a rental).

since integer | null · int64

Unix seconds from which this box was in the path. Omitted means "for as long as the chain has existed", which is what every step written before this field meant.

Chain and gear hours are derived from the listens, so without it a box added to a chain that already has history is credited with all of it. Stamped automatically on gear a chain did not name before; send one to correct the date, or null to say it was always there.

listening_context string

How you listen through a chain. Attention, not company — the two are orthogonal, and who you were with is company on the listen.

activeactive-mobilepassivebackgroundunknown
icon string | null

A glyph name from the UI's set. Not validated here — an unknown value falls back to a generic mark on render, which is a better failure than rejecting a save because the front end learned a new icon first.

Responses

200

Updated.

application/json

object

A row that was just updated.

id integer · int64 required
status string required

updated.

401

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

application/json

object

Every error body in the API has this shape.

code integer · int32 required

Mirrors the HTTP status.

error string required

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

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

No such resource, or it belongs to another user.

application/json

object

Every error body in the API has this shape.

code integer · int32 required

Mirrors the HTTP status.

error string required

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

DELETE /api/v1/chains/{id} #
Session

Delete a chain.

Session-only. Clears referencing rows first — foreign keys are on.

Parameters

id path required

The chain.

integer · int64

Responses

204

Deleted.

401

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

application/json

object

Every error body in the API has this shape.

code integer · int32 required

Mirrors the HTTP status.

error string required

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

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

No such resource, or it belongs to another user.

application/json

object

Every error body in the API has this shape.

code integer · int32 required

Mirrors the HTTP status.

error string required

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

GET /api/v1/chains/variants #
Token reador Session

Every chain variant the caller owns.

Session or a token carrying read. A variant is a chain with one step swapped — two cartridges on one turntable, or the same commute on different IEMs. They are one chain and a choice, not two chains.

Flat rather than nested under each chain, because the page groups them itself and one list beats a request per chain.

total_hours here is the listening that went through this variant. A chain's own total_hours includes every variant of it, since they all ran through that chain.

Responses

200

OK.

application/json

object
variants array required
each item
object

A chain with one step swapped — two cartridges on one turntable, or the same commute with different IEMs, are one chain and a choice.

id integer · int64 required
user_id integer · int64 required
chain_id integer · int64 required

The chain this is a variant of.

name string required
overrides array required

Only the steps that differ from the chain.

each item
object

One step of a chain, replaced.

position indexes into the parent chain's components. Only the steps that differ are stored, which is what makes a variant an override rather than a copy: editing the shared part of a chain updates every variant of it, and two cartridges on one turntable stay one chain.

A position past the end of the base components adds a step, so a chain that never named a cartridge can gain one without being rewritten.

position integer · int64 required

Index into the chain's components. One past the end adds a step.

equipment_id integer | null · int64

The gear fitted at this step. Null removes the step for this variant — a chain run without its amp, say.

name string | null

Snapshot of the name, so a deleted gear row leaves the variant readable instead of blank.

created_at integer · int64 required

Unix seconds.

total_hours number · double required

The listening that went through this variant, which is a different question from a chain's total — a chain's hours include every variant of it, since they all ran through the same 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."
}
403

Authenticated, but not permitted. Either the token lacks the required scope, or the endpoint needs the admin role. Deliberately not a 401 — re-authenticating will not help.

application/json

object

Every error body in the API has this shape.

code integer · int32 required

Mirrors the HTTP status.

error string required

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

{
  "code": 403,
  "error": "This token does not have the 'write' scope"
}
POST /api/v1/chains/variants #
Session

Create a variant.

Session-only. Only the steps that differ are stored, which is what makes this an override rather than a copy: editing the shared part of a chain updates every variant of it.

The chain and every piece of gear named must be the caller's.

Request body required

application/json

object
chain_id integer · int64 required
name string required
overrides array
each item
object

One step of a chain, replaced.

position indexes into the parent chain's components. Only the steps that differ are stored, which is what makes a variant an override rather than a copy: editing the shared part of a chain updates every variant of it, and two cartridges on one turntable stay one chain.

A position past the end of the base components adds a step, so a chain that never named a cartridge can gain one without being rewritten.

position integer · int64 required

Index into the chain's components. One past the end adds a step.

equipment_id integer | null · int64

The gear fitted at this step. Null removes the step for this variant — a chain run without its amp, say.

name string | null

Snapshot of the name, so a deleted gear row leaves the variant readable instead of blank.

Responses

200

Created.

application/json

object

A new variant's id.

id integer · int64 required
400

Malformed or rejected input.

application/json

object

Every error body in the API has this shape.

code integer · int32 required

Mirrors the HTTP status.

error string required

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

401

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

application/json

object

Every error body in the API has this shape.

code integer · int32 required

Mirrors the HTTP status.

error string required

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

{
  "code": 401,
  "error": "Authentication required. Log in to access this endpoint."
}
PATCH /api/v1/chains/variants/{id} #
Session

Rename a variant or change what it swaps.

Session-only. An omitted field is left alone.

Parameters

id path required

The variant.

integer · int64

Request body required

application/json

object
name string | null
overrides array | null
each item
object

One step of a chain, replaced.

position indexes into the parent chain's components. Only the steps that differ are stored, which is what makes a variant an override rather than a copy: editing the shared part of a chain updates every variant of it, and two cartridges on one turntable stay one chain.

A position past the end of the base components adds a step, so a chain that never named a cartridge can gain one without being rewritten.

position integer · int64 required

Index into the chain's components. One past the end adds a step.

equipment_id integer | null · int64

The gear fitted at this step. Null removes the step for this variant — a chain run without its amp, say.

name string | null

Snapshot of the name, so a deleted gear row leaves the variant readable instead of blank.

Responses

200

Saved. status is ok.

application/json

object

An acknowledgement with nothing else to report.

status names what happened — ok, updated, deleted, cleared, started and so on; the operation says which it sends. A client needs only the HTTP status to know it worked.

status string required
400

Malformed or rejected input.

application/json

object

Every error body in the API has this shape.

code integer · int32 required

Mirrors the HTTP status.

error string required

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

401

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

application/json

object

Every error body in the API has this shape.

code integer · int32 required

Mirrors the HTTP status.

error string required

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

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

No such resource, or it belongs to another user.

application/json

object

Every error body in the API has this shape.

code integer · int32 required

Mirrors the HTTP status.

error string required

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

DELETE /api/v1/chains/variants/{id} #
Session

Delete a variant.

Session-only. The listens that went through it keep their chain and fall back to its base components, which is what a null variant means everywhere else — they are not deleted along with it.

Parameters

id path required

The variant.

integer · int64

Responses

200

Deleted. status is deleted.

application/json

object

An acknowledgement with nothing else to report.

status names what happened — ok, updated, deleted, cleared, started and so on; the operation says which it sends. A client needs only the HTTP status to know it worked.

status string required
401

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

application/json

object

Every error body in the API has this shape.

code integer · int32 required

Mirrors the HTTP status.

error string required

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

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

No such resource, or it belongs to another user.

application/json

object

Every error body in the API has this shape.

code integer · int32 required

Mirrors the HTTP status.

error string required

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

POST /api/v1/chains/variants/{id}/promote #
Session

Make this setup the chain, and the chain a setup.

For the common case where the variant is the one you actually use — the other cartridge is in the headshell most of the time, so it should be what the chain is.

A relabelling, not a change. What was fitted for any given listen is identical afterwards, and so is every piece of gear's hours.

That is harder than it looks, and it is the whole of this endpoint. An override is {position, equipment_id} relative to the base, so another variant that swaps position 2 means "the base, but with X at 2" — change the base and that sentence names different gear at position 1, silently, with hours landing on a box that was never fitted. So every variant is resolved to an absolute list of steps first and re-expressed against the new base afterwards. Listens follow: those on the chain move to the new setup that restores it, and those on the promoted setup move to the chain.

name is what to call the setup the chain is demoted to — the old base has no name of its own, because it was the chain. Defaults to "Previous setup".

Refuses a setup that removes a step (400): removal shifts every later position, so promoting one would change what every other setup's overrides point at. Nothing in the UI creates such a setup.

Parameters

id path required

The variant to promote.

integer · int64

Request body required

application/json

object

GET /api/v1/chains/variants — every variant the caller owns.

Flat rather than nested under each chain: the page needs them keyed by

name string | null

What to call the setup the chain is being demoted to. The old base has no name of its own — it was the chain — so one has to be supplied.

Responses

200

The promoted setup, and the id of the one the chain became.

application/json

object

A setup promoted to be the chain.

promoted integer · int64 required

The setup that is now the chain.

old_base_variant_id integer · int64 required

The variant the old chain became.

400

That setup removes a step, or has none.

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

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/chains/duplicates #
Session

Chains that look like the same rig, and what folding one would do.

Session-only. The discovery half of the fold below, and the same preview-then-apply shape the metadata sanitiser uses: this reports, it never acts.

A pair is reported when the two chains disagree on at most two steps. distance is how many — 0 is the same wiring under two names, 1 is the swapped-box case (the same commute on different IEMs) that variants exist for. Past two they are simply different rigs, and a variant overriding half a chain is a copy wearing an override's clothes.

The chain with more listening is reported as the base, since it keeps its id and fewer rows move — but the fold takes an explicit direction, so this is a suggestion rather than the decision.

Responses

200

OK.

application/json

object

Pairs of chains that look like the same rig. Nothing has been changed.

duplicates array required
each item
object

One pair of chains that look like the same rig, and what folding would do.

Reported, never acted on — the same preview-then-apply discipline the metadata sanitiser follows, and for the same reason: a fold is not reversible, and a plurality of one step is a weak signal the operator should see before agreeing to it.

base_chain_id integer · int64 required
base_name string required
base_hours number · double required
fold_chain_id integer · int64 required
fold_name string required
fold_hours number · double required
distance integer · int64 required

How many steps disagree. 0 is the same wiring under two names.

overrides array required

What the folded chain becomes as a variant of the base.

each item
object

One step of a chain, replaced.

position indexes into the parent chain's components. Only the steps that differ are stored, which is what makes a variant an override rather than a copy: editing the shared part of a chain updates every variant of it, and two cartridges on one turntable stay one chain.

A position past the end of the base components adds a step, so a chain that never named a cartridge can gain one without being rewritten.

position integer · int64 required

Index into the chain's components. One past the end adds a step.

equipment_id integer | null · int64

The gear fitted at this step. Null removes the step for this variant — a chain run without its amp, say.

name string | null

Snapshot of the name, so a deleted gear row leaves the variant readable instead of blank.

matching_variant array | null

A variant of the base that is already wired this way, as [id, name] — the state someone is in when they built the variant and found the old chain still sitting in the list beside it.

each item
object
fold_blocked_by_variants boolean required

This one carries variants of its own, so it cannot be the side that folds. Surfaced here rather than left for the fold to refuse.

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

Fold this chain into another as a variant of it.

Session-only. Not reversible.

Every listen, side, session, binding, device default and token default of this chain is repointed at into_chain_id carrying the variant, and this chain is then deleted. Because gear hours resolve per (chain, variant) pair, the moved listening lands on the gear that was actually fitted — which is what makes two near-duplicate chains' hours add up instead of sitting in two rows that cannot be compared.

Send either variant_id (a variant of the target chain you already built) or variant_name (derive a new one from the difference between the two chains). Sending both, or neither, is a 400: on an endpoint that cannot be undone, "use the one I built" and "make me another" must not be the same request with a typo between them.

Refused with a 400 naming the reason when: the chain is folded into itself, the variant belongs to a different chain (it would resolve to components never fitted together), the name is already taken on the target, or this chain carries variants of its own — their positions index into components the fold replaces, and re-deriving them would be a guess about which box was fitted.

Parameters

id path required

The chain to fold away.

integer · int64

Request body required

application/json

object

What a fold request has to say. Deliberately not one nullable field.

variant_id (land on one that exists) and variant_name (derive a new one from the two chains) are different intents, and a single nullable field would make "use the variant I built" and "make me another one" the same request with a typo between them — on an endpoint that is not reversible.

into_chain_id integer · int64 required
variant_id integer | null · int64
variant_name string | null

Responses

200

Folded. The counts are what actually moved.

application/json

object

What a fold moved, so the caller can say it rather than claim success.

Reported per table because they are different kinds of thing: listens are the hours, a token or a device default is future attribution, and a record on the shelf is the turntable it lives with.

variant_id integer · int64 required
variant_name string required
variant_created boolean required
listens integer · int64 required
sides integer · int64 required
sessions integer · int64 required
releases integer · int64 required
devices integer · int64 required
bindings integer · int64 required
tokens integer · int64 required
400

Malformed or rejected input.

application/json

object

Every error body in the API has this shape.

code integer · int32 required

Mirrors the HTTP status.

error string required

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

401

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

application/json

object

Every error body in the API has this shape.

code integer · int32 required

Mirrors the HTTP status.

error string required

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

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

No such resource, or it belongs to another user.

application/json

object

Every error body in the API has this shape.

code integer · int32 required

Mirrors the HTTP status.

error string required

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

GET /api/v1/devices #
Session or Token read

Devices that have reported listens.

Scope: read (since v0.37).

Responses

200

OK.

application/json

object
devices array required
each item
object

A player that has submitted listens — rung 4 of the chain ladder.

id integer · int64 required
user_id integer · int64 required
machine_id string required

tapedeck_device.machine_id — the key.

name string | null
platform string | null
product string | null
device_type string | null
default_chain_id integer | null · int64
first_seen integer · int64 required
last_seen integer · int64 required
total_listens integer · int64 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

Authenticated, but not permitted. Either the token lacks the required scope, or the endpoint needs the admin role. Deliberately not a 401 — re-authenticating will not help.

application/json

object

Every error body in the API has this shape.

code integer · int32 required

Mirrors the HTTP status.

error string required

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

{
  "code": 403,
  "error": "This token does not have the 'write' scope"
}
PATCH /api/v1/devices/{id} #
Session

Set a device's default chain.

Session-only. Rung 4 of the chain ladder — the coarsest fallback.

Parameters

id path required

The device.

integer · int64

Request body required

application/json

object
chain_id integer | null · int64 required

The chain this device's listens default to — rung 4 of the ladder. Required: null clears it; absent is a 400. Must be one of your chains.

Responses

200

Updated. status is ok.

application/json

object

An acknowledgement with nothing else to report.

status names what happened — ok, updated, deleted, cleared, started and so on; the operation says which it sends. A client needs only the HTTP status to know it worked.

status string required
400

chain_id absent, or not one of your chains.

application/json

object

Every error body in the API has this shape.

code integer · int32 required

Mirrors the HTTP status.

error string required

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

401

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

application/json

object

Every error body in the API has this shape.

code integer · int32 required

Mirrors the HTTP status.

error string required

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

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

No such resource, or it belongs to another user.

application/json

object

Every error body in the API has this shape.

code integer · int32 required

Mirrors the HTTP status.

error string required

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

GET /api/v1/equipment #
Session or Token read

Your gear.

Scope: read (since v0.37). Hours are derived from the chains that use each piece.

Responses

200

OK.

application/json

object
equipment array required
each item
object

A piece of gear.

The type field is equipment_type on the way out and type on the way in. POST/PATCH take type, every read returns equipment_type.

id integer · int64 required
user_id integer · int64 required
name string required
equipment_type string required

dac, amp, transducer, cartridge, headshell, phono, turntable, tape-deck, transport, …

brand string | null
model string | null
total_hours number · double required

Derived — summed from every chain referencing this gear, never accumulated. Free-text chain steps report zero, which is honest.

first_used integer | null · int64

When Tapedeck first saw it, unix seconds.

last_used integer | null · int64

Unix seconds.

notes string | null
purchased_at integer | null · int64

When it was bought, unix seconds. Distinct from first_used — gear is usually owned for a while before anyone gets round to cataloguing it, and burn-in hours are counted from the purchase.

sources array required

(since v0.70.4) What total_hours is made of, largest first — the same per-(chain, variant) seconds the hours are derived from, so the parts always add to the whole. Empty for gear nothing has logged time through.

each item
object

Where one piece of gear's hours came from.

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

Set when the listening went through a variant of that chain — which is the case the breakdown exists for, since that is when one piece of gear accrues hours through a chain that does not name it.

variant_name string | null
hours number · double required
status string | null

Why this piece is not in a chain — new, spare or lent. Null is the usual state and means nothing has been said. Free text: the UI owns the vocabulary.

401

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

application/json

object

Every error body in the API has this shape.

code integer · int32 required

Mirrors the HTTP status.

error string required

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

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

Authenticated, but not permitted. Either the token lacks the required scope, or the endpoint needs the admin role. Deliberately not a 401 — re-authenticating will not help.

application/json

object

Every error body in the API has this shape.

code integer · int32 required

Mirrors the HTTP status.

error string required

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

{
  "code": 403,
  "error": "This token does not have the 'write' scope"
}
POST /api/v1/equipment #
Session

Add gear.

Session-only. The type field is type here and comes back as equipment_type — see the Equipment schema.

Request body required

application/json

object
name string required
type string required
brand string | null
model string | null
notes string | null
purchased_at integer | null · int64

When it was bought, unix seconds. Distinct from first_used, which is when Tapedeck first saw it — gear is usually owned for a while before anyone gets round to cataloguing it, and burn-in hours are counted from the purchase.

Responses

201

Created — or matched. Gear is keyed on (user, name): posting a name you already own returns that row's id and stamps last_used, and every other field you sent is ignored rather than applied. Use PATCH /api/v1/equipment/{id} to change one. Always 201, even when nothing was created.

application/json

object

A row that was just created.

id integer · int64 required
status string required

created.

400

Malformed or rejected input.

application/json

object

Every error body in the API has this shape.

code integer · int32 required

Mirrors the HTTP status.

error string required

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

401

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

application/json

object

Every error body in the API has this shape.

code integer · int32 required

Mirrors the HTTP status.

error string required

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

{
  "code": 401,
  "error": "Authentication required. Log in to access this endpoint."
}
PATCH /api/v1/equipment/{id} #
Session

Update gear.

Session-only. A whole-record replace, not a partial patch — every field is written, so an omitted brand clears the brand. Send the full record back.

Parameters

id path required

The gear.

integer · int64

Request body required

application/json

object

The update body is deliberately its own type rather than a reuse of CreateEquipmentRequest: status is settable here and not at creation (gear is tagged from the "Not in a chain yet" table, once it exists), and a shared struct would mean POST accepting a field it silently threw away.

Every field is written unconditionally — this endpoint is a whole-record replace, not a partial patch, which is why an omitted brand has always cleared the brand. Clients must send the full record back.

name string required
type string required
brand string | null
model string | null
notes string | null
purchased_at integer | null · int64
status string | null

Why it is not in a chain: new, spare, lent. Absent clears it.

Responses

200

Updated.

application/json

object

A row that was just updated.

id integer · int64 required
status string required

updated.

401

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

application/json

object

Every error body in the API has this shape.

code integer · int32 required

Mirrors the HTTP status.

error string required

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

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

No such resource, or it belongs to another user.

application/json

object

Every error body in the API has this shape.

code integer · int32 required

Mirrors the HTTP status.

error string required

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

DELETE /api/v1/equipment/{id} #
Session

Delete gear.

Session-only. Chains referencing it stay readable — every component keeps a name snapshot alongside its optional equipment_id.

Parameters

id path required

The gear.

integer · int64

Responses

204

Deleted.

401

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

application/json

object

Every error body in the API has this shape.

code integer · int32 required

Mirrors the HTTP status.

error string required

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

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

No such resource, or it belongs to another user.

application/json

object

Every error body in the API has this shape.

code integer · int32 required

Mirrors the HTTP status.

error string required

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

GET /api/v1/bindings #
Session or Token read

Output-device bindings.

Scope: read (since v0.37). Rung 2 of the chain ladder.

Responses

200

OK.

application/json

object
bindings array required
each item
object

An output device, and the chain it maps to — rung 2 of the chain ladder.

id integer · int64 required
user_id integer · int64 required
identifier string required

The output device, as a client reports it in tapedeck_device.output_device.

interface string | null
chain_id integer | null · int64

Null for one seen but not mapped yet — offered for one-tap assignment.

last_seen integer · int64 required

Unix seconds.

hits integer · int64 required

Listens that reported it.

401

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

application/json

object

Every error body in the API has this shape.

code integer · int32 required

Mirrors the HTTP status.

error string required

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

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

Authenticated, but not permitted. Either the token lacks the required scope, or the endpoint needs the admin role. Deliberately not a 401 — re-authenticating will not help.

application/json

object

Every error body in the API has this shape.

code integer · int32 required

Mirrors the HTTP status.

error string required

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

{
  "code": 403,
  "error": "This token does not have the 'write' scope"
}
PUT /api/v1/bindings #
Session

Bind an output device name to a chain.

Session-only.

Request body required

application/json

object
identifier string required

The output device, as a client reports it in tapedeck_device.output_device.

chain_id integer | null · int64 required

The chain it maps to. Required: null clears the mapping; absent is a 400. Must be one of your chains.

Responses

200

Saved. status is ok.

application/json

object

An acknowledgement with nothing else to report.

status names what happened — ok, updated, deleted, cleared, started and so on; the operation says which it sends. A client needs only the HTTP status to know it worked.

status string required
400

chain_id absent, or not one of your chains.

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."
}
DELETE /api/v1/bindings/{id} #
Session

Remove a binding.

Session-only.

Parameters

id path required

The binding.

integer · int64

Responses

204

Deleted.

401

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

application/json

object

Every error body in the API has this shape.

code integer · int32 required

Mirrors the HTTP status.

error string required

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

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

No such resource, or it belongs to another user.

application/json

object

Every error body in the API has this shape.

code integer · int32 required

Mirrors the HTTP status.

error string required

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