This page is agents.md, laid out for reading. Agents can fetch the Markdown directly.
Tangle API: instructions for agents
You are helping a developer integrate their app or service with Tangle, so that any Tangle user can connect their music collection and the app can read and update it. (You may also be helping someone script their own collection; that is the simpler case, covered below.) The Tangle API implements Open Music Collection (OMC) Access at
https://api.tangle.music/omc/v1. OMC is an open standard: its specification defines the records and the protocol, and this file says only what is Tangle's own. Read this whole file, then the parts of the specification it links to, before you write code.
What Tangle is
Tangle is a music app for DJs and collectors. It pulls a person's music from Spotify, Tidal, SoundCloud, YouTube Music, Apple Music, Bandcamp, Discogs, Rekordbox and local folders into one collection, where one song can have copies on several platforms. People organise it into playlists (ordered lists) and tags (unordered lists), on Tangle itself or on a platform.
In OMC's words, Tangle's server is an OMC home: it keeps each person's collection (the copy Tangle keeps in the cloud for their Backup and restore) and lets apps connect to it. Each user chooses Connect your Tangle collection in your app, sees what it asks for, and chooses Allow. Every change your app makes is a change the user could have made in Tangle, with the same effects. An app never sees a collection whose owner hasn't connected it.
Two words this file keeps apart: the developer is the person you are working with, who builds the app; a user is any Tangle user who connects it.
Open Music Collection
Open Music Collection (OMC) is an open standard for a person's music collection: the songs and albums they keep (items), where each one lives (copies on sources), and the lists they organise them into. Tangle looks after the specification; anyone can implement it.
Core 1.0 (the records, the OMC file format, extensions and keeping unknown data): https://www.tangle.music/omc/spec/
Access (the protocol apps use to read and change a collection at its home, with every endpoint): https://www.tangle.music/omc/modules/access/
OMC's own instructions for agents, with the rules every OMC app follows: https://www.tangle.music/omc/agents.md
Which document answers what: what a record or field means, and how an endpoint behaves, is in the OMC specification. What Tangle supports of it, and what Tangle adds, is in What Tangle supports below. An app written against Access works with any OMC home, Tangle included.
Your first five minutes
Ask what the developer wants to build. Almost always it is an app or service other people use: go on with step 2. If they only want to work on their own collection (a script, a one-off), skip to Scripting your own collection below.
Give the app an identity. Either way works with Tangle; the first works with every OMC home:
By address (no registration): publish a small JSON document at an
https://URL on the app's own site and use that URL as theclient_id. See Apps known by their address below. Users see the app as not reviewed by Tangle, with the site it comes from.Registered (Tangle only), which you can't do yourself: the developer signs in at https://www.tangle.music/developers/dashboard/, turns on Developer mode, opens Apps → New app, and enters the app's name, its redirect URI(s) and its type (confidential for a server that can keep a secret, public for browser, mobile, desktop and CLI apps). Ask them for the client id, and have them put the client secret (confidential apps) in the app's secret store or an environment variable, never in code or chat.
Either way, the app's name can't contain "Tangle", so users never mistake it for Tangle's own.
Build Connect your Tangle collection: a button that starts the OAuth flow (see Authentication), a callback route that exchanges the code, and storage for each connected user's tokens. Store the user's id from
GET /omc/v1/me(user.id) next to your own user record.Handle users without Backup and restore. After connecting, call
GET /omc/v1/me. Ifcollection_availableisfalse, tell the user in your app that Tangle needs Backup and restore turned on (Tangle → Settings → Library) before their collection is available. Every other call answers403 backup_and_restore_requireduntil they do.Handle disconnects. Users can disconnect your app in Tangle at any time. Then their tokens answer
401 invalid_tokenand refreshing fails withinvalid_grant; with Tangle's webhooks you also getauthorization.revoked. Mark the user disconnected and offer Connect your Tangle collection again.Test with a real account. The developer can connect their own Tangle collection to the app while developing.
Scripting your own collection
For a script that works only on the developer's own collection, a personal access token (Tangle only) is simpler than OAuth: they create one at https://www.tangle.music/developers/dashboard/ (Tokens), choose its scopes, and put it in an environment variable rather than pasting it into the conversation: export TANGLE_API_TOKEN=tng_pat_…. Check it with:
curl -s https://api.tangle.music/omc/v1/me -H "Authorization: Bearer $TANGLE_API_TOKEN"Personal access tokens only ever reach their creator's own collection. Never use one for other users.
Rules for agents
Never print, log, commit or echo a token or client secret. Read them from the environment or the app's secret store. Don't put them in URLs or in client-side code.
Keep each user's tokens apart. Store tokens per connected user, encrypted at rest, and only ever use a user's token for that user's requests.
Ask only for the scopes the app uses. Users see them on the consent page. An app that only brings music in needs nothing but
library:add.Confirm destructive changes with the user. In the app, ask before deleting a list, removing more than a handful of items, or changing lists on a platform (Spotify, Tidal, …): those changes reach the platform. Never do them in the background.
Ids are local. Ids (
trk_,rel_,lst_,src_) mean something only in the collection they came from: never build, parse or compare them across users. To find a user's item for something you know from elsewhere, look it up by content (?source=&ref=,?isrc=,?upc=,?musicbrainz=), as the specification says: https://www.tangle.music/omc/spec/#the-modelKeep what you don't understand. Send back whole records you read, unknown fields included: https://www.tangle.music/omc/spec/#extensions-and-unknown-data
Expect fields to be missing. Tangle sends only what it knows: a field it has no data for is absent, never empty or guessed.
Ask the list, every time. Each list carries
can: what your app may do with it now. Don't store it;GET /omc/v1/lists/cangives it for every list at once.Address ordered lists by position and
version. A playlist can hold a song twice, so its entries are positions in the list as you read it, and you send theversionyou read. On409 version_conflict, read the list again and retry.Retrying is safe. Adding an entry that is already in an unordered list, or removing one that isn't, changes nothing and is reported in
unchanged.Write only your own namespace. Keep your app's data in
extunder a reverse-domain name you own (com.example.app). Another app's namespace answers403 namespace_not_yours.Respect rate limits. On
429, wait the number of seconds in theRetry-Afterheader, then retry. Prefer batches (up to 100 items per request) to many single calls, and the event stream to polling.Use only
/omc/v1/*and/oauth/*. Everything else on Tangle's servers is internal and changes without notice.Stay portable. Build on what OMC Access defines, so the app works with any OMC home. Features marked Tangle only (see Tangle-only features) are optional extras: never make them the only way your app works.
Report errors with their
request_idso the developer can quote it to Tangle support.
What Tangle supports
Tangle's facts about its OMC implementation, in one place. GET /omc/v1/me gives the modules, scopes and limits at run time.
Modules and base URL
Core 1.0 and Access 0.2. /me lists them as "modules": { "core": "1.0", "org.openmusiccollection.access": "0.2" }.
The base URL is https://api.tangle.music/omc/v1. Requests and responses are JSON; GET /omc/v1/collection is NDJSON.
GET /omc/v1/me also gives:
user.id, anduser.name: the person's Tangle username, a name to show;capabilities, following the token's scopes;limits:data_bytes(32 KB: your app's namespace on one record, its copies included),module_data_bytes(1 MB: one module's data on one record),entries_per_request,ids_per_request,import_records,additions_per_hour;client_id_metadata: true: apps may connect by their address (see Authentication);collection_available:falseuntil the user turns on Backup and restore.
Scopes
Users see each scope on the consent page in these words:
| Scope | Consent line | In Tangle |
|---|---|---|
library:read | See your collection | Items, sources and module records |
lists:read | See your lists | Playlists and tags, with their entries |
lists:write | Change your lists | Create, rename and delete lists; add, remove and move entries, and their notes; your app's data on lists. Includes lists:read |
library:write | Add to and change your collection | Your app's data on items and sources, the person's rating, module records, and importing. Includes library:read and library:add |
library:add | Add music to your collection | Add items, create lists and fill the lists your app made, without reading anything else: https://www.tangle.music/omc/modules/access/#apps-that-only-add |
listens:write | Tell Tangle what you play | Send listens (see Listens) |
listens:read | See what you've played | Read listens. Includes listens:write |
What Tangle exports
Each record is OMC Core (https://www.tangle.music/omc/spec/). What Tangle fills in:
Items: recordings (Tangle's tracks) and releases (the albums their copies come from), each with its copies on the user's sources. Releases carry
label, and from Discogscatalog_numberandcountry; Discogs copies carryformat,format_detailsandcondition.ids.isrc: the ISRC the platforms give (Apple Music, Spotify imports, saved search results), the one most copies agree on.note: the comment kept in Rekordbox, iTunes or the file (provenance.note.originisembedded), or the note an app wrote, which is the person's and wins.key: in ID3TKEYform (Am,F#,Dbm), read from TKEY, Rekordbox, Camelot or Open Key. A value that isn't a key is left out.rating: 1 to 5. The person's own when an app wrote one (provenance.rating.originuser), otherwise the platforms' star rating (originplatform).play_countandlast_played_at: counted from listens (see Listens). Read only.Folders: Rekordbox playlist folders and My Tag groups are lists with
folder: trueand no entries; lists name their folder inparent, with the platform'sposition.Colours: each Rekordbox colour in use is an unordered list with
color, in a "Colours" folder on that source.Smart lists: filters saved in Tangle are lists with an OMC
filterwhen every rule is on a core field; their entries are evaluated when you read them. Other saved filters aren't exported.List entries: on a list that lives on a platform, each entry carries
ref, the platform's id of the copy it is there. An entry may have anote.
Folders, colours and smart lists are read only for apps (can all false; writes answer 422 list_not_writable), though your app may keep its own data on them.
Not exported, because Tangle doesn't have the data yet: composers, album_artists, release_type, a copy's file (size, format, hashes), cue points and grids, Rekordbox history and Intelligent Playlists. These fields are absent, not empty.
Reading
Everything in Access: https://www.tangle.music/omc/modules/access/#reading
Lookups by content:
GET /omc/v1/items?source=…&ref=…(404 source_not_foundwhen the source isn't the user's),?isrc=…,?upc=…,?musicbrainz=….GET /omc/v1/collectionsends the change cursor in theOMC-Cursorresponse header. It counts as 60 requests against the rate limit, and one runs per user at a time.GET /omc/v1/changesandGET /omc/v1/events, see Keeping up below.Every list in a response carries
can(add,remove,reorder,rename,delete): what this app may do with the list now, from the platform's rules and the token's scopes.reordermeans placing or moving entries to a position rather than only adding at the end. A Bandcamp collection is read only; Apple Music playlists only take additions, at the end.GET /omc/v1/lists/cananswers{ "lists": { "lst_…": { "add": true, … } } }for every list your app can see, so you can show it right after reading the collection file, which has nocan. When a list'scanchanges (the user connected or disconnected its platform),/changesreports the list.Every list in a response also carries
version, which changes whenever its entries do;GET /omc/v1/lists/{id}sends it inETagtoo. Neithercannorversionis ever in the collection file.
Writing
As in Access (https://www.tangle.music/omc/modules/access/#writing):
PATCHitems, sources and lists: your app's data (extin your namespace,ids,provenance, fields Tangle doesn't know, data on copies) is kept and given back unchanged. Tangle's own fields must be sent unchanged or left out (422 field_not_writable): https://www.tangle.music/omc/modules/access/#how-patch-worksrating(library:write) andnoteare the exceptions: what your app writes there is the person's own value.ids.isrc,play_countandlast_played_atare Tangle's.Lists: create (on Tangle, or on a platform with
source; a source'sext["music.tangle"].creatable_list_kindssays what it can hold), rename, delete, and change entries (below). A new list may come with its entries, notes included. Apps can't create folders, colours or smart lists yet.Module records (such as
org.openmusiccollection.feeds/follow):PUTandDELETE.Every successful write answers with an
OMC-Cursorheader: the change-feed position just past the write. Apps with onlylibrary:addget none.
Entries
Ordered lists (playlists) are addressed by position: the 0-based index in entries at the list's version. Send the version you read with every request that names a position; if the list changed since, Tangle answers 409 version_conflict with the current version in ETag. Read the list again and retry. Unordered lists (tags) are addressed by item id, without a version. https://www.tangle.music/omc/modules/access/#entries-by-position
| Request | Body |
|---|---|
POST /omc/v1/lists/{id}/entries | { "items": ["trk_…"] }, adding at the end; on an ordered list optionally "before": 3, "version": "…" |
DELETE /omc/v1/lists/{id}/entries | Ordered: { "positions": [3, 7], "version": "…" }. Unordered: { "items": ["trk_…"] } |
POST /omc/v1/lists/{id}/entries/move | { "from": 3, "before": 0, "version": "…" }: one move, which reaches the platform as one change |
PATCH /omc/v1/lists/{id}/entries | Ordered: { "position": 3, "version": "…", "note": "…" }. Unordered: { "item": "trk_…", "note": "…" }. null removes the note |
A playlist takes a song it already has only where its platform allows repeats (Spotify, Tidal and Tangle's own playlists); elsewhere the addition is reported in unchanged. Removing one occurrence of a repeated song leaves the others.
Adding items
POST /omc/v1/items takes recordings and releases (https://www.tangle.music/omc/modules/access/#adding-an-item):
When the user already has the item (matched by a copy, an identifier, or for a release its label and catalogue number), the answer is
"result": "existing"with itsid.Otherwise Tangle holds it in the user's Captured until they keep it:
202,"result": "held", noidyet, andnewsays whether it was already waiting. It appears in/changesonce kept. Tangle holds every new item, so?hold=trueis accepted and changes nothing.When the user keeps an item your app added (here or by importing), it gets a
via: the one you sent, else the shop its link was on, else your app's name and homepage (for an app known by its address, its document'sclient_uri). An item the user already had keeps theviait had. Items added with a personal token get none.?list=lst_…also puts an existing item in that list; the answer'slistisadded,alreadyornot_added(a held item, for now).An item needs a copy on a registered platform (its
url, or just itsref), aviawith aurl, or identifiers (422 missing_urlotherwise). A link to any other site, such as a record shop's page, isn't a copy: send it invia({ "name": "Hard Wax", "url": "…" }).
Importing
POST /omc/v1/import combines a piece of a collection file into the collection, as Access describes, with the same answer (matched, created, suggested, held, left, ids): https://www.tangle.music/omc/modules/access/#importing
Scope
library:write; lists in the piece also needlists:write.?dry_run=truereports what would happen without changing anything;?conflicts=home(the default) or?conflicts=importsettles values both sides have.Tangle recognises records it exported itself by their id, so importing a Tangle export again updates instead of duplicating.
What Tangle can't create from a file is held in Captured (at most 100 new items per import), or is
leftwith a reason: sources must be connected in Tangle itself, and local folders match only by id. Aratingcomes along only when the file marks it as the person's.Up to 20,000 records per request, and 20 imports an hour.
Namespaces and budgets
Your app's ext namespace is yours: an app known by its address owns the reverse-domain names of its host (app.example.com owns com.example.app); a registered app owns a namespace by writing it first. Another app writing it gets 403 namespace_not_yours. Module namespaces (org.openmusiccollection.*) are shared by every app with the scope, and the user's own changes (in Tangle, or with a personal token) are never limited. Sending back a record you read, other apps' data unchanged, is always fine.
Your app's namespace may hold up to 32 KB on a record (data_bytes) and each module's data up to 1 MB (module_data_bytes); more answers 413 data_too_large.
Listens
Tangle keeps what the user plays as listens, in ListenBrainz's format, so a player that already scrobbles needs no new code (https://www.tangle.music/omc/modules/access/#listens):
POST /omc/v1/listens(scopelistens:write) takes ListenBrainz's submit-listens body:single, orimportwith up to 1,000 listens;playing_nowis accepted and not kept. A player that talks to a ListenBrainz server can also point athttps://api.tangle.music/omc/v1/listenbrainz/1/submit-listensand send its token asAuthorization: Token ….Each listen is matched to a recording by ISRC, MusicBrainz id, Spotify id or a copy's link, then by title and artist when exactly one matches. That item's
play_countandlast_played_atfollow, and/changesreports it. The same listen sent twice is kept once.GET /omc/v1/listens(listens:read) answers ListenBrainz's shape, newest first (min_ts,max_ts,count), each listen with theitemit matched.Listens are never in the collection file. Tangle's own player doesn't send listens yet, so counts come from apps.
Not yet
Views: every connection sees the whole collection its scopes allow; only an add-only app is limited, to the lists it made (
403 out_of_viewelsewhere).viaon releases held as links: they don't get aviayet.Held items and
?list=: a held item doesn't reach the list when the user keeps it.Writing folders, colours and smart lists, and Tangle's apps showing the rating your app wrote.
Tangle's own data
Tangle's namespace is music.tangle. In ext["music.tangle"] Tangle keeps only what it alone needs to restore its own state:
on items and copies:
hidden(what the user hid in Tangle);on lists:
kind(Tangle's own kinds, such asrekordbox.colorandsaved_filter),track_count(only in list summaries, which have no entries),linked_list_ids(lists the user combined: adding an item to one adds it to the others where that platform has it) andhidden;on entries:
status: "unavailable"(greyed out on the platform);on sources:
platform,statusandcreatable_list_kinds.
Read it, but don't write it: Tangle ignores what you send there, so sending back a record you read is always safe.
Tangle's apps call recordings tracks, ordered lists playlists and unordered lists tags.
Tangle-only features
OMC separates what every home does from what one home offers. Tangle follows the specification's own table (https://www.tangle.music/omc/spec/where/): what an app needs is in OMC; these are Tangle's product, outside OMC, and no OMC app should need them:
| Tangle only | What to use for any OMC home |
|---|---|
| Registered apps and the developer dashboard | An app known by its address |
| Personal access tokens | OAuth, with a loopback redirect for scripts |
| Webhooks | /changes and /events |
| Captured (where held items wait) | "result": "held", and /changes once kept |
The older /v1 API | /omc/v1 |
Home policy, which another home may set differently, is announced in /me: limits and budgets, holding every new item, who owns a namespace, rate limits.
How a change travels
When your app adds an item to a user's list:
It is saved at once in the user's Backup and restore copy. Your response already shows it.
The user's open Tangle apps receive it within about a second.
If the list lives on a platform (Spotify, Tidal, YouTube Music, SoundCloud, Rekordbox, …), Tangle shows it as a change waiting to sync, and sends it to the platform on the next sync, or within about half a minute when live sync is on and a Tangle app is open. Records show the list as it will be; what hasn't reached the platform yet is Tangle's own state and isn't in them.
Show users this when it matters: "Added. It's in Tangle now; it reaches Spotify on your next Tangle sync (or within a minute if live sync is on)."
An item that isn't on the list's platform yet (say a Bandcamp purchase added to a Spotify playlist) is still added in Tangle; Tangle's matching finds the platform's copy before it syncs.
Authentication
Send every request with Authorization: Bearer <token> over HTTPS. Two kinds of token work:
OAuth access tokens (
tng_at_…): issued to an app for one connected user by the flow below, valid one hour, renewed with a refresh token (tng_rt_…). This is how apps reach their users' collections.Personal access tokens (
tng_pat_…, Tangle only): created by a developer at https://www.tangle.music/developers/dashboard/ for their own collection only, valid 7 to 365 days, revocable there at any time.
OAuth 2.1: Connect your Tangle collection
Access's consent rules apply (https://www.tangle.music/omc/modules/access/#consent). The app is known by its client_id: its own address (below), or one Tangle gave it when it was registered at https://www.tangle.music/developers/dashboard/ (Apps → New app, Tangle only) with a name, the redirect URIs it uses, and its type:
Public (single-page apps, mobile and desktop apps, CLIs, agents): no client secret; PKCE protects the flow.
Confidential (a server you control): also gets a client secret, sent at the token endpoint.
Redirect URIs must match exactly. Allowed: https:// URLs, http://127.0.0.1, http://[::1] or http://localhost (any port, for CLIs that listen locally), and reverse-domain schemes for native apps (com.example.app:/callback). Discovery metadata: https://api.tangle.music/.well-known/oauth-authorization-server.
1. Send the user to the consent page with a fresh PKCE pair (S256 only) and a random state:
https://www.tangle.music/oauth/authorize?response_type=code
&client_id=tng_ci_…
&redirect_uri=https%3A%2F%2Fyour.app%2Fcallback
&scope=library%3Aread%20lists%3Aread%20lists%3Awrite
&state=<random>
&code_challenge=<BASE64URL(SHA256(code_verifier))>
&code_challenge_method=S256With a client_id that is an address, put that URL in client_id, encoded like any parameter.
2. Tangle redirects back to your redirect_uri with code, your state and iss (check that state is yours and iss is https://api.tangle.music), or with error=access_denied if the user said no. Codes expire after 5 minutes and work once.
3. Exchange the code (form-encoded; JSON is accepted too):
curl -s https://api.tangle.music/oauth/token \
-d grant_type=authorization_code \
-d code=tng_ac_… \
-d redirect_uri=https://your.app/callback \
-d client_id=tng_ci_… \
-d code_verifier=<the verifier>
# Confidential apps also send their secret: -u tng_ci_…:tng_cs_… (or -d client_secret=…){ "access_token": "tng_at_…", "token_type": "Bearer", "expires_in": 3600, "refresh_token": "tng_rt_…", "scope": "library:read lists:read lists:write" }4. Refresh before the hour is up with grant_type=refresh_token&refresh_token=…&client_id=…. Every refresh returns a new refresh token; store it atomically and use only the newest. Presenting a refresh token twice disconnects that user from your app: Tangle treats it as stolen.
Approving the same scopes again skips the consent screen only for https:// redirect URIs. Loopback and custom-scheme redirects always ask, because another program on the device could claim them.
5. Disconnect a user from your side with POST https://api.tangle.music/oauth/revoke (token=…&client_id=…). Users can also disconnect your app in Tangle at any time (it is listed under connected apps, with Disconnect); your tokens then answer 401 invalid_token.
Apps known by their address
An app can connect without registering: its client_id is an https:// URL with a path on its own site, serving an OAuth Client ID Metadata Document (https://www.tangle.music/omc/modules/access/#who-the-app-is). Any OMC home that supports it (/me says client_id_metadata: true) accepts the same app.
{
"client_id": "https://app.example.com/oauth/client.json",
"client_name": "Example Digger",
"client_uri": "https://app.example.com/",
"redirect_uris": ["https://app.example.com/callback"],
"token_endpoint_auth_method": "none"
}client_idmust be the document's own URL, andtoken_endpoint_auth_methodnone: such an app is public, with PKCE and no secret.Tangle fetches the document when a user is asked to consent (public addresses only, no redirects, up to 64 KB) and again when its copy is over an hour old. It redirects only to the document's
redirect_uris.The consent page shows the app's name with the host it came from, marked as not reviewed by Tangle.
The app owns the reverse-domain namespaces of its host:
app.example.comwritescom.example.app.client_namecan't contain "Tangle".
Keeping up
An app downloads GET /omc/v1/collection once, keeps the OMC-Cursor it got, and follows GET /omc/v1/changes?cursor=… from there, re-reading what changed: https://www.tangle.music/omc/modules/access/#changes. Tangle's feed reports every change, whoever made it (your app, the user in a Tangle app, another app, a platform sync). Each change carries by, the client_id of the app whose write it was, so you can skip your own; it is absent when the user, a Tangle app or a sync made it. Adding or removing an entry is a change to the list, not to the item.
A list you create on a platform gets a new id when Tangle's sync creates it there: the change feed then reports the old lst_ id as deleted and the new one as changed.
Event stream (any token)
GET /omc/v1/events is a Server-Sent Events stream: ready with the current cursor, then changed with the new cursor whenever the collection changes, and a comment every 15 seconds to keep the connection open. After an hour it sends reconnect and closes; reconnect with the Last-Event-ID header and you get a changed at once if you missed anything. At most 10 open streams per user. EventSource can't send an Authorization header, so read it with fetch:
const res = await fetch("https://api.tangle.music/omc/v1/events", { headers: { Authorization: `Bearer ${process.env.TANGLE_API_TOKEN}` } });
const reader = res.body!.pipeThrough(new TextDecoderStream()).getReader();
let buffer = "";
for (;;) {
const { value, done } = await reader.read();
if (done) break; // reconnect
buffer += value;
let end;
while ((end = buffer.indexOf("\n\n")) >= 0) {
const block = buffer.slice(0, end);
buffer = buffer.slice(end + 2);
if (/^event: changed$/m.test(block)) await catchUp(); // GET /omc/v1/changes from your stored cursor
}
}Webhooks (Tangle only, registered apps)
Webhooks are Tangle's own feature, not part of OMC: an app that must work with any OMC home follows /changes and /events instead. Give your app a webhook URL in the dashboard (public https:// only). Tangle then POSTs small JSON events to it:
collection.changed: a user who connected your app changed their collection.data.cursoris the newest position; call/omc/v1/changeswith your stored cursor and that user's token. Several changes in quick succession arrive as one event.authorization.revoked: the user disconnected your app; delete their tokens.ping: sent by Send test event in the dashboard.
{ "id": "evt_…", "type": "collection.changed", "created_at": "2026-10-08T10:15:02Z", "user_id": "usr_…", "data": { "cursor": "chg_2kz" } }Events never contain collection data. Delivery is at least once: deduplicate by the Tangle-Event-Id header. Answer 2xx within 10 seconds; anything else is retried with backoff for about a day. Redirects are not followed.
Verify every webhook. The Tangle-Signature header is t=<unix seconds>,v1=<hex HMAC-SHA256> of "<t>.<raw body>", keyed with your app's signing secret (whsec_…, in the dashboard). Reject events older than five minutes.
import { createHmac, timingSafeEqual } from "node:crypto";
export function verifyTangleWebhook(rawBody: string, header: string, secret: string, toleranceSeconds = 300): boolean {
const parts = Object.fromEntries(header.split(",").map((kv) => kv.split("=") as [string, string]));
const t = Number(parts.t);
if (!Number.isFinite(t) || Math.abs(Date.now() / 1000 - t) > toleranceSeconds) return false;
const expected = createHmac("sha256", secret).update(`${t}.${rawBody}`).digest();
const given = Buffer.from(parts.v1 ?? "", "hex");
return given.length === expected.length && timingSafeEqual(given, expected);
}import hashlib, hmac, time
def verify_tangle_webhook(raw_body: bytes, header: str, secret: str, tolerance: int = 300) -> bool:
parts = dict(p.split("=", 1) for p in header.split(","))
t = int(parts.get("t", "0"))
if abs(time.time() - t) > tolerance:
return False
expected = hmac.new(secret.encode(), f"{t}.".encode() + raw_body, hashlib.sha256).hexdigest()
return hmac.compare_digest(expected, parts.get("v1", ""))Errors
Errors have Access's shape (https://www.tangle.music/omc/modules/access/#errors), with a request_id to quote and param naming the field when there is one. Branch on code, never on message. Every response carries X-Request-Id. Beyond Access's codes, Tangle answers:
| Status | code | What to do |
|---|---|---|
| 400 | missing_version | A request naming a position needs the list's version. |
| 403 | backup_and_restore_required | The user must turn on Backup and restore in Tangle. |
| 403 | namespace_not_yours | Another app owns that ext namespace (param names it). Use your own. |
| 409 | version_conflict | The list changed since the version you sent. Read it again (the current one is in ETag) and retry. |
| 409 | list_read_only, list_not_reorderable, list_not_renamable, list_not_deletable, list_pending_deletion, list_combined, list_creation_unsupported, source_unavailable | The list or platform can't take this change (check can first). Tell the user why. |
| 413 | data_too_large | Your namespace or a module's data is over its budget in /me limits. Nothing was cut. |
| 422 | list_not_writable | Folders, colours and smart lists are read only for apps. |
| 422 | missing_url | An item needs a copy on a platform, a via with a url, or identifiers. |
| 429 | rate_limited, too_many_streams | Wait Retry-After seconds. |
The OAuth endpoints answer with the standard {"error": "invalid_grant", "error_description": "…"} shape (RFC 6749).
Rate limits
| Limit | Per minute |
|---|---|
| Reads per token | 600 |
| Writes per token | 120 |
| All requests per user (every token and app together) | 1,200 |
| All requests from one IP address | 3,000 |
GET /omc/v1/collection | counts as 60 reads, for the token and the user; one at a time per user |
| List deletions per user and app | 10 per hour |
| Items removed from lists per user and app | 2,000 per hour |
POST /omc/v1/items per user and app (shared with Captured) | 300 per hour |
POST /omc/v1/import | 20 per hour, up to 20,000 records each |
/oauth/token per app and IP | 30 |
Every response says where you stand: RateLimit-Limit, RateLimit-Remaining, RateLimit-Reset (seconds until the window resets).
Recipes
Tag every song by an artist
H="Authorization: Bearer $TANGLE_API_TOKEN"
TAG=$(curl -s https://api.tangle.music/omc/v1/lists -H "$H" -H "Content-Type: application/json" -d '{"name":"Ben Klock","ordered":false}' | jq -r .id)
curl -s https://api.tangle.music/omc/v1/items -H "$H" -G --data-urlencode "q=Ben Klock" --data-urlencode limit=100 \
| jq '{items: [.data[] | select(.kind == "recording") | .id]}' \
| curl -s https://api.tangle.music/omc/v1/lists/$TAG/entries -H "$H" -H "Content-Type: application/json" -d @-Build a playlist from a list of songs (Python)
import os, requests
API = "https://api.tangle.music/omc/v1"
s = requests.Session()
s.headers["Authorization"] = f"Bearer {os.environ['TANGLE_API_TOKEN']}"
def find(query):
r = s.get(f"{API}/items", params={"q": query, "limit": 5}); r.raise_for_status()
return [i for i in r.json()["data"] if i["kind"] == "recording"]
wanted = ["Strobe deadmau5", "Gravity Ben Klock"]
ids, missing = [], []
for q in wanted:
hits = find(q)
(ids.append(hits[0]["id"]) if hits else missing.append(q))
playlist = s.post(f"{API}/lists", json={"name": "From my notes", "ordered": True}).json()
for i in range(0, len(ids), 100):
s.post(f"{API}/lists/{playlist['id']}/entries", json={"items": ids[i:i + 100]}).raise_for_status()
print(f"Added {len(ids)}; not in the collection: {missing}")Keep a local copy (TypeScript)
const API = "https://api.tangle.music/omc/v1";
const headers = { Authorization: `Bearer ${process.env.TANGLE_API_TOKEN}` };
const paths = { item: "items", list: "lists", source: "sources", record: "records" } as const;
async function download(store: Map<string, unknown>): Promise<string> {
const res = await fetch(`${API}/collection`, { headers });
for (const line of (await res.text()).split("\n")) {
if (!line) continue;
const record = JSON.parse(line);
if (record.type !== "omc") store.set(`${record.type}:${record.id}`, record);
}
return res.headers.get("OMC-Cursor")!;
}
async function catchUp(store: Map<string, unknown>, cursor: string): Promise<string> {
for (;;) {
const page = await (await fetch(`${API}/changes?cursor=${cursor}`, { headers })).json();
for (const c of page.changes) {
const key = `${c.type}:${c.id}`;
if (c.op === "delete") store.delete(key);
else store.set(key, await (await fetch(`${API}/${paths[c.type as keyof typeof paths]}/${c.id}`, { headers })).json());
}
cursor = page.cursor;
if (!page.has_more) return cursor;
}
}An app that only adds
With library:add alone, an app (one that follows labels, one that reads music links from email) adds a recording and puts it in a list the user picked, without seeing the rest of the collection:
curl -s "https://api.tangle.music/omc/v1/items?list=lst_…" -H "Authorization: Bearer $TOKEN" -H "Content-Type: application/json" -d '{
"kind": "recording", "title": "Night Drive", "artists": ["Mira Holt"],
"copies": [{ "platform": "bandcamp", "ref": "night-drive", "url": "https://mira.bandcamp.com/track/night-drive", "ownership": "none" }]
}'
# {"result": "held", "new": true, "list": "not_added"} while it waits in Captured,
# or {"id": "trk_…", "result": "existing", "list": "added"}A record shop's page isn't a copy: send it as "via": { "name": "Hard Wax", "url": "https://…" }, with identifiers (ids) when you have them.
Move a song in a playlist
H="Authorization: Bearer $TANGLE_API_TOKEN"
V=$(curl -s https://api.tangle.music/omc/v1/lists/$PLAYLIST -H "$H" | jq -r .version)
curl -s https://api.tangle.music/omc/v1/lists/$PLAYLIST/entries/move -H "$H" -H "Content-Type: application/json" \
-d "$(jq -n --arg v "$V" '{from: 7, before: 0, version: $v}')"
# 409 version_conflict: someone changed the list since you read it. Read it again and retry.Send what was played
curl -s https://api.tangle.music/omc/v1/listens -H "Authorization: Bearer $TOKEN" -H "Content-Type: application/json" -d '{
"listen_type": "single",
"payload": [{ "listened_at": 1793001600,
"track_metadata": { "artist_name": "Mira Holt", "track_name": "Night Drive",
"additional_info": { "isrc": "QZES72600001", "submission_client": "Example Player" } } }]
}'The matched item's play_count and last_played_at change, and /changes reports it.
Limits and guarantees
Writes are atomic per request: a request that fails changes nothing.
A response to a write already reflects the change, and its
OMC-Cursoris the position just past it.Tangle does not store audio. Copies link to the platforms.
The API is versioned in the path. Within
/omc/v1, fields are only ever added, never renamed or removed; keep fields you don't know, as OMC requires.
More
Human-friendly docs: https://www.tangle.music/developers/docs/
Open Music Collection Core 1.0: https://www.tangle.music/omc/spec/
Open Music Collection Access: https://www.tangle.music/omc/modules/access/
What belongs where (OMC, a home's choice, or one home's own): https://www.tangle.music/omc/spec/where/
Dashboard (tokens, apps, connected apps; Tangle only): https://www.tangle.music/developers/dashboard/
API terms: https://www.tangle.music/developers/terms/
