API Reference
api API play_arrow Quickstart terminal Explorer verified Webhook tester history Changelog report Errors monitor_heart Status menu_book Glossary help Help

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.

https://api.tonegrid.pro
info
All responses are JSON unless otherwise stated. Always include 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

StatusMeaning
200OK — request succeeded
201Created — resource was created
202Accepted — queued for async processing (ingestion, DDEX delivery, bulk)
204No Content — successful, nothing to return (DELETE, soft-deletes)
400Bad Request — malformed body or query, missing required headers
401Unauthorized — token missing, invalid, or revoked
403Forbidden — token authenticated but lacks scope / role / hierarchy access
404Not Found — resource does not exist or is not visible to this tenant
409Conflict — state machine refuses (e.g. submit a non-draft release), uniqueness clash, idempotency body mismatch
413Payload Too Large — file/bulk batch exceeds size limit
422Unprocessable Entity — validation failed; errors map names the bad fields
429Too Many Requests — rate limit hit; see Retry-After header
500Internal Server Error — please report with the X-Request-Id response header
503Service Temporarily Unavailable — database briefly unreachable; safe to retry

Named error strings

StringHTTPWhen it fires
Authentication token is missing.401No Authorization header and no cookie.
Invalid or expired token. Please log in again.401JWT signature mismatch or past exp.
Invalid, expired, or revoked API key.401tgk_… lookup failed: revoked_at set, expires_at past, or tenant suspended.
Tenant not found.404Unknown tenant_slug on login or unresolved X-Tenant-Domain.
Validation failed422Body validation failed; response includes errors: { field: ["msg",…] }.
Idempotency-Key reused with a different request body.422Same key + different body inside the 24h window.
Idempotency-Key must be ≤255 chars.400Header length cap.
Endpoint not found.404Path/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.409Tried to submit a release outside the allowed state.
Please add at least one track before submitting.422Submit gate; tracks count is zero.
Please select at least one store before submitting.422Submit gate; no DSPs attached.
Please set a release date before submitting.422Submit gate; release_date null.
Only draft or rejected releases can be deleted.409Delete protected: live / pending_review / qc_inspection.
Delivery to {DSP} is blocked via per-DSP override (is_blocked=true).409A release_dsp_overrides.is_blocked flag prevents delivery.
DSP '{slug}' is not attached to this release.422Need POST /releases/:uuid/dsps first.
No DSPs attached to this release.422Bulk-deliver called with empty DSP set.
DSP '{slug}' not found or inactive.404Unknown DSP slug or dsps.is_active = 0.
Webhook subscription limit (25) reached.409Per-tenant cap on active webhooks.
Unknown event pattern(s): {…}.422Webhook create/update validated against GET /webhooks/event-types.
File is not a recognisable DDEX ERN NewReleaseMessage.422Sniff 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`.422POST /ingestion/csv minimum-column check.
Bulk batch limited to 200 releases.413POST /ingestion/bulk size cap.
Animated artwork must be ≤50 MB.413POST /releases/:uuid/animated-artwork/:asset_type.
Period code already exists.409POST /finance/periods uniqueness on code.
Only draft statements can be approved.409Finance state machine.
Not a descendant of your tenant.403Parent/child hierarchy check failed.
Service temporarily unavailable.503Database 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-KeyHeader. Opaque string up to 255 chars. Stripe-style: scoped per tenant via SHA-256(tenant_id ‖ raw key).
Body-hash gateIf 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.
TTL24 hours. After that, the key is free to reuse with a new body.
Replay headerCached responses include X-ToneGrid-Idempotent-Replay: true. Useful for client-side observability.
ScopePOST / 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: true
tips_and_updates
Use a deterministic, per-logical-operation key (e.g. release.{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
  }
}
code
Treat the cursor as opaque. Don't parse it; ToneGrid may change the encoding without notice. Keep walking until meta.has_more is false (or no Link: rel=next header is returned).

Caps

per_page1 to 100 (default 50). Some endpoints allow up to 500 (sales reports).
Cursor TTLCursors 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.

CategoryLimitWindow
General120 requestsper minute
Audio upload10 requestsper minute
Distribution30 requestsper minute
Analytics60 requestsper minute
info
Rate limit status is returned in every response via 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

EnvBase URLIdentifying header
Productionhttps://api.tonegrid.pronone
Sandboxhttps://api-sandbox.tonegrid.pro/apiX-Tonegrid-Env: api-sandbox on every response
https://api-sandbox.tonegrid.pro/api

What's different from production

DatabaseSeparate sandbox DB. Sandbox data is fully isolated from production.
JWT signing secretSeparate. A leaked production token can't be used against the sandbox and vice versa.
DSP deliveryDDEX bundles are generated and the release_dsp_delivery queue advances, but the worker never ships to a real DSP endpoint.
WebhooksFully functional. Sandbox events fire against your real webhook URLs — handy for testing your verifier.
Royalty payoutsStatements + payee_statements get created; payouts rows are written but no money actually moves.
Idempotency / rate limitsSame 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())
vpn_key
Sandbox API keys (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

  1. Mint a sandbox tgk_… key.
  2. POST /api/ingestion/json with a sample release + tracks.
  3. POST /api/webhooks subscribing to release.dsp.*.* + ingestion.* with a temp URL (e.g. webhook.site).
  4. PUT /api/supply-chain/releases/:uuid/monetization + .../pricing-tier + .../territories.
  5. GET /api/releases/:uuid/ddex/preview/spotify.xml — download the literal DDEX bundle.
  6. POST /api/releases/:uuid/ddex/deliver — watch the webhook fire release.dsp.spotify.submitted.
  7. 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.

warning
Keep your API key server-side. Make sure that your API key never goes into client-side code; otherwise, your clients will be able to abuse it!
POST /auth/token Get access token

Exchange your client_id and client_secret for an OAuth 2.0 access token and refresh token.

Body parameters

ParameterTypeRequiredDescription
grant_typestringrequiredMust be client_credentials
client_idstringrequiredYour application's client ID
client_secretstringrequiredYour application's client secret
scopestringoptionalSpace-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"
}
POST /auth/token/refresh Refresh token

Use your refresh token to obtain a new access token without re-authenticating with client credentials.

Body parameters

ParameterTypeRequiredDescription
grant_typestringrequiredMust be refresh_token
refresh_tokenstringrequiredThe 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..."
}
POST /auth/token/revoke Revoke token

Immediately invalidates an access token or refresh token. Useful for logout flows or security incidents.

Body parameters

ParameterTypeRequiredDescription
tokenstringrequiredThe 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.

GET /releases List releases

Returns a paginated list of all releases in your account, ordered by creation date descending.

Query parameters

ParameterTypeRequiredDescription
pageintegeroptionalPage number (default: 1)
per_pageintegeroptionalResults per page, max 100 (default: 20)
statusstringoptionalFilter by status: draft, pending, approved, live, taken_down
typestringoptionalFilter 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
}
GET /releases/:uuid Get release

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"
  }
}
POST /releases Create release

Creates a new release in draft status. Add tracks and artwork before submitting for review.

Body parameters

ParameterTypeRequiredDescription
titlestringrequiredRelease title
typestringrequiredsingle, ep, or album
release_datestringrequiredISO 8601 date (YYYY-MM-DD)
genrestringrequiredPrimary genre
labelstringoptionalRecord label name
upcstringoptionalExisting UPC/EAN — leave blank for auto-assignment
copyright_yearintegeroptionalCopyright year (defaults to current year)
copyright_holderstringoptionalCopyright 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"
  }
}
PUT /releases/:uuid Update release

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"
  }
}
POST /releases/:uuid/submit Submit for review

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" }
DELETE /releases/:uuid Delete release

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.

GET /releases/:uuid/tracks List tracks

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"
    }
  ]
}
GET /tracks/:uuid Get track

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"
  }
}
POST /releases/:uuid/tracks Create track

Creates a track and attaches it to a release. After creation, upload audio using the Upload audio endpoint.

Body parameters

ParameterTypeRequiredDescription
titlestringrequiredTrack title
positionintegerrequiredTrack number within the release
explicitbooleanrequiredWhether the track contains explicit content
isrcstringoptionalExisting ISRC — auto-generated if omitted
languagestringoptionalISO 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"
  }
}
PUT /tracks/:uuid Update track

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"
  }
}
POST /tracks/:uuid/audio Upload audio

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.

info
Maximum file size is 500 MB. Audio upload requests are rate-limited to 10 per minute per token.
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"
}
DELETE /tracks/:uuid Delete track

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.

GET /artists List artists

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
}
GET /artists/:uuid Get artist

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"
  }
}
POST /artists Create artist

Creates a new artist profile. DSP artist IDs (Apple, Spotify) are used to link the artist to existing profiles on those platforms.

Body parameters

ParameterTypeRequiredDescription
namestringrequiredArtist name as it should appear on DSPs
countrystringoptionalISO 3166-1 alpha-2 country code
apple_artist_idstringoptionalApple Music artist ID for profile linking
spotify_artist_idstringoptionalSpotify artist ID for profile linking
biographystringoptionalShort 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"
  }
}
PUT /artists/:uuid Update artist

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).

GET /analytics/summary Summary

Returns aggregate stream counts and revenue totals across your entire catalog for the given date range.

Query parameters

ParameterTypeRequiredDescription
fromstringoptionalStart date (YYYY-MM-DD, default: 30 days ago)
tostringoptionalEnd 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"
}
GET /analytics/releases Streams by release

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 }
  ]
}
GET /analytics/territories Streams by territory

Returns stream counts grouped by territory (ISO 3166-1 alpha-2), ranked by total streams. Optionally filter by a specific release UUID.

Query parameters

ParameterTypeRequiredDescription
release_uuidstringoptionalScope to a single release
fromstringoptionalStart date (YYYY-MM-DD)
tostringoptionalEnd 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  }
  ]
}
GET /analytics/dsps Streams by DSP

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.

GET /royalties/balance Balance

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"
  }
}
GET /royalties/statements Statements

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" }
  ]
}
GET /royalties/statements/:id Get statement

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" }
    ]
  }
}
POST /royalties/withdrawals Request withdrawal

Initiates a withdrawal of available funds to your registered payout method. Minimum withdrawal is $10 USD.

Body parameters

ParameterTypeRequiredDescription
amount_usdnumberrequiredAmount to withdraw in USD (minimum: 10.00)
notestringoptionalOptional 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.

GET /distribution/dsps List DSPs

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 }
  ]
}
GET /distribution/territories List territories

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
}
POST /releases/:uuid/distribute Distribute release

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

ParameterTypeRequiredDescription
dspsarrayoptionalList of DSP slugs (e.g. ["spotify","apple_music"]). Omit to distribute to all DSPs.
scheduled_datestringoptionalISO 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" }
  ]
}
info
This endpoint returns HTTP 202 Accepted. Distribution is processed asynchronously. Use the Delivery status endpoint to track progress.
GET /releases/:uuid/distribution Delivery status

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

queued processing live | failed | taken_down
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 }
  ]
}
PATCH /releases/:uuid/distribution/territories Update territories

Updates territory availability for an already-distributed release. Changes propagate to all DSPs via a DDEX update message within 24 hours.

Body parameters

ParameterTypeRequiredDescription
worldwidebooleanoptionalSet to true to enable worldwide distribution
includearrayoptionalTerritory codes to add (ISO 3166-1 alpha-2)
excludearrayoptionalTerritory 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." }
POST /releases/:uuid/takedown Request takedown

Sends a DDEX takedown notice to the specified DSPs. The release status changes to taken_down once all DSPs confirm removal.

Body parameters

ParameterTypeRequiredDescription
dspsarrayoptionalList of DSP slugs. Omit to take down from all DSPs.
reasonstringoptionalReason for takedown (for internal records)
warning
Takedowns are irreversible through the API. To re-distribute a release after a takedown, you must create a new distribution request.
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.

verified
Schema-clean by construction. Every preview XML is produced by ToneGrid's generator (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:

SectionWhat it contains
MessageHeaderMessageThreadId, MessageId, MessageFileName, MessageSender (ToneGrid DPID + name), optional SentOnBehalfOf for SOBO, MessageRecipient (DSP DPID + name), MessageCreatedDateTime (UTC), MessageControlType (LiveMessage or TestMessage).
PartyListOne 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.
ResourceListOne 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.
ReleaseListA 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.
DealListOne 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 endpointGET /releases/:uuid/ddex/validate/:dsp_slug runs 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_titles as a BCP-47 keyed object and we emit one DisplayTitle + DisplayTitleText per locale with @LanguageAndScriptCode. Same pattern for release and track titles.
  • Immersive audioSet dolby_isrc + dolby_audio_uri on a track and ToneGrid emits a second SoundRecordingEdition with RecordingMode = ImmersiveAudio and AudioCodecType = DolbyAtmosMasterADM, plus HasImmersiveAudioMetadata = true on TechnicalDetails.
  • MD5 integrity hashesWhen you provide audio_md5, it lands as a HashSum block inside File with <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_name on supply-chain config, we emit SentOnBehalfOf in MessageHeader so DSPs route royalties to the underlying label.
  • Test-vs-live channel toggleSet is_test_delivery = true in supply-chain config and MessageControlType flips to TestMessage — 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:

submittedBundle queued for the DSP. Fires release.dsp.<slug>.submitted.
acceptedDSP acknowledged ingestion. Fires release.dsp.<slug>.accepted.
liveDSP confirmed release is live on the storefront. release_dsp_delivery.status = live, live_at populated, dsp_release_id stored. Fires release.dsp.<slug>.live.
science
Onboarding window. While ToneGrid is finalising its delivery integration with a given DSP, that DSP's deliveries run on a simulated timeline (~1 min submitted → accepted, ~1 min accepted → live) so your end-to-end integration looks identical to live delivery. As each DSP integration is brought online by ToneGrid's operations team, that DSP's deliveries transition seamlessly to live push — no client-side change required. Webhook payloads are unchanged across the transition.

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>
GET /releases/:uuid/ddex/preview/:dsp_slug Preview DDEX XML for a DSP

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":          "
GET /releases/:uuid/ddex/validate/:dsp_slug XSD-validate the generated DDEX (beyond-standard)

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.

info
Use this in CI to gate release approval on schema-cleanness. Most aggregators ship blind — ToneGrid lets you XSD-check before push.
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"
}
POST /releases/:uuid/ddex/deliver Deliver to every configured DSP

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" }
    ]
  }
}
POST /releases/:uuid/ddex/deliver/:dsp_slug Deliver to one DSP

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.

POST /releases/:uuid/ddex/redeliver Re-deliver as metadata UPDATE

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.

info
Lifecycle: row flips to 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.

GET /releases/:uuid/ddex/purge/preview/:dsp_slug Preview PurgeReleaseMessage

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>
GET /releases/:uuid/ddex/purge/validate/:dsp_slug XSD-validate the purge XML

Same as /ddex/validate but for the PurgeReleaseMessage variant. Returns validation.valid = true when schema-clean. Use in CI before queueing a takedown.

POST /releases/:uuid/ddex/purge Queue PurgeReleaseMessage to every DSP

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).

warning
Destructive. A purge instructs the DSP to remove the release from their catalog. Streams, playlist placements, and editorial coverage on the DSP side are typically lost. Use the regular Deal ValidityPeriod EndDateTime path for commercial takedowns where you might want to revive later.
POST /releases/:uuid/ddex/purge/:dsp_slug Queue PurgeReleaseMessage to one DSP

Per-DSP variant. Useful for selective takedowns — e.g. pull from one DSP that flagged the release while leaving it live everywhere else.

GET /releases/:uuid/ddex/deliveries Per-DSP delivery status

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.

POST /transfers Initiate transfer

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

ParameterTypeRequiredDescription
to_account_uuidstringrequiredUUID of the receiving account
releasesarrayoptionalArray of release UUIDs to transfer. Omit to transfer the entire catalog.
messagestringoptionalA 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"
  }
}
GET /transfers List transfers

Returns paginated transfer requests — both sent and received — filtered by direction and status.

Query parameters

ParameterTypeRequiredDescription
directionstringoptionalsent or received
statusstringoptionalpending, accepted, declined, or expired
pageintegeroptionalPage 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
}
GET /transfers/:uuid Get transfer

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"
  }
}
POST /transfers/:uuid/accept Accept transfer

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." }
POST /transfers/:uuid/decline Decline transfer

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.

verified
Every webhook is signed HMAC-SHA256 with your subscription secret. Verify before trusting any payload.

Headers on every delivery

X-ToneGrid-Signaturestringt=<unix_ts>,v1=<hex_sha256>. Verify by computing HMAC-SHA256(secret, ts + "." + raw_body) and comparing to v1.
X-ToneGrid-EventstringThe event name, e.g. release.dsp.spotify.live.
X-ToneGrid-Delivery-IduuidUnique 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.

GET /webhooks/event-types Canonical event catalogue

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."
    }
  }
}
POST /webhooks Create subscription

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
  }
}
POST /webhooks/:uuid/test Send a test event right now

Triggers webhook.test against the subscription. Add webhook.test (or *) to its events list to receive it.

GET /webhooks/:uuid/deliveries Inspect delivery log

Per-attempt log with status (queued / sending / succeeded / failed / dead), response code, body, error message, duration_ms, retry count, scheduled_at.

POST /webhooks/:uuid/regenerate-secret Rotate signing secret

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/jsonSingle release + tracks. Real-time: returns 201 with the new release UUID.
POST /ingestion/bulkArray of releases (max 200). Async: returns 202 with per-release job UUIDs.
POST /ingestion/ddexDDEX ERN 3.8.2 or 4.3 XML bundle. Raw XML body OR multipart ddex_xml. Async.
POST /ingestion/csvCSV with header row. Required cols: title, track_title. Async.
SFTP drop-zoneDrop DDEX bundles into per-tenant SFTP inbox; cron pulls them in. Same job log.
GET /ingestion/jobsEvery inbound bundle, every channel, one log. Filter by source/state/date.
GET /ingestion/jobs/:uuidJob detail: state, files staged, validation errors, resulting release_id.
bolt
Subscribe to ingestion.received, ingestion.accepted, ingestion.rejected via webhooks to get instant async status without polling.
POST /ingestion/json Real-time JSON ingest

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" } ]
  }
}
POST /ingestion/bulk Async bulk JSON

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.

POST /ingestion/ddex DDEX ERN 3.8.2 / 4.3 XML upload

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"
  }
}
POST /ingestion/csv CSV bulk import

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.

GET /ingestion/jobs/:uuid Job detail

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.

hub
Call GET /supply-chain/releases/:uuid/effective-config for the resolved per-DSP final state. That is exactly what lands in each DDEX bundle.
GET /supply-chain/dsps DSP coverage matrix

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."
  }
}
GET /supply-chain/monetization-policies Monetization policy catalogue

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.

GET /supply-chain/pricing-tiers Pricing tier catalogue

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.

GET /supply-chain/releases/:uuid Read release-level config

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.

PUT /supply-chain/releases/:uuid/monetization Set monetization policies

Body: {"monetization_policy_codes": ["subscription_streaming","permanent_download"]}. Validated against the catalogue.

PUT /supply-chain/releases/:uuid/pricing-tier Set pricing tier

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"}.

PUT /supply-chain/releases/:uuid/territories Set territory clearances

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.

GET /supply-chain/releases/:uuid/delivery-plan Per-DSP delivery plan

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.

GET /supply-chain/releases/:uuid/effective-config Resolved per-DSP final state

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.

PUT /supply-chain/releases/:uuid/dsps/:slug/overrides Per-DSP override

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.

GET /releases/:uuid/rights Read release rights
PUT /releases/:uuid/rights Update rights envelope
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_typesquare_motion (Apple 1:1) · tall_motion (Apple 3:4) · canvas_loop (Spotify Canvas 9:16)
accepted formatsMP4 (H.264), MOV (H.264 / ProRes), WebM (VP9), animated GIF. Max 50 MB.
GET /releases/:uuid/animated-artwork List uploaded animated artwork
POST /releases/:uuid/animated-artwork/:asset_type Upload animated artwork

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.

DEL /releases/:uuid/animated-artwork/:asset_type Soft-delete animated artwork

Finance

Periods → sales reports → tenant statements → payee statements → payouts. Every amount in minor units (USD cents) to avoid float drift.

GET /finance/balance Tenant balance summary
{
  "success": true,
  "data": {
    "available_usd_minor":      125430,
    "in_flight_usd_minor":       50000,
    "lifetime_paid_usd_minor": 8932100,
    "lifetime_gross_usd_minor":9201210,
    "currency": "USD"
  }
}
GET /finance/periods Accounting periods

Monthly / quarterly / annual / adhoc periods with state (open → closing → closed → paid). Used as the time bucket for sales reports and statements.

GET /finance/sales-reports Raw sales reports

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.

GET /finance/statements/:invoice Tenant statement detail

One statement per period per tenant: total streams, gross, commission, withholding, net payable. Includes the per-payee breakdown linked from payee_statements.

GET /finance/statements/:invoice.csv Download statement as CSV
GET /finance/payee-statements Per-payee breakdown

Artist / songwriter / publisher / external payees, with their split share, withholding tax, and net payable per period.

GET /finance/payouts Money sent

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.

groups
Create once, attach to many tracks. Writers/Publishers are tenant-scoped — list, edit, soft-delete. Track wiring is full-replace via PUT (idempotent).
GET /writers List writers
POST /writers Create writer
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"
  }'
GET /publishers List publishers
POST /publishers Create publisher
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"
  }'
PUT /tracks/:uuid/writers Set track writers (full replace)

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 }
    ]
  }'
PUT /tracks/:uuid/publishers Set track publishers (per-writer, per-territory shares)

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"]
    }
  ]
}
PUT /tracks/:uuid/collaborators Set track collaborators

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.

translate
preferred_locale on the release determines which title/description wins when a DSP only accepts a single language (Spotify, Apple ingestion). Default en. Must exist as a key in localized_titles when both are supplied.
GET /releases/:uuid/localizations Read release localizations
PUT /releases/:uuid/localizations Replace release localizations

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."
    }
  }'
PUT /tracks/:uuid/localizations Replace track localizations

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.

GET /tenants/clients List direct children of current tenant
POST /tenants/clients Onboard a child tenant

Role-gated by parent: platform can create any, enterprise → client | artist, client → artist, artist → nothing. Body: {name, slug, tenant_role, custom_domain?}.

POST /tenants/clients/:uuid/login-as Mint impersonation JWT

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.

GET /tenants/clients/:uuid/usage Usage rollup for a child tenant

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 Authorization headers 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-Key header. 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 cursor object: { "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=200 on bulk-export jobs. Smaller pages for interactive UIs. Going above 200 returns the same 200 with a next cursor.
  • For full exports, sweep at low priorityBulk pulls of /releases or /tracks compete with live traffic. Throttle yourself to a few requests per second, run overnight, and resume from the last next cursor on failure.

Rate limits

  • Watch X-RateLimit-RemainingEvery response carries X-RateLimit-Limit, X-RateLimit-Remaining, and X-RateLimit-Reset (epoch). When Remaining drops under 10% of Limit, 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_titles as a BCP-47 object: { "fr": "...", "yo": "...", "zh-Hans": "..." }. Same for track titles. ToneGrid renders one DisplayTitle + DisplayTitleText per 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 a HashSum in 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_slug in your release-approval CI step. If validation.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 = true on test releases. We emit MessageControlType = TestMessage in 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/deliver to 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>. Compute HMAC(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_secret via PUT /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_name on 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. Bare curl in shell loops is ~3x slower because of TLS handshake on each call.
  • Batch where the endpoint supports itSome endpoints accept arrays (eg POST /ingestion/bulk for 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.code for the machine-readable reason (eg release.not_approved, idempotency.key_mismatch). The full list is in the Errors catalogue.
  • Surface validation errors verbatim422 responses include a fields object 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-log exposes 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_allowlist via PUT /api-keys/:id. Even if the key leaks, it won't work from outside your infrastructure. CIDR notation; multiple ranges supported.