Overview
The Publishing/CWR surface covers everything needed to run music-publishing administration through the Royalti.io API: publisher and territory setup, a writer/work catalog with CWR-compliant writer splits, work registration with Performing Rights Organizations (PROs), CWR (Common Works Registration) file export and acknowledgment import, ownership/share dispute claims, and one-time catalog migration from django-music-publisher (DMP) exports. This guide walks through the flow end to end. It references the real request/response shapes documented in the API reference — see the linked pages for full schemas.Every endpoint in this guide requires bearer auth and an active
publisher add-on on the tenant. A tenant without the add-on gets 403 { message: "This feature requires an active publisher addon" } on all of these routes.Prerequisites
- Publisher add-on active on your workspace (
checkAddon('publisher')). - Authentication: a JWT access token, or a Workspace API Key (
RWAK) / User API Key (RUAK) — see the Authentication Guide. - Role: the role hierarchy is
guest < user < admin < owner. Reads are generallyuser-or-higher; creates, updates, registration/export actions, and claim resolution are generallyadmin-or-higher. Each step below states the required role.
Filing and listing claims on a work (
GET/POST /work/works/{workId}/claims) and reading registration conflicts (GET /work/registrations/{id}/conflicts) are available at user level — a non-admin caller can only file claims as a writer on that work (ownership is enforced server-side; other callers get a 403). Resolving conflicts and transitioning claim status remain admin-or-higher.Response envelopes are not uniform across this surface — check each endpoint’s reference page. Examples:
GET /work/works returns a bare array; GET /work/registrations wraps rows in { success, data, summary }; GET /publishing/claims wraps rows in { status, data }; POST /cwr/export returns { job, excludedWorks, excludedCount } with no wrapper at all.1. Set Up a Publisher
Every downstream object (writers, works, registrations, CWR exports) is scoped to aPublisher record for the tenant.
Create a publisher
- cURL
- Node.js
admin. publisherType is one of original, sub, administrator.Check for an existing publisher
GET /publisher/publishers/me returns the tenant’s first publisher record (or 404 if none exists yet) — useful for onboarding flows that need to know whether setup has already happened.Add territories
A publisher’s PR/MR/SR shares are split by territory. Requires
prShare + mrShare + srShare are each validated 0–100; overlapping territory coverage on the same publisher is rejected.admin.2. Writers and Works
Writers (composers, authors, arrangers) and works (compositions) are managed independently, then linked with per-work share splits.Create a writer
- cURL
- Node.js
admin. If territories is omitted, a single worldwide (WW) entry at 100% PR/MR/SR is applied by default. writerRole is one of CA, C, A, AR, AD, TR, SR, LY, CO, PA.Set writer splits on the work
PUT /work/works/{id}/writers replaces the full writer set for the work in one transaction — it is not an incremental add/remove. Each pool (prShare, mrShare, srShare) across all writers must sum to either 0 (unset) or 100% (±0.02).admin.3. Register Works with a PRO
AWorkRegistration tracks a work’s registration status with a specific society (e.g. ASCAP, BMI). Creating a registration never auto-submits it — submission is an explicit opt-in step.
Create or update a registration
POST /work/registrations upserts on (tenant, societyCode, workId) — calling it again for the same work/society pair updates the existing row (200) instead of duplicating it (201).- cURL
- Node.js
admin.Queue it for submission
Only rows currently Requires
pending can be queued — this is the explicit opt-in that lets the PRO submission worker pick the row up. A row that has already advanced past pending returns 400.admin.Poll status
user. Supports societyCode, status, and an inclusive statusDateFrom/statusDateTo range. Response includes a summary count per status for the filtered rows. GET /work/registrations/summary gives a tenant-wide breakdown by status and by society.Handle conflicts
When a society reports a competing claim, a Requires
RegistrationConflict row is created against the registration. List and resolve them:admin. Resolving the last open conflict on a registration bumps its registrationStatus from conflict back to submitted. Note the requireUser-vs-effective-admin gap on the list endpoint (see the warning above).4. Export and Exchange CWR Files
Generate a CWR export
POST /cwr/export renders a CWR file for a single publisher’s exportable works and persists it through the File pipeline — despite the “async job” naming internally, this call completes and returns the finished job synchronously.- cURL
- Node.js
admin. Works with no writer links, null relative shares, or bootstrap-seeded skeletons are excluded from the file and reported back in excludedWorks — never silently dropped. The legacy ?stream=true synchronous path has been removed and now returns 400.Poll status and download
user. The download endpoint streams the raw CWR text bytes as text/plain; it 400s until the job’s status is completed.Import an acknowledgment from a society
After submitting a CWR file out-of-band, upload the society’s acknowledgment (ACK) response to apply per-work registration status updates.Requires
admin. Multipart field name is file (not cwrFile). The file must contain HDR and ACK markers. After processing, GET /cwr/works/registration-status reflects the updated statuses.Parse a raw CWR file without importing
POST /cwr/import/raw parses a raw .txt CWR file and returns the result — it does not write anything to the database (useful for validating a file before deciding what to do with it).admin. Multipart field name is cwrFile (max 10 MB), distinct from the file field used by the acknowledgment endpoint above.5. Claims and Disputes
Ownership and share disputes over a work are filed per-work, then managed through a tenant-wide inbox.File a claim
admin/owner-only today (see the warning above) — the writer-self-service path exists in the service layer but isn’t reachable via the public route yet. New claims start in filed status. Supports Idempotency-Key.Work through the tenant inbox
GET /publishing/claims is filterable (status, claimType, workId, claimantWriterId) and paginated (page/size, capped at 100), and returns a tenant-wide status summary alongside the page of rows. Requires user.Status transitions follow a fixed state machine:| From | Allowed to |
|---|---|
filed | under_review, withdrawn |
under_review | counter_claimed, resolved, escalated |
counter_claimed | resolved, escalated |
escalated | resolved |
resolved, withdrawn | (terminal) |
withdrawn may only be requested by the claimant themselves, and only from filed. Every other transition requires admin/owner.Attach evidence
Evidence upload is a signed-URL two-step: request a signed GCS PUT URL, upload the file to it directly, then register the upload on the claim.Allowed MIME types: PDF, PNG/JPEG, MP3/WAV, plain text. 25 MB cap.
6. Statement Reconciliation
Publisher royalty statements (draft → pending → approved → paid) and PRO-file reconciliation — ingesting BMI/ASCAP/MLC statement exports, matching lines to works, detecting variance — live under /publisher/publishers/{id}/statements/*. This is a separate, larger surface; see the Publishers API reference for statement generation, status transitions, line matching, and variance-flag endpoints.
7. Migrate a Catalog with DMP Import
If you’re moving off django-music-publisher, its JSON works export (optionally paired with a CSV acknowledgements export) can be bulk-imported.Preview
Parses the files and returns entity counts, a 10-work sample, and conflict counts against your existing catalog (matched by ISWC/IPI/ISRC) — nothing is written yet.Requires
- cURL
- Node.js
admin. acknowledgementsFile is optional.Execute
Same inputs, run inside a single transaction across phases (writers → works → work-writer links → artists → recordings → cross-references → acknowledgements).Requires
admin.Retry a failed record
POST /dmp/import/retry-record re-runs the full phase pipeline for one work, optionally with overrides shallow-merged onto the original record to fix the field that caused the failure. Requires user role plus the publisher addon. A retry of the same (importJobId, recordIndex) within an hour replays the cached result instead of reprocessing.Related Endpoints
- Create a publisher
- Create a writer
- Create a work
- Replace a work’s writer splits
- Create/update a CWR registration
- Queue a registration for PRO submission
- Export a CWR file
- Download a completed CWR export job
- Import a CWR acknowledgment file
- List tenant claims inbox
- Transition a claim’s status
- Bulk-resolve claims
- Preview a DMP import
- Execute a DMP import