● v1 live sandbox: synthetic data

AMFTF Member API

Screen your catalogue against AMFTF's documented music-fraud records — fake and shell labels, stolen or re-delivered releases, AI-generated catalogues, impersonating artists and TikTok-store-only infringements — and act on matches inside your own workflow.

Your catalogue stays with you. Screening runs in one direction: you query AMFTF. We do not store, log or retain the identifiers you send — only request counts, for rate limiting. For catalogue-wide sweeps there is a daily bulk file you run locally, so millions of tracks can be checked without a single identifier reaching us.

Introduction

The Anti Music Fraud Task Force (AMFTF) documents music-distribution fraud: shell labels registered to publish other people's recordings, releases re-delivered under invented artist names, AI-generated catalogue published at volume, and sounds delivered to the TikTok store only, where they are invisible to any check that starts from a streaming service.

This API exists so that a distributor can find that material in its own catalogue — before it earns, before a rights holder complains, and without either side handing over data it should not have to.

Screen at ingest

Check a new delivery against documented records and the pre-delivery watchlist in one call.

Sweep the catalogue

Download a daily list — or a 117 KB filter — and screen millions of tracks locally.

Act with evidence

Every record carries a case id, an evidence link, and where audio analysis was used, a probability.

How screening works

There are two paths. Most members use both.

PathUse whenLoad on youLoad on AMFTF
A. Bulk file, screened locally
/v1/screening-list
Whole catalogue (millions of tracks)One download per dayNone
B. Batch check
/v1/check
Confirming hits · new deliveries · ad-hoc lookupsOne request per 1,000 idsSmall

Recommended shape: A for the nightly sweep, B to confirm the hits it produces and to screen new deliveries at ingest. The bulk filter is probabilistic, so every hit from path A should be confirmed through path B before anyone acts on it.

Sandbox & live

Every response carries a mode field, so you always know which dataset answered.

ModeDataSigning
sandboxEntirely synthetic. Invented ISRCs (XX9AA…), UPCs, label names (EXAMPLE-SHELL-LLC) and ids. Nothing here is a real case and nothing is an allegation about any artist, label or distributor.May be relaxed on request for first contact
liveDocumented AMFTF case records.Always required

Build against sandbox first. The shapes are identical, so nothing changes when you move to a live key — but a sandbox key cannot leak a real case, and no real identifier ever enters your logs, tickets or test fixtures during integration. That is the point of it.


Base URL & versioning

https://api.amftf.org/v1

TLS 1.2 or 1.3 only. HTTP is redirected to HTTPS; plain HTTP is never served.

  • Path-versioned. Breaking changes get a new path (/v2), never a silent change under /v1.
  • Additive within a version. New fields may appear at any time — parse defensively and ignore what you do not recognise.
  • 90 days' written notice before any breaking change, to every member with an active key.

Get a key

Keys are issued per member. Write to sher@pumpa.org.pk with your company name and a technical contact; sandbox keys are issued the same day.

You receive two values:

ValueLooks likeUsed for
keyamftf_sandbox_… / amftf_live_…Sent in the X-AMFTF-Key header. Identifies you.
secretrandom stringNever sent. Used to sign requests and to verify bulk files.

The secret is never transmitted. It signs requests; it does not travel in them. We send key and secret through separate channels and ask you to store the secret in a secrets manager, not in source control. If either is exposed, tell us and we rotate — rotation takes minutes and old keys can be disabled without downtime.

Your first request

The status endpoint needs no key. Start here to confirm connectivity:

curl -s https://api.amftf.org/v1/health

{"ok":true,"mode":"sandbox","list_version":"2026-08-07",
 "generated_at":"2026-08-07T00:00:00Z"}

Then a real screening call. This example uses a sandbox identifier, so you can run it verbatim once you have a sandbox key:

KEY="amftf_sandbox_…"
SECRET="…"
BODY='{"isrc":["XX9AA2600100"]}'
TS=$(date +%s)
SIG=$(printf '%s.%s' "$TS" "$BODY" \
  | openssl dgst -sha256 -hmac "$SECRET" -hex | sed 's/^.* //')

curl -s https://api.amftf.org/v1/check \
  -H "X-AMFTF-Key: $KEY" \
  -H "X-AMFTF-Timestamp: $TS" \
  -H "X-AMFTF-Signature: sha256=$SIG" \
  -H 'Content-Type: application/json' \
  -d "$BODY"

Authentication

Three headers on every authenticated request:

HeaderValue
X-AMFTF-KeyYour key. Required.
X-AMFTF-TimestampUnix seconds. Must be within 300 seconds of our clock.
X-AMFTF-Signaturesha256=<hex> — see below.

Optionally, a key can be locked to an IP allow-list. Tell us the egress addresses and we restrict the key to them; a request from anywhere else is refused with 403 ip_not_allowed, even with the correct signature.

Request signing

signature = "sha256=" + HMAC_SHA256(secret, timestamp + "." + raw_body)

# raw_body is the exact bytes of the request body.
# For GET requests, raw_body is empty — sign timestamp + "." only.

Two mistakes account for nearly every signature failure, so they are worth stating plainly:

  1. Re-serialising the body after signing. If your HTTP client re-encodes JSON — different key order, different whitespace — the bytes on the wire no longer match what you signed. Serialise once, sign those bytes, send those bytes.
  2. A drifting clock. The 300-second window exists to stop a captured request being replayed later. If your server's clock is off by more than five minutes, every request fails with stale_timestamp. Run NTP.

The signature covers the body, so a proxy that modifies the payload in transit invalidates it — which is the intended behaviour, not an inconvenience.

Auth failures

StatusCodeMeaning
401no_keyX-AMFTF-Key header missing.
401bad_keyKey not recognised, or disabled.
401signature_requiredThis key requires signing; timestamp or signature absent.
401bad_timestampTimestamp is not unix seconds.
401stale_timestampOutside the 300-second window (replay protection).
401bad_signatureSignature does not match the body received.
403ip_not_allowedRequest IP is not on this key's allow-list.

We deliberately do not distinguish "unknown key" from "disabled key" in the response — an attacker should not learn which keys exist.


Identifier normalisation

Both sides must normalise identically. If they don't, screening does not error — it quietly misses, which is worse. These are the exact rules we apply, on our side and in the bulk filter:

TypeRuleExample
ISRC, Spotify ids, TikTok sound idUpper-case; keep A–Z0–9 onlyxx-9aa-26-00100XX9AA2600100
UPC / EAN / GTINDigits only, left-pad to 14 (GTIN-14)000000000001000000000000010
UPC — rejectedFewer than 8 digits, or all zeros → not an identifier, ignored0000000000000 → ignored
Names (advisory only)NFKD, strip diacritics, lower-case, non-alphanumerics → single spacesÉxemple Artist!exemple artist

The UPC rule matters more than it looks. The same release is written as UPC-12, EAN-13 and GTIN-14 across systems; without a canonical form one release looks like three different products and a real match slips through.

Exact vs advisory matches

Every match carries a match field. Treat the two kinds differently — this is the single most important thing to get right in your integration.

matchBased onHow to use it
exactISRC · UPC · Spotify album/artist id · TikTok sound idSafe to route straight into your takedown or hold queue.
advisoryArtist name · title+artist pair · name similarityHuman review required. Also carries similarity (0–1) and review.

Do not auto-action advisory matches. Names collide legitimately — two unrelated artists can share one, and transliteration produces near-identical spellings for entirely different people. Names are supplied so a reviewer can compare in seconds, not so a script can act. We would much rather you tell us a name match was wrong than have a legitimate artist suspended over it.

The similarity floor is 0.86; below that we do not report a name match at all. That threshold is a judgement, not a law of nature: lower, and the noise makes reviewers stop looking; a list nobody reads is worthless.

Findings & status

Two fields, because they answer different questions: what AMFTF has determined, and what has happened since.

amftf_finding — our determination

ValueMeaning
documentedAMFTF has investigated this release and assessed it as fraudulent. Evidence is on file and a case is open. This is a conclusion, not an open question.

status — where the case stands with the distributor

ValueMeaning
suspectedDocumented by AMFTF and awaiting the distributor's own verification. It is on your side to check and act — not on ours to decide.
verifiedThe distributor or platform checked it and agrees with our finding.
taken_downRemoved following an AMFTF report.
dismissedEvidence cleared it. We retire the flag and keep the record so the same release is not re-reported.

How to read suspected. It marks a case AMFTF considers proven on the evidence we hold, handed to you for verification against your own rights and delivery records — things only you can see. The conservative wording is deliberate: AMFTF does not make legal determinations about third parties, and a release can have an explanation that exists only in your contracts. The word is careful; the finding behind it is not tentative.

Every record carries case and evidence, and where audio analysis was used, ai_likelihood — so your reviewer sees the basis, not just a flag.

Distributor disclosure

This API never returns a distributor name. Every record carries an object instead:

{"known": true,  "disclosed": false}                     // we hold it, we don't publish it
{"known": false, "reason": "tiktok_store_only"}      // we do not know it
{"known": false, "reason": "not_recorded"}           // absence, stated as absence
CaseValueWhy
Release visible on a streaming serviceknown: true
disclosed: false
We hold delivery metadata, but naming the distributor of a release means disclosing one member's data to another. Cross-distributor matters are handled by AMFTF directly.
TikTok-store-only deliveryknown: false
reason: tiktok_store_only
Not published anywhere we can see it. We do not infer it.
Streaming release, metadata gave us nothingknown: false
reason: not_recorded
Absence, stated as absence.

known: false means we do not know. It never means "no distributor" or "an unidentified bad distributor". We would rather hand you a gap than a guess: a wrong distributor attribution inside a fraud dataset damages a company that did nothing, and it would end this association's usefulness the first time it happened.

You do not need this field to act. If an identifier we return matches your catalogue, it is yours to review; if it doesn't, it isn't your problem to solve. That test is more reliable than anything we could supply.

Data retention

  • Identifiers submitted to /v1/check are matched in memory and discarded. They are not written to disk, not logged, and not used to build any profile of your catalogue.
  • Access logs record method, path, status, response size and duration. Identifiers travel in request bodies only — this API never accepts them in a query string, precisely so they cannot end up in a log line, a proxy cache, or a browser history.
  • We retain per-key request counts and timestamps, for rate limiting.
  • Nothing a member submits is shared with another member, in any form.
  • The service reads a read-only snapshot of AMFTF case data. It holds no credentials for, and no route to, any AMFTF database.

Batch check

POST/v1/check

Screen up to 1,000 identifiers per request. Accepts several identifier types in one call. Non-matching identifiers are absent from the response — and are not retained.

Request body

FieldTypeMatch
isrcstring[]exact
upcstring[]exact
spotify_album_idstring[]exact
spotify_artist_idstring[]exact
tiktok_sound_idstring[]exact
artist_namestring[]advisory
pairs{title, artist}[]advisory

All fields are optional; supply at least one. The 1,000 limit counts every element across every field, including pairs.

{
  "isrc": ["XX9AA2600100", "XX9AA2600110"],
  "upc": ["0000000000010"],
  "spotify_artist_id": ["SANDBOXARTIST000000001"],
  "tiktok_sound_id": ["9000000000000000001"],
  "artist_name": ["Example Persona"],
  "pairs": [{ "title": "Example Title", "artist": "Example Persona" }]
}

Response fields

FieldTypeNotes
checkedobjectHow many identifiers we received, per field. Use it to confirm nothing was dropped in transit.
matches[]arrayEmpty array when nothing matched. Absence of a match is not proof of legitimacy — only that it is not in our records.
matchstringexact or advisory. See above.
similaritynumber0–1. Advisory matches only.
kindstringSee fraud kinds.
backdatedboolRelease date differs from upload date — the date was presented as earlier than it is. Evidence-grade.
ai_likelihoodnumber0–100. An audio-analysis probability, not a legal determination.
tiktok_creations / creationsnumberVideo creations. Estimated — see the note under fraud kinds.
distributorobjectNever a name. See distributor disclosure.
case, evidencestringCase id and the evidence record behind it.
delivery_typestringTikTok records only: redelivery or new_delivery.

Screening list

GET/v1/screening-list

The full documented set as a downloadable file — dated, signed, and cheap to serve. Regenerated daily.

ParameterValuesNotes
formatjsonl (default) · csv · bloom
sinceYYYY-MM-DDOnly records with updated_at >= since. Incremental sync. Ignored for bloom.

Response headers

HeaderMeaning
x-amftf-list-versionStable, quotable date of this list. Record it if you need to show, for compliance, which version a catalogue was screened against.
x-amftf-list-signaturesha256=<hex> — HMAC over the body, using your own secret. Verify it: that proves the file is ours and was not altered in transit.
x-amftf-signature-basisThe exact formula, so you never have to guess: HMAC_SHA256(your_secret, list_version + '.' + body)
GET /v1/screening-list?format=jsonl&since=2026-08-01

{"isrc":"XX9AA2600100","upc":"0000000000010","status":"suspected","amftf_finding":"documented","kind":"ai_generated_catalogue","case":"AMFTF-EXAMPLE-0001","updated_at":"2026-08-01"}
{"isrc":null,"upc":"0000000000024","status":"taken_down","amftf_finding":"documented","kind":"fake_label_release","case":"AMFTF-EXAMPLE-0002","updated_at":"2026-08-02"}
{"isrc":"XX9AA2690001","upc":"0000000090001","tiktok_sound_id":"9000000000000000001","status":"suspected","kind":"tiktok_only_redelivery","case":"AMFTF-EXAMPLE-0002","updated_at":"2026-08-05"}

Use compare_digest (or your language's constant-time equivalent) rather than == when comparing signatures.

Watchlist

GET/v1/watchlist

Pre-delivery screening: catch it before ingest rather than after it is live and earning. This is the highest-value endpoint for a distributor, and the cheapest to wire in.

{
  "list_version": "2026-08-07",
  "label_names":      ["EXAMPLE-SHELL-LLC", "EXAMPLE-HOLDINGS-LLC", …],
  "artist_names":     ["Example Persona", "Exemple Artist", …],
  "isrc_registrants": ["XX9AA", "XX8BB", …],
  "upc_prefixes":     ["0000000", "0000001", …],
  "note": "Prefix hits are the strongest signal in the dataset…"
}
FieldMatch strengthHow to use it
isrc_registrants
upc_prefixes
StrongDifferently-named shell labels sharing one ISRC registrant or GS1 prefix are usually one operator behind several masks. A prefix hit on a new delivery is worth a manual look even when nothing else matches.
label_names
artist_names
AdvisoryHuman review. Same caution as any name match.

Prefixes are not proof on their own — a registrant prefix can be reassigned, and legitimate labels sometimes share an issuer. They are the best early signal we have, which is a different claim from certainty.

Status

GET/v1/health

Public — no key required. Safe to poll from a monitor. Returns which dataset is loaded and its version, and nothing about any case.

{
  "ok": true,
  "mode": "sandbox",
  "list_version": "2026-08-07",
  "generated_at": "2026-08-07T00:00:00Z"
}

Endpoint index

GET/v1/

Public. A machine-readable summary of the live endpoints, the auth scheme, the normalisation rules and the retention policy — the same facts as this page, in JSON, so your client can assert against them rather than trusting a document that may be out of date.


Bloom filter format

GET /v1/screening-list?format=bloom returns a compact probabilistic filter: roughly 117 KB per 100,000 entries at a 1% false-positive rate. Millions of tracks can be screened in memory on your own hardware without a single identifier reaching us. Every hit is then confirmed through /v1/check, where false positives disappear.

File layout

line 1  JSON header, then a newline (\n)
rest    ceil(m / 8) raw bytes

// header
{"algo":"bloom-v1","m":958500,"k":7,"hash":"sha256-double",
 "bit_order":"lsb-first","n":100000,"list_version":"2026-08-07"}

Algorithm

Documented in full so you can reimplement it in any language and not depend on a library of ours:

m = header.m                       # bit count
k = header.k                       # 7

h = SHA256(normalised_identifier)  # 32 bytes; see Normalisation
a = big_endian_uint64(h[0:8])
b = big_endian_uint64(h[8:16])

for i in 0..k-1:
    bit = (a + i*b) mod m
    test bits[bit >> 3] & (1 << (bit & 7))     # LSB first

present  = all k bits set        # → confirm via /v1/check
absent   = any bit clear         # → definitively not in the list

Screening locally

The shape we recommend for a nightly catalogue sweep:

  1. Download ?format=bloom. Verify x-amftf-list-signature against your secret. Record x-amftf-list-version.
  2. Normalise your own identifiers using the rules above. This step is where integrations silently go wrong — test it against the sandbox values first.
  3. Filter in memory. A miss is definitive: that identifier is not in our records.
  4. Confirm the hits with POST /v1/check in batches of 1,000. False positives vanish here, and you get the case, evidence and status for the real ones.
  5. Routeexact to your action queue, advisory to a human.

Why the filter rather than the full list? Both are offered and either works. The filter is smaller, faster to check, and — because it is one-way — cannot be read back as a list of our records. Nothing about your catalogue leaves your systems in either case.


Fraud kinds

kindWhat it is
fake_label_releaseReleased under a shell label registered to publish other people's recordings.
impersonated_artistA profile using a real artist's name, or a near-spelling of it, to attract their audience.
ai_persona_artistAn invented artist with no real person behind it, used to publish AI-generated catalogue at volume.
ai_generated_catalogueRecordings assessed as machine-generated by audio analysis.
metadata_backdatingRelease date presented as earlier than the actual upload, typically to claim precedence over the original.
tiktok_only_redeliveryAn existing song re-delivered to the TikTok store alone under a changed artist or title, so creations pay the wrong party.
tiktok_only_new_deliveryA track that exists only in the TikTok store — typically AI-generated or under a fake persona, with no release anywhere else.
stream_manipulationArtificial streaming activity documented against the release.

TikTok-store-only deliveries are invisible to normal catalogue checks. They never appear on Spotify or Apple, so any screening that starts from a streaming service will never see them. That is exactly why the pattern is used. On TikTok, rights holders are paid per video creation, not per view — so creations is the figure that matters, and the one we report. Any revenue number we attach is an estimate and labelled as such.

Error codes

Errors are JSON, with a stable machine-readable code. Match on the code, never on the message — messages may be reworded.

{ "error": { "code": "too_many_identifiers",
            "message": "1500 identifiers in one request; the limit is 1000." } }
StatusCodeMeaning
400empty_bodyNo JSON body sent.
400bad_jsonBody is not valid JSON, or not an object.
400bad_fieldA field has the wrong type.
400nothing_to_checkNo identifiers supplied.
400bad_formatformat is not jsonl, csv or bloom.
400bad_sincesince is not YYYY-MM-DD.
401 / 403See auth failures.
404not_foundUnknown endpoint.
413too_many_identifiersMore than 1,000 identifiers in one request.
413body_too_largeBody exceeds 2 MB.
429rate_limitedRequests-per-minute exceeded. Carries Retry-After.
429identifier_quotaIdentifiers-per-minute exceeded. Carries Retry-After.

On 429, wait for Retry-After seconds. Do not retry immediately with backoff of your own devising — the header tells you exactly how long, and hammering a limit is how a key ends up suspended.

Rate limits

LimitDefault
Requests per minute, per key60
Identifiers per request1,000
Identifiers per minute, per key10,000
Request body2 MB
Clock skew±300 seconds

Higher limits on request — tell us the shape of your workload and we will raise them. The bulk file exists precisely so that a full catalogue sweep never needs to touch these limits.

Uptime target: 99.5% monthly. The service runs on AMFTF infrastructure independent of the member portal, so portal maintenance does not affect it.

Roadmap

Phase 1 is live. The rest is deliberately unbuilt: the first partners to review shape it, and building endpoints before anyone has said what their workflow needs produces the wrong endpoints.

PhaseScopeStatus
1POST /check · GET /screening-list · GET /watchlist · GET /healthlive
2GET /fraud/labels · /fraud/releases · /fraud/tracks · /fraud/artists · /fraud/tiktok · GET /case/{id} — cursor-paginated, updated_since incremental syncplanned
3POST /outcomes — report what you actioned · optional webhook for newly documented matchesplanned

About POST /outcomes

The one thing we ask in return, and it stays optional. When you action a release we flagged (removed / under_review / dismissed, with a date), it closes the case in our records and lets us publish a distributor-responsiveness comparison that is evidence-based rather than estimated. Nothing else from a member's submission is stored or shared.

Changelog

DateChange
2026-08-07v1 Phase 1 live. /check, /screening-list (jsonl · csv · bloom), /watchlist, /health. Sandbox dataset is synthetic.
2026-08-07distributor object replaces any distributor name field, across every endpoint.
2026-08-07TikTok records split by delivery_type: redelivery and new_delivery.

Attribution & scope

Reports and internal tooling built on this data should carry:

"Fraud intelligence provided by the Anti Music Fraud Task Force (AMFTF)."

Not exposed by this API, deliberately

  • Internal licensor identifiers.
  • AMFTF's prefix-network mapping and correlation graph.
  • Member-submitted confidential material.
  • The identity of any other distributor holding a matching release. Cross-distributor matters are handled by AMFTF directly, not disclosed member to member.

Accuracy

ai_likelihood is an audio-analysis probability, not a legal determination. creations and any revenue figures are estimates and labelled as such. Every record released through this API — including every suspected one — has evidence on file and a named case behind it; nothing is published on suspicion alone.

Absence from our records is not a clean bill of health. It means the release is not in our documented set — not that it has been cleared.

Contact

Sher
AMFTF Head — Anti Music Fraud Task Force (AMFTF)
sher@pumpa.org.pk · amftf.org

For a key, an integration call, or to have a field added to v1: write, and mark up whatever in this document does not fit your workflow. That is how v1 gets finished.

If you believe a record here is wrong, tell us. A dataset like this is only useful if it is correctable, and a correction costs us far less than a wrong flag costs the company on the other end of it.