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

curl -X POST https://api.royalti.io/publisher/publishers \
  -H "Authorization: Bearer YOUR_API_TOKEN" \
  -H "Content-Type: application/json" \
  -d '{
    "name": "Afrosounds Publishing",
    "publisherType": "original",
    "ipiNameNumber": "00123456789"
  }'
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.
curl https://api.royalti.io/publisher/publishers/me \
  -H "Authorization: Bearer YOUR_API_TOKEN"
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.
curl -X POST https://api.royalti.io/publisher/publishers/8f14e45f-ceea-4d5e-8b3a-0a1b2c3d4e5f/territories \
  -H "Authorization: Bearer YOUR_API_TOKEN" \
  -H "Content-Type: application/json" \
  -d '{
    "territoryCode": "WW",
    "inclusionExclusion": "I",
    "prShare": 100,
    "mrShare": 100,
    "srShare": 100,
    "startDate": "2026-01-01T00:00:00Z"
  }'
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.
curl -X POST https://api.royalti.io/publisher/publishers/8f14e45f-ceea-4d5e-8b3a-0a1b2c3d4e5f/agreements \
  -H "Authorization: Bearer YOUR_API_TOKEN" \
  -H "Content-Type: application/json" \
  -d '{
    "agreementType": "original",
    "agreementNumber": "AGR-2026-001",
    "startDate": "2026-01-01T00:00:00Z"
  }'
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

curl -X POST https://api.royalti.io/writer/writers \
  -H "Authorization: Bearer YOUR_API_TOKEN" \
  -H "Content-Type: application/json" \
  -d '{
    "firstName": "Amaka",
    "lastName": "Eze",
    "writerRole": "C",
    "ipiNameNumber": "00123456789"
  }'
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

curl -X POST https://api.royalti.io/work/works \
  -H "Authorization: Bearer YOUR_API_TOKEN" \
  -H "Content-Type: application/json" \
  -d '{
    "workTitle": "Midnight Run",
    "iswc": "T-034524680-1",
    "workType": "original"
  }'
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).
curl -X PUT https://api.royalti.io/work/works/1b1b1b1b-1111-4111-8111-111111111111/writers \
  -H "Authorization: Bearer YOUR_API_TOKEN" \
  -H "Content-Type: application/json" \
  -d '{
    "writers": [
      { "writerId": "3b3b3b3b-3333-4333-8333-333333333333", "writerRole": "C", "prShare": 100, "mrShare": 100, "srShare": 100 }
    ]
  }'
Requires admin.
4

Link a recording

Connects a composition (CWRWork) to a master recording (Asset) via a WorkRecording junction row.
curl -X POST https://api.royalti.io/work/1b1b1b1b-1111-4111-8111-111111111111/recordings/6e6e6e6e-6666-4666-8666-666666666666 \
  -H "Authorization: Bearer YOUR_API_TOKEN" \
  -H "Content-Type: application/json" \
  -d '{ "isPrimary": true }'
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).
curl -X POST https://api.royalti.io/work/registrations \
  -H "Authorization: Bearer YOUR_API_TOKEN" \
  -H "Content-Type: application/json" \
  -d '{
    "workId": "1b1b1b1b-1111-4111-8111-111111111111",
    "societyCode": "BMI",
    "societyName": "BMI",
    "registrationStatus": "pending"
  }'
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.
curl -X POST https://api.royalti.io/work/registrations/5d5d5d5d-5555-4555-8555-555555555555/queue \
  -H "Authorization: Bearer YOUR_API_TOKEN"
Requires admin.
3

Poll status

curl "https://api.royalti.io/work/registrations?workId=1b1b1b1b-1111-4111-8111-111111111111" \
  -H "Authorization: Bearer YOUR_API_TOKEN"
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:
curl -X POST https://api.royalti.io/work/registrations/5d5d5d5d-5555-4555-8555-555555555555/conflicts/CONFLICT_ID/resolve \
  -H "Authorization: Bearer YOUR_API_TOKEN" \
  -H "Content-Type: application/json" \
  -d '{ "status": "resolved", "notes": "Confirmed with society — duplicate submission." }'
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.
curl -X POST https://api.royalti.io/cwr/export \
  -H "Authorization: Bearer YOUR_API_TOKEN" \
  -H "Content-Type: application/json" \
  -d '{
    "publisherId": "8f14e45f-ceea-4d5e-8b3a-0a1b2c3d4e5f",
    "version": "2.2"
  }'
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

curl https://api.royalti.io/cwr/status/8f14e45f-ceea-4d5e-8b3a-0a1b2c3d4e5f \
  -H "Authorization: Bearer YOUR_API_TOKEN"

curl https://api.royalti.io/cwr/jobs/7f7f7f7f-7777-4777-8777-777777777777/download \
  -H "Authorization: Bearer YOUR_API_TOKEN" \
  -o export.cwr
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.
curl -X POST https://api.royalti.io/cwr/acknowledgment \
  -H "Authorization: Bearer YOUR_API_TOKEN" \
  -F "file=@ack_response.txt"
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).
curl -X POST https://api.royalti.io/cwr/import/raw \
  -H "Authorization: Bearer YOUR_API_TOKEN" \
  -F "cwrFile=@registration.cwr"
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

curl -X POST https://api.royalti.io/work/works/1b1b1b1b-1111-4111-8111-111111111111/claims \
  -H "Authorization: Bearer YOUR_API_TOKEN" \
  -H "Content-Type: application/json" \
  -d '{
    "claimantWriterId": "3b3b3b3b-3333-4333-8333-333333333333",
    "claimedRole": "C",
    "claimType": "share_dispute"
  }'
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:
FromAllowed to
filedunder_review, withdrawn
under_reviewcounter_claimed, resolved, escalated
counter_claimedresolved, escalated
escalatedresolved
resolved, withdrawn(terminal)
withdrawn may only be requested by the claimant themselves, and only from filed. Every other transition requires admin/owner.
curl -X PATCH https://api.royalti.io/publishing/claims/2c2c2c2c-2222-4222-8222-222222222222/status \
  -H "Authorization: Bearer YOUR_API_TOKEN" \
  -H "Content-Type: application/json" \
  -H "Idempotency-Key: resolve-claim-2c2c2c2c-01" \
  -d '{ "toStatus": "resolved", "reason": "Verified split documentation with both parties." }'
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.
# 1. Request a signed upload URL
curl -X POST https://api.royalti.io/publishing/claims/2c2c2c2c-2222-4222-8222-222222222222/evidence \
  -H "Authorization: Bearer YOUR_API_TOKEN" \
  -H "Content-Type: application/json" \
  -d '{ "fileName": "split-sheet-signed.pdf", "mimeType": "application/pdf" }'

# 2. PUT the file bytes directly to the returned uploadUrl (client-side, not through the API)

# 3. Register the upload
curl -X POST https://api.royalti.io/publishing/claims/2c2c2c2c-2222-4222-8222-222222222222/evidence/attach \
  -H "Authorization: Bearer YOUR_API_TOKEN" \
  -H "Content-Type: application/json" \
  -d '{ "fileKey": "claims-evidence/19/2c2c2c2c.../split-sheet-signed.pdf", "fileName": "split-sheet-signed.pdf", "mimeType": "application/pdf", "size": 184320 }'
Allowed MIME types: PDF, PNG/JPEG, MP3/WAV, plain text. 25 MB cap.
4

Bulk-resolve

curl -X POST https://api.royalti.io/publishing/claims/bulk-resolve \
  -H "Authorization: Bearer YOUR_API_TOKEN" \
  -H "Content-Type: application/json" \
  -d '{
    "claimIds": ["2c2c2c2c-2222-4222-8222-222222222222"],
    "toStatus": "resolved",
    "reason": "Batch-resolved after quarterly rights review."
  }'
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.
curl -X POST https://api.royalti.io/dmp/import/preview \
  -H "Authorization: Bearer YOUR_API_TOKEN" \
  -F "worksFile=@dmp_works_export.json" \
  -F "acknowledgementsFile=@dmp_acknowledgements.csv"
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).
curl -X POST https://api.royalti.io/dmp/import/execute \
  -H "Authorization: Bearer YOUR_API_TOKEN" \
  -F "worksFile=@dmp_works_export.json"
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.