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

Changelog

New endpoints, fixes, schema changes, and platform updates in reverse-chronological order. Subscribe to the webhooks feed to be notified programmatically. OpenAPI 3.1 spec.

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_inspectionapproved. Lifecycle is unchanged otherwise: draftpendingqc_inspectionapproved.
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.
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: draftpendingqc_inspectionapproved). 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).
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.).
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.
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).
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).
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.
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 submittedacceptedlive). 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 (genreprimary_genre, copyright_linecopyright_holder). POST /releases now works end-to-end.
API

Multi-language metadata + cursor pagination

  • Multi-language metadata across releases, tracks, and lyrics via localized_titles / localized_descriptions as BCP-47 keyed objects. Emitted in DDEX as per-locale DisplayTitle blocks with @LanguageAndScriptCode.
  • Cursor pagination on every list endpoint. Cursor::encode / Cursor::decode helpers, opaque base64 cursors stable across inserts.
  • Idempotency-Key middleware with body-hash gate — reusing a key with a different body returns 409.
API

Rights management + per-DSP overrides + Finance surface

  • Rights management endpoints. Per-release rights matrix (streaming / download / UGC / sync / ringtone) plus per-DSP overrides for territory, monetization policies, pricing, release-date, and the full rights matrix.
  • Finance surface: Periods, Sales Reports, Tenant Statements, Payee Statements, Payouts. Every amount in minor units to avoid float drift.
  • Parent-tenant / child-tenant hierarchy for label-services and sub-distributor operators. Child tenants get their own DPID via SOBO.
API

Supply Chain config + Animated Artwork + Track properties

  • Supply Chain config: monetization policies, pricing tiers, territory clearances. GET /supply-chain/effective-config returns the resolved (default << release << per-DSP) view.
  • Animated artwork endpoints: Apple Motion (1:1 + 3:4), Spotify Canvas (9:16). MP4 / MOV / WebM / GIF accepted, max 50 MB, validated async.
  • Track properties: ISRC, audio format / bit depth / sample rate / channels / LUFS, lyric format (LRC / TTML / iTT), Dolby Atmos slot (dolby_isrc / dolby_audio_uri), explicit flag, instrumental flag, language of performance.
  • Contributors as first-class entities: Writers, Publishers, Producers, Mix & Mastering Engineers. ISNI + IPI Name Number support per contributor.
Infra

Webhooks + Ingestion + DDEX worker cron

  • Webhook publisher with HMAC-SHA256 signed events. Header X-ToneGrid-Signature: t=<ts>,v1=<hex>. Auto-retry with exponential backoff for 24h. DSP-named event catalogue (release.dsp.<slug>.submitted, .accepted, .live).
  • Multi-channel ingestion: REST JSON, DDEX XML upload, CSV, JSON bulk, SFTP drop folder. All flow through ingestion_jobs + ingestion_files with stage transitions and error reporting.
  • Cron-driven workers: webhook-retry, ingest-worker, ddex-worker, audio-validator (ffprobe). Run every minute; audio-validator every 5 min.
Foundations

API Bearer auth + sandbox + OpenAPI + status

  • api-sandbox.tonegrid.pro — identical surface to prod, separate database, no real money or DSP push.
  • Bearer token authentication via tgk_... API keys. JWT login retained for user-facing flows.
  • OpenAPI 3.1 spec at /openapi.json. Importable into Postman, Scalar, every major SDK generator.
  • Status page at /status — uptime + worker heartbeats.
  • Sandbox seeder (bin/seed-sandbox.php) — idempotent, prints copy-paste curl recipe that walks signup → release create → approve → DDEX preview → sim-mode live transitions.
  • Fixed Validator::field() regression that caused /auth/login to return 500.
  • Fixed AuditLog column-name mismatch that silently failed on every privileged action.