September 30, 2026
New
API
Cover Art Is Now Stored as JPEG: PNG and WebP Covers Are Converted on Upload
Cover art is now normalized to JPEG everywhere it enters the API. A PNG or WebP cover is converted to JPEG on the way in and stored as a .jpg CDN URL; a cover that cannot be read or converted is refused with 422 instead of being stored. This applies to both the cover_art_url field and the multipart artwork upload.
What changed
POST /releases and PATCH /releases/:uuid — the cover_art_url you pass is fetched, converted to JPEG, and re-hosted. A remote PNG/WebP, or a presigned PNG/WebP already on the ToneGrid CDN, is converted automatically, so you no longer need to pre-convert. If the image cannot be converted (corrupt or unreadable), the request returns 422.
POST /releases/:uuid/artwork (the multipart artwork upload) — a non-JPEG file is converted to JPEG before storage; an unconvertible file returns 422. The existing 3000x3000 pixel floor still applies and is checked first.
What to check in your integration
- The stored
cover_art_url returned by GET /releases/:uuid is now always a .jpg URL, even when you submitted a PNG or WebP. If you cache or compare the exact URL you sent, read it back from the release response instead of assuming it is unchanged.
- PNG transparency is flattened when the cover is converted to JPEG. Send art that looks correct on an opaque background.
September 29, 2026
API
`POST /videos` No Longer Requires an ISRC
You can now create a music video without supplying an ISRC. Omit the isrc field and the video is stored without one; a ToneGrid ISRC is assigned when the video is approved. Previously a video created without an ISRC stored an empty string, so the second ISRC-less video you created failed on the unique-ISRC constraint and returned a server error.
What changed
POST /videos — isrc is optional. When omitted or blank it is stored as null rather than an empty string, so you can create any number of videos without an ISRC and each one succeeds. The ISRC is auto-assigned at approval.
- When you do supply
isrc, it is still validated: it must be a 12-character ISRC (CC + RRR + YY + NNNNN, no hyphens) or the request returns 422.
No action required. If you were sending a placeholder ISRC to work around the previous failure, you can drop it.
September 29, 2026
API
Roster API Keys Can Now Create Releases Through `POST /ingestion/json`
An API key with roster access can now create releases through POST /ingestion/json for any artist in its tenant, the same way it already can through POST /releases. Previously only artist-role keys could create through ingestion, and a roster, owner, or team key received 403.
What changed
POST /ingestion/json — a roster-access key is now accepted as a creator. Every release it submits must name an artist_id; the artist is resolved and must belong to the key's tenant. Owner and team keys without roster access still receive 403, unchanged.
- The other ingestion channels —
POST /ingestion/bulk, POST /ingestion/ddex, and POST /ingestion/csv — are unchanged. They do not yet resolve an artist per release, so they still require an artist-role key.
If you drive ingestion with a roster key and were blocked, you can now call POST /ingestion/json directly, as long as each release carries an artist_id.
October 3, 2026
Docs
Authentication Is API Keys Only: No OAuth Flow, and How to Connect the MCP Server
The reference previously described an OAuth 2.0 client-credentials flow and a sign-up step that this API does not implement. Both have been removed. Authentication is a tgk_ API key sent as a Bearer token — there is no OAuth consent or client-credentials flow, and no POST /auth/signup or POST /api-keys endpoint to call.
Getting a key
- On the API Pro and API Scale plans you can issue and manage your own keys from the dashboard under API Keys. On other plans, keys are issued by support.
- Confirm a key with
GET /users/me: a valid key returns your workspace.
Connecting the MCP server
- The "Add custom connector" option in Claude clients expects an OAuth connector, which
mcp.tonegrid.pro does not offer. Connect instead with a config-file entry or claude mcp add --header, passing your tgk_ key in the Authorization header.
September 28, 2026
API
`POST /releases/:uuid/submit` and `POST /videos` Now Enforce Artist-Plan Limits
If you submit releases or create/submit videos using artist- or collaborator-role API keys on a tenant that runs artist plans, watch for two response codes you may not have handled before: 402 on release submission and 403 on video creation or submission.
What changed
POST /releases/:uuid/submit now checks the calling artist's or collaborator's plan release allowance before accepting the submission. Once the plan's releases_per_year limit is used up, the call returns 402 with a message naming the plan and the limit, e.g. "Your Free plan includes 2 releases every 12 months and you have used them all."
POST /videos and POST /videos/:uuid/submit now check the plan's video_distribution feature flag. If the artist's plan does not include video distribution, both return 403 with a message naming the plan, e.g. "Video distribution is not included in your Free plan."
Why
These limits already applied on ToneGrid's own web app submit flow. Until now the REST API did not enforce the same limits, so an artist- or collaborator-role API key could submit past its plan's release count or create videos on a plan that does not include them.
Who is affected
- Only tenants that have artist plans turned on, and only API keys scoped to the
artist or collaborator role. owner- and team-role keys are not subject to plan limits and are unaffected.
- Tenants that do not run artist plans see no change — both checks are a no-op when the tenant has no plan configured.
Action required
If you automate release or video submission on behalf of artist/collaborator accounts for a tenant that sells artist plans, handle 402 on POST /releases/:uuid/submit and 403 on POST /videos / POST /videos/:uuid/submit as plan-limit responses rather than generic failures. The error string is written to be shown to the artist and is safe to surface as-is. No other field on these endpoints changed.
September 30, 2026
Fix
Release Covers: PNG and WebP Now Convert to JPEG Instead of Failing QC Later
Covers have always had to end up as JPEG — stores and ToneGrid's own QC gate only ever accept JPEG — but a PNG or WebP cover was previously accepted at upload and only caught later, when QC rejected the release with "Cover art must be a JPEG." That round trip is gone on two of the three ways to set a cover.
What's fixed
POST /releases/:uuid/artwork (multipart upload): a PNG or WebP file is now converted to JPEG (alpha flattened to white, same pixel dimensions, quality 92) before it is stored. The 3000x3000 pixel floor from the September 5 breaking-change entry is still checked first, against the original file.
cover_art_url on POST /releases / PATCH /releases/:uuid, when the URL already points at ToneGrid's CDN (the presigned-upload flow: POST /releases/:uuid/artwork/presign, then PUT the file, then set cover_art_url to the returned URL): the stored file is converted to JPEG in place and the response reflects the new .jpg URL.
Not covered
cover_art_url pointing at an externally-hosted image (anything not already on ToneGrid's CDN) is still re-hosted as-is, in its original format. If you pass a remote PNG/WebP URL, convert it to JPEG on your side before sending it — it will otherwise still reach QC in its original format.
New failure mode
If a file cannot be converted (corrupt image, more than 8000x8000 pixels, or an unreadable format), the request now returns 422 with a message explaining why, instead of silently storing a cover that QC would have rejected days later.
September 29, 2026
Fix
`POST /ingestion/json` Now Accepts Roster-Scoped API Keys
A roster-scoped API key (api_keys.roster_access, shipped August 24 so one key can create releases for any artist in your tenant) worked on POST /releases but was refused with 403 on POST /ingestion/json. That gap is closed.
What's fixed
POST /ingestion/json now honours roster_access the same way POST /releases does: a roster-scoped key may create a release for any artist in its own tenant, provided it names that artist via artist_id on every release it creates through this channel.
Not changed
POST /ingestion/bulk, POST /ingestion/ddex, and POST /ingestion/csv do not resolve an artist the way the JSON channel does, so they still require an artist- or collaborator-role key and still return 403 for a roster-scoped key.
September 29, 2026
Fix
`POST /videos` No Longer Blocks on a Second Video With No ISRC
If you create music videos without supplying an isrc, a second or third blank-ISRC video used to come back 409 ("A record with these details already exists") even though you never sent an ISRC at all.
What's fixed
- An omitted
isrc on POST /videos is now stored as NULL instead of an empty string. The video catalogue's uniqueness constraint allows any number of NULL ISRCs but only one empty string, so every blank-ISRC video after the first used to collide on that constraint. Leave isrc out and it is still assigned automatically on approval — it just no longer blocks on the second one.
A genuinely duplicate, non-blank isrc still correctly returns 409. Only the false positive on blank values is fixed.
September 17, 2026
Docs
Tracks/Audio Docs Corrected: There Is No `audio_status` Field
If your integration polls a track for audio_status after uploading audio, stop — that field has never existed on this API. The reference previously documented it (plus audio_format, audio_bit_depth, audio_sample_rate) and told integrators to poll it after an "asynchronous transcode" that also does not happen. Upload is synchronous.
What the docs now say, correctly
POST /tracks/:uuid/audio (multipart, field name audio; POST /tracks/:uuid/upload is the same route) processes the file inline and returns { "success": true, "data": { "audio_url": "..." } } in the same request — there is no separate processing step and nothing to poll.
- Your completion signal is
audio_url going from null to a non-null CDN URL. That is what GET /tracks/:uuid and GET /releases/:uuid/tracks return once upload succeeds, alongside mime_type and file_size.
- If your code currently branches on
audio_status, audio_format, audio_bit_depth, or audio_sample_rate, remove that logic — the API has never returned those fields, so any branch reading them has always fallen through to your default/undefined case.
September 14, 2026
Fix
`release.rejected` Now Fires for Every QC Rejection, Not Just One Route
If you subscribe to release.rejected and noticed it silently not firing for some rejections, that gap is closed. The webhook now fires from every path that rejects a release, including ToneGrid's own QC review queue, not just the public REST route.
What was wrong
release.rejected only ever fired when a rejection went through POST /releases/:uuid/reject. Rejections made through ToneGrid's own QC review queue (staff-side, not that route) never emitted the webhook, so any release rejected there produced no event at all — the release's status still moved to rejected with a rejection_reason, but your webhook handler never heard about it.
What's fixed
release.rejected now fires with the same payload shape on every rejection path: release_id (UUID), title, and reason. No payload change if you already handle this event — you should simply start receiving it for rejections that previously produced nothing.
No action required unless you were working around the gap by polling status for rejected as a substitute; the webhook is now reliable enough to drop that workaround.
September 19, 2026
API
The API Now Has a Version in the URL: `/v1`
Every endpoint is now reachable under a versioned path, and every response tells you which version served it. Nothing you have built today breaks. This is purely additive, and there is no migration deadline.
What changed
- Canonical base URL is now
https://api.tonegrid.pro/v1 (sandbox: https://api-sandbox.tonegrid.pro/api/v1). Every example in the reference, the quickstart and the OpenAPI spec has been updated to use it.
- Unversioned paths keep working, permanently.
GET /releases is served by v1 exactly as GET /v1/releases is. The two are the same route, share the same rate-limit bucket, and share the same idempotency keys, so a retry that switches between them still deduplicates correctly.
- Two new response headers on every endpoint and every status code:
X-API-Version (the major version that served the request, currently v1) and X-ToneGrid-API-Revision (the dated payload contract, currently 2026-05-18, the same string webhook envelopes send as api_version). Both are listed in Access-Control-Expose-Headers, so browser clients can read them.
- Header pinning for clients that cannot change their base URL: send
X-API-Version: v1 on any request. If both a path prefix and the header are present, the path wins.
GET /health now reports the version surface: api_version, current_version, supported_versions and revision. The existing version field is unchanged, so monitors reading it are unaffected.
- Asking for a version we do not serve now says so.
GET /v9/releases returns 404 and a bad X-API-Version header returns 400, both naming supported_versions. Previously an unknown prefix produced a generic "Endpoint not found", which was indistinguishable from a mistyped resource.
Action required
None. If you change nothing, your integration keeps working indefinitely.
For new work, use the explicit /v1 prefix. It costs nothing and it makes the contract you depend on visible in your own code rather than implied by its absence.
Why this matters
An unversioned path is pinned to v1 forever. When a future major version ships, unversioned requests will still resolve to v1, and we will not quietly re-point them. That is the whole purpose of doing this now: your integration will never be moved onto a new major version because of a change on our side.
When a major version is eventually scheduled for retirement, its responses start carrying RFC 8594 Deprecation and Sunset headers well ahead of the date, plus a Link header pointing here. Watching for those two headers is the reliable way to hear about a retirement without reading this page. v1 is current and is not deprecated, so no such header is being sent today.
The full policy, including exactly what we treat as a breaking change versus an additive one, is in Versioning in the API reference.
September 15, 2026
API
Deprecating `release.approved` — Use `release.delivered` Instead (Sunset 2026-12-14)
A new release-level webhook event, release.delivered, now fires at the exact moment release.approved used to: when a release clears review and is handed to the delivery pipeline. Both events fire together starting today, with identical payloads. release.approved is deprecated and will stop firing on 2026-12-14.
Breaking / migration
- What changed:
release.delivered joins the core event catalogue (GET /webhooks/event-types) as the successor to release.approved. The payload shape is unchanged — release_id (release UUID) and title, same as before.
- Why:
delivered more accurately describes the moment than approved — the release has cleared review and is being queued to stores, not simply marked "approved" in isolation.
- Action required: if you subscribe to
release.approved, add release.delivered to your webhook's events array (PATCH /webhooks/:uuid). No payload changes are needed on your end — it's a drop-in rename. Remove release.approved from your subscription once you've verified the new event is arriving.
- Effective: both events fire starting 2026-09-15.
release.approved stops firing on 2026-12-14 (90 days). Subscriptions still listing only release.approved after that date will stop receiving this signal.
Not to be confused with the per-DSP release.dsp.:slug.delivered event added 2026-09-04 (see below) — that one fires once per store as each DSP enters the delivery pipeline. release.delivered is release-level and fires once per release, matching exactly how release.approved always behaved.
September 5, 2026
API
Breaking: `POST /releases/:uuid/artwork` Now Rejects Covers Under 3000x3000
If your integration uploads cover art directly, check the images you send now. POST /releases/:uuid/artwork (the multipart artwork upload) measures the image before storing it and returns 422 if it is smaller than 3000x3000 pixels, or if the file cannot be read as an image at all. Previously this endpoint stored whatever was uploaded, at any size.
Breaking / migration
- What changed:
POST /releases/:uuid/artwork now enforces a 3000x3000 pixel floor on the uploaded file. An image under that size, or one that cannot be measured (corrupt file, non-image content), returns 422 with a message naming the pixel dimensions received.
- Why: distribution partners and DSPs reject sub-3000px covers at ingest, so releases were reaching the delivery pipeline with artwork that would only fail later, one store at a time.
- Action required: make sure any automated upload sends a square JPEG or PNG at least 3000x3000 pixels. If you handle
422 responses generically today, add a check for this one specifically — it means the file needs to be re-exported at a larger size, not resubmitted as-is.
- Effective: already live, since 2026-09-05. There is no grace period on this endpoint.
This check only runs on the multipart endpoint. POST /releases/:uuid/artwork/presign plus a direct PUT to the returned URL is unchanged and does not validate dimensions server-side — if you use the presign flow, validate the image is at least 3000x3000 before the PUT, since an undersized cover uploaded that way will still fail later at store delivery.
September 4, 2026
API
Per-DSP Webhooks: a `delivered` Event on Approval, and `takedown_submitted` Now Fires
Two changes to the per-DSP lifecycle webhook events listed in GET /webhooks/event-types. If you subscribe to release.dsp.* patterns, a new event value now reaches your endpoint, and an event that was in the catalogue but never delivered now fires.
New event
release.dsp.:slug.delivered — emitted for each store the moment a release is approved to it, marking the point that store enters the delivery pipeline. Example: release.dsp.spotify.delivered.
- The store then moves through the existing
submitted → accepted → live stages, so read delivered as "handed to the store's delivery pipeline", not "live in the store" — live is still the go-live stage.
- The payload is release-UUID-addressed:
release_id is the release UUID, matching every other release lifecycle event.
delivered is a new value in the per-DSP lifecycle and now appears in GET /webhooks/event-types. If your handler enumerates lifecycle stages with a strict deserializer, add delivered or tolerate unknown values so this does not break your parser.
Fixed
release.dsp.:slug.takedown_submitted now fires when a release is taken down from a store. Previously a takedown completed without emitting this event, so a subscriber built against it received nothing; it now delivers reliably. Example: release.dsp.spotify.takedown_submitted.
As with the rest of the per-DSP lifecycle, both events neutralise to release.distribution.* for partner-routed deliveries.
September 4, 2026
Fix
Webhook Subscriptions to Apple Music, Amazon Music, YouTube Music, and 30+ Other DSPs No Longer Return 422
GET /webhooks/event-types now returns the same DSP catalogue the platform actually delivers to, and the events array on POST /webhooks / PATCH /webhooks/:uuid is validated against it. Previously the catalogue was a hand-maintained list that had drifted from the real DSP slugs, so subscribing to a correct, real event pattern for several major stores returned 422 Unknown event pattern(s).
Fixed
- Six slugs in the old catalogue did not exist on the platform and are now corrected:
applemusic → apple-music, amazonmusic → amazon-music, youtubemusic → youtube-music, instagram → instagram-facebook, tencent → tencent-music. qqmusic is removed — it was never a real DSP.
- Check your existing subscriptions. If you built a subscription from the old catalogue (for example
release.dsp.applemusic.live), that pattern still passes validation but will never match a real event — delivery has always emitted the corrected slug. Update any such pattern to the real slug above.
- About 30 DSPs that were already emitting events but missing from the catalogue are now listed, including
youtube, youtube-content-id, qobuz, 7digital, melon, bandcamp, kkbox, snapchat, vevo, and their -video variants. POST /webhooks requests subscribing to these previously returned 422 Unknown event pattern(s); they are now accepted. Full current list (55 slugs) is in the dsps field of GET /webhooks/event-types.
New events in the catalogue
- The per-DSP lifecycle gains two stages:
update_submitted (metadata update sent to the DSP) and takedown_submitted (takedown message sent), plus updated (DSP acknowledged a metadata update). Example: release.dsp.spotify.update_submitted.
- New core events:
release.qc_pending, release.artwork.uploaded, video.created, video.submitted, video.approved, video.rejected.
release.distribution.* is now a named entry in the catalogue. It was already firing for partner-routed deliveries; you could previously only catch it with a wildcard subscription because it did not appear in GET /webhooks/event-types.
September 2, 2026
API
Breaking: AI-Flagged Tracks Must Disclose Which Parts Are AI via `ai_elements`
If your integration posts AI-flagged tracks, this changes the request contract as of today. When a track carries includes_ai in track_properties, both POST /releases/:uuid/tracks and PATCH /tracks/:uuid now require a new field, ai_elements — an array naming which parts of the track AI made. A request that flags AI without it returns 422. The reason: stores render this disclosure in the track credits panel under the DDEX AI-disclosure standard the DSPs adopted in late 2025, so a bare "includes AI" flag with nothing behind it is not a usable disclosure.
Breaking / migration
- What changed: a track flagged with
includes_ai must now send ai_elements alongside ai_service. Missing or empty ai_elements on an AI-flagged track returns 422 on the ai_elements key.
- Action required now: add
ai_elements to every AI-flagged create and update. Canonical request shape:
{ "track_properties": ["includes_ai"], "ai_service": "Suno", "ai_elements": ["vocals", "instrumentation"] }
- Scope: already-approved catalogue is not retroactively re-validated. New submissions and updates are. A pipeline that posts AI-flagged tracks without
ai_elements starts failing at submission time until you add the field.
ai_elements — allowed values and behaviour
- Exactly five values, lowercase:
vocals, instrumentation, composition, lyrics, production. Send at least one.
- Must be a JSON array of strings. A non-array returns
422 on the ai_elements key.
- An unrecognised value is a hard
422 listing the allowed values — it is not silently dropped. Sending "vocal" instead of "vocals" fails the request rather than quietly registering nothing.
- Values are normalised: case-insensitive, trimmed, de-duplicated, and returned in the canonical order above.
ai_elements is echoed back on track reads.
- On
PATCH /tracks/:uuid, elements already stored on the track satisfy the requirement — updating an unrelated field on an already-disclosed track does not force you to resend ai_elements.
- Sending
track_properties without includes_ai retracts the disclosure and clears the stored ai_service, ai_service_other, and ai_elements.
No AI service is blocked any more
- The API no longer rejects any AI service by name. A named generative tool that previously returned
422 is now accepted, as is any AI service, including fully generative ones — provided the disclosure is complete. ai_service is still required on an AI-flagged track (use ai_service_other for a free-text name). What returns 422 now is an incomplete disclosure — no service, or no elements — never the choice of tool.
Full policy: tonegrid.pro/ai-music. Field reference: the POST /releases/:uuid/tracks and PATCH /tracks/:uuid entries in the API reference at api-docs.tonegrid.pro.
August 28, 2026
Docs
Webhook Signing Secret Is the `secret` Field, Not `secret_hash`
If you built signature verification against the old webhook-tester docs, check your code now: they named the wrong field. POST /webhooks never returns a secret_hash at all — the value ToneGrid signs with is the secret field, a tgwhsec_… string returned exactly once, at creation. secret_prefix (returned on GET /webhooks/:uuid) is a public identifier only, not a signing key. Signing with either the old name or the prefix will fail every hash_equals / hmac.compare_digest check.
What you need to do
- If your verifier reads
secret_hash or secret_prefix to compute the HMAC, switch it to the secret value you stored at webhook-creation time. If you didn't persist it, there is no way to recover it — call POST /webhooks/:uuid/regenerate-secret to issue a new one and store it this time.
Rotation has no overlap window
- The docs previously claimed you could set a
previous_secret via PUT /webhooks/:id and verify against both during rotation. That endpoint and field do not exist. POST /webhooks/:uuid/regenerate-secret invalidates the old secret the instant it returns the new one — there is no grace period. Deploy the new secret to your verifier first, then rotate, or you will drop events in between.
- Mutating a webhook (name, url, events, description, is_active) is
PATCH /webhooks/:uuid. PUT /webhooks/:uuid is not a route.
August 26, 2026
Docs
POST /artists Documents the Full DSP Profile Field Set
The POST /artists reference previously listed only apple_artist_id and spotify_artist_id. The endpoint has always accepted a fuller profile-linking set; the docs now match it.
New in the parameter table
slug (required) — URL-safe identifier: lowercase letters, digits, hyphens.
bio (optional) — short artist biography. biography was never a real field; if you were sending it, it was silently dropped.
status (optional) — active or inactive, defaults to active.
- DSP + social profile URLs and IDs:
spotify_uri, spotify_id, apple_id, spotify_url, apple_url, deezer_url, soundcloud_url, audiomack_url, tiktok_url, instagram_url, twitter_url, youtube_url, store_url.
Legacy aliases still accepted
apple_artist_id → apple_id, spotify_artist_id → spotify_id, spotify_artist_uri → spotify_uri. No migration required if you're already sending the old names; switch when convenient.
August 23, 2026
API
API Access Now Returns 402 When Your Account Is Past Its Invoice Grace Period
If your tenant is more than 7 days past the due date on its oldest open invoice, every API request now returns 402 instead of succeeding. Access is restored automatically the moment the invoice is paid — there is nothing to call or reset on your side.
What changed
- A tenant whose oldest open invoice is more than 7 days past due gets
402 on every authenticated API request, with body {"error": "Account access is suspended due to an outstanding invoice. Please contact support to restore access."}.
- This is separate from the pre-existing
403 for a manually suspended account ("This account has been suspended. Contact support."). If you already handle that 403, add a branch for 402 rather than folding it in — the causes and the remediation are different.
What you need to do
- If your integration treats any non-2xx as a hard failure and retries, make sure a
402 does not enter a retry loop — retrying will not clear it. Surface it to whoever owns the billing relationship.
- Deliveries queued before suspension are held, not dropped, and resume once access is restored.
August 22, 2026
API
Roster-Scoped API Keys: One Key Creates Releases for Any Artist in Your Tenant
If ToneGrid has granted your API key roster access, POST /releases can now create a release under any artist in your own tenant from that single key, instead of being locked to one artist-attributed identity per key. This is aimed at white-label partners with rosters larger than their user-account count — you no longer need a separate artist-role key per artist just to reach the API.
What changed
- A key with roster access must include
artist_id (UUID or numeric id) on every POST /releases call. Omit it and the request returns 422 naming the missing field.
artist_id must resolve to an artist inside your own tenant. An id that does not resolve, or that belongs to another tenant, returns 422 with "Artist not found in this tenant."
- Ordinary artist- and collaborator-attributed keys are unchanged: they still create under their own attributed artist automatically and do not need to send
artist_id.
What you need to do
- Roster access is granted by ToneGrid when a key is issued or rotated, not something you can self-serve toggle. If you run a multi-artist roster and only have artist-per-key access today, ask ToneGrid support to reissue your key with roster access.
- Once granted, a roster key still passes the same role-gated routes it did before (approve, tracks, DSPs, submit), so the full release lifecycle now runs on one credential across your whole roster.
August 21, 2026
API
14 New Distribution Destinations, Plus Logo and Website URLs on GET /supply-chain/dsps
GET /supply-chain/dsps now lists 55 active destinations, up from 41, and every row carries artwork you can render without hardcoding it.
New destinations
- SoundExchange, Beatport, 7digital, Qobuz, Roxi, Taobao, Canva, Pinterest, Soundtrack Your Brand, Bandcamp, Claro Musica, Melon, Trace, ACRCloud — all
delivery_type: "audio", selectable on POST /releases/:uuid/dsps and PUT /releases/:uuid/dsps by slug the same as any existing destination.
Schema (additive)
- Each object returned by
GET /supply-chain/dsps now includes logo_url, website_url, and delivery_type. logo_url was previously never populated; existing destinations now carry one too.
Removed
- Resso is retired (the service shut down) and no longer appears in
GET /supply-chain/dsps. Its six webhook event types (resso.submitted, resso.accepted, resso.rejected, resso.live, resso.taken_down, resso.failed) are removed from GET /webhooks/event-types. No live subscription referenced any of them.
August 19, 2026
Fix
Audio Uploads: the Real Ceiling Is 200 MB, and Oversized Requests Now Return 413, Not a Misleading 422
If a large master ever failed POST /tracks/:uuid/audio with 422 "No audio file provided." even though you definitely sent a file, that was this bug. It is fixed: the documented ceiling is now the real one, and an oversized request gets an honest 413.
What was broken
- Docs advertised 500 MB per file. The actual transport ceiling, enforced silently below that by the PHP layer, was 64 MB. Anything over 64 MB had its request body discarded before your file field was ever read, which surfaced as
422 "No audio file provided." — indistinguishable from actually forgetting to attach the file.
What changed
- The transport ceiling is now 200 MB per request, and it is a real, single, enforced limit — not silently lower than what is documented. A 48 kHz/16-bit stereo WAV runs about 11 MB per minute, so one request comfortably carries a track up to roughly 17 minutes; for anything longer, send FLAC (lossless, about half the size).
- A request over the ceiling now returns
413 naming the limit. 422 "No audio file provided." now means exactly what it says: the audio field was genuinely absent from the multipart body.
- The
413/422 disambiguation applies to all upload endpoints, not just audio: POST /tracks/:uuid/audio, POST /tracks/:uuid/upload, POST /releases/:uuid/artwork, POST /releases/:uuid/animated-artwork/:asset_type, POST /artists/:uuid/avatar, POST /artists/:uuid/banner, POST /users/:uuid/avatar, POST /whitelabel/logo, POST /whitelabel/favicon, POST /videos/:uuid/upload, POST /ingestion/ddex (ddex_xml), and POST /ingestion/csv (releases_csv). Image and document caps are unchanged (image 20 MB, document 10 MB) — only the audio ceiling and the error-code accuracy changed.
What you need to do
Nothing, if you were already sending files under 200 MB. If you added client-side chunking, pre-flight size checks, or retry-on-422 logic to work around the old undocumented 64 MB wall, you can remove it — 200 MB is now the real, stable ceiling.
August 6, 2026
Fix
POST /tenants/clients/:uuid/login-as No Longer Returns 500, and /auth/signup Is Removed
If you integrate against ToneGrid as a white-label parent managing client tenants, POST /tenants/clients/:uuid/login-as had returned 500 for every caller since it shipped. It is fixed and now returns a usable token.
What was broken
- The endpoint could not mint a token at all (
500) due to a server-side configuration gap, unrelated to your request.
What changed
POST /tenants/clients/:uuid/login-as now returns 200 with a token that carries your own identity, re-scoped to the client tenant — it does not impersonate a user on the client side (client tenants created via POST /tenants/clients have none). Role-gated routes behave exactly as they do on your normal key: an owner acting-as a client is still an owner, so the artist-only POST /releases gate still applies.
- The response now includes an
acting_as object ({"user_id": ..., "role": ...}) alongside the token, and the note field states explicitly that the token does not elevate your role.
Breaking / migration
- Before:
POST /auth/signup was a documented, callable route for self-service account creation.
After: POST /auth/signup is removed and returns 404. Self-service signup is not part of the supported provisioning flow.
Action required now if anything in your integration calls it: provision new users through the tenant dashboard, or request API-key issuance from ToneGrid support.
August 6, 2026
Fix
Auth tokens are usable again: /auth/login and /auth/refresh no longer return a null token
If you integrated against /auth/login or /auth/refresh and got a 200 back with nothing usable in it, that was a defect on our side and it is fixed. These routes now return a real access token, a real expiry, and a refresh token you can actually exchange.
What was broken
- Null tokens.
POST /auth/login, POST /auth/refresh and POST /auth/signup returned 200 with "access_token": null and "expires_at": null. The response looked successful, so a client that only checked the status code would carry on and then fail on the next authenticated call.
- Empty role after refresh. A token obtained from
/auth/refresh carried an empty role claim. Every role-gated route rejected it, so a refreshed session behaved as if it had no permissions at all.
- Tenant claim was the wrong identifier. The
tenant_id claim carried the numeric tenant id where the tenant UUID was expected. It never matched, so tenant resolution silently fell through to the X-Tenant-Domain header and then to host detection instead of using the token.
What you need to do
- Nothing, in most cases. Run your login flow again and you will get a usable
access_token, expires_at and refresh token. No key rotation and no migration is required.
- If you worked around this by always sending
X-Tenant-Domain to force tenant resolution, you can drop that header. The token now resolves the tenant on its own, though sending it explicitly is still supported and harmless.
- If you treated a null
access_token as a transient error and retried, you can remove that retry branch.
The token format, the signing scheme and the shape of the auth responses are all unchanged. This is a fix to what those routes actually put in the response, not a change to the contract.
July 19, 2026
API
Ingestion release-creation now requires an artist-role key; owner and team keys return 403
Creating a release through any ingestion channel now requires an artist-role API key. If your integration submits releases over /ingestion/* with an owner-role or team-role tgk_ key, those calls now return 403 and must switch to an artist-role key. This brings ingestion in line with the existing rule on POST /releases and the web release wizard: a release is always created under an artist account, never an owner or team account.
Breaking / migration
- What changed:
POST /ingestion/json, POST /ingestion/bulk, POST /ingestion/ddex, and POST /ingestion/csv now require the calling key's role to be artist or collaborator. Any other role — including owner and team-admin keys — returns 403 with {"success":false,"error":"Owners and team members cannot create releases under their own account. Releases must be created from an artist account."}.
- Why: a release belongs to the artist account that owns it.
POST /releases and the web wizard already enforced this; the ingestion channels now match, so a release has consistent ownership no matter how it was created.
- What to do: issue an artist-role API key for the artist the release belongs to and use it for every
/ingestion/* submission. Owner-role keys are unaffected everywhere else — only release creation via ingestion now rejects them. This is in effect now on api.tonegrid.pro.
July 18, 2026
API
Webhook signatures now verify correctly: rotate your secret to resume deliveries
Webhook deliveries are now signed correctly. If your endpoint was rejecting ToneGrid webhooks with 401 and {"error":"invalid_signature"}, that was a server-side signing issue on our end, now fixed. The signature scheme is unchanged, so you verify exactly as documented.
What you need to do
The fix changes how we store your signing secret, so existing subscriptions need a fresh one. Rotate it once and deliveries resume:
POST /webhooks/:uuid/regenerate-secret returns a new tgwhsec_ secret (shown once). Update your verifier to use it.
Verifying a signature (unchanged)
- Read
X-ToneGrid-Signature: t=<unix_ts>,v1=<hex>.
- Compute
HMAC-SHA256(secret, "{t}.{raw_body}") over the raw request body and compare to v1.
New and rotated webhooks sign correctly from now on. This is a one-time rotation per existing subscription.
July 13, 2026
API
Remove and replace stores on a release: PUT + DELETE on /releases/:uuid/dsps
You can now remove a single store from a release and replace a release's whole store selection in one call. Until now POST /releases/:uuid/dsps was additive-only, so there was no way to pull a store (for example YouTube or YouTube Music) off a release once it was selected without recreating the release. POST is unchanged and still additive — the new PUT and DELETE add replace-set and single-store removal, so nothing here breaks an existing integration.
New endpoints
PUT /releases/:uuid/dsps — replace-set. Same body as POST (dsps as slugs or dsp_ids as integers), but it replaces the existing selection with exactly the list you send: stores not in the list are removed, new ones are attached. Returns 200 with the resulting { "dsps": [...] } set.
DELETE /releases/:uuid/dsps/:slug — removes one store. :slug accepts a DSP slug (for example youtube-music) or a numeric DSP id. Idempotent: returns 200 with the remaining { "dsps": [...] } whether or not that store was attached, so removing an already-absent store is not an error. Requires the same auth and edit permission as the other release-DSP endpoints.
Also on POST
POST /releases/:uuid/dsps now accepts an optional { "replace": true } flag to get the same replace-set behavior as PUT. Without it, POST stays additive, so existing calls are unaffected.
- All three verbs re-sync the store selection that
POST /releases/:uuid/submit reads, so the submit gate always sees the current set.
July 9, 2026
API
AI-generated tracks must disclose the AI service or return 422
If you flag a track as AI-generated, you must now name the AI service that produced it or the request is rejected. POST /releases/:uuid/tracks and PATCH /tracks/:uuid accept a new ai_service field (and ai_service_other for a free-text name); the disclosed name is what ToneGrid QC and the release inspector read for AI review. Two validation rules now apply on both endpoints.
New fields
ai_service — name of the AI service used to generate the track (for example "Udio", "MUSE"). Accepted on POST /releases/:uuid/tracks and PATCH /tracks/:uuid.
ai_service_other — free-text service name, for when ai_service does not cover the tool you used.
- Flag a track as AI-containing by including
"includes_ai" in the track_properties array.
Validation — these requests now return 422
- Flagged AI with no disclosed service. If
track_properties contains "includes_ai" but neither ai_service nor ai_service_other is set (and no name is already on the track), the request returns 422 on ai_service. Send a service name whenever you flag includes_ai.
- Non-permitted service. A disclosed name that names a service ToneGrid does not accept is rejected with
422, whether it arrives via ai_service or ai_service_other. Suno is accepted but must be disclosed via ai_service; Mureka is rejected because it has not disclosed its training-data licensing.
Notes
- The disclosed name is stored on the track and read by QC for AI review — it is separate from the metadata and
track_properties columns, and it is additive, so disclosing it does not touch your other track fields.
- On
PATCH /tracks/:uuid, a name already disclosed on the track is honored, so re-flagging includes_ai does not force you to resend ai_service.
July 6, 2026
Foundations
Drive the API from an AI agent over MCP
You can now drive the ToneGrid API from any Model Context Protocol client — Claude, Cursor, or your own agent — at https://mcp.tonegrid.pro/mcp. The MCP tools are generated from this OpenAPI spec, so the endpoints you already call over REST are exposed as agent tools. Authenticate with your existing tgk_ API key; there is no separate credential to provision.
July 4, 2026
API
Release approval is now a two-stage flow: ToneGrid QC, then DSP delivery
If your integration calls POST /releases/:uuid/approve, its behavior changed. Approve no longer queues DSP deliveries directly — it now submits the release to ToneGrid QC review, and delivery to your selected DSPs begins only after the release passes QC. Update any logic that treats a successful approve as "delivery started."
Behavior change — read this if you approve releases
- What changed:
POST /releases/:uuid/approve now moves the release to status qc_inspection and emits release.qc_pending. It no longer attaches or queues per-DSP deliveries. Previously the same call both sent the release to review and immediately queued deliveries, firing release.approved and release.dsp.<slug>.submitted right away.
- When delivery happens now: once the release clears ToneGrid QC, your partner-selected DSP rows (the ones you set with
POST /releases/:uuid/dsps) flip from pending to submitted, and release.approved + release.dsp.<slug>.submitted fire at that point. This is now the only place per-DSP deliveries are created. A QC rejection fires release.rejected with a reason.
- New 409s: re-approving a release that is already in review or delivered now returns
409 — "already in ToneGrid QC review" for a qc_inspection release, "already passed QC and is being delivered" for an approved release, and a taken-down release cannot be re-approved here.
- Action: stop treating the
approve response (or an immediate release.approved) as your delivery-started signal. Key delivery-started logic on the release.approved / release.dsp.<slug>.submitted webhooks, which now fire after QC passes, and handle release.rejected for QC rejections. If you poll instead of using webhooks, watch the status move qc_inspection → approved. Lifecycle is unchanged otherwise: draft → pending → qc_inspection → approved.
July 4, 2026
Fix
Royalty and analytics read endpoints now return data (were 500)
The royalty and analytics read endpoints were returning an empty 500 for every tenant because the models filtered and joined on columns the live tables do not have. The queries are now aligned to the real schema and return data. If you polled these and backed off on 5xx, you can turn them back on.
Royalties
GET /royalties now returns a 200 list of statements (was 500). Statements are file-level: period, currency, total_revenue, filename, status.
GET /royalties/:uuid returns the statement plus a line_items breakdown; each line item carries dsp_name, track_title, artist_name, revenue, isrc, upc, and period (was 500).
GET /royalties/summary/:uuid returns per-artist earnings totals for the statement (was 500).
- The write path works end-to-end:
POST /royalties creates a statement, POST /royalties/:uuid/line-items adds line items, and POST /royalties/:uuid/approve then POST /royalties/:uuid/pay move it through review to payout — all previously returned 500.
- Status values: a royalty statement's
status now spans processing, completed, approved, paid, failed. If you deserialize this field strictly, add approved and paid to your handling.
Analytics
- Every analytics read now returns
200 instead of 500: GET /analytics/overview, GET /analytics/by-dsp, GET /analytics/by-territory, GET /analytics/by-release, GET /analytics/time-series, and GET /analytics/artist/:uuid.
July 4, 2026
Fix
Ingestion, track credits, and submit fixes: partner-integration blockers cleared
A batch of write-path fixes for integrators automating submissions. If you send track-level credits, run JSON ingestion, submit releases, or drive the API with an owner-role key, read this — two of these were hard blockers that returned an empty 500, and one silently dropped data you thought you were saving.
Behavior change — read this if you PATCH tracks
PATCH /tracks/:uuid now persists the track-level attribution and credit fields it previously accepted with a 200 and then silently dropped. Affected fields: artist_id, artist_name, copyright_holder, copyright_year, year_published, title_version, disc_number, language, preview_start_sec, preview_end_sec, contributors, additional_artists, and the localized localized_titles / localized_title_versions / localized_lyrics fields. If you were sending any of these, they are now actually stored — re-check the values you expect on affected tracks.
- New validation (these requests used to return
200 and may now return 422):
artist_id must reference an artist that belongs to your account. An unknown or cross-tenant id returns 422; list valid ids with GET /artists.
contributors and additional_artists must each be an array of objects, every entry carrying a non-empty name — e.g. [{"name":"Jane Doe","role":"MainArtist"}]. A bare string or a nameless entry returns 422 with that example in the error body.
Action: if your integration sends these fields, confirm your payload shape and be ready to read a 422 where you previously got a silent 200.
Fixed — endpoints that were returning an empty 500 now work
POST /ingestion/json now returns 201 with the release UUID instead of a generic 500. The ingestion path was not attributing the release to the authenticated API-key user, so the insert failed on a required column. Single-call release+track ingestion works end-to-end again.
POST /releases/:uuid/submit now returns success and moves the release to status pending (the canonical "awaiting review" state: draft → pending → qc_inspection → approved). It was writing an out-of-enum status value and returning an empty 500; if you had retry logic around submit, you can drop the workaround.
Error responses — no more empty-bodied 500s
- Any unhandled server error now returns a JSON error body carrying a short
reference id (also logged server-side) instead of an empty 500. Read reference from the response and quote it to support to correlate the exact log line. Canonical shape is unchanged: { success, error, errors{} }, now with the reference field on 500s.
- Duplicate-key conflicts now return a clean
409 ("A record with these details already exists.") instead of a 500. If you retry on 5xx, note that a genuine conflict is now a 409 and should not be retried blindly.
Access — owner-role API keys no longer 403 on write actions
- An API key whose user holds the tenant's default
owner role is now recognized at the tenant-admin tier and passes the role-gated write actions it was previously denied — review approvals and rejections (POST /releases/:uuid/approve, POST /releases/:uuid/reject), track splits (PUT /tracks/:uuid/splits), and royalty approvals. owner was previously unranked and resolved below staff, so these returned 403. If you provisioned around that by minting keys under a label_admin user, you no longer need to — an owner key now carries full tenant-scoped write rights (still strictly below platform super_admin).
May 18, 2026
Docs
Quickstart tutorial + Webhook signature tester
Two new dev-onboarding tools, both live and linked from every topbar.
/quickstart — 9-step walkthrough, ~5 minutes
- From a brand-new sandbox account to a release delivered to every DSP, end-to-end, in copy-paste curl: signup, API-key minting, artist, release draft, track + audio upload, attach DSPs, XSD-validate, approve + watch the lifecycle, preview the literal DDEX XML.
- Every step has a request pane (curl) and an expected-response pane side-by-side, plus a per-step pro-tip callout. Sticky right-rail stepper with numbered dots highlights the active step as you scroll.
- Total time and per-step minutes summed automatically. Final wrap-up card links to the webhook tester, Explorer, and Errors catalogue for next steps.
/webhook-tester — interactive HMAC-SHA256 verifier
- Paste a webhook secret + raw body + received
X-ToneGrid-Signature header value — the page computes the expected signature with the Web Crypto API and shows a match / no-match / stale-timestamp verdict.
- Runs entirely in your browser. Secrets never hit any ToneGrid server.
- "Generate sample signature" button mints a fresh valid header so devs can test their verification code against a known-good signature.
- Algorithm explained step-by-step plus copy-paste verification snippets in PHP / Node / Python / Go, and a Common Gotchas section (raw-body vs re-parsed JSON, constant-time compare, 5-min replay tolerance, lowercase hex, etc.).
May 18, 2026
Docs
Interactive API Explorer (Scalar) + Errors catalogue
Two new top-level docs surfaces — both live, both linked from every topbar.
Explorer at /explorer
- Drops Scalar on top of the official
/openapi.json spec. Browse every endpoint (74 paths across 15 tags), inspect request/response schemas, and hit live endpoints from the browser against either Production or Sandbox via the server picker.
- Auth: paste your
tgk_ API key or JWT once and Scalar persists it across requests. Try-it requests go straight to the real API — sandbox is safe for live experimentation.
- Code-sample tabs (curl / JavaScript / Python / Go / etc.) auto-generate per endpoint.
- Light/dark theme synced with the rest of the docs via the topbar toggle.
Errors catalogue at /errors
- 46 error responses curated from the live codebase, grouped by HTTP status (400 / 401 / 402 / 403 / 404 / 409 / 413 / 415 / 422 / 429 / 500 / 502 / 503).
- Each entry: scope tag (Authentication / Catalog / DDEX / Webhooks / Billing / Idempotency / etc.), exact error message, when it fires, how to fix.
- Live search + status-jump sidebar + canonical response shape (
{ success, error, errors{} }) explained up top.
- Standalone PHP page; to add a new error: edit
errors.php's $catalog array.
May 18, 2026
API
PurgeReleaseMessage + Update re-delivery (DDEX takedowns)
ToneGrid now speaks the full ERN 4.3 takedown + update vocabulary, not just NewReleaseMessage. Hard removals (corrupt metadata, mandated takedowns) are pushed as a proper PurgeReleaseMessage. Metadata corrections re-use the original delivery's MessageThreadId so DSPs correlate them as updates rather than seeing a brand-new release (preserving stream attribution + ISRC continuity).
New generator
Ern43PurgeGenerator — produces schema-valid PurgeReleaseMessage XML (MessageHeader + PurgedRelease with ReleaseId + Title). Reuses the original NewReleaseMessage's MessageThreadId so the DSP correlates the purge with the prior delivery. Validated against the official XSD with 4 test permutations.
New endpoints
GET /releases/:uuid/ddex/purge/preview/:dsp_slug (with .xml suffix for download) — preview the literal PurgeReleaseMessage XML.
GET /releases/:uuid/ddex/purge/validate/:dsp_slug — XSD-validate the purge XML before pushing.
POST /releases/:uuid/ddex/purge + POST /releases/:uuid/ddex/purge/:dsp_slug — queue PurgeReleaseMessage delivery to every / one DSP.
POST /releases/:uuid/ddex/redeliver — re-deliver as metadata UPDATE to every attached DSP (same MessageThreadId, fresh MessageId).
Lifecycle
- Purge: row flips to
takedown_submitted → fires release.dsp.<slug>.takedown_submitted → worker pushes PurgeReleaseMessage → row becomes taken_down → fires release.dsp.<slug>.taken_down with message_type: PurgeReleaseMessage.
- Update: row flips to
update_submitted → fires release.dsp.<slug>.update_submitted → worker pushes fresh NewReleaseMessage on same MessageThreadId → row returns to live → fires release.dsp.<slug>.updated with thread_continuation: true.
- Sim mode supports both new lifecycles — takedown_submitted → taken_down and update_submitted → live transitions happen ~1 minute apart, indistinguishable from live delivery in client-facing payloads.
Schema
release_dsp_delivery.status enum extended with update_submitted + takedown_submitted. Migrated on both prod + sandbox.
- OpenAPI 3.1 spec gains 5 new paths under the
DDEX tag (74 paths total now).
May 18, 2026
API
DDEX ERN 4.3 pipeline hardening + self-validate endpoint
Replaced Ern43Generator with a schema-clean implementation that validates against the official release-notification.xsd with zero errors. Previous output had attribute-name mismatches (MessageSchemaVersionId) and structural issues that some DSPs would have rejected.
Generator now emits a much richer document — SoundRecordingEdition with PLine + RecordingMode + TechnicalDetails containing DeliveryFile (codec / bitrate kbps / channels / sample rate Hz / bit depth / Duration / File with MD5 HashSum + FileSize KB). Optional second SoundRecordingEdition for Dolby Atmos (AudioCodecType = DolbyAtmosMasterADM). Per-locale DisplayTitle & DisplayTitleText via BCP-47. DisplayArtist linked to PartyList via ArtistPartyReference. Contributor parties with ISNI + IPI Name Number. ReleaseLabelReference IDREF. Release with full PLine + CLine + Genre (GenreText + SubGenre) + ResourceGroup + LinkedReleaseResourceReference for cover. IsHiResMusic auto-flag when every track is ≥88.2 kHz / 24-bit. Optional SentOnBehalfOf for SOBO delivery. MessageControlType flips to TestMessage when supply-chain config sets is_test_delivery.
New endpoint
GET /releases/:uuid/ddex/validate/:dsp_slug — runs the generated NewReleaseMessage through libxml's schemaValidate against the official ERN 4.3 XSD and returns a validation block with { valid, errors[], xsd_path, ern_version }. Beyond-standard feature most aggregators don't expose. Use it in CI to gate release approval on schema-cleanness.
api-docs
- DDEX section rewritten end-to-end: pipeline overview, element-by-element breakdown of every section the generator emits, beyond-standard feature list, delivery lifecycle table, abridged sample NewReleaseMessage XML that validates against the official XSD, all 5 endpoints documented with curl + response samples.
- New Best Practices section with 11 subsections.
- New Changelog at
/changelog (this page).
May 17, 2026
Docs
Glossary expansion + Stage 2T security hardening
Glossary expanded from 33 to 90 terms across 8 categories (added Credits and Ops & Lifecycle). Now covers ERN 4.3 / MEAD / PIE / RIN / DPID / IPI / ISNI / SOBO / monetization policy / pricing tier / territory clearance and more.
Security
RoleMiddleware::check() now enforces — calls Response::forbidden and exits on failure instead of returning a bool callers were ignoring. Earlier versions allowed silent privilege escalation on every controller that called check() as if void.
- All
/admin/* paths and internal role names removed from public docs and the OpenAPI spec. Unauthorised hits to admin routes now return 404 instead of 403 so existence isn't confirmed.
May 15, 2026
API
Multipart audio upload + Stage 2S simulation mode
The audio upload endpoint switches to AWS S3 Multipart Upload automatically for files larger than 100 MB. Peak memory pinned at 16 MB regardless of file size. Supports up to ~160 GB per file. Aborts on failure to avoid dangling parts.
Simulated DSP delivery
Approvals on releases attached to DSPs without live credentials now run on a simulated lifecycle (~1 min submitted → accepted → live). Webhooks fire on the same channels with identical payloads. As each DSP is brought online by ToneGrid Operations, that DSP's deliveries transition seamlessly to live push — no client-side change required.
Fixed
- Release model column aliases for fields that don't exist on real schema (
genre → primary_genre, copyright_line → copyright_holder). POST /releases now works end-to-end.