# DistriBeats API > Headless music distribution for labels and distributors. Read releases with masters and artwork, export DDEX ERN 3.8.2 / 4.2 feeds, create releases from your own front end, and receive webhooks as releases move through screening, approval and store delivery. Base URL: https://distribeats.com/api/public/v1 OpenAPI: https://distribeats.com/api/public/v1/openapi.json Docs: https://distribeats.com/developers Version: 2026-10-28 ## Access and pricing The DistriBeats API is an Enterprise add-on, starting at $49.99 / ₹4,999 per month (USD outside India, INR in India; the two prices are set separately, never converted). Status: beta from 8 November 2026. Requires: an active Rising Label (up to 15 artists) or Macro Label plan. Contract: a separate Enterprise agreement. It cannot be bought online: request beta access at https://distribeats.com/developer-api; after the agreement is signed our team grants API access to the workspace. Prefer a branded front end? The White Label Distribution Dashboard starts at $39.99 / ₹3,999 per month: https://distribeats.com/white-label-distribution Overview of both add-ons: https://distribeats.com/enterprise ## Authentication Every request sends a bearer key from your workspace's "Developer & API" tab. Authorization: Bearer db_live_xxxxxxxxxxxx Keys carry scopes: releases:read, releases:write, ddex:export, artists:read, royalties:read, webhooks:manage. Keys starting db_test_ validate payloads and return simulated objects without creating real work. Test keys never read or change live releases: per-release calls with a test key answer 404 and lists are empty. Keys starting db_live_ act on real catalogue. Rate limits are assigned per workspace and key (Starter: test 60/live 300; Growth: test 120/live 600; Enterprise: test 300/live 2400 requests/minute; custom limits may differ). Responses carry X-RateLimit-Limit, X-RateLimit-Remaining and X-RateLimit-Reset; a 429 means slow down and retry after the reset. ## Endpoints GET /releases?limit=25&starting_after=&status=Approved Lists releases, newest first. Response: { object:"list", data:[Release], has_more, next_cursor }. GET /releases/{id} One release, including signed download URLs (valid 30 minutes) for the cover artwork and each master. GET /releases/{id}/ddex?format=xml|json&version=3.8.2|4.2&redirect=true The pre-compiled DDEX ERN message. Feeds are generated when a release is approved, so this is a static read. format=json returns the same data as JSON. redirect=true returns a 302 to a signed storage URL. POST /releases Creates a release. Send Idempotency-Key so a retry never duplicates. Every field required by the DistriBeats release form is required here (fetch GET /catalogue for values): title, displayArtist, releaseType (Single|EP|Album), genre, subGenre, language, releaseDate (YYYY-MM-DD), pLine and cLine ("YYYY Owner"), stores[], exactly one of spotifyArtistUrl or newSpotifyArtist:true, attestations {age:true, rights:true}, coverArtUrl (or requestUploadUrls:true). Per track: title, language, compositionType (Original|Cover), copyrightType (Original|Licensed), aiUsage (None|Partial|Full), composers, writers (unless instrumental), producers, mixingEngineers, Instrumental tracks (no vocals): send instrumental:true AND language:"Instrumental" (either one sets the other); writers are then not required. masteringEngineers (full legal names), contributors[{name,role}] with at least one performer role, audioUrl (or requestUploadUrls:true). Cover/Licensed needs rightsProofUrl. Any AI track needs aiDocumentUrl (https PDF) and YouTube Content ID cannot be selected. A single's release title must equal its track title. Minimum lead time: send the release at least 9 days before releaseDate (some plans require longer). Optional: artistId, originalReleaseDate, upc (12-14 digits), catalogNumber, label, publisher (omit to use the label name), explicit, territories[] (currently ignored: every release is delivered Worldwide). label must be one of the account's usable registered label names (matched case-insensitively). Label plans include one name and can hold up to 3 (each extra name is a yearly add-on); names are registered 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. Rights owners in pLine/cLine remain independent. Track fields: title, version, isrc, iswc, displayArtist, featuring, durationSeconds, previewStart ("0:45"), explicit, instrumental, language, compositionType, copyrightType, rightsProofUrl, aiUsage, composers, writers, producers, mixingEngineers, masteringEngineers, contributors[{name,role}], audioUrl (https), audioFileName. Response 201 (a creation receipt, not the full release): { object:"release", id, release_id (same value), livemode, status ("Pending Upload" | "Submitted"; test keys: "Draft" with simulated:true), title, trackCount, next?, uploadUrls? } Call GET /releases/{id} for everything else. Retrying with the same Idempotency-Key returns the original receipt (200), even if our first answer never reached you. Keys are scoped per workspace AND per key environment: a test key and a live key never share an Idempotency-Key result. With requestUploadUrls:true the response carries uploadUrls: one URL for "cover" and one per track id, and status "Pending Upload". Upload each file with PUT to that URL, sending the SAME Authorization: Bearer header and the raw file bytes as the body. The links never expire and may be reused, so a failed transfer is always retryable. They are served from distribeats.com with open CORS, so browser uploads work. Then call submit. PUT /releases/{id}/upload/{file} file is "cover" or a track id. Headers: Authorization: Bearer , Content-Type of the file. Body: the raw bytes. Returns { uploaded: true }. Single requests are limited to 95 MiB (99,614,720 bytes). Large files (over 95 MiB, e.g. WAV masters) — chunked upload on the same URL: Total assembled file maximum: 150 MB (150,000,000 bytes). The request limit is 95 MiB (99,614,720 bytes); recommended parts are 50 MiB (52,428,800 bytes). Chunking does not bypass the total cap. Send Content-Type: audio/wav when starting the multipart upload. DistriBeats also infers the media type from the reserved filename. 1. POST /releases/{id}/upload/{file}?uploads -> { uploadId, partSize (50 MiB) } 2. PUT /releases/{id}/upload/{file}?uploadId=X&partNumber=N body = bytes of chunk N (1-based, every chunk except the last must be 5–95 MiB) -> { partNumber, etag } 3. POST /releases/{id}/upload/{file}?uploadId=X JSON { "parts": [{ "partNumber": 1, "etag": "..." }, ...] } -> { uploaded: true } Abort: DELETE /releases/{id}/upload/{file}?uploadId=X. A failed chunk can be re-sent with the same partNumber. GET /releases/{id} A release still in "Pending Upload" includes uploadUrls, so you can always resume a failed transfer without creating a second release. POST /releases/{id}/submit Checks every file arrived and sends the release to review (status "Submitted"). If files are missing: 409 files_missing with { missing[], uploadUrls } — upload those and retry. Files and documents you sent by URL (coverArtUrl, tracks[].audioUrl, aiDocumentUrl, tracks[].rightsProofUrl) are copied into our storage at submit. If one cannot be fetched: 422 files_unreadable with problems[] (e.g. "coverArtUrl: HTTP 403"). The same list is on GET /releases/{id} as apiIngestProblems. POST /releases/{id}/takedown Body: { reason }. Not yet at the stores (Draft, Pending Upload, Submitted, Under Processing, Awaiting Payment, Rejected): the release is withdrawn at once -> { status:"Unsubmitted", withdrawn:true }. Delivered: the status is kept and { takedownRequested:true } is returned; staff remove it from stores and the status becomes "Taken Down". Events: release.takedown_requested, then release.takedown_completed. Repeating the call is safe. GET /artists?limit=25&starting_after= Active artists of your workspace, including the workspace owner (workspaceOwner:true) — releases sent without artistId belong to the owner. Use an artist id as artistId when creating a release. GET /reports/streams?limit=10&starting_after=&release_id= (scope releases:read) GET /reports/royalties?limit=10&starting_after=&release_id= (scope royalties:read) Every report.streams / report.royalties payload, newest first: { object:"list", data:[Report], has_more, next_cursor }. Test keys get an empty list (reports describe live catalogue only). POST /webhooks/inbound Push your own events to us. Body: { type, data }. Returns 202 immediately. ## Release statuses Draft, Pending Upload, Awaiting Payment, Submitted, Under Processing, Approved, Sent to Stores, Live, Unsubmitted, Rejected, Takedown Requested, Taken Down, Imported. status is what the console shows: "Sent to Stores" reads "Live" from the release date. Under Processing = held for a manual check. Unsubmitted = withdrawn or returned for changes. Rejected/Unsubmitted/Taken Down carry rejectionReason. takedownRequested:true marks a pending takedown. GET /releases/{id} also returns storeLinks (store URLs found after release) and smartLinkUrl when available. ## Webhooks from DistriBeats Add an endpoint in the workspace Developer & API tab. Each call carries: X-DistriBeats-Event release.approved X-DistriBeats-Event-Id same as the payload id (stable across retries and endpoints) X-DistriBeats-Delivery-Id one per endpoint per event (stable across retries) X-DistriBeats-Timestamp milliseconds since epoch X-DistriBeats-Signature sha256= Verify with HMAC-SHA256 over "." using your endpoint's signing secret, compare in constant time, and reject anything older than five minutes: const expected = createHmac("sha256", secret).update(timestamp + "." + rawBody).digest("hex"); timingSafeEqual(Buffer.from("sha256=" + expected), Buffer.from(signatureHeader)); Reply 2xx within 10 seconds. Anything else is retried after 1m, 5m, 30m, 2h and 12h, then marked dead in the delivery log. Deliveries to an endpoint you pause or delete stop (pending ones are marked dead). Events: release.submitted sent for review release.screening_passed copyright and AI screening cleared (before the approval it leads to) release.under_review held for a manual check (Under Processing) release.approved approved; the DDEX feed exists from now release.rejected sent back; reason in release.rejectionReason release.delivered handed to the stores (Sent to Stores) release.store_live live on stores (also when Sent to Stores reaches its release date) release.takedown_requested takedown requested for a delivered release release.takedown_completed removed from stores (Taken Down) release.codes_assigned UPC or a track ISRC first known or changed release.store_links store URLs found or changed (data.storeLinks, data.smartLinkUrl) report.streams stream figures imported: data.report, one per import per workspace report.royalties royalty statement imported: data.report Each transition is sent once. If several steps happen between two checks, the skipped approval/delivery steps are still sent, in order. Payload: { id:"evt_…", object:"event", type, created (unix s), occurredAt (ISO), livemode, data }. livemode is the release's own environment. Release events carry data.release: { object:"release", id, title, artist, displayArtist, label, status, releaseDate, upc, tracks:[{ id, title, isrc }], rejectionReason, takedownRequested } upc and isrc may be null. Example: { "id":"evt_5b1f0c2a9e7d4c3b8a6f2e1d0c9b8a7f", "object":"event", "type":"release.approved", "created":1762412400, "occurredAt":"2026-11-06T07:00:00.000Z", "livemode":true, "data":{ "release":{ "object":"release", "id":"api_3f9c0b7e2d1a4c55b6e8f012", "title":"Neon Monsoon", "artist":"Aarav Sen", "displayArtist":"Aarav Sen", "label":"Monsoon Tapes", "status":"Approved", "releaseDate":"2026-11-14", "upc":"8905285301234", "tracks":[{ "id":"t1", "title":"Neon Monsoon", "isrc":"INA0S2600001" }], "rejectionReason":null, "takedownRequested":false } } } report.streams data.report: { "object":"report", "type":"streams", "id":"rpt_…", "livemode":true, "period":{ "from":"2026-10-01", "to":"2026-10-31" }, "importedAt":"…", "part":1, "parts":1, "totalStreams":48210, "releases":[{ "releaseId":"api_…", "title":"Neon Monsoon", "artist":"Aarav Sen", "upc":"8905285301234", "streams":48210, "period":{ "from":"2026-10-01", "to":"2026-10-31" }, "platforms":{ "Spotify":30120 }, "tracks":[{ "isrc":"INA0S2600001", "streams":48210 }] }] } period.from null = lifetime to date. platforms and tracks appear when the import has them. report.royalties data.report: { "object":"report", "type":"royalties", "id":"rpt_…", "livemode":true, "period":{ "from":"2026-09", "to":"2026-09", "periods":["2026-09"] }, "currency":"USD", "importedAt":"…", "payable":false, "totals":[{ "currency":"USD", "amount":"112.4031" }], "releases":[{ "releaseId":"api_…", "title":"Neon Monsoon", "artist":"Aarav Sen", "upc":"8905285301234", "amounts":[{ "currency":"USD", "amount":"112.4031" }], "tracks":[{ "trackId":"t1", "isrc":"INA0S2600001", "currency":"USD", "amount":"112.4031" }] }] } Amounts are decimal strings. Figures are informational (payable:false); payouts follow your agreement. ## Errors { "error": { "type": "invalid_request_error", "message": "...", "field": "tracks[0].composers" } } 400 malformed body · 401 bad or missing key · 403 missing scope · 404 not in your workspace · 409 feed not ready · 413 payload too large · 422 validation failed · 429 rate limited. ## Examples curl: curl https://distribeats.com/api/public/v1/releases?limit=5 \ -H "Authorization: Bearer $DISTRIBEATS_API_KEY" TypeScript: 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: "2027-01-15", // choose a date meeting today's account/API deadline pLine: "2027 Monsoon Tapes", cLine: "2027 Monsoon Tapes", stores: ["Spotify"], newSpotifyArtist: true, attestations: { age: true, rights: true }, 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" }], }], }), }); const release = await res.json(); Python: import os, requests r = requests.get( "https://distribeats.com/api/public/v1/releases/rel_123/ddex", params={"version": "4.2"}, headers={"Authorization": f"Bearer {os.environ['DISTRIBEATS_API_KEY']}"}, timeout=30, ) open("ern.xml", "wb").write(r.content) ## Lifecycle and editing rules Editable: Draft, Pending Upload. Re-PUT any upload URL to overwrite a file; re-reading GET /releases/{id} returns the uploadUrls again so an interrupted transfer can resume. Never create a second release to retry. Uploads to a release in any other status answer 409 release_locked. Locked: Submitted and everything after. There is no PATCH endpoint for a submitted release, by design — screening fingerprints the exact audio, moderation reads the exact metadata, and from Approved the DDEX feed is compiled and delivered to stores. Corrections while Submitted or Under Processing: ask support to return it to draft (the audio scan is invalidated, then re-submit). Corrections once Live: POST /releases/{id}/takedown and deliver a corrected release. ## Identifier sync release.codes_assigned carries the UPC and every track ISRC as soon as they are known or change; GET /releases/{id} returns the same values. Either may be present before approval if supplied by the caller; type both nullable. Without a webhook endpoint, poll GET /releases?status=Approved instead. ## File and metadata specs Artwork: 3000x3000 px square, JPG or PNG, RGB (not CMYK). No URLs, social handles, prices, barcodes or store logos. No upscaled or placeholder art. Audio: WAV or FLAC, stereo lossless, 16- or 24-bit, 44.1 or 48 kHz, true peak <= -0.1 dBTP. Do not re-encode an MP3 as WAV; screening detects it. Release date lead time by plan: Free 18 working days, Pro 9 working days, Label 10 working days, Pay-Once 9 calendar days through the API (the ordinary console minimum remains 8). Count working days Monday–Friday, excluding the submission day; no holiday calendar is applied. Dates are checked at creation and again at final submission. Composers, writers, mixing and mastering engineers must be full legal names on every track. ## Changelog 2026-11-06: new events release.under_review, release.takedown_requested, release.codes_assigned, release.store_links, report.streams, report.royalties; release.screening_passed is now sent. Release event payloads carry artist, label, upc, tracks[{id,title,isrc}], rejectionReason and occurredAt; livemode follows the release. Added GET /reports/streams and /reports/royalties. Release status now matches the console (adds Pending Upload, Under Processing, Unsubmitted, Takedown Requested; In Review is never used). GET /releases/{id} adds storeLinks, smartLinkUrl, rejectionReason, takedownRequested, apiIngestProblems. /artists includes the workspace owner. 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; added aiUsage, compositionType, copyrightType, rightsProofUrl, writers, aiDocumentUrl, spotifyArtistUrl/newSpotifyArtist and attestations. Removed Compilation release type. Unsupported genres, languages and stores are rejected with 422. upc/isrc: send your own or omit them and we assign them free at approval. ## Troubleshooting "Server error" on create -> verify the exact base URL https://distribeats.com/api/public/v1 and required fields; do not use redirects or an alternate API host. 401 -> key revoked, truncated, or missing its prefix. Mint a new one. 403 -> missing scope, or API access not yet enabled for the workspace (granted per workspace once the Enterprise agreement is signed; Rising Label and Macro Label workspaces can create and submit API releases). 409 files_missing -> response lists the missing files and their upload URLs; send them and submit again. 413 -> single request over 95 MiB: use chunking. Total file over 150 MB: reduce the master size; chunking cannot bypass this cap. Audio downloads but will not play -> uploaded without a media type; re-send with Content-Type: audio/wav. Webhook never arrives -> endpoint must answer 2xx within 10s and be publicly reachable; see the delivery log. Support needs three things: release id, delivery id, timestamp. ## Notes for code generators - Always send Idempotency-Key on POST /releases. - composers is mandatory on every track; stores reject deliveries without songwriter credits. - Signed media URLs expire; fetch them again rather than storing them. - Webhook deliveries are at-least-once; dedupe on the event id (payload id / X-DistriBeats-Event-Id). - Verify webhook signatures against the raw body, never re-serialised JSON. - Never place an API key in browser code. Call the API from your server.