Overview
The DistriBeats API is the headless half of DistriBeats. You keep your own app, your own artists and your own brand; we handle screening, quality control, store delivery and the DDEX feeds.
Everything is a plain REST call over HTTPS returning JSON. Audio and artwork are handed to you as signed download links rather than streamed through the API, and DDEX feeds are compiled once, when a release is approved, so reads stay fast no matter how large your catalogue grows.
Prefer not to build a front end? The White Label Distribution Dashboard (from $39.99 / ₹3,999 a month) runs a branded console on your own domain, with your logo, your emails and your legal pages.
curl https://distribeats.com/api/public/v1/releases?limit=5 \
-H "Authorization: Bearer $DISTRIBEATS_API_KEY"Access & pricing
The DistriBeats API is an Enterprise add-on starting at $49.99 / ₹4,999 per month (USD outside India, INR in India). It is in beta from 8 November 2026 and runs on top of an active Rising Label or Macro Label plan.
It cannot be bought online. Request beta access; our team reviews your workspace, sends a separate Enterprise agreement to sign in Console → Enterprise, and then grants API access to the workspace. Keys and webhooks are then available under Developer & API.
If the label plan lapses, API access pauses until it is renewed. Compare both add-ons on the Enterprise page.
Product: DistriBeats API (Enterprise add-on)
Price: from $49.99 / ₹4,999 per month
Status: beta from 8 November 2026
Requires: active Rising Label or Macro Label plan
Contract: separate Enterprise agreement
Buying: not sold online — request beta accessAuthentication
Create keys in your workspace under Developer & API. A key is shown once at creation; we only keep a hash of it, so nobody — including us — can read it back later.
Keys beginning db_test_ validate your payloads end to end and return simulated objects without creating real work, and never read or change live releases (per-release calls answer 404). Keys beginning db_live_ act on real catalogue.
Each key carries only the permissions you tick. Call the API from your server: a key in browser code is a key in everyone's hands.
Authorization: Bearer db_live_xxxxxxxxxxxxxxxxreleases:read
releases:write
ddex:export
artists:read
royalties:read
webhooks:manageReleases
GET/v1/releases?limit=25&starting_after=25&status=Approved
GET/v1/releases/{id}
POST/v1/releases
POST/v1/releases/{id}/takedown
GET/v1/reports/streams
GET/v1/reports/royalties
Lists come back newest first with has_more and next_cursor. Retrieving a single release adds signed download links for the artwork and every master, valid for thirty minutes.
Always send an Idempotency-Key when creating: a retried request returns the first result instead of a duplicate release, even when our first answer never reached you. Results are kept per workspace and per key environment, so a test key and a live key never share one.
POST /releases answers 201 with a short receipt (id, status, livemode, trackCount and any uploadUrls), not the full release; read the rest with GET /v1/releases/{id}, which also carries storeLinks, smartLinkUrl, rejectionReason, takedownRequested and apiIngestProblems (files sent by URL that could not be copied in).
Files and documents sent by URL are copied into our storage when you submit. If one cannot be fetched, submit answers 422 files_unreadable with problems[] naming each one.
POST /takedown on a release that has not reached the stores withdraws it at once (status Unsubmitted, withdrawn: true). On a delivered release the status is kept and takedownRequested: true is returned; our team removes it from stores and the status becomes Taken Down. Repeating the call is safe.
GET /v1/reports/streams (scope releases:read) and GET /v1/reports/royalties (scope royalties:read) return every stream and royalty report we have sent as webhooks, newest first, paged like every list. Filter with release_id. Royalty amounts are decimal strings and informational; payouts follow your agreement.
Every track needs composers with full legal names. Stores reject deliveries with missing songwriter credits, so the API rejects them first.
Statuses match the console: Draft → Pending Upload → Awaiting Payment → Submitted → Under Processing → Approved → Sent to Stores → Live, with Unsubmitted, Rejected, Takedown Requested and Taken Down as exits. A release sent to stores reads Live from its release date.
const res = await fetch(
"https://distribeats.com/api/public/v1/releases",
{
method: "POST",
headers: {
Authorization: `Bearer ${process.env.DISTRIBEATS_API_KEY}`,
"Content-Type": "application/json",
"Idempotency-Key": crypto.randomUUID(),
},
body: JSON.stringify({
title: "Neon Monsoon",
displayArtist: "Aarav Sen",
releaseType: "Single",
genre: "Electronic",
subGenre: "Electronic/House",
language: "English",
releaseDate: "2026-11-14",
stores: ["Spotify", "Apple Music", "YouTube Music"],
newSpotifyArtist: true,
attestations: { age: true, rights: true },
label: "Monsoon Tapes",
pLine: "2026 Monsoon Tapes",
cLine: "2026 Monsoon Tapes",
requestUploadUrls: true,
tracks: [
{
title: "Neon Monsoon",
language: "English",
compositionType: "Original",
copyrightType: "Original",
aiUsage: "None",
composers: "Aarav Sen, Riya Kapoor",
writers: "Riya Kapoor",
producers: "Aarav Sen",
mixingEngineers: "Dev Malhotra",
masteringEngineers: "Dev Malhotra",
contributors: [{ name: "Aarav Sen", role: "Lead Vocals" }],
previewStart: "0:42",
},
],
}),
},
);
const release = await res.json();
// Upload each file to its link, with the SAME bearer key.
// The links do not expire, so a failed transfer can simply be retried.
await fetch(release.uploadUrls[release.id + "-t1"], {
method: "PUT",
headers: { Authorization: "Bearer " + API_KEY, "Content-Type": "audio/wav" },
body: masterWavFile,
});
await fetch(release.uploadUrls.cover, {
method: "PUT",
headers: { Authorization: "Bearer " + API_KEY, "Content-Type": "image/jpeg" },
body: coverFile,
});
// Finally, send it to review
await fetch(`https://distribeats.com/api/public/v1/releases/${release.id}/submit`, {
method: "POST",
headers: { Authorization: "Bearer " + API_KEY },
});{
"object": "release",
"id": "rel_9f2c41",
"release_id": "rel_9f2c41",
"livemode": true,
"status": "Pending Upload",
"title": "Neon Monsoon",
"trackCount": 1,
"uploadUrls": {
"cover": "https://distribeats.com/api/public/v1/releases/rel_9f2c41/upload/cover",
"rel_9f2c41-t1": "https://distribeats.com/api/public/v1/releases/rel_9f2c41/upload/rel_9f2c41-t1"
}
}Lifecycle & edits
A release is freely editable while it is Draft or Pending Upload: overwrite any file, replace the artwork, and re-create with the same Idempotency-Key if you need to correct the payload before submitting.
From Submitted onward the release is locked. Screening fingerprints the exact audio you sent, moderation reads the exact metadata you sent, and from Approved the DDEX ERN feed is compiled and handed to stores. Swapping a file or a title behind those steps would leave our screening result, our feed and the DSP's copy all describing different things — which is how deliveries get rejected and royalties get misrouted.
So there is deliberately no PATCH endpoint for a submitted release. Corrections after submission go one of two ways: while still submitted or under processing, ask support to send it back to draft (screening is invalidated and you re-submit); once live, call POST /v1/releases/{id}/takedown and deliver a corrected release.
Practical rule: validate everything with a db_test_ key first. Test keys run the full validation path and return simulated objects, so a bad payload costs you nothing.
Draft
-> Pending Upload files requested, nothing sent yet
-> Awaiting Payment plan or pay-once charge outstanding
-> Submitted files complete, screening queued
-> Under Processing held for a manual check
-> Approved DDEX feeds compiled
-> Sent to Stores delivered to DSPs
-> Live available in stores
Exits: Unsubmitted (withdrawn or returned), Rejected,
Takedown Requested -> Taken DownLarge master uploads
POST/v1/releases/{id}/upload/{file}?uploads
PUT/v1/releases/{id}/upload/{file}?uploadId=…&partNumber=1
POST/v1/releases/{id}/upload/{file}?uploadId=…
A single PUT to an upload link carries up to 95 MiB — plenty for artwork and most audio. For WAV masters above that, use the chunked flow on the very same upload link: open an upload session, send each part (50 MiB, every part except the last between 5 and 95 MiB), then complete with the list of part numbers and etags. Masters up to 150 MB are accepted.
A failed part can simply be re-sent with the same part number, and an abandoned session is cleaned up with DELETE on the same URL. Once every file has arrived, callPOST /v1/releases/{id}/submit to send the release to review.
// Splits a master into 50 MiB parts, sends them one by one,
// then asks DistriBeats to join them. Works up to 150 MB.
async function uploadMasterInChunks(
uploadUrl: string, // release.uploadUrls[trackId]
apiKey: string,
file: Blob,
) {
const start = await fetch(uploadUrl + "?uploads", {
method: "POST",
headers: { Authorization: "Bearer " + apiKey, "Content-Type": "audio/wav" },
}).then((r) => r.json());
// start = { uploadId, partSize }
const parts: { partNumber: number; etag: string }[] = [];
const total = Math.ceil(file.size / start.partSize);
for (let n = 1; n <= total; n++) {
const chunk = file.slice((n - 1) * start.partSize, n * start.partSize);
const part = await fetch(
uploadUrl + "?uploadId=" + start.uploadId + "&partNumber=" + n,
{
method: "PUT",
headers: {
Authorization: "Bearer " + apiKey,
"Content-Type": "audio/wav",
},
body: chunk,
},
).then((r) => r.json());
parts.push({ partNumber: n, etag: part.etag });
}
await fetch(uploadUrl + "?uploadId=" + start.uploadId, {
method: "POST",
headers: {
Authorization: "Bearer " + apiKey,
"Content-Type": "application/json",
},
body: JSON.stringify({ parts }),
});
}Re-uploads & resuming
Replacing a file before submission. Upload links are permanent and idempotent: PUT the same link again and the new bytes replace the old ones. Use this to swap a re-mastered WAV, fix artwork, or recover from a transfer that died halfway. No new release, no new link, no support ticket.
Resuming. A release sitting in Pending Upload always returns its uploadUrls from GET /v1/releases/{id}, so a crashed job can pick up exactly where it stopped. Never create a second release to retry an upload.
Submit told you files are missing. POST /submit answers 409 files_missing with { missing[], uploadUrls }. Send those files, then submit again.
After submission. Upload links stop accepting writes once screening starts — see Lifecycle & edits above for how corrections are handled from there.
// 1. Ask what is still missing
const r = await fetch(
"https://distribeats.com/api/public/v1/releases/" + id,
{ headers: { Authorization: "Bearer " + API_KEY } },
).then((x) => x.json());
// Still "Pending Upload"? uploadUrls come back with it.
// 2. Re-send only the files that did not arrive.
await fetch(r.uploadUrls["rel_9f2c41-t1"], {
method: "PUT",
headers: {
Authorization: "Bearer " + API_KEY,
"Content-Type": "audio/wav",
},
body: newMasterFile, // overwrites the previous bytes
});
// 3. Submit again
await fetch(
"https://distribeats.com/api/public/v1/releases/" + id + "/submit",
{ method: "POST", headers: { Authorization: "Bearer " + API_KEY } },
);File & metadata specs
We screen every master before delivery, so files that fail store specs are caught here rather than weeks later at a DSP. Send the original master, not an MP3 re-encoded as WAV — the screening detects it.
Release dates. Send every release at least 9 days before its release date. The API checks dates at creation and again at final submission. Free requires 18 working days, Pro 9 working days, and Label 10 working days; PayOnce requires 9 calendar days through the API. Working days are Monday–Friday, excluding submission day. Store editorial teams need that lead time to consider a pitch.
Mandatory credits. Every track needs composers with full legal names — not stage names, not initials. Stores reject deliveries with missing songwriter credits, so we reject them first with a clear field error.
Identifiers. Send your own upc and isrc if you have them; leave them out and we assign them free of charge at approval.
Content-Type. Always send the real media type on uploads. We also infer it from the reserved filename, which is what keeps masters directly playable after assembly.
Size 3000 x 3000 px (square, 1:1)
Format JPG or PNG, RGB (never CMYK)
Max 95 MiB in a single PUT
Rejected blurry or upscaled images, URLs,
social handles, prices, barcodes,
store logos (Spotify, Apple, etc.),
placeholder or "coming soon" artFormat WAV or FLAC (lossless, stereo)
Bit depth 16-bit or 24-bit
Sample 44.1 kHz or 48 kHz
True peak -0.1 dBTP or lower
Size up to 95 MiB single PUT,
up to 150 MB total (150,000,000 bytes)
Content-Type: audio/wav / audio/flacUPC & ISRC sync
Release events carry the release's status, UPC and every track's ISRC, and release.codes_assigned fires the moment a UPC or ISRC is first known or changes — so most integrations never need to call back. They are still a summary, not the full record.
The authoritative copy is GET /v1/releases/{id}. It returns the assigned upc, each track's isrc in tracks[].isrc, the current status and deliveryState, screening results, and fresh signed download links.
Missing codes are assigned at Approved; caller-supplied codes may already be present before then. Treat them as nullable in your schema, and re-hydrate on release.codes_assigned.
No webhook endpoint yet? Poll GET /v1/releases?status=Approved on a schedule instead — but webhooks are cheaper for both of us.
// Webhooks are notifications, not the record of truth.
app.post("/distribeats-webhook", async (req, res) => {
if (!verify(req, req.rawBody, SECRET)) return res.sendStatus(401);
res.json({ ok: true }); // ack first, work after
const releaseId = req.body.data.release.id;
const full = await fetch(
"https://distribeats.com/api/public/v1/releases/" + releaseId,
{ headers: { Authorization: "Bearer " + API_KEY } },
).then((r) => r.json());
await db.releases.update(releaseId, {
status: full.status,
upc: full.upc, // may be null before approval
tracks: full.tracks.map((t) => ({
id: t.id,
isrc: t.isrc, // assigned at approval
})),
});
});DDEX feeds
GET/v1/releases/{id}/ddex?format=xml&version=4.2
We compile ERN 3.8.2 and ERN 4.2 the moment a release is approved and store both, alongside a JSON mirror for teams who would rather not parse XML. Requests read the stored file, so a feed download never waits on generation.
Contributor credits map to DDEX roles automatically: songwriters and composers become Composer, mixing and mastering engineers become Mixer and MasteringEngineer, and any extra performer keeps the instrument you named.
Add redirect=true to receive a 302 to a signed storage URL instead of the body — handy for piping straight into your own pipeline.
curl "https://distribeats.com/api/public/v1/releases/rel_9f2c41/ddex?version=4.2" \
-H "Authorization: Bearer $DISTRIBEATS_API_KEY" \
-o ern-4.2.xml
# JSON mirror, same data
curl "https://distribeats.com/api/public/v1/releases/rel_9f2c41/ddex?format=json" \
-H "Authorization: Bearer $DISTRIBEATS_API_KEY"Artists
GET/v1/artists?limit=25
Returns the artists attached to your workspace with their plan, approval state and, where known, their Spotify and Apple artist identifiers. Pass an artist id as artistId when creating a release so the catalogue lands on the right roster.
curl https://distribeats.com/api/public/v1/artists \
-H "Authorization: Bearer $DISTRIBEATS_API_KEY"Webhooks
Add an endpoint in your workspace and pick the events you want. We sign every call and retry a failure after one minute, five minutes, thirty minutes, two hours and twelve hours; after that the delivery is marked dead in your log. Pausing or deleting an endpoint stops its pending deliveries. Each status change is sent once, in order — if several steps happen close together, the approval and delivery steps in between are still sent.
release.submittedAn artist or your API sends a release for review.release.screening_passedCopyright and AI screening cleared the release (sent before the approval it leads to).release.under_reviewThe release is held for a manual check (status Under Processing).release.approvedQuality control approved it. The DDEX feed exists from this moment.release.rejectedQuality control sent it back, with the reason in release.rejectionReason.release.deliveredThe release was handed to the stores (Sent to Stores).release.store_liveThe release is live on stores (also when Sent to Stores reaches its release date).release.takedown_requestedA takedown was requested for a delivered release; staff will remove it.release.takedown_completedThe release was removed from stores (Taken Down).release.codes_assignedThe UPC or a track ISRC became known or changed. Read release.upc and release.tracks[].isrc.release.store_linksStore URLs (DistriLinks) were found or changed after release.report.streamsStream figures for your releases were imported or updated: one report per import per workspace (very large imports arrive in parts).report.royaltiesA royalty statement for your releases was imported: period, currency and amounts per release and ISRC.Reply with any 2xx as soon as you have stored the event, then do your work afterwards. You can also push your own events to us at POST /v1/webhooks/inbound; we accept them instantly and process them out of band.
Schema notes that trip people up: event ids always begin evt_, livemode follows the release (false for releases made with a test key), and upc and isrc are null until assigned — so type them as nullable, not optional. Deliveries are at-least-once: key your handler on the event id (also sent as X-DistriBeats-Event-Id), which stays the same across retries and endpoints.
Reject a timestamp more than five minutes old, compare signatures in constant time, and verify against the raw request body — re-serialised JSON will never match.
import { createHmac, timingSafeEqual } from "crypto";
export function verify(req, rawBody, secret) {
const ts = req.headers["x-distribeats-timestamp"];
const sig = String(req.headers["x-distribeats-signature"] || "")
.replace("sha256=", "");
if (Math.abs(Date.now() - Number(ts)) > 5 * 60_000) return false;
const expected = createHmac("sha256", secret)
.update(`${ts}.${rawBody}`)
.digest("hex");
const a = Buffer.from(sig);
const b = Buffer.from(expected);
return a.length === b.length && timingSafeEqual(a, b);
}{
"id": "evt_8a1f",
"object": "event",
"type": "release.approved",
"created": 1790000000,
"occurredAt": "2026-09-21T14:13:20.000Z",
"livemode": true,
"data": {
"release": {
"object": "release",
"id": "rel_9f2c41",
"title": "Neon Monsoon",
"artist": "Aarav Sen",
"displayArtist": "Aarav Sen",
"label": "Monsoon Tapes",
"status": "Approved",
"releaseDate": "2026-10-02",
"upc": "0196922334455",
"tracks": [
{ "id": "rel_9f2c41-t1", "title": "Neon Monsoon", "isrc": "INA0S2600001" }
],
"rejectionReason": null,
"takedownRequested": false
}
}
}{
"type": "report.streams",
"data": {
"report": {
"object": "report", "type": "streams", "id": "rpt_1c2d",
"period": { "from": "2026-10-01", "to": "2026-10-31" },
"totalStreams": 48210, "part": 1, "parts": 1,
"releases": [{
"releaseId": "rel_9f2c41", "upc": "0196922334455",
"streams": 48210,
"tracks": [{ "isrc": "INA0S2600001", "streams": 48210 }]
}]
}
}
}{
"type": "report.royalties",
"data": {
"report": {
"object": "report", "type": "royalties", "id": "rpt_9a8b",
"period": { "from": "2026-09", "to": "2026-09" },
"currency": "USD", "payable": false,
"totals": [{ "currency": "USD", "amount": "112.4031" }],
"releases": [{
"releaseId": "rel_9f2c41",
"amounts": [{ "currency": "USD", "amount": "112.4031" }],
"tracks": [{ "isrc": "INA0S2600001", "currency": "USD", "amount": "112.4031" }]
}]
}
}
}Required fields
Every field required in the DistriBeats release form is required by the API, so a release created here is complete for delivery with no manual fixes. Fetch GET /catalogue for accepted genres, languages, stores and performer roles instead of hard-coding them.
Release: title, displayArtist, releaseType, genre, subGenre, language, releaseDate, pLine and cLine as "YYYY Owner", stores, exactly one of spotifyArtistUrl or newSpotifyArtist: true, attestations.age and attestations.rights, and artwork (coverArtUrl or upload links).
Every track: title, language, compositionType (Original or Cover), copyrightType (Original or Licensed), aiUsage (None, Partial or Full), composers, writers (unless instrumental), producers, mixingEngineers and masteringEngineers as full legal names, at least one performer in contributors, and audio.
Instrumental tracks: for a track with no vocals send instrumental: true and language: "Instrumental". Either one sets the other, and writers are not required.
Registered label: label must be one of your account's usable registered label names (matched case-insensitively). Label plans include one name and can hold up to three, each extra on a yearly add-on; register them in Console → Label HQ → Label names — never through the API. Omit label to use the main name; an unregistered or lapsed name is rejected with 422. This does not lock the © or ℗ rights owners.
Proof: Cover or Licensed tracks need rightsProofUrl. If any track uses AI, send aiDocumentUrl (an https PDF of commercial-use rights); YouTube Content ID is not available for AI tracks. A single's release title must equal its track title — put mix info in version.
GET https://distribeats.com/api/public/v1/catalogue
// genres, languages, stores, performerRoles,
// releaseTypes, aiUsage, compositionTypes, recordingRightsErrors & limits
400 malformed body · 401 missing or invalid key · 403 key lacks the scope · 404 not in your workspace · 409 feed not ready · 413 payload too large · 422 validation failed · 429 rate limited.
Limits are assigned per workspace and key: Starter test/live 60/300, Growth 120/600, Enterprise 300/2400 requests per minute; custom allowances may differ. Authenticated responses carry X-RateLimit-Remaining and X-RateLimit-Reset. Need more? Ask and we will raise your ceiling.
{
"error": {
"type": "invalid_request_error",
"message": "tracks[0].composers is required — stores reject deliveries without songwriter credits.",
"field": "tracks[0].composers"
}
}Changelog
2026-11-06. New events release.under_review, release.takedown_requested, release.codes_assigned, release.store_links, report.streams and report.royalties; release.screening_passed is now sent. Release events carry artist, label, UPC, track ISRCs, rejection reason and occurredAt. Added GET /reports/streams and GET /reports/royalties. Release status matches the console. Takedown withdraws undelivered releases. label accepts any usable registered label name.
2026-10-16. POST /releases now requires every release-form field. Added GET /catalogue and the fields aiUsage, compositionType, copyrightType, rightsProofUrl, writers, aiDocumentUrl, spotifyArtistUrl, newSpotifyArtist and attestations. The Compilation release type was removed. Unsupported genres, languages and stores now return 422 with the offending field.
2026-09-01. First public version: releases, uploads, DDEX feeds, artists and webhooks.
Troubleshooting
"Server error" on create. Check the exact base URL and all required fields. Redirects may discard a POST body; send to https://distribeats.com/api/public/v1 directly.
401 authentication_error. The key was revoked, truncated, or copied without its prefix. Keys are shown once; mint a new one in Developer & API.
403 on an endpoint you expected. Either the key lacks that scope, or API access is not yet enabled for the workspace. Access is granted per workspace by our team once the Enterprise agreement for the DistriBeats API is signed; Rising Label and Macro Label workspaces can request it.
409 files_missing on submit. One or more uploads never completed. The response lists exactly which, with their upload links.
413 on an upload. A single request exceeded 95 MiB, or the assembled file exceeded 150 MB. Chunking handles the request limit, not the total file limit.
Track downloads but will not play. The upload was stored without a media type. Re-send with Content-Type: audio/wav; reserved filenames are also inferred automatically now.
Webhook never arrives. Check your endpoint answers 2xx within ten seconds and is reachable from the public internet. Failures retry after 1 min, 5 min, 30 min, 2 h and 12 h; the delivery log in Developer & API shows every attempt and its response body.
Still stuck? Send us the release id, the delivery id, and the timestamp. With those three we can read the exact request you made.
Base URL must be exactly:
https://distribeats.com/api/public/v1
Do not use redirects or an alternate API host.
Always send Idempotency-Key on create.
Always send the real Content-Type on uploads.
Keep the release id we return — it is the
only handle for support.For AI coding tools
The whole API is published as OpenAPI 3.1 and as a plain-text llms.txt reference. Paste either link into Cursor, Windsurf, Claude, ChatGPT or your own agent and it can generate a working client without guesswork.
Use the current OpenAPI schema alongside GET /catalogue for supported values and the required-field guidance above.
https://distribeats.com/developers/llms.txt
https://distribeats.com/api/public/v1/openapi.json