BTRMUSIC Partners
BTR Music / Partner platform / API v1

Deliver music.
Track everything.

The integration guide for distributors, aggregators and labels. Submit DDEX ERN packages, resolve artist links, monitor releases, handle takedowns and download usage reports.

Base URLhttps://partners.btrmusic.com/api/v1
AuthenticationBearer API key
DeliveryERN 4.3.x ZIP via API
RecommendedERN 4.3.2
Jump to a section
Code examples · choose a language
01 / Before you build

How the integration works

Use JSON over HTTPS at https://partners.btrmusic.com/api/v1. Upload a ZIP containing the DDEX ERN XML, audio and artwork. The API responds 202; validation runs asynchronously. Poll the delivery or listen for webhooks, then monitor the resulting release through BTR review and publication.

01 / AccessGet a scoped key

Apply, then create a sandbox key in the portal.

02 / SubmitUpload the ERN ZIP

One package through the Partner API.

03 / ResolveCheck delivery status

Read issues and connect an existing artist when required.

04 / OperateWatch the release

BTR reviews publication; your team handles updates, takedowns and reports.

Current integration boundaryDelivery is API ZIP upload. There is no SFTP channel. Submit validated tracks with POST /api/v1/releases/{upc}/submit (catalog:write); BTR humans still approve them. Artist connections use existing BTR Music accounts and require BTR admin approval. A staged delivery is not a live release. Sandbox is onboarding — deliver, validate and preview; submission requires a live account with an executed agreement.

Two different lifecycles

Delivery: package intake and validation.

received→processing→staged

Other delivery outcomes are needs_artist, rejected (no release written) and failed (inspect and retry).

Release: artist resolution, review and BTR-side publication.

needs_artist / review→pending→live

Releases may also be queued, publishing, publish_failed, takedown or rejected. Their state is separate from delivery state.

01b / Working with BTR

Partnering

Everything a distributor, aggregator or label needs before applying — commercial terms, reporting, onboarding, artist identity, content rules, contacts and catalogue scope. No email required to get started.

1. Commercial model

BTR pays the partner royalties on qualified streams of partner-delivered catalogue, settled monthly in USD from the usage report. The rate, basis (per-stream or revenue share), payment terms, minimum threshold and any adjustments are set in your Order Form. There is no integration or delivery fee.

2. Usage reporting

Monthly, per partner, in USD, as a DSR flat file (DSR-FlatFile/BasicAudioProfile/BTR-1.0). A usage is a qualified stream: 60 seconds of playback for a standard-length track (scaled for very short or very long tracks), counted per stream. A month closes 14 days after month-end and is then final; pull it from the API or the portal. See DSR_LAYOUT.md and the sample report.

3. Onboarding and certification

Apply, and BTR approves a sandbox account: deliver, validate and preview, but not submit. Certify against the certification checklist with a representative release — an ERN 4.3.x package with full metadata, audio and artwork, valid ISRC/UPC and duplicate handling, territories and deal windows, a redelivery, a takedown and one connected artist. Once the Order Form is executed, BTR switches the account on, you mint a live key, and you deliver. There is no waiting period after certification.

4. Artist connections and needs_artist

Every release attaches to an artist's BTR Music account. Request a connection in the portal or with POST /api/v1/artists using the artist's BTR username and account email; BTR approves it. Map a release in the portal or with POST /api/v1/releases/{upc}/artist, and the delivery re-queues automatically — no redelivery needed. Carry the BTR user ID as the artist PartyId (ProprietaryId Namespace="BTR") to avoid the needs_artist hold. Artists without an account create one at beatstorapon.com. There are no proxy or managed identities.

5. Content integrity and artificial streaming

Only qualified streams are reported and settled. Every track passes BTR's content-integrity scan and human moderation before publication, and partner catalogue is subject to the same rights, abuse and repeat-infringer controls as direct uploads. BTR may hold or remove content and withhold settlement on usage under investigation. See the Partner Terms.

6. Contacts

Partnerships and technical: malcolm.fitzgerald@btrmusic.com. Operations and takedown escalation: contact@btrmusic.com. Takedowns are self-serve and automated (API, PurgeReleaseMessage, portal) — removal from all BTR Music surfaces normally within five minutes, 24/7. Escalations are acknowledged within one business day (AEST).

7. Catalogue scope

Full back catalogue plus new releases. No size cap and no new-releases-only restriction — deliver at your own pace after go-live.

Fan-support featuresDirect fan-support features pay the artist's own BTR account and sit outside the partner agreement. See beatstorapon.com/artists.
02 / First working request

Quickstart

Create a sandbox key in the portal, give it deliveries:write and catalog:read, and set it in your environment. Test keys begin btrp_test_; live keys begin btrp_live_. Sandbox is for onboarding — deliver, validate and preview; submitting tracks for approval requires a live account with an executed agreement.

export BTR_API_KEY='btrp_test_xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx'

1. Confirm your account

GET /me tells you which partner and scopes the key belongs to, plus current limits and accepted ERN versions.

curl -sS -H "Authorization: Bearer $BTR_API_KEY" \
  https://partners.btrmusic.com/api/v1/me
package main
import ("fmt"; "io"; "net/http"; "os")
func main() {
  req, err := http.NewRequest(http.MethodGet, "https://partners.btrmusic.com/api/v1/me", nil)
  if err != nil { panic(err) }
  req.Header.Set("Authorization", "Bearer "+os.Getenv("BTR_API_KEY"))
  res, err := http.DefaultClient.Do(req)
  if err != nil { panic(err) }
  defer res.Body.Close()
  body, err := io.ReadAll(res.Body)
  if err != nil { panic(err) }
  if res.StatusCode >= 300 { fmt.Fprintln(os.Stderr, res.Status, string(body)); os.Exit(1) }
  fmt.Println(string(body))
}
import os
import requests
r = requests.get("https://partners.btrmusic.com/api/v1/me", headers={
    "Authorization": "Bearer " + os.environ["BTR_API_KEY"]}, timeout=30)
r.raise_for_status()
print(r.json())
const res = await fetch("https://partners.btrmusic.com/api/v1/me", {
  headers: { Authorization: `Bearer ${process.env.BTR_API_KEY}` }
});
if (!res.ok) throw new Error(`HTTP ${res.status}: ${await res.text()}`);
console.log(await res.json());
using System.Net.Http;
using System.Net.Http.Headers;
using var http = new HttpClient();
http.DefaultRequestHeaders.Authorization = new AuthenticationHeaderValue(
    "Bearer", Environment.GetEnvironmentVariable("BTR_API_KEY"));
var res = await http.GetAsync("https://partners.btrmusic.com/api/v1/me");
res.EnsureSuccessStatusCode();
Console.WriteLine(await res.Content.ReadAsStringAsync());
Example response / HTTP 200
{
  "partner": {"slug":"your-slug","name":"Your Distribution","status":"active","sandbox":true,"dpid":"PADPIDA…"},
  "key": {"name":"ci","prefix":"btrp_test_abc","scopes":["deliveries:write","catalog:read"],"sandbox":true},
  "limits": {"max_upload_bytes":2147483648,"api_rate_per_min":120,"upload_rate_per_min":20},
  "ern": {"accepted":["4.3.x"],"recommended":"4.3.2","rejected":["3.x","4.0","4.1","4.2"]}
}

2. Upload a release

Send the ERN ZIP in multipart field file. This needs deliveries:write. Expect 202 with a delivery ref and Location header.

curl -sS -X POST https://partners.btrmusic.com/api/v1/deliveries \
  -H "Authorization: Bearer $BTR_API_KEY" \
  -F file=@release.zip
package main
import ("fmt"; "io"; "mime/multipart"; "net/http"; "os")
func main() {
  f, err := os.Open("release.zip"); if err != nil { panic(err) }; defer f.Close()
  r, w := io.Pipe()
  form := multipart.NewWriter(w)
  go func() {
    part, e := form.CreateFormFile("file", "release.zip")
    if e == nil { _, e = io.Copy(part, f) }
    if e == nil { e = form.Close() }
    w.CloseWithError(e)
  }()
  req, err := http.NewRequest(http.MethodPost, "https://partners.btrmusic.com/api/v1/deliveries", r)
  if err != nil { panic(err) }
  req.Header.Set("Authorization", "Bearer "+os.Getenv("BTR_API_KEY"))
  req.Header.Set("Content-Type", form.FormDataContentType())
  res, err := http.DefaultClient.Do(req); if err != nil { panic(err) }; defer res.Body.Close()
  body, err := io.ReadAll(res.Body); if err != nil { panic(err) }
  if res.StatusCode != http.StatusAccepted { fmt.Fprintln(os.Stderr, res.Status, string(body)); os.Exit(1) }
  fmt.Println(string(body))
}
import os
import requests
with open("release.zip", "rb") as package:
    r = requests.post("https://partners.btrmusic.com/api/v1/deliveries",
        headers={"Authorization": "Bearer " + os.environ["BTR_API_KEY"]},
        files={"file": ("release.zip", package, "application/zip")}, timeout=300)
r.raise_for_status()
print(r.json())  # delivery.ref
import { openAsBlob } from "node:fs";
const form = new FormData();
form.set("file", await openAsBlob("release.zip"), "release.zip");
const res = await fetch("https://partners.btrmusic.com/api/v1/deliveries", {
  method: "POST", headers: { Authorization: `Bearer ${process.env.BTR_API_KEY}` }, body: form
});
if (!res.ok) throw new Error(`HTTP ${res.status}: ${await res.text()}`);
console.log(await res.json());
using System.Net.Http;
using System.Net.Http.Headers;
using var http = new HttpClient();
http.DefaultRequestHeaders.Authorization = new AuthenticationHeaderValue(
    "Bearer", Environment.GetEnvironmentVariable("BTR_API_KEY"));
using var form = new MultipartFormDataContent();
using var file = File.OpenRead("release.zip");
form.Add(new StreamContent(file), "file", "release.zip");
var res = await http.PostAsync("https://partners.btrmusic.com/api/v1/deliveries", form);
res.EnsureSuccessStatusCode();
Console.WriteLine(await res.Content.ReadAsStringAsync());
Example response / HTTP 202 / Location: /api/v1/deliveries/{ref}
{"delivery":{"ref":"0a9e1f2c-4b6d-4e8a-9f10-1c2d3e4f5a6b","source":"api","file_name":"release.zip","status":"received","created_at":"2026-09-25T02:14:00Z"}}

3. Check validation, then watch publication

Poll /deliveries/{ref} until it reaches staged, needs_artist, rejected or failed. If staged, read the release separately until it reaches live. A delivery can be accepted while the release still awaits review.

curl -sS https://partners.btrmusic.com/api/v1/deliveries/0a9e1f2c-4b6d-4e8a-9f10-1c2d3e4f5a6b \
  -H "Authorization: Bearer $BTR_API_KEY"
package main
import ("fmt"; "io"; "net/http"; "os")
func main() {
  req, err := http.NewRequest(http.MethodGet, "https://partners.btrmusic.com/api/v1/deliveries/0a9e1f2c-4b6d-4e8a-9f10-1c2d3e4f5a6b", nil)
  if err != nil { panic(err) }
  req.Header.Set("Authorization", "Bearer "+os.Getenv("BTR_API_KEY"))
  res, err := http.DefaultClient.Do(req); if err != nil { panic(err) }; defer res.Body.Close()
  body, err := io.ReadAll(res.Body); if err != nil { panic(err) }
  if res.StatusCode >= 300 { fmt.Fprintln(os.Stderr, res.Status, string(body)); os.Exit(1) }
  fmt.Println(string(body))
}
import os
import requests
r = requests.get("https://partners.btrmusic.com/api/v1/deliveries/0a9e1f2c-4b6d-4e8a-9f10-1c2d3e4f5a6b", headers={"Authorization": "Bearer " + os.environ["BTR_API_KEY"]}, timeout=30)
r.raise_for_status()
print(r.json())
const res = await fetch("https://partners.btrmusic.com/api/v1/deliveries/0a9e1f2c-4b6d-4e8a-9f10-1c2d3e4f5a6b", { headers: { Authorization: `Bearer ${process.env.BTR_API_KEY}` } });
if (!res.ok) throw new Error(`HTTP ${res.status}: ${await res.text()}`);
console.log(await res.json());
using System.Net.Http;
using System.Net.Http.Headers;
using var http = new HttpClient();
http.DefaultRequestHeaders.Authorization = new AuthenticationHeaderValue(
    "Bearer", Environment.GetEnvironmentVariable("BTR_API_KEY"));
using var res = await http.GetAsync("https://partners.btrmusic.com/api/v1/deliveries/0a9e1f2c-4b6d-4e8a-9f10-1c2d3e4f5a6b");
res.EnsureSuccessStatusCode();
Console.WriteLine(await res.Content.ReadAsStringAsync());
03 / Security

Authentication & scopes

Every route except /health needs an API key. Send Authorization: Bearer <key> or X-API-Key: <key>. Use HTTPS only; keys are stored as SHA-256 hashes. The full key appears once at creation; the portal then shows only its 15-character prefix.

Authorization: Bearer btrp_live_xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx
# Alternative: X-API-Key: btrp_live_xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx

Key formats are btrp_live_<40> and btrp_test_<40>. Create, scope and revoke keys in the portal. To rotate a key, create a new one, deploy it, then revoke the old one; there is no rotate endpoint or key IP allowlist.

ScopePermission
deliveries:writeUpload deliveries.
catalog:readRead deliveries, releases and artists.
catalog:writeRequest artist connections and map release artists.
takedown:writeTake a release down.
reports:readGenerate, list and download reports.

/me and /reference/genres need any valid key. A 401 means the key is missing, unknown, revoked or expired; a 403 means a valid key is not permitted by scope, account state or environment.

04 / Platform rules

Conventions

TopicRule
FormatJSON UTF-8 for API objects; UTC ISO 8601 timestamps; territories use ISO 3166-1 alpha-2 or Worldwide.
ISRC12 characters without dashes on the wire; format CC-XXX-YY-NNNNN.
Release identifierUPC/EAN (ICPN), 12–14 digits; GRid is accepted as an alternative. {upc} routes also accept the release id.
Artist identifierbtr_user_id is a 24-character hex music_app.users._id.
PaginationOffset-based limit and offset. Deliveries: 50 default, 200 max. Releases: 100 default, 500 max. Artists and reports are not paginated.
Rate limits120 API requests and 20 uploads per minute per key. GET /me shows your actual limits. A 429 includes Retry-After in seconds; no X-RateLimit-* headers.

Idempotency & retry

There is no Idempotency-Key header. The ERN MessageId identifies a delivery; re-sending an accepted message is skipped with an ERN.DUPLICATE warning. Use a new MessageId for corrected content. Takedown and artist mapping are idempotent. Back off on 429 and 5xx; inspect asynchronous delivery errors before resubmitting.

05 / Debugging

Errors

Failing calls use one JSON envelope. Branch on error.code, not the message. Some errors include details. There is no request-id response header; give support the route, UTC time and key prefix, never your full key.

{"error":{"code":"insufficient_scope","message":"this key lacks the catalog:read scope","details":{"scopes":["reports:read"]}}}

Statuses: 200 success; 201 artist request; 202 accepted delivery; 400 bad request; 401 authentication; 403 permission; 404 not found; 409 conflict; 415 unsupported media type; 429 rate limit; 5xx server. Oversized uploads currently return 400 upload_failed; 412, 413 and 422 are not used.

HTTPError codeMeaningRetry?
400bad_requestMalformed body or invalid/missing field.After correction
400upload_failedUnreadable upload or size cap exceeded.After correction
400bad_periodInvalid YYYY-MM period.After correction
400assign_failedArtist mapping failed.After correction
400artist_create_failedAccount mismatch or artist connected elsewhere.After correction
401unauthenticated, invalid_key, expired_keyMissing/malformed, unknown/revoked or expired key.After fixing key
403insufficient_scope, sandbox_keyWrong scope or environment.After fixing access
403partner_pending, partner_suspendedPartner not active.After account review
403partner_sandboxPartner is in onboarding or has no executed agreement; submissions are not accepted.After account review
404not_foundUnknown route, delivery, release or report.No
409period_not_closedReporting period still open.After close
415unsupported_media_typeUnsupported upload Content-Type.After correction
429rate_limitedLimit exceeded.Use Retry-After
500internalUnexpected server error.Back off
Endpoint group / Catalogue intake

Deliveries

One delivery is one ERN package. 202 means accepted for processing, not accepted for publication.

Create a delivery

POST/deliveriesdeliveries:write

Send multipart field file or a raw application/zip body with optional X-File-Name. The examples use both supported upload methods. The account's max_upload_bytes defaults to 2 GB. Response 202 includes delivery and a Location header.

curl -sS -X POST https://partners.btrmusic.com/api/v1/deliveries \
  -H "Authorization: Bearer $BTR_API_KEY" -F file=@release.zip
# Raw body alternative:
curl -sS -X POST https://partners.btrmusic.com/api/v1/deliveries \
  -H "Authorization: Bearer $BTR_API_KEY" \
  -H "Content-Type: application/zip" -H "X-File-Name: release.zip" \
  --data-binary @release.zip
package main
import ("fmt"; "io"; "net/http"; "os")
func main() {
  f, err := os.Open("release.zip"); if err != nil { panic(err) }; defer f.Close()
  req, err := http.NewRequest(http.MethodPost, "https://partners.btrmusic.com/api/v1/deliveries", f)
  if err != nil { panic(err) }
  req.Header.Set("Content-Type", "application/zip")
  req.Header.Set("X-File-Name", "release.zip")
  req.Header.Set("Authorization", "Bearer "+os.Getenv("BTR_API_KEY"))
  res, err := http.DefaultClient.Do(req); if err != nil { panic(err) }; defer res.Body.Close()
  body, err := io.ReadAll(res.Body); if err != nil { panic(err) }
  if res.StatusCode != 202 { fmt.Fprintln(os.Stderr, res.Status, string(body)); os.Exit(1) }
  fmt.Println(string(body))
}
import os
import requests
with open("release.zip", "rb") as package:
    r = requests.post("https://partners.btrmusic.com/api/v1/deliveries", headers={"Authorization": "Bearer " + os.environ["BTR_API_KEY"]}, files={"file": ("release.zip", package, "application/zip")}, timeout=300)
r.raise_for_status()
print(r.json())
import { openAsBlob } from "node:fs";
const form = new FormData();
form.set("file", await openAsBlob("release.zip"), "release.zip");
const res = await fetch("https://partners.btrmusic.com/api/v1/deliveries", {
  method: "POST", headers: { Authorization: `Bearer ${process.env.BTR_API_KEY}` }, body: form
});
if (!res.ok) throw new Error(`HTTP ${res.status}: ${await res.text()}`);
console.log(await res.json());
using System.Net.Http;
using System.Net.Http.Headers;
using var http = new HttpClient();
http.DefaultRequestHeaders.Authorization = new AuthenticationHeaderValue(
    "Bearer", Environment.GetEnvironmentVariable("BTR_API_KEY"));
using var file = File.OpenRead("release.zip");
using var content = new StreamContent(file);
content.Headers.ContentType = new MediaTypeHeaderValue("application/zip");
using var req = new HttpRequestMessage(HttpMethod.Post, "https://partners.btrmusic.com/api/v1/deliveries") { Content = content };
req.Headers.TryAddWithoutValidation("X-File-Name", "release.zip");
using var res = await http.SendAsync(req);
res.EnsureSuccessStatusCode();
Console.WriteLine(await res.Content.ReadAsStringAsync());

Retrieve a delivery

GET/deliveries/{ref}catalog:read

Returns delivery and resulting releases. Issues are arrays of {code, message, where}; see delivery issue codes.

curl -sS https://partners.btrmusic.com/api/v1/deliveries/{ref} \
  -H "Authorization: Bearer $BTR_API_KEY"
package main
import ("fmt"; "io"; "net/http"; "os")
func main() {
  req, err := http.NewRequest(http.MethodGet, "https://partners.btrmusic.com/api/v1/deliveries/0a9e1f2c-4b6d-4e8a-9f10-1c2d3e4f5a6b", nil)
  if err != nil { panic(err) }
  req.Header.Set("Authorization", "Bearer "+os.Getenv("BTR_API_KEY"))
  res, err := http.DefaultClient.Do(req); if err != nil { panic(err) }; defer res.Body.Close()
  body, err := io.ReadAll(res.Body); if err != nil { panic(err) }
  if res.StatusCode >= 300 { fmt.Fprintln(os.Stderr, res.Status, string(body)); os.Exit(1) }
  fmt.Println(string(body))
}
import os
import requests
r = requests.get("https://partners.btrmusic.com/api/v1/deliveries/0a9e1f2c-4b6d-4e8a-9f10-1c2d3e4f5a6b", headers={"Authorization": "Bearer " + os.environ["BTR_API_KEY"]}, timeout=30)
r.raise_for_status()
print(r.json())
const res = await fetch("https://partners.btrmusic.com/api/v1/deliveries/0a9e1f2c-4b6d-4e8a-9f10-1c2d3e4f5a6b", { headers: { Authorization: `Bearer ${process.env.BTR_API_KEY}` } });
if (!res.ok) throw new Error(`HTTP ${res.status}: ${await res.text()}`);
console.log(await res.json());
using System.Net.Http;
using System.Net.Http.Headers;
using var http = new HttpClient();
http.DefaultRequestHeaders.Authorization = new AuthenticationHeaderValue(
    "Bearer", Environment.GetEnvironmentVariable("BTR_API_KEY"));
using var res = await http.GetAsync("https://partners.btrmusic.com/api/v1/deliveries/0a9e1f2c-4b6d-4e8a-9f10-1c2d3e4f5a6b");
res.EnsureSuccessStatusCode();
Console.WriteLine(await res.Content.ReadAsStringAsync());
Example / HTTP 200
{
  "delivery": {
    "ref": "0a9e1f2c-…",
    "status": "staged",
    "source": "api",
    "message_types": ["NewReleaseMessage"],
    "ern_version": "4.3",
    "errors": [],
    "warnings": [{
      "code": "REL.COVER_SIZE",
      "message": "1400x1400 cover or larger is recommended",
      "where": "Release[0634479384622]/ResourceGroup"
    }],
    "processed_at": "2026-09-25T02:14:03Z"
  },
  "releases": [{
    "upc": "0634479384622",
    "title": "Night Signals",
    "status": "review"
  }]
}

List deliveries

GET/deliveriescatalog:read

Filters: status, since (RFC 3339; updated_at >=). limit defaults to 50, max 200; offset defaults to 0. Response includes total, limit and offset.

curl -sS "https://partners.btrmusic.com/api/v1/deliveries?status=rejected&limit=50" \
  -H "Authorization: Bearer $BTR_API_KEY"
package main
import ("fmt"; "io"; "net/http"; "os")
func main() {
  req, err := http.NewRequest(http.MethodGet, "https://partners.btrmusic.com/api/v1/deliveries?status=rejected&limit=50", nil)
  if err != nil { panic(err) }
  req.Header.Set("Authorization", "Bearer "+os.Getenv("BTR_API_KEY"))
  res, err := http.DefaultClient.Do(req); if err != nil { panic(err) }; defer res.Body.Close()
  body, err := io.ReadAll(res.Body); if err != nil { panic(err) }
  if res.StatusCode >= 300 { fmt.Fprintln(os.Stderr, res.Status, string(body)); os.Exit(1) }
  fmt.Println(string(body))
}
import os
import requests
r = requests.get("https://partners.btrmusic.com/api/v1/deliveries?status=rejected&limit=50", headers={"Authorization": "Bearer " + os.environ["BTR_API_KEY"]}, timeout=30)
r.raise_for_status()
print(r.json())
const res = await fetch("https://partners.btrmusic.com/api/v1/deliveries?status=rejected&limit=50", { headers: { Authorization: `Bearer ${process.env.BTR_API_KEY}` } });
if (!res.ok) throw new Error(`HTTP ${res.status}: ${await res.text()}`);
console.log(await res.json());
using System.Net.Http;
using System.Net.Http.Headers;
using var http = new HttpClient();
http.DefaultRequestHeaders.Authorization = new AuthenticationHeaderValue(
    "Bearer", Environment.GetEnvironmentVariable("BTR_API_KEY"));
using var res = await http.GetAsync("https://partners.btrmusic.com/api/v1/deliveries?status=rejected&limit=50");
res.EnsureSuccessStatusCode();
Console.WriteLine(await res.Content.ReadAsStringAsync());
Endpoint group / Catalogue operations

Releases

Releases come from delivered NewReleaseMessages. Read status, map an existing artist, or issue a takedown. Partners submit tracks for review; only BTR humans approve them.

Retrieve a release

GET/releases/{upc}catalog:read

{upc} accepts UPC/ICPN or release id. Live tracks include a btr_url; mapped releases include artist_url.

curl -sS https://partners.btrmusic.com/api/v1/releases/0634479384622 \
  -H "Authorization: Bearer $BTR_API_KEY"
package main
import ("fmt"; "io"; "net/http"; "os")
func main() {
  req, err := http.NewRequest(http.MethodGet, "https://partners.btrmusic.com/api/v1/releases/0634479384622", nil)
  if err != nil { panic(err) }
  req.Header.Set("Authorization", "Bearer "+os.Getenv("BTR_API_KEY"))
  res, err := http.DefaultClient.Do(req); if err != nil { panic(err) }; defer res.Body.Close()
  body, err := io.ReadAll(res.Body); if err != nil { panic(err) }
  if res.StatusCode >= 300 { fmt.Fprintln(os.Stderr, res.Status, string(body)); os.Exit(1) }
  fmt.Println(string(body))
}
import os
import requests
r = requests.get("https://partners.btrmusic.com/api/v1/releases/0634479384622", headers={"Authorization": "Bearer " + os.environ["BTR_API_KEY"]}, timeout=30)
r.raise_for_status()
print(r.json())
const res = await fetch("https://partners.btrmusic.com/api/v1/releases/0634479384622", { headers: { Authorization: `Bearer ${process.env.BTR_API_KEY}` } });
if (!res.ok) throw new Error(`HTTP ${res.status}: ${await res.text()}`);
console.log(await res.json());
using System.Net.Http;
using System.Net.Http.Headers;
using var http = new HttpClient();
http.DefaultRequestHeaders.Authorization = new AuthenticationHeaderValue(
    "Bearer", Environment.GetEnvironmentVariable("BTR_API_KEY"));
using var res = await http.GetAsync("https://partners.btrmusic.com/api/v1/releases/0634479384622");
res.EnsureSuccessStatusCode();
Console.WriteLine(await res.Content.ReadAsStringAsync());

List releases

GET/releasescatalog:read

Filters: status, upc, since. Pagination: limit defaults to 100, max 500; offset defaults to 0.

curl -sS "https://partners.btrmusic.com/api/v1/releases?status=live&limit=100" \
  -H "Authorization: Bearer $BTR_API_KEY"
package main
import ("fmt"; "io"; "net/http"; "os")
func main() {
  req, err := http.NewRequest(http.MethodGet, "https://partners.btrmusic.com/api/v1/releases?status=live&limit=100", nil)
  if err != nil { panic(err) }
  req.Header.Set("Authorization", "Bearer "+os.Getenv("BTR_API_KEY"))
  res, err := http.DefaultClient.Do(req); if err != nil { panic(err) }; defer res.Body.Close()
  body, err := io.ReadAll(res.Body); if err != nil { panic(err) }
  if res.StatusCode >= 300 { fmt.Fprintln(os.Stderr, res.Status, string(body)); os.Exit(1) }
  fmt.Println(string(body))
}
import os
import requests
r = requests.get("https://partners.btrmusic.com/api/v1/releases?status=live&limit=100", headers={"Authorization": "Bearer " + os.environ["BTR_API_KEY"]}, timeout=30)
r.raise_for_status()
print(r.json())
const res = await fetch("https://partners.btrmusic.com/api/v1/releases?status=live&limit=100", { headers: { Authorization: `Bearer ${process.env.BTR_API_KEY}` } });
if (!res.ok) throw new Error(`HTTP ${res.status}: ${await res.text()}`);
console.log(await res.json());
using System.Net.Http;
using System.Net.Http.Headers;
using var http = new HttpClient();
http.DefaultRequestHeaders.Authorization = new AuthenticationHeaderValue(
    "Bearer", Environment.GetEnvironmentVariable("BTR_API_KEY"));
using var res = await http.GetAsync("https://partners.btrmusic.com/api/v1/releases?status=live&limit=100");
res.EnsureSuccessStatusCode();
Console.WriteLine(await res.Content.ReadAsStringAsync());

Map a release's artist

POST/releases/{upc}/artistcatalog:write

Resolve needs_artist using the 24-character hex btr_user_id of an existing BTR account already approved and linked to your partner. The delivery re-queues; poll the ref in the response note.

curl -sS -X POST https://partners.btrmusic.com/api/v1/releases/0634479384622/artist \
  -H "Authorization: Bearer $BTR_API_KEY" -H "Content-Type: application/json" \
  -d '{"btr_user_id":"6512a1b2c3d4e5f60718293a"}'
package main
import ("fmt"; "io"; "net/http"; "os"; "strings")
func main() {
  req, err := http.NewRequest(http.MethodPost, "https://partners.btrmusic.com/api/v1/releases/0634479384622/artist", strings.NewReader(`{"btr_user_id":"6512a1b2c3d4e5f60718293a"}`))
  if err != nil { panic(err) }
  req.Header.Set("Authorization", "Bearer "+os.Getenv("BTR_API_KEY"))
  req.Header.Set("Content-Type", "application/json")
  res, err := http.DefaultClient.Do(req); if err != nil { panic(err) }; defer res.Body.Close()
  body, err := io.ReadAll(res.Body); if err != nil { panic(err) }
  if res.StatusCode >= 300 { fmt.Fprintln(os.Stderr, res.Status, string(body)); os.Exit(1) }
  fmt.Println(string(body))
}
import os
import requests
r = requests.post("https://partners.btrmusic.com/api/v1/releases/0634479384622/artist", headers={"Authorization": "Bearer " + os.environ["BTR_API_KEY"]}, json={"btr_user_id":"6512a1b2c3d4e5f60718293a"}, timeout=30)
r.raise_for_status()
print(r.json())
const res = await fetch("https://partners.btrmusic.com/api/v1/releases/0634479384622/artist", { method: "POST", headers: { Authorization: `Bearer ${process.env.BTR_API_KEY}`, "Content-Type": "application/json" }, body: JSON.stringify({"btr_user_id":"6512a1b2c3d4e5f60718293a"}) });
if (!res.ok) throw new Error(`HTTP ${res.status}: ${await res.text()}`);
console.log(await res.json());
using System.Net.Http;
using System.Net.Http.Headers;
using var http = new HttpClient();
http.DefaultRequestHeaders.Authorization = new AuthenticationHeaderValue(
    "Bearer", Environment.GetEnvironmentVariable("BTR_API_KEY"));
using var res = await http.PostAsync("https://partners.btrmusic.com/api/v1/releases/0634479384622/artist", new StringContent("{\"btr_user_id\":\"6512a1b2c3d4e5f60718293a\"}", System.Text.Encoding.UTF8, "application/json"));
res.EnsureSuccessStatusCode();
Console.WriteLine(await res.Content.ReadAsStringAsync());

Take down a release

POST/releases/{upc}/takedowntakedown:write

Hides every track of the release on BTR Music and marks it takedown. Idempotent. reason is optional but recommended. A PurgeReleaseMessage has the same effect.

curl -sS -X POST https://partners.btrmusic.com/api/v1/releases/0634479384622/takedown \
  -H "Authorization: Bearer $BTR_API_KEY" -H "Content-Type: application/json" \
  -d '{"reason":"Rights reverted by the licensor"}'
package main
import ("fmt"; "io"; "net/http"; "os"; "strings")
func main() {
  req, err := http.NewRequest(http.MethodPost, "https://partners.btrmusic.com/api/v1/releases/0634479384622/takedown", strings.NewReader(`{"reason":"Rights reverted by the licensor"}`))
  if err != nil { panic(err) }
  req.Header.Set("Authorization", "Bearer "+os.Getenv("BTR_API_KEY"))
  req.Header.Set("Content-Type", "application/json")
  res, err := http.DefaultClient.Do(req); if err != nil { panic(err) }; defer res.Body.Close()
  body, err := io.ReadAll(res.Body); if err != nil { panic(err) }
  if res.StatusCode >= 300 { fmt.Fprintln(os.Stderr, res.Status, string(body)); os.Exit(1) }
  fmt.Println(string(body))
}
import os
import requests
r = requests.post("https://partners.btrmusic.com/api/v1/releases/0634479384622/takedown", headers={"Authorization": "Bearer " + os.environ["BTR_API_KEY"]}, json={"reason":"Rights reverted by the licensor"}, timeout=30)
r.raise_for_status()
print(r.json())
const res = await fetch("https://partners.btrmusic.com/api/v1/releases/0634479384622/takedown", { method: "POST", headers: { Authorization: `Bearer ${process.env.BTR_API_KEY}`, "Content-Type": "application/json" }, body: JSON.stringify({"reason":"Rights reverted by the licensor"}) });
if (!res.ok) throw new Error(`HTTP ${res.status}: ${await res.text()}`);
console.log(await res.json());
using System.Net.Http;
using System.Net.Http.Headers;
using var http = new HttpClient();
http.DefaultRequestHeaders.Authorization = new AuthenticationHeaderValue(
    "Bearer", Environment.GetEnvironmentVariable("BTR_API_KEY"));
using var res = await http.PostAsync("https://partners.btrmusic.com/api/v1/releases/0634479384622/takedown", new StringContent("{\"reason\":\"Rights reverted by the licensor\"}", System.Text.Encoding.UTF8, "application/json"));
res.EnsureSuccessStatusCode();
Console.WriteLine(await res.Content.ReadAsStringAsync());

Submit tracks for approval

POST/releases/{upc}/submitcatalog:write

Send staged tracks into BTR's human moderation queue. Body {"resource_ids":["…"]} submits those resources; omit it or send {} for every eligible track in the release. Returns 202 with {"queued":n,"release_id":"…"}; existing queued/pending/approved versions are skipped. Only a live partner with an executed agreement may submit — a sandbox or not-yet-executed account gets 403 partner_sandbox. A release whose tracks are already queued or awaiting moderation cannot be redelivered until those decisions finish. Poll GET /releases/{upc} for each track's status, production_status, rejection_reason and processing_error.

curl -sS -X POST https://partners.btrmusic.com/api/v1/releases/0634479384622/submit \
  -H "Authorization: Bearer $BTR_API_KEY" -H "Content-Type: application/json" \
  -d '{"resource_ids":["6512a1b2c3d4e5f60718293a"]}'
package main
import ("fmt"; "io"; "net/http"; "os"; "strings")
func main() {
  req, err := http.NewRequest(http.MethodPost, "https://partners.btrmusic.com/api/v1/releases/0634479384622/submit", strings.NewReader(`{"resource_ids":["6512a1b2c3d4e5f60718293a"]}`))
  if err != nil { panic(err) }
  req.Header.Set("Authorization", "Bearer "+os.Getenv("BTR_API_KEY"))
  req.Header.Set("Content-Type", "application/json")
  res, err := http.DefaultClient.Do(req); if err != nil { panic(err) }; defer res.Body.Close()
  body, err := io.ReadAll(res.Body); if err != nil { panic(err) }
  if res.StatusCode >= 300 { fmt.Fprintln(os.Stderr, res.Status, string(body)); os.Exit(1) }
  fmt.Println(string(body))
}
import os
import requests
r = requests.post("https://partners.btrmusic.com/api/v1/releases/0634479384622/submit", headers={"Authorization": "Bearer " + os.environ["BTR_API_KEY"]}, json={"resource_ids":["6512a1b2c3d4e5f60718293a"]}, timeout=30)
r.raise_for_status()
print(r.json())
const res = await fetch("https://partners.btrmusic.com/api/v1/releases/0634479384622/submit", { method: "POST", headers: { Authorization: `Bearer ${process.env.BTR_API_KEY}`, "Content-Type": "application/json" }, body: JSON.stringify({"resource_ids":["6512a1b2c3d4e5f60718293a"]}) });
if (!res.ok) throw new Error(`HTTP ${res.status}: ${await res.text()}`);
console.log(await res.json());
using System.Net.Http;
using System.Net.Http.Headers;
using var http = new HttpClient();
http.DefaultRequestHeaders.Authorization = new AuthenticationHeaderValue(
    "Bearer", Environment.GetEnvironmentVariable("BTR_API_KEY"));
using var res = await http.PostAsync("https://partners.btrmusic.com/api/v1/releases/0634479384622/submit", new StringContent("{\"resource_ids\":[\"6512a1b2c3d4e5f60718293a\"]}", System.Text.Encoding.UTF8, "application/json"));
res.EnsureSuccessStatusCode();
Console.WriteLine(await res.Content.ReadAsStringAsync());
Publication happens on BTR's side. After a delivery is staged, submit its tracks for human approval and poll the release for decisions and rejection reasons. Submit selected or all ready tracks using POST /api/v1/releases/{upc}/submit. Submission requires a live account with an executed agreement; sandbox is onboarding — deliver, validate and preview only.
Endpoint group / Identity

Artists

Connect catalogue to an existing BTR Music account. An artist connection request requires BTR admin approval; this API does not create accounts.

Request a connection

POST/artistscatalog:write

Supply the artist's BTR username, the email on that account and confirmation of rights. Missing or empty btr_username or email returns 400 bad_request naming the field; rights_confirmed absent or not true returns 400 bad_request with "rights_confirmed must be true". Returns 201 for a pending request. name, country, bio and party_id are accepted but ignored; account details come from the matched BTR account. Only an account mismatch or an artist connected elsewhere returns 400 artist_create_failed.

FieldTypeRequired
btr_usernamestringYes
emailstringYes
rights_confirmedboolean, trueYes
curl -sS -X POST https://partners.btrmusic.com/api/v1/artists \
  -H "Authorization: Bearer $BTR_API_KEY" -H "Content-Type: application/json" \
  -d '{"btr_username":"nova-lang","email":"artist@example.com","rights_confirmed":true}'
package main
import ("fmt"; "io"; "net/http"; "os"; "strings")
func main() {
  req, err := http.NewRequest(http.MethodPost, "https://partners.btrmusic.com/api/v1/artists", strings.NewReader(`{"btr_username":"nova-lang","email":"artist@example.com","rights_confirmed":true}`))
  if err != nil { panic(err) }
  req.Header.Set("Authorization", "Bearer "+os.Getenv("BTR_API_KEY"))
  req.Header.Set("Content-Type", "application/json")
  res, err := http.DefaultClient.Do(req); if err != nil { panic(err) }; defer res.Body.Close()
  body, err := io.ReadAll(res.Body); if err != nil { panic(err) }
  if res.StatusCode >= 300 { fmt.Fprintln(os.Stderr, res.Status, string(body)); os.Exit(1) }
  fmt.Println(string(body))
}
import os
import requests
r = requests.post("https://partners.btrmusic.com/api/v1/artists", headers={"Authorization": "Bearer " + os.environ["BTR_API_KEY"]}, json={"btr_username":"nova-lang","email":"artist@example.com","rights_confirmed":true}, timeout=30)
r.raise_for_status()
print(r.json())
const res = await fetch("https://partners.btrmusic.com/api/v1/artists", { method: "POST", headers: { Authorization: `Bearer ${process.env.BTR_API_KEY}`, "Content-Type": "application/json" }, body: JSON.stringify({"btr_username":"nova-lang","email":"artist@example.com","rights_confirmed":true}) });
if (!res.ok) throw new Error(`HTTP ${res.status}: ${await res.text()}`);
console.log(await res.json());
using System.Net.Http;
using System.Net.Http.Headers;
using var http = new HttpClient();
http.DefaultRequestHeaders.Authorization = new AuthenticationHeaderValue(
    "Bearer", Environment.GetEnvironmentVariable("BTR_API_KEY"));
using var res = await http.PostAsync("https://partners.btrmusic.com/api/v1/artists", new StringContent("{\"btr_username\":\"nova-lang\",\"email\":\"artist@example.com\",\"rights_confirmed\":true}", System.Text.Encoding.UTF8, "application/json"));
res.EnsureSuccessStatusCode();
Console.WriteLine(await res.Content.ReadAsStringAsync());

List linked artists

GET/artistscatalog:read
curl -sS https://partners.btrmusic.com/api/v1/artists \
  -H "Authorization: Bearer $BTR_API_KEY"
package main
import ("fmt"; "io"; "net/http"; "os")
func main() {
  req, err := http.NewRequest(http.MethodGet, "https://partners.btrmusic.com/api/v1/artists", nil)
  if err != nil { panic(err) }
  req.Header.Set("Authorization", "Bearer "+os.Getenv("BTR_API_KEY"))
  res, err := http.DefaultClient.Do(req); if err != nil { panic(err) }; defer res.Body.Close()
  body, err := io.ReadAll(res.Body); if err != nil { panic(err) }
  if res.StatusCode >= 300 { fmt.Fprintln(os.Stderr, res.Status, string(body)); os.Exit(1) }
  fmt.Println(string(body))
}
import os
import requests
r = requests.get("https://partners.btrmusic.com/api/v1/artists", headers={"Authorization": "Bearer " + os.environ["BTR_API_KEY"]}, timeout=30)
r.raise_for_status()
print(r.json())
const res = await fetch("https://partners.btrmusic.com/api/v1/artists", { headers: { Authorization: `Bearer ${process.env.BTR_API_KEY}` } });
if (!res.ok) throw new Error(`HTTP ${res.status}: ${await res.text()}`);
console.log(await res.json());
using System.Net.Http;
using System.Net.Http.Headers;
using var http = new HttpClient();
http.DefaultRequestHeaders.Authorization = new AuthenticationHeaderValue(
    "Bearer", Environment.GetEnvironmentVariable("BTR_API_KEY"));
using var res = await http.GetAsync("https://partners.btrmusic.com/api/v1/artists");
res.EnsureSuccessStatusCode();
Console.WriteLine(await res.Content.ReadAsStringAsync());
Endpoint group / Usage

Reports

Monthly qualified usage as a DSR flat file. A month closes 14 days after month-end. Profile: DSR-FlatFile/BasicAudioProfile/BTR-1.0. Files: BTR_<SLUG>_<YYYY-MM>.tsv or CSV.

Generate or retrieve a finalized report

POST/reports/{period}reports:read

{period} must be a real YYYY-MM month. Open periods return 409 period_not_closed. A finalized period returns its existing report.

curl -sS -X POST https://partners.btrmusic.com/api/v1/reports/2026-08 \
  -H "Authorization: Bearer $BTR_API_KEY"
package main
import ("fmt"; "io"; "net/http"; "os")
func main() {
  req, err := http.NewRequest(http.MethodPost, "https://partners.btrmusic.com/api/v1/reports/2026-08", nil)
  if err != nil { panic(err) }
  req.Header.Set("Authorization", "Bearer "+os.Getenv("BTR_API_KEY"))
  res, err := http.DefaultClient.Do(req); if err != nil { panic(err) }; defer res.Body.Close()
  body, err := io.ReadAll(res.Body); if err != nil { panic(err) }
  if res.StatusCode >= 300 { fmt.Fprintln(os.Stderr, res.Status, string(body)); os.Exit(1) }
  fmt.Println(string(body))
}
import os
import requests
r = requests.post("https://partners.btrmusic.com/api/v1/reports/2026-08", headers={"Authorization": "Bearer " + os.environ["BTR_API_KEY"]}, timeout=30)
r.raise_for_status()
print(r.json())
const res = await fetch("https://partners.btrmusic.com/api/v1/reports/2026-08", { method: "POST", headers: { Authorization: `Bearer ${process.env.BTR_API_KEY}` } });
if (!res.ok) throw new Error(`HTTP ${res.status}: ${await res.text()}`);
console.log(await res.json());
using System.Net.Http;
using System.Net.Http.Headers;
using var http = new HttpClient();
http.DefaultRequestHeaders.Authorization = new AuthenticationHeaderValue(
    "Bearer", Environment.GetEnvironmentVariable("BTR_API_KEY"));
using var res = await http.PostAsync("https://partners.btrmusic.com/api/v1/reports/2026-08", null);
res.EnsureSuccessStatusCode();
Console.WriteLine(await res.Content.ReadAsStringAsync());

Download a report

GET/reports/{period}reports:read

Default is the tab-separated DSR flat file. ?format=csv contains period, isrc, upc, track_title, artist, release_title, territory, qualified_streams, unique_listeners, btr_track_id.

curl -sS "https://partners.btrmusic.com/api/v1/reports/2026-08?format=csv" \
  -H "Authorization: Bearer $BTR_API_KEY" -o usage.csv
package main
import ("fmt"; "io"; "net/http"; "os")
func main() {
  req, err := http.NewRequest(http.MethodGet, "https://partners.btrmusic.com/api/v1/reports/2026-08?format=csv", nil)
  if err != nil { panic(err) }
  req.Header.Set("Authorization", "Bearer "+os.Getenv("BTR_API_KEY"))
  res, err := http.DefaultClient.Do(req); if err != nil { panic(err) }; defer res.Body.Close()
  if res.StatusCode >= 300 { fmt.Fprintln(os.Stderr, res.Status); os.Exit(1) }
  out, err := os.Create("usage.csv"); if err != nil { panic(err) }; defer out.Close()
  if _, err := io.Copy(out, res.Body); err != nil { panic(err) }
}
import os
import requests
with requests.get("https://partners.btrmusic.com/api/v1/reports/2026-08?format=csv", headers={"Authorization": "Bearer " + os.environ["BTR_API_KEY"]}, stream=True, timeout=60) as r:
    r.raise_for_status()
    with open("usage.csv", "wb") as output:
        for chunk in r.iter_content(chunk_size=65536):
            output.write(chunk)
import { writeFile } from "node:fs/promises";
const res = await fetch("https://partners.btrmusic.com/api/v1/reports/2026-08?format=csv", { headers: { Authorization: `Bearer ${process.env.BTR_API_KEY}` } });
if (!res.ok) throw new Error(`HTTP ${res.status}: ${await res.text()}`);
await writeFile("usage.csv", Buffer.from(await res.arrayBuffer()));
using System.Net.Http;
using System.Net.Http.Headers;
using var http = new HttpClient();
http.DefaultRequestHeaders.Authorization = new AuthenticationHeaderValue(
    "Bearer", Environment.GetEnvironmentVariable("BTR_API_KEY"));
using var res = await http.GetAsync("https://partners.btrmusic.com/api/v1/reports/2026-08?format=csv");
res.EnsureSuccessStatusCode();
await File.WriteAllBytesAsync("usage.csv", await res.Content.ReadAsByteArrayAsync());

List reports

GET/reportsreports:read
curl -sS https://partners.btrmusic.com/api/v1/reports \
  -H "Authorization: Bearer $BTR_API_KEY"
package main
import ("fmt"; "io"; "net/http"; "os")
func main() {
  req, err := http.NewRequest(http.MethodGet, "https://partners.btrmusic.com/api/v1/reports", nil)
  if err != nil { panic(err) }
  req.Header.Set("Authorization", "Bearer "+os.Getenv("BTR_API_KEY"))
  res, err := http.DefaultClient.Do(req); if err != nil { panic(err) }; defer res.Body.Close()
  body, err := io.ReadAll(res.Body); if err != nil { panic(err) }
  if res.StatusCode >= 300 { fmt.Fprintln(os.Stderr, res.Status, string(body)); os.Exit(1) }
  fmt.Println(string(body))
}
import os
import requests
r = requests.get("https://partners.btrmusic.com/api/v1/reports", headers={"Authorization": "Bearer " + os.environ["BTR_API_KEY"]}, timeout=30)
r.raise_for_status()
print(r.json())
const res = await fetch("https://partners.btrmusic.com/api/v1/reports", { headers: { Authorization: `Bearer ${process.env.BTR_API_KEY}` } });
if (!res.ok) throw new Error(`HTTP ${res.status}: ${await res.text()}`);
console.log(await res.json());
using System.Net.Http;
using System.Net.Http.Headers;
using var http = new HttpClient();
http.DefaultRequestHeaders.Authorization = new AuthenticationHeaderValue(
    "Bearer", Environment.GetEnvironmentVariable("BTR_API_KEY"));
using var res = await http.GetAsync("https://partners.btrmusic.com/api/v1/reports");
res.EnsureSuccessStatusCode();
Console.WriteLine(await res.Content.ReadAsStringAsync());
Endpoint group / Reference

Genre taxonomy

GET/reference/genresany valid key

The versioned taxonomy is cacheable for one hour. Send a current genre name as DDEX GenreText, and optionally a value from its subgenre groups as SubGenre. A missing/retired genre triggers REL.GENRE; an invalid subgenre triggers REL.SUBGENRE_UNSUPPORTED.

curl -sS https://partners.btrmusic.com/api/v1/reference/genres \
  -H "Authorization: Bearer $BTR_API_KEY"
package main
import ("fmt"; "io"; "net/http"; "os")
func main() {
  req, err := http.NewRequest(http.MethodGet, "https://partners.btrmusic.com/api/v1/reference/genres", nil)
  if err != nil { panic(err) }
  req.Header.Set("Authorization", "Bearer "+os.Getenv("BTR_API_KEY"))
  res, err := http.DefaultClient.Do(req); if err != nil { panic(err) }; defer res.Body.Close()
  body, err := io.ReadAll(res.Body); if err != nil { panic(err) }
  if res.StatusCode >= 300 { fmt.Fprintln(os.Stderr, res.Status, string(body)); os.Exit(1) }
  fmt.Println(string(body))
}
import os
import requests
r = requests.get("https://partners.btrmusic.com/api/v1/reference/genres", headers={"Authorization": "Bearer " + os.environ["BTR_API_KEY"]}, timeout=30)
r.raise_for_status()
print(r.json())
const res = await fetch("https://partners.btrmusic.com/api/v1/reference/genres", { headers: { Authorization: `Bearer ${process.env.BTR_API_KEY}` } });
if (!res.ok) throw new Error(`HTTP ${res.status}: ${await res.text()}`);
console.log(await res.json());
using System.Net.Http;
using System.Net.Http.Headers;
using var http = new HttpClient();
http.DefaultRequestHeaders.Authorization = new AuthenticationHeaderValue(
    "Bearer", Environment.GetEnvironmentVariable("BTR_API_KEY"));
using var res = await http.GetAsync("https://partners.btrmusic.com/api/v1/reference/genres");
res.EnsureSuccessStatusCode();
Console.WriteLine(await res.Content.ReadAsStringAsync());
Endpoint group / Utility

Me & health

Key and partner information

GET/meany valid key

Partner, key prefix and scopes, current limits and ERN support. See the quickstart response.

Liveness

GET/healthpublic

Unauthenticated. Returns 200 when the database is reachable, 503 otherwise.

curl -sS https://partners.btrmusic.com/api/v1/health
package main
import ("fmt"; "io"; "net/http"; "os")
func main() {
  req, err := http.NewRequest(http.MethodGet, "https://partners.btrmusic.com/api/v1/health", nil)
  if err != nil { panic(err) }
  res, err := http.DefaultClient.Do(req); if err != nil { panic(err) }; defer res.Body.Close()
  body, err := io.ReadAll(res.Body); if err != nil { panic(err) }
  if res.StatusCode >= 300 { fmt.Fprintln(os.Stderr, res.Status, string(body)); os.Exit(1) }
  fmt.Println(string(body))
}
import requests
r = requests.get("https://partners.btrmusic.com/api/v1/health", timeout=30)
r.raise_for_status()
print(r.json())
const res = await fetch("https://partners.btrmusic.com/api/v1/health", { headers: {  } });
if (!res.ok) throw new Error(`HTTP ${res.status}: ${await res.text()}`);
console.log(await res.json());
using System.Net.Http;
using System.Net.Http.Headers;
using var http = new HttpClient();
using var res = await http.GetAsync("https://partners.btrmusic.com/api/v1/health");
res.EnsureSuccessStatusCode();
Console.WriteLine(await res.Content.ReadAsStringAsync());
Example / HTTP 200
{"ok":true,"service":"btr-partners","time":"2026-09-25T02:14:00Z"}
Reference / Package QC

Delivery issue codes

Validation results arrive after the initial 202. Issues have code, message and where (XML path or filename). An error rejects the entire package; a warning does not. For a rejected package, fix and redeliver with a new MessageId.

PrefixCodes (suffixes)
PKGOPEN · EXPIRED · NO_XML · READ · MISSING_AUDIO · MISSING_COVER · AUDIO_HASH · COVER_HASH
ERNPARSE · VERSION · MESSAGE_ID · MESSAGE_ID_DUP · DUPLICATE (warning) · SENDER (warning) · NO_RELEASE · PURGE_EMPTY · PURGE_ID · XSD
RELID · UPC_FORMAT · GRID_FORMAT · ID_DUP · TITLE · ARTIST · PLINE (warning) · CLINE (warning) · COVER · COVER_HASH · COVER_QUALITY · COVER_SIZE (warning) · GENRE · SUBGENRE_UNSUPPORTED · NO_TRACKS
DEALNONE · USETYPE · TERRITORY · TERRITORY_FORMAT · RANGE · START (warning) · INACTIVE (warning)
TRKISRC · ISRC_FORMAT · ISRC_DUP · TITLE · ARTIST · AUDIO · AUDIO_FORMAT · AUDIO_HASH · DURATION (warning) · REMOVED (warning)
OtherSANDBOX (warning) · PURGE.UNKNOWN (warning) · PURGE.APPLY · SYS.* (internal; retry)
Common fixesREL.GENRE / REL.SUBGENRE_UNSUPPORTED: use current reference data. TRK.AUDIO_HASH: add a SHA-256 HashSum to every audio file. DEAL.USETYPE: grant OnDemandStream. PKG.MISSING_AUDIO / PKG.MISSING_COVER: check that the URI resolves inside the ZIP.
Reference / Asynchronous events

Webhooks

Set an HTTPS callback in the portal. BTR sends signed JSON when delivery or release states change. Webhooks are best-effort: persist events before returning 2xx and keep polling as a fallback.

Event types

delivery.staged · delivery.needs_artist · delivery.rejected · delivery.failed · release.pending · release.live · release.takedown

Payload and headers

POST https://your-domain.example/webhooks/btr
X-BTR-Event: release.live
X-BTR-Timestamp: 1790302440
X-BTR-Signature: sha256=…

{"id":"e3f1c2a4-…","type":"release.live","created_at":"2026-09-25T02:14:00Z","partner":"your-slug","data":{…}}

Verify the HMAC-SHA256 signature over the exact bytes timestamp + "." + raw_request_body. Compare in constant time, reject timestamps more than five minutes old, then deduplicate by payload id. Never re-serialize JSON before checking the signature.

# curl can send a test webhook, but signature verification belongs in your server.
# Select Go, Python, Node.js or C# above for a verification example.
// Imports: crypto/hmac, crypto/sha256, encoding/hex, time, strconv.
func verify(secret, ts, sig string, body []byte) bool {
  n, err := strconv.ParseInt(ts, 10, 64)
  if err != nil || dabs(time.Now().Unix()-n) > 300 { return false }
  mac := hmac.New(sha256.New, []byte(secret))
  mac.Write([]byte(ts + ".")); mac.Write(body)
  want := "sha256=" + hex.EncodeToString(mac.Sum(nil))
  return hmac.Equal([]byte(want), []byte(sig))
}
func dabs(n int64) int64 { if n < 0 { return -n }; return n }
import hashlib, hmac, time

def verify(secret: str, timestamp: str, signature: str, raw_body: bytes) -> bool:
    try:
        if abs(time.time() - int(timestamp)) > 300: return False
    except ValueError:
        return False
    msg = timestamp.encode() + b"." + raw_body
    want = "sha256=" + hmac.new(secret.encode(), msg, hashlib.sha256).hexdigest()
    return hmac.compare_digest(want, signature)
import { createHmac, timingSafeEqual } from "node:crypto";
export function verify(secret, timestamp, signature, rawBody) {
  const ts = Number(timestamp);
  if (!Number.isSafeInteger(ts) || Math.abs(Date.now() / 1000 - ts) > 300) return false;
  const expected = "sha256=" + createHmac("sha256", secret)
    .update(timestamp + ".").update(rawBody).digest("hex");
  const a = Buffer.from(expected), b = Buffer.from(signature);
  return a.length === b.length && timingSafeEqual(a, b);
}
// Imports: System.Security.Cryptography, System.Text, System.Linq.
static bool Verify(string secret, string timestamp, string signature, byte[] rawBody) {
    if (!long.TryParse(timestamp, out var ts) ||
        Math.Abs(DateTimeOffset.UtcNow.ToUnixTimeSeconds() - ts) > 300) return false;
    using var mac = new HMACSHA256(Encoding.UTF8.GetBytes(secret));
    var message = Encoding.UTF8.GetBytes(timestamp + ".").Concat(rawBody).ToArray();
    var want = "sha256=" + Convert.ToHexString(mac.ComputeHash(message)).ToLowerInvariant();
    return CryptographicOperations.FixedTimeEquals(
        Encoding.UTF8.GetBytes(want), Encoding.UTF8.GetBytes(signature));
}

BTR makes three best-effort attempts with increasing backoff (approximately 5 seconds, then 20 seconds). Events are not ordered; there is no manual replay. Use the event id to deduplicate.

Reference / Response fields

Data model

Delivery

FieldTypeMeaning
refUUID stringPublic delivery reference.
sourcestringapi or portal.
statusstringreceived, processing, needs_artist, staged, rejected, failed.
message_typesstring[]NewReleaseMessage or PurgeReleaseMessage.
ern_versionstringe.g. 4.3.
errors, warningsIssue[]{code, message, where}.
upcs, release_idsstring[]Affected releases.
file_namestringUploaded package name.
file_sizeintegerPackage size in bytes.
file_sha256stringSHA-256 of the package.
created_bystringKey or user that delivered.
attemptsintegerProcessing attempts.
processed_atdate-timeWhen processing finished.

Release

FieldTypeMeaning
upc, gridstringRelease identifiers.
title, display_artist, labelstringDisplay metadata.
p_line, c_linestringCopyright lines.
genre, sub_genrestringCurrent taxonomy.
explicitbooleanAdvisory flag.
release_datestringYYYY-MM-DD.
dealsDeal[]Commercial terms and territories.
idstringRelease ObjectId.
release_typestringSingle, EP or Album.
track_countintegerNumber of tracks.
cover_urlstringCover image URL, present when published.
statusstringneeds_artist, review, queued, publishing, publish_failed, pending, live, takedown, rejected.
btr_user_id, btr_usernamestringMapped artist account.
takedown_reasonstringReason, when taken down.
taken_down_atdate-timeWhen taken down.
created_at, updated_atdate-timeTimestamps.

Resource (track)

FieldTypeMeaning
isrcstringRecording identifier.
sequenceintegerTrack order.
title, display_artiststringDisplay metadata.
duration_secnumberDuration.
track_id, btr_urlstringPresent when live.
statusstringreview, queued, publishing, publish_failed, pending, live, takedown, rejected.
production_statusstringModeration / publish sub-state.
rejection_reasonstringSet when a track is rejected.
processing_errorstringSet when publication failed.

Deal

FieldTypeMeaning
commercial_modelstringCommercial model.
use_typesstring[]Must include OnDemandStream or Stream.
territories, excluded_territoriesstring[]ISO alpha-2 or Worldwide.
start, enddate-timeValidity window.

Artist

FieldTypeMeaning
name, countrystringCountry is ISO alpha-3.
identity_statusstringpending_admin_link, linked, managed, declined, disconnected.
btr_user_id, btr_usernamestringLinked BTR account.
profile_urlstringArtist page URL.

Report

FieldTypeMeaning
periodstringYYYY-MM.
profilestringDSR-FlatFile/BasicAudioProfile/BTR-1.0.
rows, usages, territoriesintegerReport counts.
file_namestringBTR_<SLUG>_<YYYY-MM>.tsv.
Reference / Compatibility

Versioning

The API version is in its path: /api/v1. New optional fields, endpoints, enum values and parameters may be added without changing that path, so clients should ignore unknown fields. Breaking changes use a new version path; the previous path continues during an announced transition. There are no sunset headers today.