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

# Register an uploaded evidence file on a claim

> **POST /publishing/claims/{id}/evidence/attach**

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

## Description

**POST /publishing/claims/{id}/evidence/attach**

**Description:**
Companion to `POST /publishing/claims/{id}/evidence` — after the
client's signed PUT to GCS succeeds, call this to append the file to
the claim's `evidenceUrls` array and record a system message.
`fileKey` must fall under the tenant+claim-scoped prefix returned by
the evidence endpoint. Idempotent on `fileKey` — repeat calls with
the same key return the claim unchanged, no duplicate row.

Supports the `Idempotency-Key` header (route key
`p5.claim.evidence-attach`).

**Authorization:**

* RBAC: `user` or higher; claimant-or-admin check enforced in the
  service
* Requires an active `publisher` addon

**Method:**
POST

## Code Examples

<CodeGroup>
  ```javascript Node.js theme={null}
  const response = await fetch('https://api.royalti.io/publishing/claims/example-id/evidence/attach', {
    method: 'POST',
    headers: {
      'Authorization': `Bearer ${token}`,
      'Content-Type': 'application/json'
    },
    body: JSON.stringify({
      "fileKey": "sample-fileKey",
      "fileName": "sample-fileName",
      "mimeType": "sample-mimeType",
      "size": 1
    })
  });

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

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

  response = requests.post(
    'https://api.royalti.io/publishing/claims/example-id/evidence/attach',
    headers={
      'Authorization': f'Bearer {token}'
    },
    json={"fileKey":"sample-fileKey","fileName":"sample-fileName","mimeType":"sample-mimeType","size":1}
  )

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

  ```bash cURL theme={null}
  curl -X POST https://api.royalti.io/publishing/claims/example-id/evidence/attach \
    -H "Authorization: Bearer YOUR_TOKEN" \
    -H "Content-Type: application/json" \
    -d '{"fileKey":"sample-fileKey","fileName":"sample-fileName","mimeType":"sample-mimeType","size":1}'

  ```
</CodeGroup>


## OpenAPI

````yaml post /publishing/claims/{id}/evidence/attach
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:
  /publishing/claims/{id}/evidence/attach:
    post:
      tags:
        - Publishing Claims
      summary: Register an uploaded evidence file on a claim
      description: |-
        **POST /publishing/claims/{id}/evidence/attach**

        **Description:**
        Companion to `POST /publishing/claims/{id}/evidence` — after the
        client's signed PUT to GCS succeeds, call this to append the file to
        the claim's `evidenceUrls` array and record a system message.
        `fileKey` must fall under the tenant+claim-scoped prefix returned by
        the evidence endpoint. Idempotent on `fileKey` — repeat calls with
        the same key return the claim unchanged, no duplicate row.

        Supports the `Idempotency-Key` header (route key
        `p5.claim.evidence-attach`).

        **Authorization:**
        - RBAC: `user` or higher; claimant-or-admin check enforced in the
          service
        - Requires an active `publisher` addon

        **Method:**
        POST
      parameters:
        - name: id
          in: path
          required: true
          schema:
            type: string
            format: uuid
        - name: Idempotency-Key
          in: header
          required: false
          schema:
            type: string
      requestBody:
        required: true
        content:
          application/json:
            schema:
              type: object
              required:
                - fileKey
                - fileName
                - mimeType
                - size
              properties:
                fileKey:
                  type: string
                  description: >-
                    Must start with `claims-evidence/{tenantId}/{claimId}/` —
                    the prefix returned by the evidence endpoint.
                fileName:
                  type: string
                mimeType:
                  type: string
                size:
                  type: integer
                  description: Bytes. Must not exceed 25 MB (26214400).
            example:
              fileKey: >-
                claims-evidence/19/1b1b1b1b-1111-4111-8111-111111111111/1720000000000-split-sheet-signed.pdf
              fileName: split-sheet-signed.pdf
              mimeType: application/pdf
              size: 184320
      responses:
        '200':
          description: Evidence attached (or already-attached claim returned unchanged)
          content:
            application/json:
              schema:
                type: object
                properties:
                  status:
                    type: string
                    example: success
                  data:
                    $ref: '#/components/schemas/WriterClaim'
        '400':
          description: >-
            Missing/invalid fields, mimeType not allowed, size exceeds the cap,
            or fileKey does not match the expected tenant+claim prefix
        '401':
          $ref: '#/components/responses/UnauthorizedError'
        '403':
          description: >-
            Caller is not a party to this claim, or the publisher addon is
            inactive
        '404':
          description: Claim not found
        '409':
          description: Idempotency-Key reused with a different request body
      security:
        - bearerAuth: []
components:
  schemas:
    WriterClaim:
      type: object
      description: >-
        Ownership/share/metadata dispute filed against a work. Full claim
        lifecycle (transition, messages, evidence, bulk-resolve) is documented
        separately under `publishing-claims.yaml`; only the two work-scoped
        routes (file, list-for-work) live under `/work`.
      properties:
        id:
          type: string
          format: uuid
        tenantId:
          type: integer
        workId:
          type: string
          format: uuid
        claimantWriterId:
          type: string
          format: uuid
        claimantUserId:
          type: string
          format: uuid
        claimedRole:
          type: string
          enum:
            - CA
            - C
            - A
            - AR
            - SA
            - SR
            - TR
            - PA
        claimedShares:
          type: object
          properties:
            pr:
              type: number
            mr:
              type: number
            sr:
              type: number
        claimType:
          type: string
          enum:
            - ownership
            - share_dispute
            - registration_conflict
            - metadata
        status:
          type: string
          enum:
            - filed
            - under_review
            - counter_claimed
            - resolved
            - escalated
            - withdrawn
        evidenceUrls:
          type: array
          items:
            type: string
        resolution:
          type: object
          nullable: true
          properties:
            decision:
              type: string
              enum:
                - approved
                - rejected
                - partial
                - escalated
            reason:
              type: string
            resolvedById:
              type: string
            resolvedAt:
              type: string
              format: date-time
        parentClaimId:
          type: string
          format: uuid
          nullable: true
        createdAt:
          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
  responses:
    UnauthorizedError:
      description: Missing or invalid bearer token
      content:
        application/json:
          schema:
            $ref: '#/components/schemas/ErrorResponse'
  securitySchemes:
    bearerAuth:
      type: http
      scheme: bearer
      bearerFormat: JWT
      description: >-
        JWT Authorization header using the Bearer scheme. Format: "Bearer
        {token}"

````