API Reference · v1
DistroShield API
REST API for pre-upload music quality control. Each /v1/analyze request runs five modules in parallel — AI detection, duplicate check, metadata validation, recording fingerprint, and local catalog fingerprint — returning a combined recommendation (pass / review / block) before your track reaches any DSP. Built for distributors' ingest pipelines.
https://api.distroshield.com
distroshield-v7c
Model-specific attribution (live since 2026-05-04)
The API returns a classification_origin field that identifies the generator. When classification is ai, this field is one of suno_unlicensed, licensed_ai (Udio / ElevenLabs), or unknown_ai. When classification is human or hybrid, the field mirrors that value. Additive and backward-compatible.
Attribution is informational — the operational decision is yours. Common practice for indie distributors: distribute AI tracks to DSPs but withhold Content ID monetization claims until licensing clarity. Some DSPs demonetize AI streams automatically; Believe and TuneCore block proactively. Pick the policy that fits your DSP relationships and catalog mix.
Authentication
Every request (except GET /health) requires a Bearer API key issued by DistroShield. Keep your key secret — it's your client identity and billing binding.
Authorization: Bearer ds_YOUR_API_KEY_HERE
Keys are provisioned manually during onboarding. Missing or invalid key → 401 unauthorized.
Quickstart
Minimal call that analyzes a track and returns classification. Replace YOUR_API_KEY and AUDIO_URL.
curl -X POST https://api.distroshield.com/v1/analyze \
-H "Authorization: Bearer YOUR_API_KEY" \
-H "Content-Type: application/json" \
-d '{
"audio_url": "AUDIO_URL",
"metadata": { "title": "Song A", "artist": "Artist A" },
"client_track_id": "internal-123"
}'
Typical latency: 3–6 seconds (audio download + inference). For high throughput, use POST /v1/batch.
Which endpoint should I use?
Both endpoints run the same underlying analysis pipeline and return the same response shape per track. The difference is latency and volume:
/v1/analyze
Sync
One track per request. Response inline in ~3–6 s. Best when you need the verdict before returning control to the caller.
- Consumer spot-checks (backend of a lookup UI)
- Interactive dev testing
- Low-volume integrations (< a few tracks/minute)
- Any flow where "give me the verdict now" beats throughput
/v1/batch
Async
Up to 1000 tracks per request. Queued and processed in background. Poll GET /v1/batch/:id or receive a webhook when each track completes.
- Distributor ingest pipelines (daily / hourly batches)
- Marketplace pre-acquisition due diligence
- Ongoing catalog protection sweeps
- Any workflow where throughput beats immediate response
/v1/batch. If a human is waiting for the answer on the other end of the call, use /v1/analyze.
/v1/analyze
Analyze a track
Synchronously analyze one audio URL. Returns classification, AI score, and the derived recommendation (pass / review / block).
Request body
| Field | Type | Description |
|---|---|---|
audio_url | string, required | Publicly fetchable HTTPS URL to the audio file (MP3, WAV, FLAC, M4A). Signed Firebase / S3 URLs are fine. |
metadata | object, optional | Track metadata (see below). Helps local signals. |
metadata.title | string | Track title. |
metadata.artist | string | Artist name. Names matching AI-generator patterns boost local_score. |
metadata.isrc | string | ISRC code. |
metadata.duration_seconds | integer | Track duration in seconds. |
client_track_id | string, optional | Your internal track ID (≤255 chars). Echoed back for correlation. |
Example
POST https://api.distroshield.com/v1/analyze
Authorization: Bearer ds_...
Content-Type: application/json
{
"audio_url": "https://firebasestorage.googleapis.com/.../track.wav?token=...",
"metadata": {
"title": "Mi Bello Puerto",
"artist": "Juan Pérez",
"isrc": "USRC17607839",
"duration_seconds": 187
},
"client_track_id": "mhm-2026-03296"
}
Response 200 OK
The response combines five independent quality signals — AI detection, duplicate check, metadata validation, recording fingerprint, and local catalog fingerprint — into a single recommendation. Details on each module below.
{
"analysis_id": "an_8_2HwDlNC1f_",
"db_id": 42,
// ---- Module 1: AI detection ----
"ai_score": 0.0655,
"classification": "human",
"classification_origin": "human", // mirrors classification when not ai
"origin_confidence": 0.9775,
"confidence": 0.9775,
"signals": {
"local_score": 0.15,
"local_reasons": ["missing_artist"],
"model_score": 0.0373,
"weights": { "local": 0.25, "model": 0.75 }
},
"model_version": "distroshield-v13",
"attribution_model_version": null, // populated only when classification == "ai"
// ---- Module 2: Duplicate check (only if metadata.isrc or title+artist given) ----
"duplicate_score": 0,
"duplicate_matches": [],
"sources_checked": ["spotify", "deezer", "youtube"],
// ---- Module 3: Metadata validation (only if metadata given) ----
"metadata_validation": {
"score": 0.95,
"summary": { "high": 0, "medium": 0, "low": 1 },
"issues": [
{ "severity": "low", "type": "ddex_missing_recommended",
"detail": "Recommended field \"genre\" is missing" }
]
},
// ---- Module 4: Recording fingerprint (only if audio_url given) ----
"recording_fingerprint": {
"matched": false,
"matches": [],
"highest_score": 0,
"distinct_artists_at_perfect_score": 0,
"submitted_isrc_matched": false,
"submitted_artist_matched": false,
"review_reason": null
},
// ---- Module 5: Local catalog fingerprint (only if audio_url given) ----
"local_fingerprint": {
"duration": 224.5,
"matches": [],
"top_score": 0,
"review_reason": null
},
// ---- Additional fields when classification is "ai" or "hybrid" ----
// "attribution_predictions": [ { "label": "suno_unlicensed", "score": 0.99 }, ... ],
// "attribution_model_version": "distroshield-v8-attribution",
// "v7c_verification_predictions": [ { "label": "human", "score": 0.82 }, ... ],
// "v7c_verification_model_version": "distroshield-v7c",
// "v8_rescue": { // audit trail of the v8 rescue chain
// "applied": true,
// "rescue_class": "models_consensus_no_external",
// "v7c_agrees": true,
// "external_gate": { "passed": false, "isrc_in_dsp": false, "acr_match": false }
// },
// "vocals_stem_rescue": { // vocals-stem isolation rescue trail
// "applied": false,
// "reason": "skipped_models_consensus_ai"
// }
// ---- Combined decision ----
"recommendation": "pass",
"analyzed_at": "2026-08-04T00:28:12.000Z"
}
The same response, rendered
Here's what the JSON above looks like when your compliance panel renders it. Every field surfaced, verdict at the top, one recommendation at the bottom. The JSON is what the API returns; the UI is yours to design — this is one possible layout.
How each JSON field maps to the rendering above
metadata.title/artist/isrc→ header lineclassification+recommendation→ verdict badges (top-right and footer)ai_score+confidence+model_version→ Module 1 tileduplicate_matches.length+sources_checked→ Module 2 tilemetadata_validation.summary+ top issue.detail→ Module 3 tilerecording_fingerprint.matched→ Module 4 tilelocal_fingerprint.matches.length→ Module 5 tileanalyzed_at+ certificate download fromGET /v1/analysis/:id/certificate→ footer
Response fields
| Field | Meaning |
|---|---|
ai_score | Final score 0.0–1.0 (ensemble of model + local signals). Higher = more likely AI. |
classification | Derived from ai_score against your client thresholds: human, hybrid, or ai. |
classification_origin | Sub-attribution when AI is detected. One of: human, hybrid, suno_unlicensed (Suno-style — Warner deal Nov 2025, UMG/Sony pending), licensed_ai (Udio / ElevenLabs — major-label deals), unknown_ai (MusicGen / Stable Audio / Riffusion / unknown). When classification is human or hybrid, this mirrors that value. Informational only — the distribution / Content ID decision belongs to your policy. |
origin_confidence | Confidence (0–1) in the classification_origin attribution. |
attribution_model_version | Optional. Identifier of the attribution model that produced classification_origin (e.g. distroshield-v8-attribution). null when only the primary binary model ran. |
confidence | How confident the model is in the classification (0.0–1.0). |
signals | Breakdown: model_score (pure ML), local_score (metadata rules), weights used. |
recommendation | pass (ship it), review (human-in-the-loop), or block (do not deliver to DSP). Upgraded from pass to review when a strong duplicate, high-severity metadata issue, or recording-fingerprint match is detected. |
review_reason | Set when recommendation was upgraded from pass. One of: duplicate_detected, metadata_issue, recording_fraud_match, cross_distributor_recording_fraud. |
duplicate_score | 0–1. Highest confidence that this track already exists somewhere public. See Duplicate check. |
duplicate_matches | Up to 10 ranked matches from Spotify, Deezer, YouTube. |
sources_checked | Which DSPs were queried successfully. |
metadata_validation | Per-field issues + overall score. See Metadata validation. |
recording_fingerprint | Audio-hash match against 100M+ commercial recordings. Catches re-uploads with metadata changed. See Recording fingerprint. |
local_fingerprint | Audio-hash match against DistroShield's own growing catalog. Catches cross-distributor fraud on indie / unreleased tracks that never entered commercial catalogs. See Local catalog fingerprint. |
attribution_predictions | Per-class probabilities from the v8 attribution model. Present only when classification is ai or hybrid. Lets you render the decision-support breakdown (which AI subclass and by how much). |
v7c_verification_predictions | Per-class probabilities from the v7c verification model. Third-opinion tie-breaker used in the rescue chain. Present only when the rescue chain ran. |
v8_rescue | Audit trail of the v8-attribution rescue chain: which rescue class applied, whether v7c agreed, external-gate result (ISRC in DSPs, ACR match). Present only when the rescue chain ran. |
vocals_stem_rescue | Audit trail of the vocals-stem isolation rescue path (last-chance rescue for modern commercial productions where the full-track primary model misfires). Present only when the rescue triggered. |
db_id | Internal integer ID. Use this to PATCH reviews or fetch later. |
model_version | Detector version that scored this track (currently distroshield-v13). Pin to monitor drift. |
recommendation: "block" should enter a review queue, not be deleted.
Response modules
Each /v1/analyze response is produced by five independent quality-control modules running in parallel. Each module is optional — fields appear only when the module ran:
Acoustic analysis of the audio itself, using DistroShield's fine-tuned classifier trained on real AI and human catalogs. Always runs. Returns ai_score, classification, signals.
Cross-references ISRC and title+artist against Spotify, Deezer, YouTube in parallel. Runs when metadata has ISRC or title+artist. Returns duplicate_score, duplicate_matches.
ISRC format validation, DDEX completeness, identity fraud detection (ISRC belongs to someone else), artist-name impersonation checks. Runs when metadata is provided. Returns metadata_validation.
Audio-fingerprint match against 100M+ commercial recordings. Detects copyright infringement — unauthorized re-uploads, covers of released tracks, sample use of commercial material, and cross-distributor identity fraud. Runs when an audio URL is provided. Returns recording_fingerprint.
Audio-fingerprint match against DistroShield's own growing corpus. Detects copyright infringement on indie and unreleased material — masters previously submitted by another client, cover-of-a-cover chains, and cross-distributor abuse invisible to public catalogs. Every analysis grows the catalog automatically. Returns local_fingerprint.
Module 2 · Duplicate check
Checks whether the track already exists in major public music sources. Executes when metadata.isrc OR both metadata.title and metadata.artist are provided. Skipped otherwise.
Sources
| Source | ISRC lookup | Title+artist search |
|---|---|---|
| Spotify Web API | ✓ exact | ✓ fuzzy |
| Deezer public API | ✓ exact | ✓ fuzzy |
| YouTube Data API v3 | — | ✓ fuzzy |
Scoring
- Exact ISRC match on any source →
match_score: 1.0 - Fuzzy title+artist match (≥0.5) →
match_score: 0.5–1.0 duplicate_scoreis the top match's score + 5% boost per additional source that also matched
Example match
{
"source": "spotify",
"match_type": "isrc", // or "title_artist"
"match_score": 1.0,
"id": "7qiZfU4dY1lWllzX7mPBI3",
"title": "Shape of You",
"artist": "Ed Sheeran",
"url": "https://open.spotify.com/track/7qiZfU4dY1lWllzX7mPBI3"
}
Module 3 · Metadata validation
Validates the declared metadata across four dimensions. Runs locally (<10 ms) when metadata is provided.
Checks performed
| Check | What it catches |
|---|---|
| ISRC format | Structure (12 chars: 2 letters + 3 alnum + 7 digits), country code validity, future-year detection. |
| DDEX completeness | Mandatory fields (title, artist) + recommended DDEX 4 fields (isrc, duration_seconds, album, genre, language, release_date). Missing recommended fields are consolidated into a single issue to avoid noise. |
| Identity fraud | Cross-references the declared title + artist against what Spotify / Deezer have registered under that ISRC. Catches the "I claim this ISRC as my new song" fraud where someone submits a track using another artist's ISRC. |
| Artist impersonation | Fuzzy match against a curated list of globally-famous + LATAM-top artists. Flags suspicious near-matches like "3d Sheeran" vs "Ed Sheeran". |
| Duration anomaly checks | Flags duration-based fraud and anomaly patterns (including known Content ID abuse). Specific thresholds are kept private to avoid tipping off bad actors — legitimate tracks are not affected. |
Issue structure
{
"severity": "high", // high | medium | low
"type": "isrc_identity_artist_mismatch",
"detail": "ISRC is registered on spotify as artist 'The Weeknd' but you declared 'Mi Artista'",
"registered": { // present on identity-mismatch only
"source": "spotify",
"artist": "The Weeknd",
"title": "Blinding Lights",
"url": "https://open.spotify.com/track/..."
}
}
Scoring
metadata_validation.score starts at 1.0 and subtracts penalties: high -0.40, medium -0.15, low -0.05. Clamped to [0, 1]. Any high severity issue upgrades the overall recommendation from pass to review.
Module 4 · Recording fingerprint · copyright infringement (commercial catalog)
Copyright infringement detection against a catalog of 100M+ commercial recordings. The technical mechanism is audio fingerprinting: hashes the submitted audio and matches it against every commercially-released recording indexed. Detects unauthorized re-uploads of copyrighted material, covers of released tracks, sample use of commercial recordings, and the case other modules are structurally blind to: a fraudster re-uploading someone else's exact recording with the metadata changed.
Two vocabularies for the same feature — fingerprint is what labels and distributors know it as, copyright infringement detection is what marketplaces, rights-holders, and legal teams call it. The response field name is recording_fingerprint.
Executes when an audio_url is provided. Skipped otherwise.
Fields
| Field | Meaning |
|---|---|
matched | True when at least one recording in the catalog matches. |
matches | Up to 10 ranked matches. Each contains title, artists, album, label, score (0–100), release_date, isrc, upc, plus direct URLs to spotify_url, deezer_url, youtube_url when available. |
highest_score | Top match's score (0–100). 100 = perfect audio match. |
distinct_artists_at_perfect_score | How many different artist identities own a near-perfect (≥95) match. ≥2 means the same recording is registered under multiple identities across distributors — per-se evidence of cross-distributor identity fraud. |
submitted_isrc_matched | True when the ISRC in your metadata matches one of the ISRCs returned by the fingerprint match. Surfacing-only — does NOT downgrade the flag (a fraudster can copy any ISRC). |
submitted_artist_matched | Same idea, for artist name. |
review_reason | Set to cross_distributor_recording_fraud when 2+ distinct artists match at score ≥95, or recording_fraud_match when at least one match is at score ≥80. Otherwise null. |
error | Optional. Set when the fingerprint backend returned an error (e.g. timeout, billing issue). The pipeline continues without this signal in that case. |
Routing
- • A single match at score ≥80 sets
review_reasontorecording_fraud_matchand upgradesrecommendationfrompasstoreview. - • Two or more matches at score ≥95 with different artists takes precedence and sets
review_reasontocross_distributor_recording_fraud— the highest-confidence form of fraud the API surfaces. - • The signal never auto-blocks. Human-in-the-loop is mandatory.
Module 5 · Local fingerprint · copyright infringement (indie / unreleased)
Copyright infringement detection against DistroShield's own growing catalog. Same audio-fingerprint mechanism as Module 4, but pointed at a different corpus. Fills the gap where the commercial catalog is structurally blind: indie and unreleased tracks that never entered any commercial catalog but were previously submitted to DistroShield by a different client. Catches the pattern where the same recording is re-submitted through multiple distributor pipelines with different identities — a common form of copyright abuse on indie material invisible to Spotify / Deezer / YouTube fingerprinting.
Two vocabularies for the same feature — local fingerprint for the technical audience, copyright infringement on indie catalog for the rights-holder audience. The response field name is local_fingerprint.
Executes when an audio_url is provided. Every analysis grows the catalog — no manual seeding needed. Runs in parallel with Module 4; fires independently, so both can flag on the same track (rare but possible for tracks that live in both commercial and internal catalogs).
Fields
| Field | Meaning |
|---|---|
duration | Duration of the submitted audio, in seconds. |
matches | Ranked list of matches from the internal DistroShield catalog. Each contains track_id, client_id (the client who originally submitted the matching track), title, artist, isrc, and score (0–1 audio-hash similarity). |
top_score | Best match's similarity score (0–1). 1.0 = identical audio hash. |
review_reason | Set to cross_distributor_recording_fraud when a match at score ≥0.95 comes from a different client_id — the same audio was submitted through multiple distributor pipelines. Otherwise null. |
/v1/batch
Batch analyze (async)
Submit many tracks at once. Returns a batch ID immediately; tracks are processed asynchronously by the worker queue. Results delivered via webhook (recommended) or polled via GET /v1/batch/:id.
Request body
{
"tracks": [
{ "audio_url": "...", "metadata": {...}, "client_track_id": "..." },
{ "audio_url": "...", "metadata": {...}, "client_track_id": "..." }
],
"webhook_url": "https://your-server.com/distroshield-webhook"
}
Response 202 Accepted
{
"batch_id": "ba_17", // prefixed form — use this in status_url
"db_id": 17, // raw numeric ID (also accepted in GET /v1/batch/:id)
"track_count": 2,
"status": "queued", // 'queued' | 'processing' | 'completed' | 'failed'
"status_url": "/v1/batch/17" // GET this to fetch per-track results
}
Response returns in <500ms — the server enqueues the tracks and hands back immediately. Two delivery options for the results:
- Polling: your client hits GET /v1/batch/:id every 10–15s until
status: "completed". Simpler to implement; no webhook infrastructure needed. - Webhook: if
webhook_urlis set, each track result is POSTed there as it completes with HMAC-SHA256 signature inX-DistroShield-Signature. Faster (no polling overhead) but requires public endpoint + signature validation.
/v1/batch/:id
Fetch batch status & results
Retrieve batch progress and per-track analysis results. The :id segment accepts either the raw numeric ID (17) or the prefixed form (ba_17) — both are equivalent. Scoped to your client — you can only fetch batches you created.
GET https://api.distroshield.com/v1/batch/17 Authorization: Bearer ds_...
Response 200 OK
{
"id": 17,
"batch_id": "ba_17",
"client_id": 42,
"track_count": 2,
"completed_count": 2, // increment as worker processes each track
"status": "completed", // 'queued' | 'processing' | 'completed' | 'failed'
"webhook_url": null,
"created_at": "2026-08-04T00:26:09.000Z",
"completed_at": "2026-08-04T00:31:47.000Z",
"tracks": [
{
"client_track_id": "mhmusik_track_46101_1",
"track_id": 3265,
"title": "Heart of Stone",
"artist": "Miguel Ángel García Zamora",
"isrc": "USABC1234567",
"analysis": {
// SAME SHAPE as the POST /v1/analyze response (see above).
// All five modules (AI detection, duplicate, metadata, recording,
// local fingerprint) + recommendation + review_reason + rescue
// audit trails included when applicable.
"analysis_id": "an_...",
"db_id": 3242,
"ai_score": 0.7478,
"classification": "ai",
"classification_origin": "suno_unlicensed",
"origin_confidence": 0.987,
"recommendation": "block",
"review_reason": null,
"duplicate_score": 0,
"duplicate_matches": [],
"metadata_validation": { /* ... */ },
"recording_fingerprint": { /* ... */ },
"local_fingerprint": { /* ... */ },
"attribution_predictions": [ /* ... */ ],
"v7c_verification_predictions": [ /* ... */ ],
"v8_rescue": { /* audit trail when rescue chain fired */ },
"vocals_stem_rescue": { /* vocals-stem isolation rescue trail */ },
"signals": { /* ... */ },
"model_version": "distroshield-v13",
"analyzed_at": "2026-08-04T00:28:12.000Z",
"review_status": "none" // owner-review workflow state (batch-specific)
}
},
{
"client_track_id": "mhmusik_track_46101_2",
/* ... second track ... */
}
]
}
Polling strategy
status: "queued"— batch accepted, no tracks processed yet. Poll again in 10–15s.status: "processing"— worker started at least one track. Comparecompleted_countvstrack_countfor progress.tracks[]already contains partial results for completed tracks; queued tracks are absent from the array until the worker picks them up.status: "completed"— all tracks processed,tracks[]is complete. Distribute results to your DB, matching each result byclient_track_id.status: "failed"— the whole batch failed catastrophically (rare — check API logs or contact support). Individual track failures don't set this; a single failed track appears intracks[]with its own error payload.
/v1/analysis/:id
Retrieve a single analysis
Fetch a full analysis row by its db_id. Scoped to your client — you can only fetch analyses for tracks you own.
GET https://api.distroshield.com/v1/analysis/42
Authorization: Bearer ds_...
→ {
"id": 42,
"track_id": 58,
"ai_score": "0.0655",
"classification": "human",
"confidence": "0.9775",
"signals_json": "{...}",
"model_version": "distroshield-v13",
"review_status": "none",
"review_verdict": null,
"reviewer_email": null,
"reviewed_at": null,
"review_notes": null,
"analyzed_at": "2026-04-24T16:27:01.000Z",
"client_id": 1
}
/v1/analysis/:id/certificate
Download signed provenance certificate
Returns the signed PDF certificate for a completed analysis. Same format shipped to consumer /lookup users — same hash algorithm, same verify URL, same signed layout. This is the artifact your artist attaches to a takedown appeal, your compliance team files with DSPs, or a marketplace investor sees before tokenizing a royalty stream. Ownership is scoped to your API key: you can only download certificates for analyses tied to tracks your client submitted.
Query params
| Param | Type | Description |
|---|---|---|
lang | string | Optional. es for Spanish rendering, anything else falls back to English (default). |
Example
curl -X GET "https://api.distroshield.com/v1/analysis/42/certificate?lang=en" \ -H "Authorization: Bearer ds_..." \ -o distroshield-certificate-an_42.pdf
Response headers
| Header | Value |
|---|---|
Content-Type | application/pdf |
Content-Disposition | attachment; filename="distroshield-certificate-an_<id>.pdf" |
Cache-Control | private, no-store |
What the PDF contains
- SHA-256 hash of the analyzed audio — recomputable at any time to prove the audio hasn't been altered since analysis.
- Verdict and model version — the classification (
human,hybrid, orai) and the detector model that produced it. - UTC timestamp at analysis time — establishes when the certificate was issued, prior to any subsequent takedown or dispute.
- Track metadata as submitted (title, artist, ISRC) — ties the certificate to the release record.
- Cross-DSP duplicate matches from the recording fingerprint module, when present.
- Verifiable signature — anyone can validate the certificate at distroshield.com/verify without contacting DistroShield servers.
See a sample certificate PDF for the exact format your integration will receive.
/v1/analyses
List analyses (review queue)
Paginated list of analyses for your client. Use the status query param to filter — this is how your review queue UI fetches pending items.
Query parameters
| Param | Default | Values |
|---|---|---|
status | pending | none, pending, reviewed, overridden, all |
classification | — | human, hybrid, ai |
limit | 50 | Integer 1–200 |
offset | 0 | Integer ≥0 |
Example
GET /v1/analyses?status=pending&classification=hybrid&limit=20
Authorization: Bearer ds_...
→ {
"count": 20,
"limit": 20,
"offset": 0,
"items": [
{
"id": 42,
"ai_score": "0.5231",
"classification": "hybrid",
"model_version": "distroshield-v13",
"review_status": "pending",
"analyzed_at": "2026-04-24T16:27:01.000Z",
"track_id": 58,
"title": "Track title",
"artist": "Artist",
"source_url": "https://..."
},
...
]
}
/v1/analyses/:id/review
Record human verdict
After a reviewer decides whether a flagged track is actually AI or human, persist their verdict. These labels feed into future model retraining.
Request body
| Field | Type | Description |
|---|---|---|
verdict | string, required | human — track is fully human-madeai — track is fully AI-generateduncertain — reviewer cannot decidehybrid_confirmed — reviewer confirms the track legitimately mixes AI and human elements (e.g. AI-generated instruments with human vocals). Future model retraining uses this as a soft label so the model learns to produce intermediate scores instead of forcing every track into the binary human/ai decision. |
notes | string, optional, nullable | Free-text note about why (≤2000 chars). May be omitted or sent as null. |
reviewer_email | string, optional, nullable | Email of the person reviewing. May be omitted or sent as null; defaults to the client's contact_email. |
PATCH https://api.distroshield.com/v1/analyses/42/review
Authorization: Bearer ds_...
Content-Type: application/json
{
"verdict": "human",
"notes": "Heavy autotune but definitely human — female reggaeton vocal",
"reviewer_email": "j@mhmusik.com"
}
→ {
"analysis_id": 42,
"original_classification": "ai",
"human_verdict": "human",
"review_status": "overridden",
"reviewer_email": "j@mhmusik.com",
"reviewed_at": "2026-04-24T17:30:12.000Z"
}
review_status becomes overridden when the verdict disagrees with the model's original classification, else reviewed.
/v1/webhooks
Register a webhook
DistroShield will POST async analysis results to your URL. The secret returned is used to sign each delivery with HMAC-SHA256 — verify signatures to reject forgeries.
POST /v1/webhooks
Authorization: Bearer ds_...
{
"url": "https://your-server.com/distroshield-webhook",
"events": ["analysis.completed", "batch.completed"]
}
→ {
"webhook_id": 3,
"secret": "whsec_abc...", // shown ONCE — store it securely
"url": "https://...",
"events": [...]
}
Each delivery retries with exponential backoff (30s → 4d, up to 8 attempts) until your endpoint responds 2xx.
Payload shape
Every delivery is a JSON POST to your registered URL. Body shape for the analysis.completed event:
{
"event": "analysis.completed",
"batch_id": "ba_...", // present when the analysis came from POST /v1/batch; null for sync analyze
"track_id": 58,
"client_track_id": "internal-123", // whatever you passed when submitting the analysis; null if omitted
"result": {
// Identical shape to the POST /v1/analyze response body.
// All fields documented at #analyze — ai_score, classification,
// duplicate_matches, recording_fingerprint, local_fingerprint,
// recommendation, analyzed_at, etc.
}
}
Handling both sync (/v1/analyze response body) and async (webhook result field) with the same code is the intent — the shape is deliberately identical.
Verifying the HMAC signature
Every delivery includes an X-DistroShield-Signature header. Format: sha256=<hex>. Compute the same HMAC over the raw request body using the webhook secret you stored at registration; reject the delivery if it doesn't match. Node example:
import crypto from 'node:crypto';
app.post('/distroshield-webhook', (req, res) => {
const signature = req.header('X-DistroShield-Signature'); // "sha256=abc123..."
const secret = process.env.DISTROSHIELD_WEBHOOK_SECRET; // from the /v1/webhooks response
// IMPORTANT: use the RAW body bytes, not a re-serialized JSON.stringify(req.body).
// With express.json(), req.rawBody must be captured via a verify hook.
const expected = 'sha256=' + crypto
.createHmac('sha256', secret)
.update(req.rawBody)
.digest('hex');
// Constant-time compare to prevent timing attacks.
const ok = crypto.timingSafeEqual(
Buffer.from(signature),
Buffer.from(expected),
);
if (!ok) return res.sendStatus(401);
const { event, result, track_id, client_track_id } = req.body;
// ... process result.recommendation, result.classification, etc.
res.sendStatus(200);
});
Missing or wrong signature → respond 401 and don't process the payload. Anyone can POST to your URL; the signature is what proves the payload originated from DistroShield.
/v1/usage
Usage metrics
Current period API usage for your client. Use to monitor your quota.
GET /v1/usage
Authorization: Bearer ds_...
→ {
"period_start": "2026-04-01T00:00:00Z",
"period_end": "2026-04-30T23:59:59Z",
"requests": { "analyze": 312, "batch": 4, "total": 316 },
"analyses_by_classification": {
"human": 287, "hybrid": 18, "ai": 11
}
}
Error codes
Errors return JSON with an error code. Some include additional context.
| Status | Code | Meaning |
|---|---|---|
400 | invalid_body | Request body didn't match the schema. Check the issues array. |
400 | invalid_query | Query parameters didn't validate. |
400 | invalid_analysis_id | The :id path parameter wasn't a positive integer. |
401 | unauthorized | Missing or malformed Authorization header (no Bearer token at all). |
401 | invalid_api_key | Bearer token present but doesn't match any active client — expired, rotated, or from the wrong environment (dev key against prod, or vice versa). |
402 | quota_exceeded | Trial cap reached or Pro tier hit Enterprise threshold. Response includes usage object with current count and quota. |
404 | not_found | Resource ID doesn't exist OR doesn't belong to your client. Used for analyses, batches, and certificates. Deliberately doesn't distinguish "doesn't exist" from "not yours" — avoids leaking existence. |
415 | unsupported_audio_format | The audio file at audio_url could not be decoded. Response includes supported_formats array. Supported: WAV, FLAC, OGG, MP3, M4A, AAC. |
429 | rate_limited | You exceeded your rate limit window. Retry after the period resets. |
500 | internal_error | Unexpected server error. Contact support with the request ID in response headers. |
502 | audio_fetch_failed | We could not fetch the audio from the URL you provided. Check that the URL is reachable and not expired (e.g. signed URL TTL). |
502 | inference_unavailable | The inference service is temporarily unavailable. Retry shortly. |
Rate limits
Default: 300 requests per minute per API key. Response headers expose your current state:
X-RateLimit-Limit: 300 X-RateLimit-Remaining: 297 X-RateLimit-Reset: 60
Exceeding returns 429 rate_limited. Need higher limits? Contact us for Pro/Enterprise tiers.
Detection model
Current production version: distroshield-v13 (primary, human vs AI) paired with distroshield-v8-attribution for generator attribution and distroshield-v7c as a verification tie-breaker. Trained on a curated mix of: real distributor-verified AI tracks, direct-from-source Suno and Udio samples, and thousands of human tracks from a real distributor catalog spanning reggaetón, latin pop, dembow, regional mexicano, cristiano/gospel, electronic, hip-hop, bachata, cumbia, folk and other Latin American styles. Continuously improved from production review verdicts.
model_version is stamped on every analysis response so you can pin against drift or A/B new versions. When we promote a new primary model, the previous version stays available on request for hot rollback.
Scores are probabilistic. Human-in-the-loop review is mandatory. Never auto-block tracks without a human override path — the API is a gate, not a judge.