The API
A public commons of CLAP audio embeddings. Reads need no key, no account and no
registration. Everything below works from a terminal right now; the schema is at
/docs, and the contract as a package is
clapback-client.
What is in here
25,886 CLAP embeddings — 512-dimensional unit vectors describing what a recording sounds like. Computing one costs seconds of CPU and a 600 MB model. Comparing two is a dot product. That asymmetry is the whole reason this exists.
Recordings are keyed by the SHA256 of an AcoustID fingerprint, so the corpus never learns a title, an artist or a filename. A hash names a recording only to someone who already holds it, so a client may also claim a MusicBrainz recording id for a row — counted per client, never verified, and shown beside every result that has one. 23,196 rows are named so far. The rest are hashes, and a result that is one says so rather than handing you a blank.
All of it is CC0 — public domain, as MusicBrainz's core data and AcousticBrainz's were. Contributing dedicates what you send.
For tool authors
You do not have to speak this API by hand. The contract a tool follows — fingerprint and
hash canonically, look up before contributing, send client_id and
pipeline_version, back off when told to — is published as
clapback-client, with
no dependency beyond the standard library. A tool that already has an embedder contributes
under its own pipeline identity without installing ONNX Runtime.
pip install clapback-client
from clapback_client import Corpus, fingerprint_file, hash_fingerprint
key = hash_fingerprint(fingerprint_file("track.flac"))
row = Corpus().lookup(key, pipeline_version) # None on a miss — then embed and contribute
If you run beets, beets-clapback
is that contract already wired to your library — absubmit reborn; if you run
Picard, the Picard
plugin is the same contract carried in a zip, since a bundled Picard cannot install
packages. The reference
client, clapback-cli,
exercises all of it end to end. Why the way in is a package rather than an application is
ADR-0011.
Get an embedding
curl "https://clapback.seethroughlab.com/v1/embeddings/{fingerprint_hash}?pipeline_version=laion%2Fclap-htsat-unfused%2Bfrontend1%2Bartifact1%2Bpool1%2Bfp32"
Returns the vector, what produced it, how many submissions it has had, its
recording_mbid when anyone has claimed one, and — when it has — how many
independent installs confirmed or contradicted the recording's vector, across every key it
is held under (below, "How close is close"). 404 if the corpus has
not seen that hash from that pipeline — which is not quite "that recording".
The fingerprint string depends on the path that computed it. Measured
2026-09-16 on 56 FLACs: the fpcalc binary and pyacoustid's library path agree
on 24, and on 10 of the 24 CD-quality ones; two official fpcalc builds from
different ffmpeg generations agree on 37. The rest differ by a few bits, which AcoustID's
matcher absorbs and a SHA256 cannot. The key is exact within one path and may differ
across two, so a miss here does not mean the corpus lacks the recording. A tool that holds
a MusicBrainz recording id should ask by it — /v1/recordings/{mbid}, below —
which is the same on every path. The measurement and the decision are
ADR-0019.
Ask by pipeline_version if you intend to use the vector. It is
half the corpus key, so it is the only parameter that selects exactly one row and the only
one that tells you the answer is comparable with vectors you computed yourself.
analysis_version and clap_model_version are still accepted and are
filters on metadata — ask by those alone and you can be handed a perfectly valid vector from
a pipeline you cannot use.
Escape the +. A pipeline identity is +-joined, and
+ in a query string means a space — so an unescaped identity matches nothing and
404s as though the recording were missing. Use %2B. The server maps spaces back
for you, since a space cannot occur in an identity, but do not rely on that in code you
expect other people to read.
Look up a library
curl -X POST https://clapback.seethroughlab.com/v1/embeddings/lookup \
-H 'Content-Type: application/json' \
-d '{"keys": [{"fingerprint_hash": "..."}, {"recording_mbid": "1c6da765-da50-476b-a000-61e7cf45ded8"}],
"pipeline_version": "laion/clap-htsat-unfused+frontend1+artifact1+pool1+fp32",
"vectors": false}'
Up to 100 keys — hashes, MusicBrainz recording ids and acoustid_track_ids
mixed — answered in order: the row if
held, null if not, each entry naming the key it answered. A held entry is
exactly what the single lookup returns for that key; "vectors": false leaves
out the 512 floats, for a tool asking "which of these do you hold, and what are they
called?" — under 40 KB per hundred rather than ~670 KB. A recording id answers
with the row most clients have claimed under it, whichever fingerprinting path keyed it.
More than 100 is a 422, not a partial answer.
The lookup limit is counted per key, not per request. A batch of a hundred
spends a hundred of the 300 per minute, so a whole library is a number of requests you can
reason about; the client's lookup_many chunks, honours Retry-After
and continues
(ADR-0015).
For the whole corpus rather than your library's slice of it, take the
export instead.
Compute one yourself
The reference implementation is published, so a miss costs you a dependency rather than a
research project. No torch, no transformers — it runs on ONNX
Runtime.
pip install clapback-embed
from clapback_embed import embed_file, embed_text
vector = embed_file("track.flac") # 512 floats, unit length
query = embed_text("dreamy ambient with piano") # the same space
Two machines running it produce the same vector. That is the point: a
corpus built from many contributors can only tell disagreement about audio from
disagreement about implementations if there is one implementation. Everything that
could vary is pinned and versioned, and PIPELINE_VERSION is the identity of all
of it together.
A tool with its own pipeline contributes under its own identity — a
string of five +-separated tokens: the checkpoint as its publisher names it,
the front-end, the windowing rule, the pooling rule, and the precision of what was
sent (a vector quantised for your own storage and dequantised on the way out is
int8, not fp32). Ours reads
laion/clap-htsat-unfused+frontend1+artifact1+pool1+fp32. The corpus does not
parse it; it compares the whole string, so two tools that agree on every token share a
population and two that differ in one do not. What each token means is declared once, in
your README. The convention, with worked examples, is in the
client package's README
(ADR-0014).
curl https://clapback.seethroughlab.com/v1/pipelines
Every identity the corpus holds rows under, most populated first, with its row count, how many rows are named, and the first and latest contribution. Ask here before choosing an identity: if yours already exists, contributing under it joins that population; if not, yours starts one. Today there is one.
How close is close
Measured, not asserted — cosine distance from 1.0:
| Same audio, two architectures (arm64 vs x86_64) | 6.6e-11 |
| CPU vs CUDA | 6.6e-14 |
| Two different rips of one recording | 3e-04 – 3e-03 |
| Genuinely different music | far below either |
Honest computation agrees about a thousand times more tightly than two rips of the same CD differ. That gap is what makes near-duplicate detection across formats and masters work, and what a consensus scheme can eventually stand on.
And now it is stood on, per recording. Every result that names a recording
carries recording_confirmations and recording_contradictions: how
many independent installs sent a vector for that recording, under that pipeline, inside the
identical band of 0.999999, and how many sent one outside it. Counted across
every key the recording is held under — two fingerprinting paths that keyed one file twice
are one population, joined by the recording id — by distinct client_id, never
counting a client for agreeing with its own row, and never counting a submission that sent
no id. A contradiction is served, not averaged away: a different rip of the same recording
lands outside the band, and that is worth seeing. There is no verdict and no
confirmed: true; the corpus reports what happened and the caller decides
(ADR-0008,
ADR-0019).
Ask what a vector is near
curl -X POST https://clapback.seethroughlab.com/v1/similar \
-H 'Content-Type: application/json' \
-d '{"embedding": [ ...512 floats... ], "limit": 20,
"pipeline_version": "laion/clap-htsat-unfused+frontend1+artifact1+pool1+fp32"}'
Returns the nearest recordings by cosine similarity, each labelled with the pipeline that produced it — vectors from two pipelines are not comparable, so a result that did not say which one it came from would invite exactly that mistake. Backed by an HNSW index: 3 ms across the corpus.
analysis_version is a contributor's own counter and only approximates
comparability. pipeline_version is the real filter — every stored row has one,
and results from two pipelines are not comparable with each other however close their
cosine similarity looks.
Each neighbour carries a recording_mbid when anyone has claimed one,
and is a bare hash when nobody has. A named neighbour is a MusicBrainz recording
you can look up without holding it; recording_claims says how many distinct
clients stand behind the name. An unnamed one is still a hash — shown as one, because a
result you cannot resolve should say so. The vector can come from audio or from text:
embed_text("distant piano under tape hiss") is a query in the same space.
One neighbour per recording. A recording two installs keyed differently —
two fingerprinting paths, or two rips — is two rows a fraction apart in this space, and
would otherwise be two adjacent results you could not tell from two recordings. Named rows
sharing a recording under one pipeline are folded into the nearest of them, and
collapsed says how many were; unnamed rows are never folded, because nothing
says two of them are one recording
(ADR-0019
point 4). searched still counts what was ranked.
Name a recording
curl -X POST https://clapback.seethroughlab.com/v1/recordings/claims \
-H 'Content-Type: application/json' \
-d '{"fingerprint_hash": "...", "recording_mbid": "1c6da765-da50-476b-a000-61e7cf45ded8",
"client_id": "any-opaque-string-your-install-keeps"}'
Attaches a MusicBrainz recording id to a row the corpus already holds,
without touching its contributor count. This exists so nobody re-sends a vector to name it:
a repeat contribution is recorded as agreement, and a library naming itself would read as a
library agreeing with itself thousands of times. Idempotent; 404 if the hash is
not held. A contribution can carry recording_mbid directly, which is the same
claim made at the same time.
A claim is counted, not trusted. The corpus never resolves an id against MusicBrainz; a row's recording is whichever id the most distinct clients assert, and the count travels with it so a reader can see one client's word for what it is (ADR-0012).
A second kind of name: the AcoustID track id. Send
acoustid_track_id beside the MBID, or alone, on a contribution or a claim. It
is what AcoustID's own matcher assigns to a cluster of near-identical fingerprints — the
same across decoders, fpcalc versions and lossy re-encodes of one rip — so it
joins two keys of one recording where a tool has no MusicBrainz match. Admitted, not
required: it needs AcoustID's service and coverage, which the offline key does not. Reads
carry it as acoustid_track_id and acoustid_claims; the two kinds
are never mixed, and ?type=acoustid_track asks the recording route by it
(ADR-0019
point 6). As with the MBID: claim only what you established yourself.
curl https://clapback.seethroughlab.com/v1/recordings/1c6da765-da50-476b-a000-61e7cf45ded8
Goes the other way: every row claimed under that recording, with its vector, most-claimed first. What does this recording sound like — without holding it. It is also the lookup that does not depend on the fingerprinting path: two rows under one pipeline here are one file keyed twice, and a tool that holds the id finds both.
curl https://clapback.seethroughlab.com/v1/recordings/by-hash/{fingerprint_hash}
Which recording is this? Every id claimed for a row, most-supported first,
each with how many distinct installs asserted it, no vector — the dissent that a lookup's
single recording_mbid summarises away. In order: it works only for recordings
the corpus holds; the id is the one most independent installs asserted and the count is how
many; it is not verified and it is not AcoustID; a count of 1 means one install said so.
AcoustID answers this by fuzzy match against a database built for it — this is an
exact-hash shortcut that is silent when the corpus has not seen the recording, never a
replacement. And never send an id you learned here back as your own claim:
a tool that does counts itself as independent confirmation of what it copied, and the
server cannot tell
(ADR-0018).
Contribute one
curl -X POST https://clapback.seethroughlab.com/v1/embeddings \
-H 'Content-Type: application/json' \
-d '{"fingerprint_hash": "...", "embedding": [ ...512 floats... ],
"analysis_version": 7, "clap_model_version": "laion/clap-htsat-unfused",
"client_id": "any-opaque-string-your-install-keeps",
"pipeline_version": "laion/clap-htsat-unfused+frontend1+artifact1+pool1+fp32",
"recording_mbid": "1c6da765-da50-476b-a000-61e7cf45ded8"}'
First write wins. A submission for a recording the corpus already holds is not discarded — it is compared against the stored vector and the similarity recorded, which is how this finds out whether independent machines actually agree.
pipeline_version is required. It is
clapback_embed.PIPELINE_VERSION, and it is the only field that says whether
your vector can be compared with anyone else's: the checkpoint alone does not, since
windowing or pooling can move every vector without changing it. It is
half the corpus key,
so a submission without one has no key to be stored under. client_id beside it
stays optional, and the contrast is deliberate: an unattributed vector is still evidence,
an unidentified one cannot be compared with anything.
The server believes what you send. That catches the forgotten bump and the stale build, which are the realistic failures; it is not a defence against a contributor who lies, and nothing here should be read as if it were.
curl -X POST https://clapback.seethroughlab.com/v1/embeddings/batch \
-H 'Content-Type: application/json' \
-d '{"contributions": [ { ...exactly the body above... }, ... ]}'
A library, by the hundred. Up to 100 contributions, each exactly the
single body, answered one result per row in order — created or
confirmed with its count, or refused with the code and detail the
row would have got alone. Every guarantee runs per row in the same code the single
endpoint is: agreement recorded per row, the ceiling and the quota checked per row, so a
batch that crosses either is accepted up to the line and refused past it. Not
atomic, on purpose — 97 created, 2 confirmed and 1 refused is 99 rows contributed,
and you retry a refused row on its own result. client_id is required on every
row here: a vector nobody can confirm is admissible one at a time and not by the hundred.
600 rows per minute per address, counted per row; a body over 10 MB is a
413
(ADR-0016).
One identifier may write 50,000 rows in a rolling 24 hours — created or
confirmed — which is ten percent of the corpus ceiling, so no single install can reach it
in a day. Past that, each write is a 429 with Retry-After, and
lookups are unaffected
(ADR-0004
point 9's third bound, built 2026-09-16).
Endpoints
| Method | Path | Status |
|---|---|---|
GET | /v1/embeddings/{hash} | open |
POST | /v1/embeddings/lookup | open · a read that needs a body · limit per key |
POST | /v1/similar | open · a read that needs a body |
GET | /v1/recordings/{mbid} | open |
GET | /v1/recordings/by-hash/{hash} | open |
GET | /v1/pipelines | open |
POST | /v1/recordings/claims | open · a write |
GET | /v1/features/{hash} | open · legacy |
GET | /v1/analysis-detail/{hash} | open · legacy |
GET | /health, /health/db | open |
POST | /v1/embeddings | open |
POST | /v1/embeddings/batch | open · limit per row · client_id required |
Contribution is open. It was refused until the corpus had a way to retract a
recording, which
ADR-0004
makes a precondition rather than a follow-up; that path now exists. Send a
client_id — any opaque string your installation keeps — or your submission is
accepted and can never count toward independent agreement. The features endpoints are
legacy: they still serve 77,770 rows and are not where this is
going, because bpm and key are claims about the world that consensus cannot verify — which
is what stopped AcousticBrainz.
Rate limits
300 per minute for reads and 30 per minute for writes, per address and per
route — a claim is a write. Measured 2026-09-16: a route with the key in its path, such as
/v1/embeddings/{hash}, has one window per key, so the read limit bounds
repeats of one key and a scan of many is bounded by round trips instead; the batch lookup
counts its 300 per key, and the batch contribute its 600 per row. Pace a single-row
backfill under the write limit, not at it: the client retries three times and then gives
up, and a run at the limit loses submissions silently; the batch calls honour
Retry-After instead. One identifier may write 50,000 rows a day. There is no
key to obtain and no plan to buy.
If you need more, the corpus is a few hundred megabytes and open source — run your own.