> ## Documentation Index
> Fetch the complete documentation index at: https://apidocs.royalti.io/llms.txt
> Use this file to discover all available pages before exploring further.

# Publishing & CWR Integration Guide

> Set up publishers, register works with PROs, exchange CWR files, manage ownership claims, and migrate catalogs from DMP

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

<Note>
  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.
</Note>

## 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](/introduction/authentication).
* **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.

<Note>
  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.
</Note>

<Note>
  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.
</Note>

## 1. Set Up a Publisher

Every downstream object (writers, works, registrations, CWR exports) is scoped to a `Publisher` record for the tenant.

<Steps>
  <Step title="Create a publisher">
    <Tabs>
      <Tab title="cURL">
        ```bash theme={null}
        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"
          }'
        ```
      </Tab>

      <Tab title="Node.js">
        ```javascript theme={null}
        const response = await fetch('https://api.royalti.io/publisher/publishers', {
          method: 'POST',
          headers: {
            'Authorization': `Bearer ${API_TOKEN}`,
            'Content-Type': 'application/json'
          },
          body: JSON.stringify({
            name: 'Afrosounds Publishing',
            publisherType: 'original',
            ipiNameNumber: '00123456789'
          })
        });

        const publisher = await response.json();
        ```
      </Tab>
    </Tabs>

    Requires `admin`. `publisherType` is one of `original`, `sub`, `administrator`.
  </Step>

  <Step title="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.

    ```bash theme={null}
    curl https://api.royalti.io/publisher/publishers/me \
      -H "Authorization: Bearer YOUR_API_TOKEN"
    ```
  </Step>

  <Step title="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.

    ```bash theme={null}
    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`.
  </Step>

  <Step title="Add an agreement">
    Agreement rows (`original`, `sub-publishing`, `administration`, `collection`) carry territory and right-type terms separately from the territory table above.

    ```bash theme={null}
    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.
  </Step>
</Steps>

## 2. Writers and Works

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

<Steps>
  <Step title="Create a writer">
    <Tabs>
      <Tab title="cURL">
        ```bash theme={null}
        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"
          }'
        ```
      </Tab>

      <Tab title="Node.js">
        ```javascript theme={null}
        const response = await fetch('https://api.royalti.io/writer/writers', {
          method: 'POST',
          headers: {
            'Authorization': `Bearer ${API_TOKEN}`,
            'Content-Type': 'application/json'
          },
          body: JSON.stringify({
            firstName: 'Amaka',
            lastName: 'Eze',
            writerRole: 'C',
            ipiNameNumber: '00123456789'
          })
        });

        const writer = await response.json();
        ```
      </Tab>
    </Tabs>

    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`.
  </Step>

  <Step title="Create a work">
    ```bash theme={null}
    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.
  </Step>

  <Step title="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).

    ```bash theme={null}
    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`.
  </Step>

  <Step title="Link a recording">
    Connects a composition (`CWRWork`) to a master recording (`Asset`) via a `WorkRecording` junction row.

    ```bash theme={null}
    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.
  </Step>
</Steps>

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

<Steps>
  <Step title="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`).

    <Tabs>
      <Tab title="cURL">
        ```bash theme={null}
        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"
          }'
        ```
      </Tab>

      <Tab title="Node.js">
        ```javascript theme={null}
        const response = await fetch('https://api.royalti.io/work/registrations', {
          method: 'POST',
          headers: {
            'Authorization': `Bearer ${API_TOKEN}`,
            'Content-Type': 'application/json'
          },
          body: JSON.stringify({
            workId: '1b1b1b1b-1111-4111-8111-111111111111',
            societyCode: 'BMI',
            societyName: 'BMI',
            registrationStatus: 'pending'
          })
        });

        const registration = await response.json();
        ```
      </Tab>
    </Tabs>

    Requires `admin`.
  </Step>

  <Step title="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`.

    ```bash theme={null}
    curl -X POST https://api.royalti.io/work/registrations/5d5d5d5d-5555-4555-8555-555555555555/queue \
      -H "Authorization: Bearer YOUR_API_TOKEN"
    ```

    Requires `admin`.
  </Step>

  <Step title="Poll status">
    ```bash theme={null}
    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.
  </Step>

  <Step title="Handle conflicts">
    When a society reports a competing claim, a `RegistrationConflict` row is created against the registration. List and resolve them:

    ```bash theme={null}
    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).
  </Step>
</Steps>

## 4. Export and Exchange CWR Files

<Steps>
  <Step title="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.

    <Tabs>
      <Tab title="cURL">
        ```bash theme={null}
        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"
          }'
        ```
      </Tab>

      <Tab title="Node.js">
        ```javascript theme={null}
        const response = await fetch('https://api.royalti.io/cwr/export', {
          method: 'POST',
          headers: {
            'Authorization': `Bearer ${API_TOKEN}`,
            'Content-Type': 'application/json'
          },
          body: JSON.stringify({
            publisherId: '8f14e45f-ceea-4d5e-8b3a-0a1b2c3d4e5f',
            version: '2.2'
          })
        });

        const { job, excludedWorks, excludedCount } = await response.json();
        ```
      </Tab>
    </Tabs>

    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`.
  </Step>

  <Step title="Poll status and download">
    ```bash theme={null}
    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`.
  </Step>

  <Step title="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.

    ```bash theme={null}
    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.
  </Step>

  <Step title="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).

    ```bash theme={null}
    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.
  </Step>
</Steps>

## 5. Claims and Disputes

Ownership and share disputes over a work are filed per-work, then managed through a tenant-wide inbox.

<Steps>
  <Step title="File a claim">
    ```bash theme={null}
    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`.
  </Step>

  <Step title="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`.

    ```bash theme={null}
    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." }'
    ```
  </Step>

  <Step title="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.

    ```bash theme={null}
    # 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.
  </Step>

  <Step title="Bulk-resolve">
    ```bash theme={null}
    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. <strong>All-or-nothing</strong>: if any id fails (not found, cross-tenant, invalid transition), the entire batch rolls back and `updated` comes back empty.
  </Step>
</Steps>

## 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](/api-reference/publishers) 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.

<Steps>
  <Step title="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.

    <Tabs>
      <Tab title="cURL">
        ```bash theme={null}
        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"
        ```
      </Tab>

      <Tab title="Node.js">
        ```javascript theme={null}
        const form = new FormData();
        form.append('worksFile', worksFileBlob, 'dmp_works_export.json');
        form.append('acknowledgementsFile', ackFileBlob, 'dmp_acknowledgements.csv');

        const response = await fetch('https://api.royalti.io/dmp/import/preview', {
          method: 'POST',
          headers: { 'Authorization': `Bearer ${API_TOKEN}` },
          body: form
        });

        const { preview } = await response.json();
        ```
      </Tab>
    </Tabs>

    Requires `admin`. `acknowledgementsFile` is optional.
  </Step>

  <Step title="Execute">
    Same inputs, run inside a single transaction across phases (writers → works → work-writer links → artists → recordings → cross-references → acknowledgements).

    ```bash theme={null}
    curl -X POST https://api.royalti.io/dmp/import/execute \
      -H "Authorization: Bearer YOUR_API_TOKEN" \
      -F "worksFile=@dmp_works_export.json"
    ```

    Requires `admin`.

    <Warning>
      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.
    </Warning>
  </Step>

  <Step title="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.
  </Step>
</Steps>

## Related Endpoints

* [Create a publisher](/api-reference/publishers/post-publisher-publishers)
* [Create a writer](/api-reference/writers/post-writer-writers)
* [Create a work](/api-reference/works/post-work-works)
* [Replace a work's writer splits](/api-reference/works/put-work-works-id-writers)
* [Create/update a CWR registration](/api-reference/works/post-work-registrations)
* [Queue a registration for PRO submission](/api-reference/works/post-work-registrations-id-queue)
* [Export a CWR file](/api-reference/cwr-export/post-cwr-export)
* [Download a completed CWR export job](/api-reference/cwr-export/get-cwr-jobs-id-download)
* [Import a CWR acknowledgment file](/api-reference/cwr-export/post-cwr-acknowledgment)
* [List tenant claims inbox](/api-reference/publishing-claims/get-publishing-claims)
* [Transition a claim's status](/api-reference/publishing-claims/patch-publishing-claims-id-status)
* [Bulk-resolve claims](/api-reference/publishing-claims/post-publishing-claims-bulk-resolve)
* [Preview a DMP import](/api-reference/dmp-import/post-dmp-import-preview)
* [Execute a DMP import](/api-reference/dmp-import/post-dmp-import-execute)
