Skip to main content

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 generally user-or-higher; creates, updates, registration/export actions, and claim resolution are generally admin-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 a Publisher record for the tenant.
1

Create a publisher

Requires admin. publisherType is one of original, sub, administrator.
2

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

Add territories

A publisher’s PR/MR/SR shares are split by territory. prShare + mrShare + srShare are each validated 0–100; overlapping territory coverage on the same publisher is rejected.
Requires admin.
4

Add an agreement

Agreement rows (original, sub-publishing, administration, collection) carry territory and right-type terms separately from the territory table above.
Requires admin. terms defaults to { territories: [WW], rightTypes: [PR,MR,SR], shares: { pr: 50, mr: 50, sr: 50 } } when omitted.

2. Writers and Works

Writers (composers, authors, arrangers) and works (compositions) are managed independently, then linked with per-work share splits.
1

Create a writer

Requires 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.
2

Create a work

Requires admin. Only workTitle is required — accepts any writable CWRWork field.
3

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).
Requires admin.
4

Link a recording

Connects a composition (CWRWork) to a master recording (Asset) via a WorkRecording junction row.
Requires admin. Returns the work’s full recordings list, not just the new link.

3. Register Works with a PRO

A WorkRegistration 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.
1

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).
Requires admin.
2

Queue it for submission

Only rows currently 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.
Requires admin.
3

Poll status

Requires 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.
4

Handle conflicts

When a society reports a competing claim, a RegistrationConflict row is created against the registration. List and resolve them:
Requires 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

1

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.
Requires 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.
2

Poll status and download

Both require user. The download endpoint streams the raw CWR text bytes as text/plain; it 400s until the job’s status is completed.
3

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

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

File a claim

Effectively 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.
2

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:withdrawn may only be requested by the claimant themselves, and only from filed. Every other transition requires admin/owner.
3

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

Bulk-resolve

Requires admin. Up to 200 claims per call, in a single transaction. All-or-nothing: if any id fails (not found, cross-tenant, invalid transition), the entire batch rolls back and updated comes back empty.

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

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 admin. acknowledgementsFile is optional.
2

Execute

Same inputs, run inside a single transaction across phases (writers → works → work-writer links → artists → recordings → cross-references → acknowledgements).
Requires admin.
The import is best-effort per record: the transaction still commits even when individual rows error out — a failed record is collected in that phase’s errors[] array, not thrown. Always check result.*.errors, not just the HTTP status, before treating a run as fully successful.
3

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.