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

# Create a writer

> **POST /writer/writers**

<Note>
  This endpoint requires authentication. Include your Bearer token in the Authorization header.
</Note>

## Description

**POST /writer/writers**

**Description:**
Creates a new CWR writer for the tenant. If `territories` is
omitted (or empty), defaults to a single worldwide entry at 100%
PR/MR/SR. Runs `WriterValidator` (IPI name/base number format,
territory shares) before insert. If `tenantUserId` is supplied it
must belong to an existing `TenantUser` on this tenant.

**Authorization:**

* Required role: `admin` or higher

**Method:**
POST

## Code Examples

<CodeGroup>
  ```javascript Node.js theme={null}
  const response = await fetch('https://api.royalti.io/writer/writers', {
    method: 'POST',
    headers: {
      'Authorization': `Bearer ${token}`,
      'Content-Type': 'application/json'
    },
    body: JSON.stringify({
      "ref": "#/components/schemas/WriterCreateRequest"
    })
  });

  const data = await response.json();
  console.log(data);
  ```

  ```python Python theme={null}
  import requests

  response = requests.post(
    'https://api.royalti.io/writer/writers',
    headers={
      'Authorization': f'Bearer {token}'
    },
    json={"ref":"#/components/schemas/WriterCreateRequest"}
  )

  data = response.json()
  print(data)
  ```

  ```bash cURL theme={null}
  curl -X POST https://api.royalti.io/writer/writers \
    -H "Authorization: Bearer YOUR_TOKEN" \
    -H "Content-Type: application/json" \
    -d '{"ref":"#/components/schemas/WriterCreateRequest"}'

  ```
</CodeGroup>


## OpenAPI

````yaml post /writer/writers
openapi: 3.0.0
info:
  title: Royalti.io API
  description: "# Royalti API\r\n\r\nThis is the Royalti music royalty management platform API server.\r\n\r\n## Overview\r\n\r\nThe Royalti API provides comprehensive music royalty management services including:\r\n- Music publishing and writer management\r\n- Royalty processing and analytics\r\n- DDEX integration for music industry standards\r\n- File processing and pattern recognition\r\n- Payment processing and distribution\r\n\r\n## Authentication\r\n\r\nThe API uses JWT-based authentication with multiple protection levels:\r\n- Public endpoints for basic operations\r\n- Protected endpoints requiring valid JWT tokens\r\n- Admin endpoints for administrative functions\r\n\r\n## Features\r\n\r\n- Multi-dimensional royalty analytics\r\n- CWR (Collective Works Registration) support\r\n- DDEX integration for music metadata\r\n- Advanced file processing with pattern recognition\r\n- Real-time data processing with queue system"
  version: 2.6.0
  contact:
    name: Royalti.io Support
    email: support@royalti.io
    url: https://royalti.io
  license:
    name: Proprietary
    url: https://royalti.io/terms
servers:
  - url: https://api.royalti.io
    description: Production server
  - url: https://api-dev.royalti.io
    description: Development server
  - url: http://localhost:8084
    description: Local development
security:
  - bearerAuth: []
tags:
  - name: Accounting
    description: Accounting and financial transaction operations
  - name: Addons
    description: |
      List, activate, configure, and toggle tenant addons. All routes
      require a valid tenant session (`bearerAuth`) and the `addonsAccess`
      plan feature (returns `403 FEATURE_REQUIRED` on `FREE`-plan tenants,
      regardless of role). Response envelope is `{ success, data }` (note:
      `success`, not the `status` field used by most other Royalti.io
      routers).
  - name: Admin Impersonation
    description: Admin user-impersonation endpoints
  - name: Billing
    description: |
      Subscription lifecycle (local + Stripe), plans, custom/Stripe invoices,
      usage summaries, the Stripe Customer Portal and Customer Session,
      payment links, and cache/maintenance utilities. All routes require a
      valid tenant session (`bearerAuth`); most additionally require
      `admin` or `owner` role via RBAC — see each endpoint's Authorization
      note. Response envelope is `{ status, message, data }` except where
      noted.
  - name: Catalog Export
    description: >
      Generate catalog exports (BackBeat CWR-workstation import, Too
      Lost/Sonosuite

      distributor formats, and catalog CSV/XLSX exports for assets, products,

      artists, labels, splits, and releases) from a code-defined template

      registry. Admin-only — distinct from the member-accessible generic

      `?export=csv` list-endpoint query param, which remains untouched.
  - name: Catalog Import
    description: |
      Tenant-scoped catalog importer built on the metadata engine: search an
      artist on a configured provider, fetch and merge cross-provider metadata
      into staged `CatalogImportItem` rows, review a read-only diff against
      the tenant's existing catalog, optionally adjust individual staged
      rows, then explicitly apply or reject the batch. Admin-only — every
      route requires the tenant `admin` role or higher.

      **Flow:** `GET /providers` (optional, to see what's active) →
      `GET /search` (resolve an artist to an external id) →
      `POST /fetch` (queues a background job) →
      `GET /job/{jobId}` (poll until complete) →
      `GET /batch/{batchId}` and `GET /batch/{batchId}/diff` (review) →
      `PUT /items/{id}` (optional per-row overrides) →
      `POST /apply` or `POST /reject` (explicit confirm).

      **Review-then-apply, always:** fetch only ever stages
      `CatalogImportItem` rows — it never writes to `Asset`/`Product`. The
      only tenant-catalog write path is `POST /apply`, which is always an
      explicit, reviewer-initiated call.

      **Idempotency:** re-running the same import is safe. Staged rows are
      deduplicated against already-pending/imported rows by ISRC/UPC before
      insert, and `POST /apply` only ever touches `status: pending` rows
      scoped to the batch/items and tenant supplied — re-applying an
      already-applied batch or a batch with no matching pending rows is a
      no-op (`imported: 0, failed: 0`).
  - name: Currencies
    description: |
      Global currency reference data, per-tenant currency enablement, and
      exchange rates used to convert royalty data between currencies for
      reporting. All routes are mounted behind the platform's standard
      `basicAuth` + RBAC stack, so every endpoint requires a valid bearer
      token even where no `require*` role gate is applied in code. Response
      envelope is `{ success, data }` for currency endpoints and
      `{ status, data }` for exchange-rate endpoints (the two controllers
      predate a shared convention).
  - name: CWR Export
    description: |
      CISAC Common Works Registration (CWR) file generation for a tenant's
      publisher. Builds an async export job against the tenant's works and
      writers, persists the rendered file to Cloud Storage, and exposes job
      status + a download endpoint for the resulting file. All endpoints
      require the `publisher` addon. Mounted at `/cwr`.
  - name: CWR Acknowledgment
    description: |
      Upload and parse CWR acknowledgment (ACK) files returned by a
      performing rights society after a registration submission, and
      inspect/update per-work registration status derived from them.
  - name: DDEX
    description: DDEX operations (ERN/MEAD, messages, delivery, providers)
  - name: DMP Import
    description: |
      Imports a tenant's catalog (writers, works, work-writer relationships,
      artists, recordings, cross-references, and acknowledgements) from a
      django-music-publisher (DMP) works JSON export, with an optional
      acknowledgements CSV export layered on top. Mounted at `/dmp`.
      Import is best-effort and per-record: a failed row is captured in
      that phase's `errors[]` array rather than aborting the whole run, so
      a "completed" response can still contain 0 created rows if every
      record errored — check `result.*.errors` before treating a response
      as fully successful.
  - name: Downloads
    description: >-
      Generate and manage data downloads (royalty reports, accounting exports,
      etc.)
  - name: Label
    description: Label management operations
  - name: PRO Credentials
    description: |
      CRUD for `TenantPROCredentials` — one credential set per (tenant,
      society) pair, used by the CWR submission pipeline to deliver
      registration files to a PRO over SFTP, API, or EMAIL. Mounted at
      `/pro-credentials`. All routes require `admin` and an active
      `publisher` addon (enforced router-wide via `router.use()`, not
      per-route).

      **Credentials are never returned by the API.** The model's `toJSON()`
      strips `credentialsEncrypted` (ciphertext) and `decryptedCredentials`
      (plaintext, populated only in-process for the connection-test
      endpoint) from every serialized response — list/get/create/update
      responses include the account metadata (`societyCode`, `accountId`,
      `submissionMethod`, `endpointConfig`, `isActive`, `lastUsedAt`) but
      never the credential payload itself, masked or otherwise. Write
      requests still accept plaintext `credentials` in the body (over TLS);
      they are encrypted at rest (pgcrypto-backed) before the row is
      persisted.
  - name: Publisher-Writer Agreements
    description: |
      Manage `PublisherWriterAgreement` records — the contractual
      relationship between a `Publisher` and a `CWRWriter`, including its
      territory grants, status lifecycle (draft → pending → active →
      expired/terminated), and validity dates. Territory conflicts are
      checked against a writer/publisher pair's OTHER active agreements
      on every create/update/add-territory call.

      All endpoints require an active `publisher` addon on the tenant
      (`checkAddon('publisher')`). Unlike most routers in this API, this
      controller does NOT use the shared `asyncWrapper`/centralized error
      handler — every method catches its own errors and writes the JSON
      response directly, so error bodies here are `{ message, error? }`,
      not the `{ status, message }` shape used elsewhere. Role
      requirements come entirely from the global RBAC route table
      (`cwr.permissions.ts`): reads need `user` or higher, writes need
      `admin` or higher — there is no route-level `requireUser`/
      `requireAdmin` call in this router's own file.
  - name: Publishers
    description: |
      Core Publisher CRUD (name, IPI numbers, publisher type, settings) plus the
      `/publishers/me` singleton lookup and the `/publishers-with-user-data`
      convenience list. `POST`/`PUT`/`DELETE` require `admin`; reads require
      only `user`.
  - name: Publisher Territories
    description: |
      Per-publisher territory rows (`PublisherTerritory`) carrying PR/MR/SR
      share splits, inclusion/exclusion, and CWR 2.2+ formula/assignment/license
      fields. Distinct from the `territories` JSONB column on `Publisher` itself
      — these endpoints manage the normalized `PublisherTerritories` table.
  - name: Publisher Agreements
    description: |
      Per-publisher agreement rows (`PublisherAgreement`) — original,
      sub-publishing, administration, or collection agreements with territory
      and right-type terms.
  - name: Publisher CWR Export
    description: |
      Generates a CWR registration file (text/plain) for a single publisher's
      exportable works. One publisher per call — a CWR file carries a single
      sender/publisher chain, so batch `publisherIds` requests are rejected.
  - name: Publisher Statements
    description: |
      Publisher royalty statement generation, listing, detail, status
      transitions, and PDF export (`PublisherStatement`). Statement status
      follows a locked state machine: `draft → pending → approved → paid`
      (with `pending → draft` and `approved → pending` as the only backward
      transitions).
  - name: Statement Reconciliation
    description: |
      Phase 6 reconciliation surface built on top of Publisher Statements:
      PRO-file ingestion (BMI/ASCAP/MLC/MANUAL), line-level work matching
      (manual + fuzzy auto-match), variance detection against expected
      royalties, variance-flag comment threads, manual statement adjustments
      (H-05, locked once a statement is `approved`/`paid`), and emailing the
      rendered statement PDF to recipients.
  - name: Sub-Publishing
    description: |
      Sub-publishing agreements between an original publisher and a
      sub-publisher (`SubPublishingAgreement`), territory-conflict checking,
      and agreement discussion threads (H-07, backed by the polymorphic
      `DisputeMessage` model with `subjectType: sub_publishing_agreement`).
  - name: Recoupment Ledger
    description: |
      Read-only running-balance ledger (`RecoupmentLedger`) for a
      sub-publishing agreement's advance/recoupment activity.
  - name: Publishing Analytics
    description: >
      Tenant-scoped aggregation endpoints over `PublisherStatementLine` and

      `RecoupmentLedger` (WP-09, locks the G-ANALYTICS response-shape

      contract). Every aggregation returns the same uniform envelope:

      `{ dimension, rows: [{ key, label, ...metrics }], totals, currency, meta?
      }`.

      All support optional `periodStart`/`periodEnd` (ISO date, inclusive)

      filtering — see each endpoint for period semantics (statement-period

      overlap vs. ledger event-date).
  - name: Publishing Bootstrap
    description: |
      Scan → review → apply pipeline that bootstraps skeleton CWR works from
      the recording catalog. Scans stage proposals only; the diff is
      read-only and recomputed live; POST /apply is the single CWR write
      path. Ownership data (shares/publishers/agreements) is never seeded.
  - name: Publishing Claims
    description: |
      Tenant-wide inbox for `WriterClaim` disputes over CWR work ownership,
      share splits, registration conflicts, or metadata. Mounted at
      `/publishing`. Per-work claim endpoints (file a claim, list a work's
      claims) live on the `/work/works/{workId}/claims` routes instead, to
      keep work-scoped CRUD together — this file documents only the
      tenant-wide inbox surface (`/publishing/claims*`).

      All routes require an active `publisher` addon. Resolve/transition
      and bulk-resolve require `admin`; list/get/message/evidence routes
      accept any authenticated tenant user — finer-grained authorization
      (claimant-only actions such as withdraw) is enforced inside the
      service layer, not by RBAC middleware.
  - name: Source Creator
    description: >
      AI-assisted wizard for onboarding a new royalty data source: file

      analysis, AI column-mapping suggestions, BigQuery SQL generation,

      sandbox testing, and draft-source persistence. All routes require

      the `royaltyAccess` subscription feature
      (`requireFeature('royaltyAccess')`)

      — tenants below the required plan get a `403` with upgrade-plan

      details instead of a generic forbidden error.
  - name: Internal Webhooks
    description: Internal system webhooks for royalty processing and downloads
  - name: Payment Webhooks
    description: Payment processor webhook endpoints
  - name: Billing Webhooks
    description: Stripe billing and subscription webhooks
  - name: Infrastructure Webhooks
    description: Cloudflare domain and SSL webhooks
  - name: Distribution Webhooks
    description: Digital distribution platform webhooks (FUGA)
  - name: Branding
    description: |
      Per-tenant brand tokens (primary/accent colours, font, logo, logo mark)
      used to theme the tenant's workspace UI and emails. Both reading and
      updating brand tokens require an admin role.
  - name: Custom Domains
    description: |
      Custom workspace domains (e.g. `app.yourlabel.com`) that resolve to a
      tenant's Royalti workspace. Domains are provisioned with automatic SSL
      and validated via a DNS TXT record or an HTTP file challenge. Adding a
      domain requires an admin role; removing it requires the tenant owner.
      A tenant may have at most one active custom domain at a time.
  - name: Email Domains
    description: |
      The tenant's custom email sending domain, used so outbound
      transactional and royalty emails are sent from the tenant's own
      domain instead of the shared `royalti.io` sender. Domain ownership is
      proven with DNS records (DKIM, SPF, and a return-path record) that
      must be added before the domain is verified. Viewing the configuration
      requires an admin role; registering, deleting, or triggering
      verification requires the tenant owner.
  - name: Email Settings
    description: |
      Combined view of the tenant's email sending configuration: the custom
      email domain status plus sender/branding fields (from-name, reply-to,
      logo, colours, footer). Some branding fields only take effect once
      the tenant has a verified custom email domain. Any tenant member can
      view settings; updating them requires an admin role or higher.
  - name: Email Templates
    description: |
      Read-only catalogue of the transactional email templates Royalti can
      send (welcome, password reset, invite, payment notifications, etc.),
      with tenant-branded HTML/text preview rendering and a test-send
      endpoint for confirming how a template looks with the tenant's
      current branding. All endpoints in this cluster require an admin role.
  - name: Works & CWR Registrations
    description: |
      Musical works (`CWRWork`) are the composition-level entities behind
      publishing: title, ISWC, writer splits, and their links to society
      registrations, recordings (masters), and ownership claims. All routes
      in this group are mounted at `/work` and require an active `publisher`
      addon on the tenant (`checkAddon('publisher')`), plus a valid session
      (`requireUser` for reads, `requireAdmin` for writes unless noted
      otherwise). Role hierarchy: `guest < user < admin < owner`; `requireAdmin`
      allows `admin` and above.

      Registrations (`WorkRegistration`) track a work's submission to a
      performing-rights society (PRO) — ASCAP, BMI, PRS, etc. — including the
      NWR (New Work Registration) CWR transaction pipeline shipped 2026-07-05
      (PR #485): a registration is created in `pending` status, explicitly
      queued for PRO submission (`queuedAt` set), picked up by the
      `proSubmissionService` worker, and may land in `submitted`,
      `registered`, `rejected`, or `conflict`. Only `pending` rows can be
      (re)queued — a row that has left `pending` is never re-armed.
      Registration conflicts represent a competing claim from a society or
      third party against a specific registration and are tracked
      separately from work-scoped ownership claims (`WriterClaim`).
  - name: Writers
    description: |
      Manage CWR (Common Works Registration) writers — the composers,
      authors, arrangers, and translators credited on a tenant's musical
      works — and their associations with individual works. Writers are
      distinct from `TenantUser` accounts; a writer may optionally be
      linked to a `TenantUser` via `tenantUserId` (`userData` in
      responses) to pull in platform account details.

      All endpoints require an active `publisher` addon on the tenant
      (`checkAddon('publisher')`) in addition to the stated role. Reads
      require the `user` role or higher; writes (create/update/delete and
      work associations) require `admin` or higher, enforced both by the
      global RBAC route table (`cwr.permissions.ts`) and, for this router,
      duplicated at the route level via `requireUser`/`requireAdmin`.
paths:
  /writer/writers:
    post:
      tags:
        - Writers
      summary: Create a writer
      description: |-
        **POST /writer/writers**

        **Description:**
        Creates a new CWR writer for the tenant. If `territories` is
        omitted (or empty), defaults to a single worldwide entry at 100%
        PR/MR/SR. Runs `WriterValidator` (IPI name/base number format,
        territory shares) before insert. If `tenantUserId` is supplied it
        must belong to an existing `TenantUser` on this tenant.

        **Authorization:**
        - Required role: `admin` or higher

        **Method:**
        POST
      requestBody:
        required: true
        content:
          application/json:
            schema:
              $ref: '#/components/schemas/WriterCreateRequest'
            examples:
              minimal:
                summary: Minimal writer (default worldwide territory applied)
                value:
                  firstName: Chinedu
                  lastName: Okafor
                  writerRole: CA
              withTerritories:
                summary: Writer with explicit territory split
                value:
                  firstName: Amaka
                  lastName: Eze
                  writerRole: C
                  ipiNameNumber: '00123456789'
                  affiliatedPRO: COSON
                  territories:
                    - territoryCode: WW
                      inclusionExclusion: I
                      prShare: 100
                      mrShare: 100
                      srShare: 100
      responses:
        '201':
          description: Writer created
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/Writer'
        '400':
          description: >-
            Validation failure (IPI format, territory shares, or unknown
            tenantUserId)
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/ErrorResponse'
        '401':
          description: Missing/invalid authentication
        '403':
          description: Publisher addon not active, or caller below the required role
        '500':
          description: Internal server error
      security:
        - bearerAuth: []
components:
  schemas:
    WriterCreateRequest:
      type: object
      required:
        - firstName
        - lastName
        - writerRole
      properties:
        firstName:
          type: string
        lastName:
          type: string
        ipiNameNumber:
          type: string
        ipiBaseNumber:
          type: string
        isrCode:
          type: string
          description: >-
            Accepted in the request body but NOT a persisted `CWRWriter` column
            — currently a no-op for storage purposes.
        writerRole:
          type: string
          enum:
            - CA
            - C
            - A
            - AR
            - AD
            - TR
            - SR
            - LY
            - CO
            - PA
        territories:
          type: array
          items:
            $ref: '#/components/schemas/WriterTerritory'
          description: Defaults to worldwide 100% PR/MR/SR if omitted.
        tenantUserId:
          type: string
          format: uuid
          description: >-
            Must reference an existing `TenantUser` on this tenant, or the
            request fails with 400.
        affiliatedPRO:
          type: string
    Writer:
      type: object
      properties:
        id:
          type: string
          format: uuid
        tenantId:
          type: integer
        firstName:
          type: string
        lastName:
          type: string
        ipiNameNumber:
          type: string
          nullable: true
          description: 11-digit CISAC IPI name number. Unique per tenant when set.
        ipiBaseNumber:
          type: string
          nullable: true
          description: 13-character CISAC IPI base number. Unique per tenant when set.
        writerRole:
          type: string
          description: |
            CWR writer designation code. One of: `CA` (Composer & Author),
            `C` (Composer), `A` (Author/Lyricist), `AR` (Arranger),
            `AD` (Adaptor), `TR` (Translator), `SR` (Sub-Arranger),
            `LY` (Lyricist), `CO` (Composer), `PA` (Performer).
          enum:
            - CA
            - C
            - A
            - AR
            - AD
            - TR
            - SR
            - LY
            - CO
            - PA
        affiliatedPRO:
          type: string
          nullable: true
          description: >-
            Performing rights organization the writer is affiliated with (e.g.
            ASCAP, BMI). Free text — not enum-validated.
        mrSociety:
          type: string
          nullable: true
          maxLength: 3
        srSociety:
          type: string
          nullable: true
          maxLength: 3
        generallyControlled:
          type: boolean
          default: false
        canBeControlled:
          type: boolean
          default: false
        publisherFee:
          type: number
          nullable: true
          minimum: 0
          maximum: 100
        saan:
          type: string
          nullable: true
          maxLength: 14
          description: Society-Assigned Agreement Number.
        territories:
          type: array
          items:
            $ref: '#/components/schemas/WriterTerritory'
          description: >-
            Defaults to a single worldwide (`WW`) entry at 100% PR/MR/SR if
            omitted on create.
        settings:
          type: object
          description: >-
            Free-form writer settings (e.g. `defaultTerritory`,
            `defaultShares`). Not accepted via the create/update request bodies
            — set separately or defaulted to `{}`.
        tenantUserId:
          type: string
          format: uuid
          nullable: true
        userData:
          $ref: '#/components/schemas/WriterUserData'
        createdAt:
          type: string
          format: date-time
        updatedAt:
          type: string
          format: date-time
    ErrorResponse:
      type: object
      description: >-
        Centralized error handler shape (all errors on this router are thrown
        `CustomAPIError` subclasses).
      properties:
        status:
          type: string
          example: error
        message:
          type: string
        code:
          type: string
        details:
          type: object
    WriterTerritory:
      type: object
      required:
        - territoryCode
        - inclusionExclusion
        - prShare
        - mrShare
        - srShare
      properties:
        territoryCode:
          type: string
          example: WW
          description: >-
            CISAC TIS territory code, or a formula (e.g. "2136 BUT NOT 2250").
            `WW` = worldwide.
        inclusionExclusion:
          type: string
          enum:
            - I
            - E
          description: >-
            Whether this entry includes (I) or excludes (E) the given territory
            code.
        prShare:
          type: number
          minimum: 0
          maximum: 100
          description: Performance rights share (%) for this territory.
        mrShare:
          type: number
          minimum: 0
          maximum: 100
          description: Mechanical rights share (%) for this territory.
        srShare:
          type: number
          minimum: 0
          maximum: 100
          description: Synchronization rights share (%) for this territory.
        startDate:
          type: string
          format: date-time
          nullable: true
        endDate:
          type: string
          format: date-time
          nullable: true
    WriterUserData:
      type: object
      description: >-
        Linked TenantUser account, included when the writer has a
        `tenantUserId`.
      properties:
        id:
          type: string
          format: uuid
        firstName:
          type: string
        lastName:
          type: string
        email:
          type: string
          format: email
        ipi:
          type: string
          description: Only present on `GET /writer/writers-with-user-data`.
  securitySchemes:
    bearerAuth:
      type: http
      scheme: bearer
      bearerFormat: JWT
      description: >-
        JWT Authorization header using the Bearer scheme. Format: "Bearer
        {token}"

````