ToneGrid API Reference
The ToneGrid REST API gives you programmatic access to your music catalog, distribution pipeline, analytics, royalties, and more. All requests are made over HTTPS to https://api.tonegrid.pro.
Base URL
All API requests must be made to the following base URL over HTTPS. HTTP requests will be redirected.
Content-Type: application/json on requests with a body and Accept: application/json on all requests.Errors
Conventional HTTP status codes plus a machine-readable error string. Treat the HTTP status as the routing key; treat the error string as the precise reason. Validation failures include a per-field errors map.
HTTP status codes
| Status | Meaning |
|---|---|
| 200 | OK — request succeeded |
| 201 | Created — resource was created |
| 202 | Accepted — queued for async processing (ingestion, DDEX delivery, bulk) |
| 204 | No Content — successful, nothing to return (DELETE, soft-deletes) |
| 400 | Bad Request — malformed body or query, missing required headers |
| 401 | Unauthorized — token missing, invalid, or revoked |
| 403 | Forbidden — token authenticated but lacks scope / role / hierarchy access |
| 404 | Not Found — resource does not exist or is not visible to this tenant |
| 409 | Conflict — state machine refuses (e.g. submit a non-draft release), uniqueness clash, idempotency body mismatch |
| 413 | Payload Too Large — file/bulk batch exceeds size limit |
| 422 | Unprocessable Entity — validation failed; errors map names the bad fields |
| 429 | Too Many Requests — rate limit hit; see Retry-After header |
| 500 | Internal Server Error — please report with the X-Request-Id response header |
| 503 | Service Temporarily Unavailable — database briefly unreachable; safe to retry |
Named error strings
| String | HTTP | When it fires |
|---|---|---|
| Authentication token is missing. | 401 | No Authorization header and no cookie. |
| Invalid or expired token. Please log in again. | 401 | JWT signature mismatch or past exp. |
| Invalid, expired, or revoked API key. | 401 | tgk_… lookup failed: revoked_at set, expires_at past, or tenant suspended. |
| Tenant not found. | 404 | Unknown tenant_slug on login or unresolved X-Tenant-Domain. |
| Validation failed | 422 | Body validation failed; response includes errors: { field: ["msg",…] }. |
| Idempotency-Key reused with a different request body. | 422 | Same key + different body inside the 24h window. |
| Idempotency-Key must be ≤255 chars. | 400 | Header length cap. |
| Endpoint not found. | 404 | Path/method doesn't match any registered route. |
| Release not found. | 404 | :uuid path param doesn't resolve to a release visible to this tenant. |
| Only draft or rejected releases can be submitted. | 409 | Tried to submit a release outside the allowed state. |
| Please add at least one track before submitting. | 422 | Submit gate; tracks count is zero. |
| Please select at least one store before submitting. | 422 | Submit gate; no DSPs attached. |
| Please set a release date before submitting. | 422 | Submit gate; release_date null. |
| Only draft or rejected releases can be deleted. | 409 | Delete protected: live / pending_review / qc_inspection. |
| Delivery to {DSP} is blocked via per-DSP override (is_blocked=true). | 409 | A release_dsp_overrides.is_blocked flag prevents delivery. |
| DSP '{slug}' is not attached to this release. | 422 | Need POST /releases/:uuid/dsps first. |
| No DSPs attached to this release. | 422 | Bulk-deliver called with empty DSP set. |
| DSP '{slug}' not found or inactive. | 404 | Unknown DSP slug or dsps.is_active = 0. |
| Webhook subscription limit (25) reached. | 409 | Per-tenant cap on active webhooks. |
| Unknown event pattern(s): {…}. | 422 | Webhook create/update validated against GET /webhooks/event-types. |
| File is not a recognisable DDEX ERN NewReleaseMessage. | 422 | Sniff failed on POST /ingestion/ddex: not a valid ERN 3.8.2 / 4.2 / 4.3 envelope. |
| CSV header must include `title` and `track_title`. | 422 | POST /ingestion/csv minimum-column check. |
| Bulk batch limited to 200 releases. | 413 | POST /ingestion/bulk size cap. |
| Animated artwork must be ≤50 MB. | 413 | POST /releases/:uuid/animated-artwork/:asset_type. |
| Period code already exists. | 409 | POST /finance/periods uniqueness on code. |
| Only draft statements can be approved. | 409 | Finance state machine. |
| Not a descendant of your tenant. | 403 | Parent/child hierarchy check failed. |
| Service temporarily unavailable. | 503 | Database connection failed. Safe to retry with backoff. |
Response shape
{
"success": false,
"error": "Tenant not found."
}{
"success": false,
"error": "Validation failed",
"errors": {
"email": ["The email must be a valid email address."],
"tenant_slug": ["The tenant_slug field is required."],
"events": ["Unknown event pattern: release.dsp.spotify.totallyfake"]
}
}Idempotency
Make POST / PUT / PATCH requests safe to retry. Add an Idempotency-Key header; if the same key with the same body is sent again inside 24 hours, ToneGrid returns the stored response instead of running the operation a second time.
Semantics
| Idempotency-Key | Header. Opaque string up to 255 chars. Stripe-style: scoped per tenant via SHA-256(tenant_id ‖ raw key). |
| Body-hash gate | If the same key is reused with a different request body, the call returns HTTP 422 with "Idempotency-Key reused with a different request body." Retrying with the same body returns the cached response. |
| TTL | 24 hours. After that, the key is free to reuse with a new body. |
| Replay header | Cached responses include X-ToneGrid-Idempotent-Replay: true. Useful for client-side observability. |
| Scope | POST / PUT / PATCH on routes that mutate state (releases, tracks, artists, webhooks, ingestion, supply-chain, rights, tenants). GET / DELETE ignore the header. |
Example
# First call — runs normally, stores the response for 24h
curl -X POST https://api.tonegrid.pro/ingestion/json \
-H "Authorization: Bearer tgk_..." \
-H "Idempotency-Key: client-batch-2026-07-12-r0001" \
-H "Content-Type: application/json" \
--data @release.json
# Network blip — client retries with the EXACT same key + body
# Response is replayed from cache, no second release created.
# Header: X-ToneGrid-Idempotent-Replay: truerelease.{your-internal-id}.create). Don't use timestamps or random UUIDs — they defeat the purpose.Pagination
List endpoints support two modes — pick offset for shallow paging, cursor for deep / streaming scans.
Offset mode (default)
Familiar ?page=N&per_page=N. Returns meta.total, meta.current_page, meta.last_page. Best for UIs showing “page 3 of 47”.
curl https://api.tonegrid.pro/ingestion/jobs?page=2&per_page=50 \
-H "Authorization: Bearer tgk_..."{
"success": true,
"data": [ /* up to 50 items */ ],
"meta": {
"mode": "offset",
"total": 2347,
"per_page": 50,
"current_page": 2,
"last_page": 47
}
}Cursor mode (deep / streaming)
Add ?cursor=opaque. Performance does NOT degrade as you walk deeper into the list (no OFFSET on big tables). Best for syncing every job / every event into a downstream system.
# First call — no cursor
curl https://api.tonegrid.pro/ingestion/jobs?per_page=100 \
-H "Authorization: Bearer tgk_..."
# Subsequent calls — pass the next_cursor returned by the previous response
curl "https://api.tonegrid.pro/ingestion/jobs?per_page=100&cursor=eyJpZCI6MTIzNDV9" \
-H "Authorization: Bearer tgk_..."{
"success": true,
"data": [ /* up to 100 items */ ],
"meta": {
"mode": "cursor",
"per_page": 100,
"next_cursor": "eyJpZCI6MTIyNDV9",
"has_more": true
}
}Link: <https://api.tonegrid.pro/ingestion/jobs?per_page=100&cursor=eyJpZCI6MTIyNDV9>; rel="next"meta.has_more is false (or no Link: rel=next header is returned).Caps
| per_page | 1 to 100 (default 50). Some endpoints allow up to 500 (sales reports). |
| Cursor TTL | Cursors do not expire — but if the underlying row is deleted, the cursor may skip ahead by that row. Cursor walks are not strongly consistent with concurrent writes. |
Rate Limits
Requests are rate-limited per access token. Limits vary by endpoint category.
| Category | Limit | Window |
|---|---|---|
| General | 120 requests | per minute |
| Audio upload | 10 requests | per minute |
| Distribution | 30 requests | per minute |
| Analytics | 60 requests | per minute |
X-RateLimit-Limit, X-RateLimit-Remaining, and X-RateLimit-Reset headers.Sandbox Environment
Test every endpoint against a real, callable API surface — same request and response shapes as production, isolated database, isolated JWT secret, no DDEX bundle is shipped to live DSPs.
Environments
| Env | Base URL | Identifying header |
|---|---|---|
| Production | https://api.tonegrid.pro | none |
| Sandbox | https://api-sandbox.tonegrid.pro/api | X-Tonegrid-Env: api-sandbox on every response |
What's different from production
| Database | Separate sandbox DB. Sandbox data is fully isolated from production. |
| JWT signing secret | Separate. A leaked production token can't be used against the sandbox and vice versa. |
| DSP delivery | DDEX bundles are generated and the release_dsp_delivery queue advances, but the worker never ships to a real DSP endpoint. |
| Webhooks | Fully functional. Sandbox events fire against your real webhook URLs — handy for testing your verifier. |
| Royalty payouts | Statements + payee_statements get created; payouts rows are written but no money actually moves. |
| Idempotency / rate limits | Same as production. Use the sandbox to validate retry behaviour. |
First call — sanity check
# Returns 401 if no auth, 200 with data if your tgk_ key is valid
curl -i https://api-sandbox.tonegrid.pro/api/supply-chain/dsps \
-H "Authorization: Bearer tgk_..."// Node 20+ (built-in fetch)
const res = await fetch("https://api-sandbox.tonegrid.pro/api/supply-chain/dsps", {
headers: { Authorization: `Bearer ${process.env.TONEGRID_KEY}` }
});
console.log(res.status, await res.json());import os, requests
r = requests.get(
"https://api-sandbox.tonegrid.pro/api/supply-chain/dsps",
headers={"Authorization": f"Bearer {os.environ['TONEGRID_KEY']}"},
)
print(r.status_code, r.json())tgk_…) are separate from production. Mint one from your ToneGrid dashboard → Sandbox workspace → API Keys. The same key works for every sandbox endpoint.End-to-end testing recipe
- Mint a sandbox
tgk_…key. POST /api/ingestion/jsonwith a sample release + tracks.POST /api/webhookssubscribing torelease.dsp.*.*+ingestion.*with a temp URL (e.g. webhook.site).PUT /api/supply-chain/releases/:uuid/monetization+.../pricing-tier+.../territories.GET /api/releases/:uuid/ddex/preview/spotify.xml— download the literal DDEX bundle.POST /api/releases/:uuid/ddex/deliver— watch the webhook firerelease.dsp.spotify.submitted.GET /api/supply-chain/releases/:uuid/effective-config— confirm overrides resolved correctly.
Authentication
ToneGrid uses OAuth 2.0. Exchange your client credentials for an access token, then include it as a Bearer token in the Authorization header on every API request.
Exchange your client_id and client_secret for an OAuth 2.0 access token and refresh token.
Body parameters
| Parameter | Type | Required | Description |
|---|---|---|---|
| grant_type | string | required | Must be client_credentials |
| client_id | string | required | Your application's client ID |
| client_secret | string | required | Your application's client secret |
| scope | string | optional | Space-separated list of scopes (e.g. releases:read analytics:read) |
curl https://api.tonegrid.pro/auth/token \
-X POST \
-H "Content-Type: application/json" \
-d '{
"grant_type": "client_credentials",
"client_id": "your_client_id",
"client_secret": "your_client_secret"
}'
{
"access_token": "eyJhbGciOiJSUzI1NiIsInR5cCI6IkpXVCJ9...",
"token_type": "Bearer",
"expires_in": 3600,
"refresh_token": "def50200a1b2c3d4e5f6...",
"scope": "releases:read releases:write analytics:read"
}
Use your refresh token to obtain a new access token without re-authenticating with client credentials.
Body parameters
| Parameter | Type | Required | Description |
|---|---|---|---|
| grant_type | string | required | Must be refresh_token |
| refresh_token | string | required | The refresh token from a previous token response |
curl https://api.tonegrid.pro/auth/token/refresh \
-X POST \
-H "Content-Type: application/json" \
-d '{
"grant_type": "refresh_token",
"refresh_token": "def50200a1b2c3d4e5f6..."
}'
{
"access_token": "eyJhbGciOiJSUzI1NiIsInR5cCI6IkpXVCJ9...",
"token_type": "Bearer",
"expires_in": 3600,
"refresh_token": "def50200new_refresh_token..."
}
Immediately invalidates an access token or refresh token. Useful for logout flows or security incidents.
Body parameters
| Parameter | Type | Required | Description |
|---|---|---|---|
| token | string | required | The access token or refresh token to revoke |
curl https://api.tonegrid.pro/auth/token/revoke \
-X POST \
-H "Authorization: Bearer <access_token>" \
-H "Content-Type: application/json" \
-d '{ "token": "eyJhbGciOiJSUzI1NiIsInR5cCI6IkpXVCJ9..." }'
{ "message": "Token revoked successfully." }
Releases
A Release represents a single, EP, or album. Create a release first, then attach tracks, artwork, and metadata before submitting for review.
Returns a paginated list of all releases in your account, ordered by creation date descending.
Query parameters
| Parameter | Type | Required | Description |
|---|---|---|---|
| page | integer | optional | Page number (default: 1) |
| per_page | integer | optional | Results per page, max 100 (default: 20) |
| status | string | optional | Filter by status: draft, pending, approved, live, taken_down |
| type | string | optional | Filter by type: single, ep, album |
curl "https://api.tonegrid.pro/releases?status=live&page=1" \
-H "Authorization: Bearer <access_token>"
{
"data": [
{
"uuid": "a1b2c3d4-e5f6-7890-abcd-ef1234567890",
"title": "Eclipse",
"type": "single",
"status": "live",
"upc": "196922453871",
"release_date": "2026-04-22",
"artwork_url": "https://cdn.tonegrid.pro/artwork/a1b2c3d4.jpg",
"created_at": "2026-03-10T09:00:00Z"
}
],
"total": 1,
"page": 1,
"per_page": 20
}
Retrieves a single release with full metadata, track list, and distribution state.
curl "https://api.tonegrid.pro/releases/a1b2c3d4-e5f6-7890-abcd-ef1234567890" \
-H "Authorization: Bearer <access_token>"
{
"release": {
"uuid": "a1b2c3d4-e5f6-7890-abcd-ef1234567890",
"title": "Eclipse",
"type": "single",
"status": "live",
"upc": "196922453871",
"release_date": "2026-04-22",
"genre": "Afrobeats",
"subgenre": "Afropop",
"language": "en",
"label": "InterSpace Distribution",
"copyright_year": 2026,
"copyright_holder": "InterSpace Distribution Ltd",
"artwork_url": "https://cdn.tonegrid.pro/artwork/a1b2c3d4.jpg",
"tracks": [
{ "uuid": "t1t2t3t4-u5u6-7890-abcd-123456789012", "title": "Eclipse", "position": 1 }
],
"created_at": "2026-03-10T09:00:00Z",
"updated_at": "2026-04-22T08:00:00Z"
}
}
Creates a new release in draft status. Add tracks and artwork before submitting for review.
Body parameters
| Parameter | Type | Required | Description |
|---|---|---|---|
| title | string | required | Release title |
| type | string | required | single, ep, or album |
| release_date | string | required | ISO 8601 date (YYYY-MM-DD) |
| genre | string | required | Primary genre |
| label | string | optional | Record label name |
| upc | string | optional | Existing UPC/EAN — leave blank for auto-assignment |
| copyright_year | integer | optional | Copyright year (defaults to current year) |
| copyright_holder | string | optional | Copyright holder name |
curl https://api.tonegrid.pro/releases \
-X POST \
-H "Authorization: Bearer <access_token>" \
-H "Content-Type: application/json" \
-d '{
"title": "Eclipse",
"type": "single",
"release_date": "2026-05-01",
"genre": "Afrobeats",
"label": "InterSpace Distribution"
}'
{
"release": {
"uuid": "a1b2c3d4-e5f6-7890-abcd-ef1234567890",
"title": "Eclipse",
"type": "single",
"status": "draft",
"release_date": "2026-05-01",
"genre": "Afrobeats",
"upc": "196922453871",
"created_at": "2026-04-26T11:00:00Z"
}
}
Updates release metadata. Only releases in draft or pending status can be updated. All fields are optional — only supplied fields are updated.
curl "https://api.tonegrid.pro/releases/a1b2c3d4-e5f6-7890-abcd-ef1234567890" \
-X PUT \
-H "Authorization: Bearer <access_token>" \
-H "Content-Type: application/json" \
-d '{ "release_date": "2026-05-15", "genre": "Afropop" }'
{
"release": {
"uuid": "a1b2c3d4-e5f6-7890-abcd-ef1234567890",
"title": "Eclipse",
"status": "draft",
"release_date": "2026-05-15",
"genre": "Afropop",
"updated_at": "2026-04-26T11:05:00Z"
}
}
Submits a draft release for ToneGrid's quality review. The release transitions to pending status. Requires at least one attached track and artwork.
curl "https://api.tonegrid.pro/releases/a1b2c3d4-e5f6-7890-abcd-ef1234567890/submit" \
-X POST \
-H "Authorization: Bearer <access_token>"
{ "message": "Release submitted for review.", "status": "pending" }
Permanently deletes a release in draft status. Live or distributed releases cannot be deleted — request a takedown instead.
curl "https://api.tonegrid.pro/releases/a1b2c3d4-e5f6-7890-abcd-ef1234567890" \
-X DELETE \
-H "Authorization: Bearer <access_token>"
{ "message": "Release deleted." }
Tracks
Tracks are the audio assets that belong to a release. Each track must have an ISRC (auto-generated if not provided) and an uploaded WAV or FLAC audio file.
Returns all tracks for a release, ordered by position.
curl "https://api.tonegrid.pro/releases/a1b2c3d4-e5f6-7890-abcd-ef1234567890/tracks" \
-H "Authorization: Bearer <access_token>"
{
"tracks": [
{
"uuid": "t1t2t3t4-u5u6-7890-abcd-123456789012",
"title": "Eclipse",
"position": 1,
"isrc": "NGABC2600001",
"duration": 214,
"explicit": false,
"audio_status": "processed",
"created_at": "2026-03-10T09:10:00Z"
}
]
}
Returns full detail for a single track including audio processing status and contributor credits.
curl "https://api.tonegrid.pro/tracks/t1t2t3t4-u5u6-7890-abcd-123456789012" \
-H "Authorization: Bearer <access_token>"
{
"track": {
"uuid": "t1t2t3t4-u5u6-7890-abcd-123456789012",
"title": "Eclipse",
"position": 1,
"isrc": "NGABC2600001",
"duration": 214,
"explicit": false,
"language": "en",
"audio_status": "processed",
"audio_format": "WAV",
"audio_bit_depth": 24,
"audio_sample_rate": 44100,
"contributors": [
{ "name": "Davido", "role": "MainArtist" },
{ "name": "InterSpace Distribution", "role": "MusicPublisher" }
],
"created_at": "2026-03-10T09:10:00Z"
}
}
Creates a track and attaches it to a release. After creation, upload audio using the Upload audio endpoint.
Body parameters
| Parameter | Type | Required | Description |
|---|---|---|---|
| title | string | required | Track title |
| position | integer | required | Track number within the release |
| explicit | boolean | required | Whether the track contains explicit content |
| isrc | string | optional | Existing ISRC — auto-generated if omitted |
| language | string | optional | ISO 639-1 language code (default: en) |
curl "https://api.tonegrid.pro/releases/a1b2c3d4-e5f6-7890-abcd-ef1234567890/tracks" \
-X POST \
-H "Authorization: Bearer <access_token>" \
-H "Content-Type: application/json" \
-d '{ "title": "Eclipse", "position": 1, "explicit": false }'
{
"track": {
"uuid": "t1t2t3t4-u5u6-7890-abcd-123456789012",
"title": "Eclipse",
"position": 1,
"isrc": "NGABC2600001",
"explicit": false,
"audio_status": "pending",
"created_at": "2026-04-26T11:00:00Z"
}
}
Updates track metadata. Only available while the parent release is in draft status.
curl "https://api.tonegrid.pro/tracks/t1t2t3t4-u5u6-7890-abcd-123456789012" \
-X PUT \
-H "Authorization: Bearer <access_token>" \
-H "Content-Type: application/json" \
-d '{ "title": "Eclipse (Radio Edit)", "explicit": false }'
{
"track": {
"uuid": "t1t2t3t4-u5u6-7890-abcd-123456789012",
"title": "Eclipse (Radio Edit)",
"explicit": false,
"updated_at": "2026-04-26T11:10:00Z"
}
}
Uploads the audio file for a track using multipart/form-data. Accepted formats: WAV (recommended, 24-bit/44.1 kHz minimum) or FLAC. The file is asynchronously transcoded; poll audio_status on the track resource.
curl "https://api.tonegrid.pro/tracks/t1t2t3t4-u5u6-7890-abcd-123456789012/audio" \
-X POST \
-H "Authorization: Bearer <access_token>" \
-F "audio=@/path/to/eclipse_master.wav"
{
"message": "Audio upload received. Processing started.",
"audio_status": "processing"
}
Removes a track from its release. Only possible while the release is in draft status.
curl "https://api.tonegrid.pro/tracks/t1t2t3t4-u5u6-7890-abcd-123456789012" \
-X DELETE \
-H "Authorization: Bearer <access_token>"
{ "message": "Track deleted." }
Artists
Artist profiles are used to attribute tracks and releases. An artist can be a primary artist, featuring artist, or contributor. Artists are reusable across multiple releases.
Returns all artists associated with your account, ordered alphabetically.
curl "https://api.tonegrid.pro/artists" \
-H "Authorization: Bearer <access_token>"
{
"artists": [
{
"uuid": "ar1b2c3d-e5f6-7890-abcd-ef1234567890",
"name": "Davido",
"apple_artist_id": "1234567890",
"spotify_artist_id": "0Y5tJX1MQlPlqiwlOH1tJY",
"created_at": "2025-01-10T09:00:00Z"
}
],
"total": 1
}
Returns full detail for a single artist profile including DSP identifiers and biography.
curl "https://api.tonegrid.pro/artists/ar1b2c3d-e5f6-7890-abcd-ef1234567890" \
-H "Authorization: Bearer <access_token>"
{
"artist": {
"uuid": "ar1b2c3d-e5f6-7890-abcd-ef1234567890",
"name": "Davido",
"biography": "Afrobeats pioneer and global superstar.",
"country": "NG",
"apple_artist_id": "1234567890",
"spotify_artist_id": "0Y5tJX1MQlPlqiwlOH1tJY",
"created_at": "2025-01-10T09:00:00Z",
"updated_at": "2026-01-15T10:00:00Z"
}
}
Creates a new artist profile. DSP artist IDs (Apple, Spotify) are used to link the artist to existing profiles on those platforms.
Body parameters
| Parameter | Type | Required | Description |
|---|---|---|---|
| name | string | required | Artist name as it should appear on DSPs |
| country | string | optional | ISO 3166-1 alpha-2 country code |
| apple_artist_id | string | optional | Apple Music artist ID for profile linking |
| spotify_artist_id | string | optional | Spotify artist ID for profile linking |
| biography | string | optional | Short artist biography |
curl https://api.tonegrid.pro/artists \
-X POST \
-H "Authorization: Bearer <access_token>" \
-H "Content-Type: application/json" \
-d '{
"name": "Rema",
"country": "NG",
"spotify_artist_id": "46pWGuE3dSwY3bMMXGBvVS"
}'
{
"artist": {
"uuid": "ar9z8y7x-w6v5-4321-dcba-fedcba987654",
"name": "Rema",
"country": "NG",
"spotify_artist_id": "46pWGuE3dSwY3bMMXGBvVS",
"created_at": "2026-04-26T11:00:00Z"
}
}
Updates an artist profile. All body parameters are optional — only supplied fields are changed.
curl "https://api.tonegrid.pro/artists/ar9z8y7x-w6v5-4321-dcba-fedcba987654" \
-X PUT \
-H "Authorization: Bearer <access_token>" \
-H "Content-Type: application/json" \
-d '{ "apple_artist_id": "1109699158" }'
{
"artist": {
"uuid": "ar9z8y7x-w6v5-4321-dcba-fedcba987654",
"name": "Rema",
"apple_artist_id": "1109699158",
"updated_at": "2026-04-26T11:20:00Z"
}
}
Analytics
Query stream counts and audience data across releases, territories, and DSPs. All analytics endpoints accept an optional date range via from and to query parameters (ISO 8601 dates).
Returns aggregate stream counts and revenue totals across your entire catalog for the given date range.
Query parameters
| Parameter | Type | Required | Description |
|---|---|---|---|
| from | string | optional | Start date (YYYY-MM-DD, default: 30 days ago) |
| to | string | optional | End date (YYYY-MM-DD, default: today) |
curl "https://api.tonegrid.pro/analytics/summary?from=2026-04-01&to=2026-04-26" \
-H "Authorization: Bearer <access_token>"
{
"from": "2026-04-01",
"to": "2026-04-26",
"total_streams": 4820344,
"total_revenue_usd": "19821.50",
"top_release": {
"uuid": "a1b2c3d4-e5f6-7890-abcd-ef1234567890",
"title": "Eclipse",
"streams": 3102000
},
"top_dsp": "Spotify",
"top_territory": "NG"
}
Returns stream counts broken down by release, ranked by total streams in the period.
curl "https://api.tonegrid.pro/analytics/releases?from=2026-04-01&to=2026-04-26" \
-H "Authorization: Bearer <access_token>"
{
"from": "2026-04-01",
"to": "2026-04-26",
"data": [
{ "release_uuid": "a1b2c3d4-e5f6-7890-abcd-ef1234567890", "title": "Eclipse", "streams": 3102000 },
{ "release_uuid": "b2c3d4e5-f6a7-8901-bcde-f12345678901", "title": "Moonrise", "streams": 1718344 }
]
}
Returns stream counts grouped by territory (ISO 3166-1 alpha-2), ranked by total streams. Optionally filter by a specific release UUID.
Query parameters
| Parameter | Type | Required | Description |
|---|---|---|---|
| release_uuid | string | optional | Scope to a single release |
| from | string | optional | Start date (YYYY-MM-DD) |
| to | string | optional | End date (YYYY-MM-DD) |
curl "https://api.tonegrid.pro/analytics/territories?from=2026-04-01&to=2026-04-26" \
-H "Authorization: Bearer <access_token>"
{
"data": [
{ "territory": "NG", "country_name": "Nigeria", "streams": 1840000 },
{ "territory": "GH", "country_name": "Ghana", "streams": 620000 },
{ "territory": "US", "country_name": "United States", "streams": 512000 },
{ "territory": "GB", "country_name": "United Kingdom", "streams": 398000 }
]
}
Returns stream counts per DSP. Useful for understanding platform performance across your catalog.
curl "https://api.tonegrid.pro/analytics/dsps?from=2026-04-01&to=2026-04-26" \
-H "Authorization: Bearer <access_token>"
{
"data": [
{ "dsp": "Spotify", "streams": 2410000 },
{ "dsp": "Apple Music", "streams": 1020000 },
{ "dsp": "Boomplay", "streams": 880344 },
{ "dsp": "YouTube Music","streams": 510000 }
]
}
Royalties
Access your earnings, download royalty statements, and request withdrawals. Royalty data is updated monthly when DSPs deliver their reports.
Returns the current available and pending balance for your account in USD.
curl "https://api.tonegrid.pro/royalties/balance" \
-H "Authorization: Bearer <access_token>"
{
"balance": {
"available_usd": "19821.50",
"pending_usd": "4200.00",
"currency": "USD",
"last_updated": "2026-04-15T00:00:00Z"
}
}
Returns a list of monthly royalty statements, ordered newest first. Each statement covers one calendar month.
curl "https://api.tonegrid.pro/royalties/statements" \
-H "Authorization: Bearer <access_token>"
{
"statements": [
{ "id": "stmt_202603", "period": "2026-03", "total_usd": "19821.50", "status": "finalized", "created_at": "2026-04-10T00:00:00Z" },
{ "id": "stmt_202602", "period": "2026-02", "total_usd": "15304.00", "status": "finalized", "created_at": "2026-03-10T00:00:00Z" }
]
}
Returns full detail for a single royalty statement, broken down by release and DSP. Includes a download URL for the PDF statement.
curl "https://api.tonegrid.pro/royalties/statements/stmt_202603" \
-H "Authorization: Bearer <access_token>"
{
"statement": {
"id": "stmt_202603",
"period": "2026-03",
"total_usd": "19821.50",
"status": "finalized",
"pdf_url": "https://api.tonegrid.pro/royalties/statements/stmt_202603/pdf",
"breakdown": [
{ "release_title": "Eclipse", "dsp": "Spotify", "streams": 2100000, "revenue_usd": "8400.00" },
{ "release_title": "Eclipse", "dsp": "Apple Music", "streams": 900000, "revenue_usd": "5220.00" },
{ "release_title": "Moonrise","dsp": "Spotify", "streams": 1100000, "revenue_usd": "4400.00" }
]
}
}
Initiates a withdrawal of available funds to your registered payout method. Minimum withdrawal is $10 USD.
Body parameters
| Parameter | Type | Required | Description |
|---|---|---|---|
| amount_usd | number | required | Amount to withdraw in USD (minimum: 10.00) |
| note | string | optional | Optional reference note |
curl https://api.tonegrid.pro/royalties/withdrawals \
-X POST \
-H "Authorization: Bearer <access_token>" \
-H "Content-Type: application/json" \
-d '{ "amount_usd": 500.00 }'
{
"withdrawal": {
"id": "wth_a1b2c3d4",
"amount_usd": "500.00",
"status": "processing",
"estimated_arrival": "2026-04-28",
"created_at": "2026-04-26T11:00:00Z"
}
}
Distribution
The Distribution API lets you deliver approved releases to Digital Service Providers, manage territory availability, and request takedowns — all programmatically.
Returns all available Digital Service Providers that ToneGrid delivers to, along with their delivery method and average delivery time.
curl "https://api.tonegrid.pro/distribution/dsps" \
-H "Authorization: Bearer <access_token>"
{
"dsps": [
{ "id": 1, "name": "Spotify", "slug": "spotify", "delivery_method": "ddex_ern", "avg_delivery_days": 1 },
{ "id": 2, "name": "Apple Music", "slug": "apple_music", "delivery_method": "ddex_ern", "avg_delivery_days": 3 },
{ "id": 3, "name": "Boomplay", "slug": "boomplay", "delivery_method": "ddex_ern", "avg_delivery_days": 2 },
{ "id": 4, "name": "Audiomack", "slug": "audiomack", "delivery_method": "direct_api","avg_delivery_days": 1 },
{ "id": 5, "name": "YouTube Music","slug": "youtube_music", "delivery_method": "ddex_ern", "avg_delivery_days": 5 }
]
}
Returns all supported distribution territories as ISO 3166-1 alpha-2 codes. Use code WW to represent worldwide distribution.
curl "https://api.tonegrid.pro/distribution/territories" \
-H "Authorization: Bearer <access_token>"
{
"territories": [
{ "code": "WW", "name": "Worldwide" },
{ "code": "NG", "name": "Nigeria" },
{ "code": "GH", "name": "Ghana" },
{ "code": "US", "name": "United States" },
{ "code": "GB", "name": "United Kingdom" }
],
"total": 249
}
Queues an approved release for distribution to one or more DSPs. The release must be in approved status. Omit dsps to distribute to all available DSPs.
Body parameters
| Parameter | Type | Required | Description |
|---|---|---|---|
| dsps | array | optional | List of DSP slugs (e.g. ["spotify","apple_music"]). Omit to distribute to all DSPs. |
| scheduled_date | string | optional | ISO 8601 date to schedule delivery (e.g. 2026-05-01) |
curl "https://api.tonegrid.pro/releases/a1b2c3d4-e5f6-7890-abcd-ef1234567890/distribute" \
-X POST \
-H "Authorization: Bearer <access_token>" \
-H "Content-Type: application/json" \
-d '{ "dsps": ["spotify", "apple_music", "boomplay"] }'
{
"message": "Distribution queued.",
"deliveries": [
{ "dsp": "Spotify", "status": "queued", "scheduled_at": "2026-04-26T12:00:00Z" },
{ "dsp": "Apple Music", "status": "queued", "scheduled_at": "2026-04-26T12:00:00Z" },
{ "dsp": "Boomplay", "status": "queued", "scheduled_at": "2026-04-26T12:00:00Z" }
]
}
Returns real-time distribution status for each DSP a release has been delivered to, including live URLs once the release goes live.
Delivery status values
curl "https://api.tonegrid.pro/releases/a1b2c3d4-e5f6-7890-abcd-ef1234567890/distribution" \
-H "Authorization: Bearer <access_token>"
{
"release_uuid": "a1b2c3d4-e5f6-7890-abcd-ef1234567890",
"deliveries": [
{ "dsp": "Spotify", "status": "live", "live_url": "https://open.spotify.com/album/...", "delivered_at": "2026-04-22T08:10:00Z" },
{ "dsp": "Apple Music", "status": "live", "live_url": "https://music.apple.com/album/...", "delivered_at": "2026-04-24T16:30:00Z" },
{ "dsp": "Boomplay", "status": "processing", "live_url": null, "delivered_at": null }
]
}
Updates territory availability for an already-distributed release. Changes propagate to all DSPs via a DDEX update message within 24 hours.
Body parameters
| Parameter | Type | Required | Description |
|---|---|---|---|
| worldwide | boolean | optional | Set to true to enable worldwide distribution |
| include | array | optional | Territory codes to add (ISO 3166-1 alpha-2) |
| exclude | array | optional | Territory codes to remove |
curl "https://api.tonegrid.pro/releases/a1b2c3d4-e5f6-7890-abcd-ef1234567890/distribution/territories" \
-X PATCH \
-H "Authorization: Bearer <access_token>" \
-H "Content-Type: application/json" \
-d '{ "worldwide": true, "exclude": ["IR", "KP", "CU"] }'
{ "message": "Territory update queued. DSPs will be notified via DDEX within 24 hours." }
Sends a DDEX takedown notice to the specified DSPs. The release status changes to taken_down once all DSPs confirm removal.
Body parameters
| Parameter | Type | Required | Description |
|---|---|---|---|
| dsps | array | optional | List of DSP slugs. Omit to take down from all DSPs. |
| reason | string | optional | Reason for takedown (for internal records) |
curl "https://api.tonegrid.pro/releases/a1b2c3d4-e5f6-7890-abcd-ef1234567890/takedown" \
-X POST \
-H "Authorization: Bearer <access_token>" \
-H "Content-Type: application/json" \
-d '{ "reason": "Artist request." }'
{
"message": "Takedown notice sent to 5 DSP(s).",
"estimated_completion": "2026-04-28T00:00:00Z"
}
DDEX ERN 4.3 Pipeline
ToneGrid speaks DDEX ERN 4.3 — the current Electronic Release Notification standard published by Digital Data Exchange. Every release you approve is rendered to a NewReleaseMessage per DSP, signed, packaged, and direct-pushed to the DSP's ingestion endpoint. The output validates against the official release-notification.xsd — you can call the /validate endpoint to confirm before delivery.
Ern43Generator) and validated against the official DDEX XSD using libxml schemaValidate. Most aggregators ship XML and hope the DSP parses it. ToneGrid lets you XSD-check before push, every time, no extra setup.What gets generated
For each (release × DSP) pair, ToneGrid emits a single ern:NewReleaseMessage document containing:
| Section | What it contains |
|---|---|
MessageHeader | MessageThreadId, MessageId, MessageFileName, MessageSender (ToneGrid DPID + name), optional SentOnBehalfOf for SOBO, MessageRecipient (DSP DPID + name), MessageCreatedDateTime (UTC), MessageControlType (LiveMessage or TestMessage). |
PartyList | One Party per credited entity — artist, label, and every contributor (writers, producers, mix engineers, etc.). ISNI and IPI Name Numbers attach as PartyId children where validated. |
ResourceList | One SoundRecording per track + one Image for cover art. Each SoundRecording has one primary SoundRecordingEdition with full TechnicalDetails (codec, bitrate, channels, sample rate, bit depth, file URI, MD5 hash, file size), and an optional second SoundRecordingEdition of type ImmersiveEdition for Dolby Atmos. |
ReleaseList | A single Release with ReleaseId (GRid / ICPN-UPC / ProprietaryId), DisplayTitle (+ per-locale variants), DisplayArtist linked to PartyList, ReleaseLabelReference, PLine, CLine, Genre (with SubGenre), ReleaseDate + OriginalReleaseDate, ParentalWarningType, ResourceGroup with SequenceNumber per track, IsHiResMusic. |
DealList | One ReleaseDeal with the effective per-DSP terms: TerritoryCode (or ExcludedTerritoryCode), ValidityPeriod with StartDateTime, CommercialModelType list, UseType list (OnDemandStream, PermanentDownload, UserMakeAvailableLabelProvided, …), PriceInformation with WholesalePricePerUnit + currency. |
Beyond-standard features
- XSD self-validate endpoint
GET /releases/:uuid/ddex/validate/:dsp_slugruns the generated XML through the official DDEX ERN 4.3 XSD and returns a validation report — line, column, severity, message. Use it in CI to gate release approval on schema-cleanness. - Per-locale metadataPass
localized_titlesas a BCP-47 keyed object and we emit oneDisplayTitle+DisplayTitleTextper locale with@LanguageAndScriptCode. Same pattern for release and track titles. - Immersive audioSet
dolby_isrc+dolby_audio_urion a track and ToneGrid emits a secondSoundRecordingEditionwithRecordingMode = ImmersiveAudioandAudioCodecType = DolbyAtmosMasterADM, plusHasImmersiveAudioMetadata = trueon TechnicalDetails. - MD5 integrity hashesWhen you provide
audio_md5, it lands as aHashSumblock insideFilewith<Algorithm>MD5</Algorithm>— DSPs use this to verify the binary they pulled matches what you said you sent. - HiResMusic auto-flagIf every track is ≥88.2 kHz / 24-bit, the
<IsHiResMusic>true</IsHiResMusic>flag is set at the Release level — eligible for the HiResMusic logo on platforms that honour it. - Sent-on-behalf-ofFor sub-distributor partners and label services: when your tenant carries a
sobo_party_dpid+sobo_party_nameon supply-chain config, we emitSentOnBehalfOfin MessageHeader so DSPs route royalties to the underlying label. - Test-vs-live channel toggleSet
is_test_delivery = truein supply-chain config andMessageControlTypeflips toTestMessage— DSPs treat the payload as non-binding for QA. - Per-DSP override resolutionTerritory, monetization policies, pricing, rights, and release-date can all be overridden per DSP. The preview endpoint shows the resolved effective payload, so what you see is exactly what each DSP receives.
Delivery lifecycle
Approving a release creates one release_dsp_delivery row per attached DSP. The DDEX worker advances each row through three lifecycle stages, firing a webhook at each transition:
submitted | Bundle queued for the DSP. Fires release.dsp.<slug>.submitted. |
accepted | DSP acknowledged ingestion. Fires release.dsp.<slug>.accepted. |
live | DSP confirmed release is live on the storefront. release_dsp_delivery.status = live, live_at populated, dsp_release_id stored. Fires release.dsp.<slug>.live. |
Sample NewReleaseMessage (abridged)
Real output from GET /releases/:uuid/ddex/preview/spotify.xml for a 2-track EP — validates against the official ERN 4.3 XSD:
<?xml version="1.0" encoding="UTF-8"?>
<ern:NewReleaseMessage xmlns:ern="http://ddex.net/xml/ern/43"
AvsVersionId="2022"
LanguageAndScriptCode="en"
ReleaseProfileVersionId="Audio">
<MessageHeader>
<MessageThreadId>TG-THREAD-08bbb9acb793932b36dad693</MessageThreadId>
<MessageId>TG-2542cbd010b9d16c71c1f63bee330982</MessageId>
<MessageFileName>TG_spotify_rel-1234_20260518115400.xml</MessageFileName>
<MessageSender>
<PartyId>PADPIDA2026012301U</PartyId>
<PartyName><FullName>ToneGrid</FullName></PartyName>
</MessageSender>
<MessageRecipient>
<PartyId>PADPIDA2014020404J</PartyId>
<PartyName><FullName>Spotify</FullName></PartyName>
</MessageRecipient>
<MessageCreatedDateTime>2026-05-18T11:54:00Z</MessageCreatedDateTime>
<MessageControlType>LiveMessage</MessageControlType>
</MessageHeader>
<PartyList>
<Party><PartyReference>P_ARTIST_1</PartyReference>
<PartyName ApplicableTerritoryCode="Worldwide"><FullName>Ada Okafor</FullName></PartyName></Party>
<Party><PartyReference>P_LABEL_1</PartyReference>
<PartyName ApplicableTerritoryCode="Worldwide"><FullName>ToneGrid Records</FullName></PartyName></Party>
</PartyList>
<ResourceList>
<SoundRecording>
<ResourceReference>A0001</ResourceReference>
<Type>MusicalWorkSoundRecording</Type>
<SoundRecordingEdition>
<ResourceId><ISRC>NGTON2600001</ISRC></ResourceId>
<PLine ApplicableTerritoryCode="Worldwide">
<Year>2026</Year>
<PLineText>2026 ToneGrid Records</PLineText>
</PLine>
<RecordingMode>Stereo</RecordingMode>
<TechnicalDetails>
<TechnicalResourceDetailsReference>T0001</TechnicalResourceDetailsReference>
<DeliveryFile>
<Type>AudioFile</Type>
<AudioCodecType>PCM</AudioCodecType>
<BitRate UnitOfMeasure="kbps">4608</BitRate>
<NumberOfChannels>2</NumberOfChannels>
<SamplingRate UnitOfMeasure="Hz">96000</SamplingRate>
<BitsPerSample>24</BitsPerSample>
<Duration>PT3M38S</Duration>
<File>
<URI>s3://tonegrid-prod/audio/trk-aaa-001.wav</URI>
<HashSum><Algorithm>MD5</Algorithm><HashSumValue>d41d8cd98f00b204e9800998ecf8427e</HashSumValue></HashSum>
<FileSize>122071</FileSize>
</File>
<IsProvidedInDelivery>true</IsProvidedInDelivery>
</DeliveryFile>
</TechnicalDetails>
</SoundRecordingEdition>
<DisplayTitleText ApplicableTerritoryCode="Worldwide">Lagos Skyline</DisplayTitleText>
<DisplayTitleText LanguageAndScriptCode="fr">Horizon de Lagos</DisplayTitleText>
<DisplayTitleText LanguageAndScriptCode="yo">Itọju Eko</DisplayTitleText>
<DisplayTitle ApplicableTerritoryCode="Worldwide">
<TitleText>Lagos Skyline</TitleText>
<SubTitle SubTitleType="Version">Radio Edit</SubTitle>
</DisplayTitle>
<DisplayArtistName ApplicableTerritoryCode="Worldwide">Ada Okafor</DisplayArtistName>
<DisplayArtist SequenceNumber="1">
<ArtistPartyReference>P_ARTIST_1</ArtistPartyReference>
<DisplayArtistRole>MainArtist</DisplayArtistRole>
</DisplayArtist>
<Duration>PT3M38S</Duration>
<ParentalWarningType ApplicableTerritoryCode="Worldwide">NotExplicit</ParentalWarningType>
<IsHiResMusic>true</IsHiResMusic>
</SoundRecording>
<!-- … track 2 with optional immersive Dolby Atmos edition … -->
<Image>
<ResourceReference>A_COVER_1</ResourceReference>
<Type>FrontCoverImage</Type>
<ResourceId><ProprietaryId Namespace="tonegrid.pro">rel-12345-cover</ProprietaryId></ResourceId>
<TechnicalDetails>
<TechnicalResourceDetailsReference>T_COVER_1</TechnicalResourceDetailsReference>
<ImageCodecType>JPEG</ImageCodecType>
<ImageHeight UnitOfMeasure="Pixel">3000</ImageHeight>
<ImageWidth UnitOfMeasure="Pixel">3000</ImageWidth>
<File><URI>s3://tonegrid-prod/artwork/rel-12345/cover.jpg</URI></File>
</TechnicalDetails>
</Image>
</ResourceList>
<ReleaseList>
<Release LanguageAndScriptCode="en">
<ReleaseReference>R0</ReleaseReference>
<ReleaseType>EP</ReleaseType>
<ReleaseId>
<GRid>A12425GABC1234567X</GRid>
<ICPN>195497123456</ICPN>
<ProprietaryId Namespace="tonegrid.pro">rel-12345-67890-abcde</ProprietaryId>
</ReleaseId>
<DisplayTitleText ApplicableTerritoryCode="Worldwide">Midnight in Lagos</DisplayTitleText>
<DisplayTitle ApplicableTerritoryCode="Worldwide"><TitleText>Midnight in Lagos</TitleText></DisplayTitle>
<DisplayArtistName ApplicableTerritoryCode="Worldwide">Ada Okafor</DisplayArtistName>
<DisplayArtist SequenceNumber="1">
<ArtistPartyReference>P_ARTIST_1</ArtistPartyReference>
<DisplayArtistRole>MainArtist</DisplayArtistRole>
</DisplayArtist>
<ReleaseLabelReference>P_LABEL_1</ReleaseLabelReference>
<PLine ApplicableTerritoryCode="Worldwide"><Year>2026</Year><PLineText>2026 ToneGrid Records</PLineText></PLine>
<CLine ApplicableTerritoryCode="Worldwide"><Year>2026</Year><CLineText>2026 ToneGrid Records</CLineText></CLine>
<Duration>PT7M43S</Duration>
<Genre ApplicableTerritoryCode="Worldwide">
<GenreText>Afrobeats</GenreText>
<SubGenre>Alté</SubGenre>
</Genre>
<ReleaseDate ApplicableTerritoryCode="Worldwide">2026-06-17</ReleaseDate>
<ParentalWarningType ApplicableTerritoryCode="Worldwide">Explicit</ParentalWarningType>
<ResourceGroup>
<SequenceNumber>1</SequenceNumber>
<ResourceGroupContentItem>
<SequenceNumber>1</SequenceNumber>
<ReleaseResourceReference>A0001</ReleaseResourceReference>
</ResourceGroupContentItem>
<ResourceGroupContentItem>
<SequenceNumber>2</SequenceNumber>
<ReleaseResourceReference>A0002</ReleaseResourceReference>
</ResourceGroupContentItem>
<LinkedReleaseResourceReference>A_COVER_1</LinkedReleaseResourceReference>
</ResourceGroup>
<IsHiResMusic>true</IsHiResMusic>
</Release>
</ReleaseList>
<DealList>
<ReleaseDeal>
<DealReleaseReference>R0</DealReleaseReference>
<Deal>
<DealTerms>
<TerritoryCode>Worldwide</TerritoryCode>
<ValidityPeriod><StartDateTime>2026-06-17T00:00:00Z</StartDateTime></ValidityPeriod>
<CommercialModelType>AdvertisementSupportedModel</CommercialModelType>
<CommercialModelType>SubscriptionModel</CommercialModelType>
<CommercialModelType>PayAsYouGoModel</CommercialModelType>
<UseType>OnDemandStream</UseType>
<UseType>NonInteractiveStream</UseType>
<UseType>PermanentDownload</UseType>
<UseType>UserMakeAvailableLabelProvided</UseType>
<PriceInformation PriceType="StandardRetailPrice">
<WholesalePricePerUnit CurrencyCode="USD">9.99</WholesalePricePerUnit>
</PriceInformation>
</DealTerms>
</Deal>
</ReleaseDeal>
</DealList>
</ern:NewReleaseMessage>Returns the rendered NewReleaseMessage for this (release × DSP) pair in a JSON envelope (XML inline + sha256 + size_bytes). Append .xml to the slug to download as a raw .xml file instead.
curl "https://api.tonegrid.pro/releases/$UUID/ddex/preview/spotify" \
-H "Authorization: Bearer $TG_TOKEN"curl -O -J "https://api.tonegrid.pro/releases/$UUID/ddex/preview/spotify.xml" \
-H "Authorization: Bearer $TG_TOKEN"
# saves: $UUID-spotify.xml{
"success": true,
"data": {
"release_id": "a1b2c3d4-e5f6-7890-abcd-ef1234567890",
"dsp": { "slug": "spotify", "name": "Spotify" },
"ern_version": "4.3",
"choreography": "ERN-Direct",
"xml": "Same XML the preview endpoint returns, but additionally run through the official DDEX ERN 4.3 XSD validator (release-notification.xsd). Response includes a validation block listing every schema error with line, column, severity, and message. Returns validation.valid = true when the document is fully schema-clean.
curl "https://api.tonegrid.pro/releases/$UUID/ddex/validate/apple-music" \
-H "Authorization: Bearer $TG_TOKEN" | jq .data.validation{
"valid": true,
"errors": [],
"xsd_path": "/opt/ddex-schemas/ern43/release-notification.xsd",
"ern_version": "4.3"
}{
"valid": false,
"errors": [
{
"level": "error",
"line": 68,
"column": 0,
"message": "Element 'BitRate', attribute 'UnitOfMeasure': The value 'Kbps' is not an element of the set {'bps', 'Gbps', 'kbps', 'Mbps'}."
}
],
"ern_version": "4.3"
}Queues delivery to every DSP attached to the release. Honours per-DSP is_blocked overrides (those are skipped, reported in the response). Returns 202 with the per-DSP queue state and fires release.dsp.<slug>.submitted webhook for each.
curl -X POST "https://api.tonegrid.pro/releases/$UUID/ddex/deliver" \
-H "Authorization: Bearer $TG_TOKEN" \
-H "Idempotency-Key: deliver-$UUID-$(date +%s)"{
"success": true,
"message": "3 DSP(s) queued for direct DDEX delivery.",
"data": {
"queued": [
{ "dsp": "spotify", "state": "submitted" },
{ "dsp": "apple-music", "state": "submitted" },
{ "dsp": "tidal", "state": "submitted" }
],
"skipped": [
{ "dsp": "youtube-music", "reason": "is_blocked" }
]
}
}Same as above but scoped to a single DSP. Useful for re-deliveries, takedown follow-ups, or DSP-by-DSP backfills. Returns 409 if the DSP has is_blocked = true on this release.
Use when the metadata for an already-live release has changed (typo, new contributor, swapped cover). The worker pushes a fresh NewReleaseMessage to every attached DSP, reusing the original MessageThreadId so the DSP correlates it as an update to the prior delivery — not a brand-new release. Avoids losing stream attribution and ISRC continuity.
update_submitted, fires release.dsp.<slug>.update_submitted. After the worker pushes successfully, row returns to live and fires release.dsp.<slug>.updated with thread_continuation: true.curl -X POST "https://api.tonegrid.pro/releases/$UUID/ddex/redeliver" \
-H "Authorization: Bearer $TG_TOKEN" \
-H "Idempotency-Key: redeliver-$UUID-$(date +%s)"DDEX Purge (Hard Takedown)
PurgeReleaseMessage is the DDEX message type for hard removals — releases the DSP must drop wholesale because the metadata is corrupt, the rights have been pulled, or a legal takedown was mandated. For normal commercial takedowns, prefer re-delivering a NewReleaseMessage whose Deal ValidityPeriod EndDateTime is now.
MessageThreadId reuses the original NewReleaseMessage thread so DSPs correlate the purge with the prior delivery in their own systems. The body of the message contains just the ReleaseId (GRid / ICPN / ProprietaryId) + Title — minimal by design.
Returns the literal DDEX ERN 4.3 PurgeReleaseMessage the worker would push to this DSP. Append .xml to download as a raw .xml file. Validates against the official XSD.
<ern:PurgeReleaseMessage xmlns:ern="http://ddex.net/xml/ern/43"
AvsVersionId="2022"
LanguageAndScriptCode="en">
<MessageHeader>
<MessageThreadId>TG-THREAD-08bbb9acb793932b36dad693</MessageThreadId>
<MessageId>TG-PURGE-2e985218e6a526bde64d5505be72</MessageId>
<MessageFileName>TG_purge_spotify_a1b2c3d4_20260518174650.xml</MessageFileName>
<MessageSender>
<PartyId>PADPIDA2026012301U</PartyId>
<PartyName><FullName>ToneGrid</FullName></PartyName>
</MessageSender>
<MessageRecipient>
<PartyId>PADPIDA2014020404J</PartyId>
<PartyName><FullName>Spotify</FullName></PartyName>
</MessageRecipient>
<MessageCreatedDateTime>2026-05-18T17:46:50Z</MessageCreatedDateTime>
<MessageControlType>LiveMessage</MessageControlType>
</MessageHeader>
<PurgedRelease>
<ReleaseId>
<GRid>A12425GABC1234567X</GRid>
<ICPN>195497123456</ICPN>
<ProprietaryId Namespace="tonegrid.pro">a1b2c3d4-e5f6-7890-abcd-ef1234567890</ProprietaryId>
</ReleaseId>
<Title><TitleText>Midnight in Lagos</TitleText></Title>
</PurgedRelease>
</ern:PurgeReleaseMessage>Same as /ddex/validate but for the PurgeReleaseMessage variant. Returns validation.valid = true when schema-clean. Use in CI before queueing a takedown.
Hard takedown across every attached DSP. Each row flips to takedown_submitted and fires release.dsp.<slug>.takedown_submitted. The worker then pushes the PurgeReleaseMessage; on success the row becomes taken_down and release.dsp.<slug>.taken_down fires (with message_type: PurgeReleaseMessage).
ValidityPeriod EndDateTime path for commercial takedowns where you might want to revive later.Per-DSP variant. Useful for selective takedowns — e.g. pull from one DSP that flagged the release while leaving it live everywhere else.
For every DSP attached to the release: current status (pending / submitted / live / rejected / taken_down / error), DSP-assigned release ID, live URL, submitted_at, live_at, error_message, updated_at.
curl "https://api.tonegrid.pro/releases/$UUID/ddex/deliveries" \
-H "Authorization: Bearer $TG_TOKEN"{
"success": true,
"data": {
"release_id": "a1b2c3d4-...",
"deliveries": [
{
"uuid": "del-9ab1...",
"dsp": "spotify",
"dsp_name": "Spotify",
"status": "live",
"dsp_release_id": "spotify:album:7v0Y...",
"live_url": "https://open.spotify.com/album/7v0Y...",
"submitted_at": "2026-06-17T00:00:14Z",
"live_at": "2026-06-17T00:02:11Z",
"error_message": null,
"updated_at": "2026-06-17T00:02:11Z"
}
]
}
}Transfers
Transfers let you move release ownership between artist accounts or labels within ToneGrid. Common use cases include onboarding a catalog from an existing distributor, reassigning releases when an artist switches labels, or transferring legacy catalog entries. All transfers require acceptance by the receiving party.
Creates a new catalog transfer request. The receiving party gets a notification and must accept within 72 hours, after which the transfer expires.
Body parameters
| Parameter | Type | Required | Description |
|---|---|---|---|
| to_account_uuid | string | required | UUID of the receiving account |
| releases | array | optional | Array of release UUIDs to transfer. Omit to transfer the entire catalog. |
| message | string | optional | A note to the receiving party |
curl https://api.tonegrid.pro/transfers \
-X POST \
-H "Authorization: Bearer <access_token>" \
-H "Content-Type: application/json" \
-d '{
"to_account_uuid": "e5f6a7b8-c9d0-1234-efab-567890123456",
"releases": ["a1b2c3d4-e5f6-7890-abcd-ef1234567890"],
"message": "Artist moving to new label. Please accept ownership of listed releases."
}'
{
"transfer": {
"uuid": "f6a7b8c9-d0e1-2345-fabc-678901234567",
"status": "pending",
"from_account": "label@example.com",
"to_account_uuid": "e5f6a7b8-c9d0-1234-efab-567890123456",
"release_count": 1,
"expires_at": "2026-04-29T11:00:00Z",
"created_at": "2026-04-26T11:00:00Z"
}
}
Returns paginated transfer requests — both sent and received — filtered by direction and status.
Query parameters
| Parameter | Type | Required | Description |
|---|---|---|---|
| direction | string | optional | sent or received |
| status | string | optional | pending, accepted, declined, or expired |
| page | integer | optional | Page number (default: 1) |
curl "https://api.tonegrid.pro/transfers?direction=received&status=pending" \
-H "Authorization: Bearer <access_token>"
{
"data": [
{
"uuid": "f6a7b8c9-d0e1-2345-fabc-678901234567",
"status": "pending",
"from_account": "oldlabel@example.com",
"release_count": 1,
"expires_at": "2026-04-29T11:00:00Z",
"created_at": "2026-04-26T11:00:00Z"
}
],
"total": 1,
"page": 1,
"per_page": 20
}
Retrieves a single transfer request including the full list of releases included in the transfer.
curl "https://api.tonegrid.pro/transfers/f6a7b8c9-d0e1-2345-fabc-678901234567" \
-H "Authorization: Bearer <access_token>"
{
"transfer": {
"uuid": "f6a7b8c9-d0e1-2345-fabc-678901234567",
"status": "pending",
"from_account": "oldlabel@example.com",
"to_account": "newlabel@example.com",
"message": "Artist moving to new label.",
"release_count": 1,
"releases": [
{ "uuid": "a1b2c3d4-e5f6-7890-abcd-ef1234567890", "title": "Eclipse", "type": "single" }
],
"expires_at": "2026-04-29T11:00:00Z",
"created_at": "2026-04-26T11:00:00Z"
}
}
Accepts a pending transfer. Ownership of all included releases is immediately transferred to the accepting account. DSP deliveries are re-attributed. Only the receiving account can accept.
curl "https://api.tonegrid.pro/transfers/f6a7b8c9-d0e1-2345-fabc-678901234567/accept" \
-X POST \
-H "Authorization: Bearer <access_token>"
{ "message": "Transfer accepted. 1 release(s) transferred to your account." }
Declines a pending transfer request. The sending account is notified. The transfer moves to declined status and cannot be re-activated.
curl "https://api.tonegrid.pro/transfers/f6a7b8c9-d0e1-2345-fabc-678901234567/decline" \
-X POST \
-H "Authorization: Bearer <access_token>"
{ "message": "Transfer declined." }
Webhooks
ToneGrid pushes signed events to your URL the moment something happens in the supply chain. Per-DSP events name the DSP directly (release.dsp.spotify.live, release.dsp.applemusic.accepted) so the pipeline is visible at every hop.
Headers on every delivery
| X-ToneGrid-Signature | string | t=<unix_ts>,v1=<hex_sha256>. Verify by computing HMAC-SHA256(secret, ts + "." + raw_body) and comparing to v1. |
| X-ToneGrid-Event | string | The event name, e.g. release.dsp.spotify.live. |
| X-ToneGrid-Delivery-Id | uuid | Unique per delivery attempt. Use for idempotent processing on your side. |
Retry policy
Failed deliveries (non-2xx response, timeout >10s, connection refused) retry with exponential backoff: 1m, 5m, 15m, 1h, 3h, 8h, 24h, 48h. After 8 failed attempts the delivery is marked dead. Inspect failures via GET /webhooks/:uuid/deliveries.
Returns every event you can subscribe to plus the supported wildcard patterns. Per-DSP events follow release.dsp.<dsp_slug>.<stage> where stage is one of submitted, accepted, rejected, live, taken_down, failed.
{
"success": true,
"data": {
"events": [
"release.created",
"release.submitted",
"release.dsp.spotify.submitted",
"release.dsp.spotify.accepted",
"release.dsp.spotify.live",
"release.dsp.applemusic.live",
"release.dsp.youtubemusic.live",
"ingestion.received",
"ingestion.accepted",
"ingestion.rejected",
"royalty_statement.ready"
],
"wildcards": {
"*": "Subscribe to every event.",
"release.dsp.*.live": "Every DSP go-live event.",
"release.dsp.spotify.*": "Every Spotify lifecycle event.",
"ingestion.*": "Every inbound ingestion event."
}
}
}Returns the raw tgwhsec_… secret once. Store it server-side; you'll need it to verify signatures. Limit: 25 active subscriptions per tenant.
curl -X POST https://api.tonegrid.pro/webhooks \
-H "Authorization: Bearer tgk_..." \
-H "Content-Type: application/json" \
-d '{
"name": "Production hook",
"url": "https://example.com/tonegrid/webhook",
"events": ["release.dsp.*.live", "ingestion.*", "royalty_statement.ready"],
"description": "Notify our backend when DSPs go live"
}'{
"success": true,
"message": "Webhook created. Store the secret now — it will not be shown again.",
"data": {
"uuid": "9a8b...c1d0",
"name": "Production hook",
"url": "https://example.com/tonegrid/webhook",
"secret_prefix": "tgwhsec_a1b2c3d",
"secret": "tgwhsec_a1b2c3d4e5f6...long-secret-only-shown-now",
"events": ["release.dsp.*.live","ingestion.*","royalty_statement.ready"],
"is_active": true
}
}Triggers webhook.test against the subscription. Add webhook.test (or *) to its events list to receive it.
Per-attempt log with status (queued / sending / succeeded / failed / dead), response code, body, error message, duration_ms, retry count, scheduled_at.
Old secret invalid immediately. New tgwhsec_… returned in response. Update your verifier before the next event fires.
Ingestion
Push releases into ToneGrid via the channel that fits your existing systems. Every channel converges on the same downstream pipeline — direct DDEX delivery to DSP ingestion endpoints.
| POST /ingestion/json | Single release + tracks. Real-time: returns 201 with the new release UUID. |
| POST /ingestion/bulk | Array of releases (max 200). Async: returns 202 with per-release job UUIDs. |
| POST /ingestion/ddex | DDEX ERN 3.8.2 or 4.3 XML bundle. Raw XML body OR multipart ddex_xml. Async. |
| POST /ingestion/csv | CSV with header row. Required cols: title, track_title. Async. |
| SFTP drop-zone | Drop DDEX bundles into per-tenant SFTP inbox; cron pulls them in. Same job log. |
| GET /ingestion/jobs | Every inbound bundle, every channel, one log. Filter by source/state/date. |
| GET /ingestion/jobs/:uuid | Job detail: state, files staged, validation errors, resulting release_id. |
ingestion.received, ingestion.accepted, ingestion.rejected via webhooks to get instant async status without polling.Atomic: release + every track is created in one transaction. Returns 201 with the release object and a job UUID for traceability.
curl -X POST https://api.tonegrid.pro/ingestion/json \
-H "Authorization: Bearer tgk_..." \
-H "Idempotency-Key: client-release-001" \
-H "Content-Type: application/json" \
-d '{
"title": "Midnight in Lagos",
"type": "ep",
"genre": "Afrobeats",
"release_date": "2026-07-12",
"upc": "0123456789012",
"explicit_content": false,
"tracks": [
{"title":"Opening","track_number":1,"duration_seconds":215,"isrc":"USRC12345678"},
{"title":"Lagos Nights","track_number":2,"duration_seconds":192,"isrc":"USRC12345679"}
]
}'{
"success": true,
"message": "Release ingested via rest_json — 2 tracks created.",
"data": {
"ingestion_job_id": "7a3c...d910",
"release": { "uuid": "a1b2-...", "title": "Midnight in Lagos", "type": "ep" },
"tracks": [ { "uuid": "...", "title": "Opening" }, { "uuid": "...", "title": "Lagos Nights" } ]
}
}Submit up to 200 releases in one call. Each becomes its own job, processed by the worker. Returns 202 with the list of job UUIDs to poll or subscribe to.
Send the DDEX NewReleaseMessage as either raw XML body (Content-Type: application/xml) or multipart field ddex_xml. We sniff the ERN version + MessageId, stage the bundle, and queue it for the worker.
curl -X POST https://api.tonegrid.pro/ingestion/ddex \
-H "Authorization: Bearer tgk_..." \
-H "Content-Type: application/xml" \
--data-binary @release-bundle.xml{
"success": true,
"message": "DDEX bundle received. Track status via GET /ingestion/jobs/7a3c...",
"data": {
"ingestion_job_id": "7a3c...d910",
"state": "received",
"ddex_message_id": "PADPID-MSG-2026-07-12-001",
"ddex_ern_version": "4.3"
}
}Multipart upload field releases_csv. Required header columns: title, track_title. Optional: type, genre, release_date, upc, isrc, track_number, duration_seconds, audio_url, artist_name, explicit, language. Rows with the same title become tracks of one release.
State transitions: received → queued → parsing → validating → staged → accepted | rejected | failed. Includes manifest file refs, DDEX MessageId/ERN version, error_code + validation_errors when rejected, and the resulting release_id when accepted.
Supply Chain
The configuration layer that controls exactly what reaches each DSP for each release: which monetization policies apply, which pricing tier, which territories, and which DSPs receive the bundle. Every override is resolved into the final DDEX message we ship.
GET /supply-chain/releases/:uuid/effective-config for the resolved per-DSP final state. That is exactly what lands in each DDEX bundle.Every active DSP labelled with its delivery method, DDEX ERN version, ingestion protocol, certification level, and territory set. Direct DDEX feeds are marked delivery_method: "ddex_sftp_direct" or "ddex_rest_direct".
{
"success": true,
"data": {
"dsps": [
{ "slug": "spotify", "name": "Spotify", "delivery_method": "ddex_sftp_direct", "ddex_ern_version": "4.3", "certification_level": "preferred_provider_platinum", "supports_atmos": true, "supports_video": false, "territory_set": "global" },
{ "slug": "applemusic", "name": "Apple Music", "delivery_method": "ddex_sftp_direct", "ddex_ern_version": "4.3", "certification_level": "certified", "supports_atmos": true, "supports_video": true, "territory_set": "global" },
{ "slug": "youtubemusic", "name": "YouTube Music", "delivery_method": "ddex_rest_direct", "ddex_ern_version": "4.3", "certification_level": "certified", "supports_atmos": false, "supports_video": true, "territory_set": "global" }
],
"summary": { "total_active": 24, "direct_ddex_sftp": 18, "direct_ddex_rest": 4, "partner_routed": 2 },
"note": "Every DSP is delivered via DDEX ERN. Direct = bundle is pushed straight to the DSP's ingestion endpoint by ToneGrid."
}
}Returns the 8 standard codes: ad_supported_streaming, subscription_streaming, permanent_download, conditional_download, ringtone, ringback_tone, ugc_monetization, restricted. Use codes when calling PUT /supply-chain/releases/:uuid/monetization.
Six tiers: back_catalog, budget, mid, front_line, premium, custom. Each carries default per-track + per-album amounts in minor units (USD cents). custom means you supply pricing_custom_track_minor + pricing_custom_album_minor + pricing_currency.
Returns the current supply-chain configuration for a release: monetization policies, pricing tier (+ custom amounts), territory mode and codes. When unset, the response is the default config with is_default: true.
Body: {"monetization_policy_codes": ["subscription_streaming","permanent_download"]}. Validated against the catalogue.
Body: {"pricing_tier_code": "front_line"} or for custom: {"pricing_tier_code":"custom","pricing_custom_track_minor":129,"pricing_custom_album_minor":999,"pricing_currency":"USD"}.
Body: {"territory_mode":"worldwide"}, or {"territory_mode":"include","territory_codes":["NG","GH","KE","ZA"]}, or {"territory_mode":"exclude","territory_codes":["US","CA"]}. Codes are ISO 3166-1 alpha-2.
For every DSP attached to the release, shows the delivery method, DDEX ERN version, ingestion protocol, certification level, and current status. This is the supply-chain proof: clients see the literal pipeline that will execute.
Release-level config + per-DSP overrides, merged into the final state that lands in each DDEX bundle. Shows pricing, territory, monetization, rights, and the is_blocked kill-switch per DSP.
Override release-level config for a single DSP. Any subset of: pricing_tier_code, pricing_custom_*, pricing_currency, territory_mode, territory_codes, monetization_policy_codes, rights_override, is_blocked, release_date_override. DELETE the same path to clear and inherit again.
Rights
The rights envelope baked into every DDEX bundle: usage flags (streaming / download / UGC / sync), master + composition copyright lines, mechanical licensing posture, ISWC + IPI + PRO affiliation, sample clearance status, and AI disclosure.
curl -X PUT https://api.tonegrid.pro/releases/{uuid}/rights \
-H "Authorization: Bearer tgk_..." \
-H "Content-Type: application/json" \
-d '{
"streaming": true,
"download": true,
"ugc": true,
"sync": false,
"p_line": "(P) 2026 Lagos Sound Recordings",
"c_line": "(C) 2026 Lagos Sound Recordings",
"copyright_year": 2026,
"mech_license_held": true,
"mech_license_provider": "MLC",
"iswc": "T-123456789-0",
"ipi_name_number": "123456789",
"pro_affiliation": "ASCAP",
"sample_clearance_status": "cleared",
"ai_disclosure": "none"
}'Animated Artwork
Apple Motion Artwork (square 1:1 + tall 3:4) and Spotify Canvas (9:16 looping video) — uploaded per release, packaged into the DDEX bundle for DSPs that support it.
| asset_type | square_motion (Apple 1:1) · tall_motion (Apple 3:4) · canvas_loop (Spotify Canvas 9:16) |
| accepted formats | MP4 (H.264), MOV (H.264 / ProRes), WebM (VP9), animated GIF. Max 50 MB. |
Multipart upload field asset. Replaces any existing artwork of that type (previous version soft-deleted). Dimensions / duration / codec metadata are filled in asynchronously by the asset-validation worker.
Finance
Periods → sales reports → tenant statements → payee statements → payouts. Every amount in minor units (USD cents) to avoid float drift.
{
"success": true,
"data": {
"available_usd_minor": 125430,
"in_flight_usd_minor": 50000,
"lifetime_paid_usd_minor": 8932100,
"lifetime_gross_usd_minor":9201210,
"currency": "USD"
}
}Monthly / quarterly / annual / adhoc periods with state (open → closing → closed → paid). Used as the time bucket for sales reports and statements.
Per period × DSP × release × track × country. Filters: ?period=2026-04&dsp=spotify&release=&track=&isrc=&country=. Response includes streams + downloads + gross revenue + FX rate + USD-normalized revenue + aggregate totals.
One statement per period per tenant: total streams, gross, commission, withholding, net payable. Includes the per-payee breakdown linked from payee_statements.
Artist / songwriter / publisher / external payees, with their split share, withholding tax, and net payable per period.
Actual payouts via Tipalti / Paystack / wire / SEPA / PayPal / Wise / Payoneer / manual rails. Includes external reference, state, sent_at, succeeded_at, failed_at, error_message.
Credits — Writers, Publishers, Collaborators
First-class entities for the composition side of a release. Writers carry ISWC + IPI + PRO affiliation, publishers carry IPI + admin/owner flags + per-territory shares, collaborators carry one of 27 industry roles (producer, mix engineer, featured artist, instrumentalist, etc.). All three feed the DDEX ResourceList Composer, RightsController, and Contributor blocks.
curl -X POST https://api.tonegrid.pro/writers \
-H "Authorization: Bearer tgk_..." \
-H "Content-Type: application/json" \
-d '{
"name": "Ayodeji Balogun",
"legal_name": "Ayodeji Ibrahim Balogun",
"iswc": "T-123456789-0",
"ipi_name_number": "123456789",
"pro_affiliation": "PRS",
"country": "NG"
}'curl -X POST https://api.tonegrid.pro/publishers \
-H "Authorization: Bearer tgk_..." \
-H "Content-Type: application/json" \
-d '{
"name": "Lagos Sound Publishing",
"ipi_name_number": "987654321",
"pro_affiliation": "ASCAP",
"country": "NG",
"is_controlled": true,
"mech_license_via":"MLC"
}'Replaces the entire writer list for the track. Composer shares must sum to exactly 100% (or all be 0 to clear). Returns 422 with the running total if the math doesn't work.
curl -X PUT https://api.tonegrid.pro/tracks/{uuid}/writers \
-H "Authorization: Bearer tgk_..." \
-H "Content-Type: application/json" \
-d '{
"writers": [
{ "writer_uuid":"…", "role":"composer_lyricist", "composer_share_percent": 60, "master_share_percent": 0 },
{ "writer_uuid":"…", "role":"composer", "composer_share_percent": 40, "master_share_percent": 0 }
]
}'Optional for_writer_uuid links a publisher to a specific writer's share. Optional territory_codes scopes the admin deal (NULL = worldwide).
{
"publishers": [
{
"publisher_uuid": "…",
"for_writer_uuid": "…",
"share_percent": 100,
"is_administrator": true,
"is_original_owner": false,
"territory_codes": ["NG","GH","KE","ZA"]
}
]
}27 role enum: producer, executive_producer, co_producer, vocal_producer, mix_engineer, mastering_engineer, recording_engineer, assistant_engineer, featured_artist, vocalist, background_vocalist, guest_artist, instrumentalist, arranger, conductor, remixer, orchestra, choir, programmer, dj, sound_designer, a_and_r, liner_notes, photographer, illustrator, styled_by, other. role_detail carries the instrument when role=instrumentalist. credit_order controls liner-notes ordering.
{
"collaborators": [
{ "name":"Sarz", "role":"producer", "credit_order": 1 },
{ "name":"Tems", "role":"featured_artist", "credit_order": 2, "spotify_uri":"spotify:artist:..." },
{ "name":"P2J", "role":"mix_engineer", "credit_order": 3 },
{ "name":"Chris Gehringer","role":"mastering_engineer","credit_order": 4 },
{ "name":"Drum group", "role":"instrumentalist", "role_detail":"Drums", "credit_order": 5 }
]
}Localizations — Multi-language Metadata
Carry titles, descriptions, and lyrics in as many languages as you need. Locale keys are BCP-47 codes (en, en-US, fr, fr-CA, ja, ko, yo, ig, ha, sw, zu, ar, zh-CN, zh-TW, …). The DDEX bundle emits one <DisplayTitle> per locale with @LanguageAndScriptCode, so DSPs that accept multi-language ingest get every locale.
en. Must exist as a key in localized_titles when both are supplied.
Full replace. Send any subset of preferred_locale, localized_titles, localized_descriptions. Omitted keys are left untouched.
curl -X PUT https://api.tonegrid.pro/releases/{uuid}/localizations \
-H "Authorization: Bearer tgk_..." \
-H "Content-Type: application/json" \
-d '{
"preferred_locale": "en",
"localized_titles": {
"en": "Midnight in Lagos",
"fr": "Minuit à Lagos",
"yo": "Òjò Òru Èkó",
"ja": "ラゴスの真夜中",
"zh-CN": "拉各斯午夜"
},
"localized_descriptions": {
"en": "Late-night Afrobeats EP recorded in Lagos.",
"fr": "EP afrobeats nocturne enregistré à Lagos."
}
}'Per-track localized titles, title_versions, and lyrics. The DDEX bundle wires localized lyrics into <LyricsRights> alongside the per-locale title blocks. Apple synced lyrics via TTML use the localized_lyrics map keyed by BCP-47.
Tenants
Parent → child tenant hierarchy. ToneGrid models platform → enterprise → client → artist. Each tenant can manage its descendants, see usage rollups, and mint impersonation tokens to act as them for support / bulk-onboarding flows.
Role-gated by parent: platform can create any, enterprise → client | artist, client → artist, artist → nothing. Body: {name, slug, tenant_role, custom_domain?}.
Returns a short-lived Bearer token scoped to the child tenant. Use it for any follow-up call. Carries acting_as: true + on_behalf_of: <parent_uuid> claims. Every call made with the token is audit-logged.
Releases / tracks / artists counts + lifetime streams + lifetime revenue (USD minor units) for the child tenant.
Best Practices
Guidelines and recommendations for optimal API usage and performance. Treat these as the difference between a working integration and a robust one.
Authentication & auth tokens
- Prefer API keys for server-to-serverUse
Authorization: Bearer tgk_...API keys for backend integrations. They never expire and can be rotated/revoked individually per environment without touching login credentials. JWT login tokens are short-lived (1h) and meant for user-facing flows. - One key per environment, per serviceCreate separate API keys for sandbox vs prod, and for each backend service that calls the API. When something leaks, you rotate just that key. Naming convention:
tg-backend-prod-billing,tg-ci-sandbox, etc. - Never log tokensStrip
Authorizationheaders from your application logs before they hit your log aggregator. Toolchains like Sentry and Datadog do this by default if you add the header name to their scrubbing list.
Idempotency on every write
- Always send Idempotency-Key on POST / PUT / PATCHEvery mutating request supports the
Idempotency-Keyheader. Send a UUIDv4 you generate client-side, scoped to one logical operation. Retries reuse the same key — we replay the stored response instead of running the operation twice. - Key scope = operation, not requestIf "approve release X" retries 5 times due to network blips, all 5 attempts share one key. If the user clicks "approve" again 10 minutes later, that's a new operation and needs a new key.
- Don't reuse keys across endpointsEach key is unique per (key, endpoint, body-hash). Reusing a key with a different body returns
409. This is a safety net, not a feature — design your keys so this never happens.
Pagination
- Use cursor pagination, never offsetEvery list endpoint returns a
cursorobject:{ "next": "...", "prev": "..." }. Pass?cursor=<next>to walk forward. Cursors are stable across inserts — offsets are not. For very large catalogues, never iterate by?page=N. - Default page size = 50, max 200Pass
?limit=200on bulk-export jobs. Smaller pages for interactive UIs. Going above 200 returns the same 200 with anextcursor. - For full exports, sweep at low priorityBulk pulls of
/releasesor/trackscompete with live traffic. Throttle yourself to a few requests per second, run overnight, and resume from the lastnextcursor on failure.
Rate limits
- Watch X-RateLimit-RemainingEvery response carries
X-RateLimit-Limit,X-RateLimit-Remaining, andX-RateLimit-Reset(epoch). WhenRemainingdrops under 10% ofLimit, back off proactively rather than waiting for a 429. - Respect Retry-After on 429429 responses include
Retry-After: <seconds>. Sleep that long, then retry the same request (idempotency key intact). Don't burst-retry — that just gets you blocked harder. - Use exponential backoff for 5xxFor 502/503/504, retry with delays of 1s, 2s, 4s, 8s … capped at 60s with jitter. Stop after ~5 attempts and let your job queue requeue if needed. ToneGrid has CDN-level retries, so a sustained 5xx is real.
Catalog & metadata
- Set ISRC and UPC before approveToneGrid will mint them if blank, but you keep cleaner royalty matching by setting them yourself at
POST /releases. Use the recipient territory's standard (US: UPC-A 12 digit; EU: EAN-13). Always uppercase ISRC. - Send multi-language metadata up frontPass
localized_titlesas a BCP-47 object:{ "fr": "...", "yo": "...", "zh-Hans": "..." }. Same for track titles. ToneGrid renders oneDisplayTitle+DisplayTitleTextper locale in the DDEX. DSPs that support multi-language metadata (Apple, Spotify, Deezer) will index all of them. - audio_md5 enables binary verificationCompute the MD5 of your master before upload and pass it as
audio_md5. We include it as aHashSumin the DDEX so DSPs verify the pulled file matches what we claimed to send. Mismatches = rejected delivery. - 3000x3000 JPEG is the safe cover-art defaultApple Music wants ≥3000px, Spotify wants ≥640px. Send 3000x3000 JPEG and you cover both. Don't send PNG unless you need transparency; JPEG compresses better for photos.
- Set audio_bit_depth / sample_rate / channels on every trackRequired for the DDEX TechnicalDetails block. WAV 24-bit / 44.1 kHz / stereo is the floor. 24/96 unlocks the HiResMusic flag on Apple. 5.1+ channels enable surround indexing on platforms that support it.
DDEX delivery
- XSD-validate before you approveCall
GET /releases/:uuid/ddex/validate/:dsp_slugin your release-approval CI step. Ifvalidation.valid != true, surface the error block to the artist before they hit publish. Catches schema-breaking metadata at the source. - Use per-DSP overrides sparinglyPer-DSP overrides for territory / pricing / release-date are powerful but increase surface area. Keep them for genuine business needs (Japan-only physical exclusive, US-only pre-order). Don't override what the supply-chain default already says.
- is_test_delivery for QA bundlesSet
supply_chain.is_test_delivery = trueon test releases. We emitMessageControlType = TestMessagein the DDEX so DSPs treat the bundle as non-binding QA, not a real ingestion. - Re-deliver instead of re-creatingGot a metadata correction after live? Update the release fields, then
POST /releases/:uuid/ddex/deliverto push an update via the same MessageThreadId. Don't soft-delete + re-create — you lose stream attribution and ISRC continuity.
Webhooks
- Verify the X-ToneGrid-Signature on every eventHMAC-SHA256 signed using your webhook secret. Format:
t=<ts>,v1=<hex>. ComputeHMAC(secret, t + "." + raw_body)and constant-time-compare with v1. Reject events older than 5 minutes to prevent replay. - Return 200 quickly, process asyncWe retry any non-2xx with exponential backoff for up to 24h. Acknowledge fast (under 5s), enqueue the work, return 200. Slow handlers cause queue back-pressure on both sides.
- Idempotent handlersEvery event has an
event_id. Store it. If you see the same event_id twice, skip. We do retry the same payload and that's by design — your handler must be safe to call repeatedly. - Use one secret per environmentRotate secrets quarterly. We support overlapping secrets during rotation: set
previous_secretviaPUT /webhooks/:id, verify with either, then drop the old one a week later.
Tenants & multi-account
- Use parent-tenant hierarchy for label servicesIf you're a sub-distributor or label-services operator, model your clients as child tenants under your parent tenant. Create child tenants via
POST /tenants/clients. Their releases roll up into your accounting, but DSPs see them as autonomous catalogs (their own DPID via SOBO). - SOBO for partner-routed deliverySet
sobo_party_dpid+sobo_party_nameon the child's supply-chain config. The DDEX MessageHeader will then include<SentOnBehalfOf>— DSPs route royalties to the underlying label's DPID, not yours. - Don't share API keys across tenantsEach child tenant gets its own API keys. Token = tenant. We enforce tenant boundaries at the data layer; cross-tenant reads return 404 even if the UUID exists.
Sandbox usage
- Develop against api-sandbox.tonegrid.proIdentical surface to prod, separate database, no real money or DSP push. Webhooks fire against your dev endpoints. All payment integrations behave in test mode.
- Seed data with bin/seed-sandbox.phpReturns a copy-paste curl recipe that walks through signup, release create, track add, audio upload, approve, DDEX preview, validate, and observed sim-mode delivery transitions. Use it to bootstrap a dev environment in one shot.
- Test webhook signature handling hereGenerate intentionally bad signatures in sandbox to make sure your handler rejects them. Cheaper than discovering you trust unsigned events in production.
Performance
- Reuse HTTPS connectionsKeep-alive is on. If you're using
requests(Python) or similar, use a Session object. Barecurlin shell loops is ~3x slower because of TLS handshake on each call. - Batch where the endpoint supports itSome endpoints accept arrays (eg
POST /ingestion/bulkfor up to 100 releases). Use those for high-volume catalogue migrations instead of N individual calls. - Compress request bodiesFor payloads >100 KB, send
Content-Encoding: gzip. We accept it transparently and it saves real upload time on releases with rich metadata. - Multipart for >100 MB audioThe audio upload endpoint switches to AWS S3 Multipart Upload automatically above 100 MB. From the client side, just send the file — we slice it into 16 MB parts under the hood. Peak memory pinned at 16 MB regardless of file size.
Error handling
- Branch on HTTP status, then error code4xx = your request, fix it. 5xx = our problem, retry. Within 4xx, inspect
error.codefor the machine-readable reason (egrelease.not_approved,idempotency.key_mismatch). The full list is in the Errors catalogue. - Surface validation errors verbatim422 responses include a
fieldsobject mapping field path → error message. Display these directly in your UI; they're written for humans (artists submitting releases) and translated through the API per-tenant locale. - Never retry 4xxIf you got a 400/401/403/404/409/422, the same request will give the same answer. Fix the input first. Auto-retry loops on 4xx generate unrecoverable webhook storms that fill up your queue.
Security
- Never trust client-side auth for sensitive opsApprove, deliver, payout, takedown — all must originate from your server with a backend API key, not from the artist's browser. The endpoints accept either, but exposing them to client JS is a footgun.
- Audit logs are your friendEvery privileged action writes to the audit log.
GET /audit-logexposes the tenant-scoped subset. Review weekly. Filter by actor + action_type to catch anomalies. - IP allow-list backend keysFor backend-only API keys, set
ip_allowlistviaPUT /api-keys/:id. Even if the key leaks, it won't work from outside your infrastructure. CIDR notation; multiple ranges supported.