openapi: 3.0.3
info:
  title: WasaWasa Distribution API
  description: |
    Partner API for publishing releases, uploading assets, listing your catalog, and reading analytics, revenue, and reports.
    **Developer documentation and this specification** are at `https://developer.wasawasamusic.com`.
    HTTP calls go to the API gateway below.

    **Auth:** every request requires both `x-api-key` (`ww_live_` / `ww_test_`) and `x-api-secret` (`wws_live_` / `wws_test_`).
    Do **not** send `Authorization: Bearer` with the partner key (not a JWT — gateway errors).
    Do **not** use header `x-distribution-api-key` — the name is `x-api-key`.

    **Publish flow:**
    1. `POST /assets` (multipart `file` + `type` = `cover`|`audio`) → always `{ "url": "..." }`.
       For audio, response also includes `duration` / `duration_seconds` (auto-detected).
    2. `POST /publish` with `album.cover_url` and each `tracks[].audio_url` set to that `url`.
       You may omit `tracks[].duration`; publish fills it from the upload record when missing.
    3. On HTTP 200, **persist** `nia_r_code`, `tracks[].nia_t_code`, `album_id`, `artist_id` in your DB.
       Multi-label partners send `label_id` (or a mapped `label_name`) on `/publish` and `/ddex-ern`.

    **Reference catalog** (`GET /catalog`): genres, moods, languages, voice_types, track_types,
    explicit_content_levels, music_types, publication_modes, availability_modes, currencies (+ rates), instruments, countries.
    **Partner catalog** (`GET /releases`, `GET /albums`, `GET /tracks`, `GET /labels`): releases delivered under your
    `label_id` (or `published_via_api_key_id` for this key). Artists appear because you delivered work for them, not
    because of a `label_artists` roster row.
    Lookups accept UUID, NIA-R / NIA-T, ISRC, ISWC, GTIN (UPC/EAN/ICPN), and `partner_artist_id`. Nested `GET /releases/{id}`, `GET /tracks/{id}`,
    `GET /artists/{id}` return a fat payload. Limited `PATCH /releases/{id}` (title, release_date, description).
    Distinct from the reference catalog.

    **New artist:** country from platform `countries` (catalog or `GET /countries?q=`).
    Call `GET /artists-check-email?email=` before `artist_info`. Free-text countries are rejected.
    **Existing artist:** `GET /artists/resolve` (email / partner_artist_id / name) then confirm. UUID lookup
    `GET /artists?id=` still works when you already stored the id. Do not ask the artist to paste a UUID.
    A roster row is **not** required to deliver onto an existing artist. The work is stored on `albums.label_id`.
    The first mapping of this partner to that existing artist holds the release in `pending_review` (no live flip,
    no `album.published` webhook until approved).
    **Featuring:** `GET /artists-search?q=` (min 3 chars).
    **Rights holders:** `tracks[].rights` on publish (artist_id / rightholder_id / email). Name-only payable splits are rejected. An email not on WasaWasa receives an invitation; the share is pending until they join.

    **ISRC:** `GET /isrc?isrc=` looks up MusicBrainz and lists matching tracks in your catalog. `POST /isrc` with
    `apply: true` fills empty duration only.

    **DDEX ingest:** `POST /ddex-ern` accepts DDEX ERN 3.8.2 `NewReleaseMessage` (`ern/382`, namespace
    `http://ddex.net/xml/ern/382`) as XML or ZIP. DDEX describes the release; `label_id` + the API key choose the
    account. `LabelName` is imprint only. `partner_artist_id` (or XML PartyId) maps your artist id in this partner
    namespace — never match on name. `artist_id` does not require a roster link. First partner → existing-artist
    mapping is held for review. Returns 202 `queued`; poll `GET /ddex-jobs?job_id=`. Takedown/update ERNs are not
    processed. Audio max 250 MB/track, ZIP 512 MB, XML 8 MB. Rate limit default 60/min (`RATE_LIMITED`).

    **Music type rules:** `single` = 1 track, `ep` = 2–6, `album` = 7+. Mismatch → `INVALID_TRACK_COUNT_FOR_MUSIC_TYPE`.
    **Multi-value track tags:** prefer `track_genre_values`, `track_ambiance_values`, `track_mood_values` (arrays).
    Legacy singles `track_genre` / `mood` still accepted.
    **Revenue FX:** wallet amounts are stored in platform reference currency (usually EUR). Pass `?currency=`
    from catalog currencies (default XOF); amounts are converted via admin `exchange_rates`.
    (**Music distribution only**; podcasts and audiobooks are not in this catalog.)
  version: 1.8.0
  contact:
    email: support@wasawasamusic.com

servers:
  - url: https://gateway.wasawasamusic.com/v1
    description: Production API gateway (partner / DDEX)

tags:
  - name: Catalog
    description: Reference lists (genres, currencies, …) and partner release catalog
  - name: Identity
    description: Artist resolve (email / partner id / name / ISNI) and ISNI registry — never auto-merge on name
  - name: Upload
  - name: Publish
  - name: DDEX
    description: Standard ERN 3.8.2 ingest (XML/ZIP), jobs, auto-publish settings
  - name: Analytics
  - name: Revenue
  - name: Royalties
  - name: Rights
  - name: Licenses
  - name: Reports

components:
  securitySchemes:
    ApiKey:
      type: apiKey
      in: header
      name: x-api-key
      description: Distribution API key (`ww_live_…` or `ww_test_…`). Required.
    ApiSecret:
      type: apiKey
      in: header
      name: x-api-secret
      description: Distribution API secret (`wws_live_…` or `wws_test_…`). Required with the matching key.

  schemas:
    ErrorBody:
      type: object
      properties:
        error:
          type: string
        code:
          type: string
        details:
          type: string
        available_currencies:
          type: array
          items:
            type: string
          description: Present on INVALID_CURRENCY / MISSING_EXCHANGE_RATE

    PartnerReleaseTrack:
      type: object
      properties:
        nia_t_code:
          type: string
          nullable: true
        title:
          type: string
        track_number:
          type: integer
        duration:
          type: integer
          nullable: true
          description: Seconds
        featuring:
          type: array
          items:
            type: string
        track_genre:
          type: string
          nullable: true
        track_genre_values:
          type: array
          items:
            type: string
        mood:
          type: string
          nullable: true
        track_ambiance_values:
          type: array
          items:
            type: string
        track_mood_values:
          type: array
          items:
            type: string

    RevenueBlock:
      type: object
      properties:
        total_revenue:
          type: number
          description: Amount in the requested display currency
        currency:
          type: string
          example: XOF
        reference_currency:
          type: string
          example: EUR
        total_revenue_reference:
          type: number
          description: Amount in wallet reference currency (usually EUR)
        exchange_rate:
          type: number
          nullable: true
          description: 1 reference = exchange_rate display units (e.g. 1 EUR = 655.957 XOF)
        transaction_count:
          type: integer
        tracks:
          type: array
          items:
            type: object
            properties:
              nia_t_code:
                type: string
                nullable: true
              title:
                type: string
              stream_count:
                type: integer
              revenue_share:
                type: number
                description: Proportional to stream_count when wallet txs are album-level

# Partner auth = x-api-key + x-api-secret only (no Authorization Bearer).
security:
  - ApiKey: []
    ApiSecret: []

paths:
  /catalog:
    get:
      tags: [Catalog]
      summary: Reference lists for music publish payloads
      description: |
        Returns genres, moods, languages, voice types, track types, explicit levels, music types,
        publication/availability modes, currencies with rates from reference, instruments, and countries.
        Scope is always music; there is no `content_type` query parameter.
        Use `currencies[].code` for purchase `album.availability.currency` and for revenue/reports `?currency=`.
      responses:
        '200':
          description: Catalog payload
          content:
            application/json:
              schema:
                type: object
                properties:
                  success:
                    type: boolean
                  catalog_version:
                    type: string
                  scope:
                    type: string
                    example: music
                  counts:
                    type: object
                    additionalProperties:
                      type: integer
                  note:
                    type: string
                  genres:
                    type: array
                    items:
                      type: object
                      properties:
                        id:
                          type: string
                          format: uuid
                        name:
                          type: string
                  moods:
                    type: array
                    items:
                      type: object
                      properties:
                        value:
                          type: string
                        label:
                          type: string
                  languages:
                    type: array
                    items:
                      type: object
                      properties:
                        value:
                          type: string
                        label:
                          type: string
                        region:
                          type: string
                  voice_types:
                    type: array
                    items:
                      type: object
                  track_types:
                    type: array
                    items:
                      type: object
                  explicit_content_levels:
                    type: array
                    description: AAH1 / AAH2 / AAH3 — use `value` for album.explicit_content_level
                    items:
                      type: object
                      properties:
                        value:
                          type: string
                          example: AAH1
                        label:
                          type: string
                          example: Tout public
                        description:
                          type: string
                  music_types:
                    type: array
                    items:
                      type: object
                      properties:
                        value:
                          type: string
                        label:
                          type: string
                        min_tracks:
                          type: integer
                        max_tracks:
                          type: integer
                          nullable: true
                  publication_modes:
                    type: array
                    items:
                      type: object
                  availability_modes:
                    type: array
                    description: streaming | free | purchase — use value for album.availability.mode
                    items:
                      type: object
                      properties:
                        value:
                          type: string
                        label:
                          type: string
                        requires_price:
                          type: boolean
                  reference_currency:
                    type: string
                    example: EUR
                  currencies:
                    type: array
                    description: Platform-configured currencies; required for purchase mode and revenue FX
                    items:
                      type: object
                      properties:
                        code:
                          type: string
                          example: XOF
                        name:
                          type: string
                        symbol:
                          type: string
                        rate_from_reference:
                          type: number
                          nullable: true
                          description: 1 reference_currency = this many units of code
                  instruments:
                    type: array
                    items:
                      type: object
                      properties:
                        value:
                          type: string
                        label:
                          type: string
                  countries:
                    type: array
                    description: Use `code` (preferred) for artist_info.country when creating a new artist
                    items:
                      type: object
                      properties:
                        code:
                          type: string
                          example: ML
                        name:
                          type: string
                          example: Mali
        '401':
          description: Missing or invalid API key
        '403':
          description: API disabled for partner

  /releases:
    get:
      tags: [Catalog]
      summary: Partner catalog (artists + releases with NIA)
      description: |
        Lists releases in your API scope (`albums.label_id` **or** `published_via_api_key_id` for this key),
        plus the artists those deliveries belong to. A `label_artists` roster row is not required.
        Includes NIA-R / NIA-T and multi-value tags. Not the same as `GET /catalog` (reference metadata).
      parameters:
        - name: include
          in: query
          schema:
            type: string
            enum: [all, artists, releases]
            default: all
        - name: environment
          in: query
          schema:
            type: string
            enum: [all, production, sandbox]
            default: all
        - name: artist_id
          in: query
          schema:
            type: string
            format: uuid
        - name: partner_artist_id
          in: query
          schema:
            type: string
        - name: isrc
          in: query
          schema:
            type: string
        - name: iswc
          in: query
          schema:
            type: string
        - name: gtin
          in: query
          schema:
            type: string
        - name: upc
          in: query
          schema:
            type: string
        - name: ean
          in: query
          schema:
            type: string
        - name: nia_r
          in: query
          schema:
            type: string
        - name: album_id
          in: query
          schema:
            type: string
            format: uuid
        - name: music_type
          in: query
          schema:
            type: string
            enum: [single, ep, album]
        - name: limit
          in: query
          schema:
            type: integer
            minimum: 1
            maximum: 500
            default: 100
        - name: offset
          in: query
          schema:
            type: integer
            minimum: 0
            default: 0
      responses:
        '200':
          description: Partner artists and/or releases
          content:
            application/json:
              schema:
                type: object
                properties:
                  success:
                    type: boolean
                  partner:
                    type: object
                    properties:
                      id:
                        type: string
                        format: uuid
                      company_name:
                        type: string
                      label_id:
                        type: string
                        format: uuid
                        nullable: true
                  summary:
                    type: object
                    properties:
                      artists_count:
                        type: integer
                      releases_count:
                        type: integer
                      tracks_count:
                        type: integer
                  artists:
                    type: array
                    items:
                      type: object
                      properties:
                        id:
                          type: string
                          format: uuid
                        artist_name:
                          type: string
                        avatar_url:
                          type: string
                          nullable: true
                        country:
                          type: string
                          nullable: true
                        open_url:
                          type: string
                        link_status:
                          type: string
                          nullable: true
                        source:
                          type: string
                          enum: [label, release]
                  releases:
                    type: array
                    items:
                      type: object
                      properties:
                        nia_r_code:
                          type: string
                          nullable: true
                        title:
                          type: string
                        cover_url:
                          type: string
                          nullable: true
                        music_type:
                          type: string
                        environment:
                          type: string
                          enum: [sandbox, production]
                        artist_name:
                          type: string
                          nullable: true
                        tracks:
                          type: array
                          items:
                            $ref: '#/components/schemas/PartnerReleaseTrack'
                  pagination:
                    type: object
                    properties:
                      limit:
                        type: integer
                      offset:
                        type: integer
                      returned:
                        type: integer
        '401':
          description: Missing or invalid credentials
        '403':
          description: API disabled

  /releases/{id}:
    get:
      tags: [Catalog]
      summary: Fat release (UUID or NIA-R)
      parameters:
        - name: id
          in: path
          required: true
          schema: { type: string }
        - name: include
          in: query
          schema: { type: string }
      responses:
        '200':
          description: Full album payload
        '404':
          description: ALBUM_NOT_FOUND
    patch:
      tags: [Catalog]
      summary: Limited metadata update (title, release_date, description)
      parameters:
        - name: id
          in: path
          required: true
          schema: { type: string }
      requestBody:
        content:
          application/json:
            schema:
              type: object
              properties:
                title: { type: string }
                release_date: { type: string, format: date }
                description: { type: string }
      responses:
        '200':
          description: Updated album; enqueue album.updated
        '400':
          description: EMPTY_PATCH
        '404':
          description: ALBUM_NOT_FOUND

  /artists-search:
    get:
      tags: [Artists]
      summary: Search platform artists for featuring
      description: |
        Min 3 characters. Returns avatar_url, artist_name, is_verified, profile_url.
        Use results in tracks[].featuring on publish. Manual display names allowed if no match.
      parameters:
        - name: q
          in: query
          required: true
          schema:
            type: string
            minLength: 3
        - name: exclude_artist_id
          in: query
          required: false
          schema:
            type: string
            format: uuid
      responses:
        '200':
          description: Matching artists
        '400':
          description: Query too short
        '401':
          description: Missing or invalid credentials

  /artists:
    get:
      tags: [Artists]
      summary: Lookup artist by UUID (existing account)
      description: |
        When the partner UI collects artist_id for an existing WasaWasa artist, call this endpoint
        to show artist_name + avatar_url for confirmation before publish.
        403 `ARTIST_NOT_ON_ROSTER` means the UUID is not in this partner’s aliases or catalog scope.
        It is **not** a publish / DDEX gate.
      parameters:
        - name: id
          in: query
          required: true
          schema:
            type: string
            format: uuid
          description: Also accepts artist_id as the query param name
      responses:
        '200':
          description: Artist found (name, avatar_url, profile_url, …)
        '400':
          description: Missing or invalid UUID
        '401':
          description: Missing or invalid credentials
        '403':
          description: ARTIST_NOT_ON_ROSTER — UUID not in this partner’s aliases or catalog scope
        '404':
          description: ARTIST_NOT_FOUND

  /artists/resolve:
    get:
      tags: [Identity]
      summary: Resolve an artist without pasting a UUID
      description: |
        At least one of email, partner_artist_id, q (name, min 3), isni.
        Email or a mapped partner_artist_id → match=exact. Name or ISNI → candidates only (never exact, never verified).
        Alias: GET /artists?resolve=1 with the same query params.
      parameters:
        - name: email
          in: query
          schema: { type: string, format: email }
        - name: partner_artist_id
          in: query
          schema: { type: string }
        - name: q
          in: query
          schema: { type: string, minLength: 3 }
        - name: isni
          in: query
          schema: { type: string }
      responses:
        '200':
          description: match exact | candidates | none
        '400':
          description: MISSING_RESOLVE_QUERY / QUERY_TOO_SHORT
        '401':
          description: Missing or invalid credentials

  /artists/{id}:
    get:
      tags: [Identity]
      summary: Fat artist profile (UUID or partner_artist_id)
      parameters:
        - name: id
          in: path
          required: true
          schema: { type: string }
      responses:
        '200':
          description: Artist profile (PII only if in partner scope)
        '404':
          description: ARTIST_NOT_FOUND

  /artists/{id}/identifiers:
    post:
      tags: [Identity]
      summary: Declare an ISNI (no automatic match)
      description: |
        Body `{ type: "ISNI", value, partner_artist_id? }`. Stored submitted_by_partner, automatic_match false.
        Conflict if the same ISNI is already verified on another artist.
      parameters:
        - name: id
          in: path
          required: true
          schema: { type: string }
      requestBody:
        required: true
        content:
          application/json:
            schema:
              type: object
              required: [type, value]
              properties:
                type: { type: string, enum: [ISNI] }
                value: { type: string }
                partner_artist_id: { type: string }
      responses:
        '200':
          description: Identifier stored
        '400':
          description: INVALID_ISNI
        '403':
          description: Artist not in this partner’s aliases or catalog scope (lookup only — not a publish gate)

  /identity/isni/search:
    get:
      tags: [Identity]
      summary: Search the public ISNI registry
      description: Never returns verified true from a name match. Rate-limited.
      parameters:
        - name: q
          in: query
          required: true
          schema: { type: string, minLength: 2 }
      responses:
        '200':
          description: Registry records (verified false)
        '429':
          description: RATE_LIMITED

  /identity/isni/{isni}:
    get:
      tags: [Identity]
      summary: Lookup one ISNI
      parameters:
        - name: isni
          in: path
          required: true
          schema: { type: string }
      responses:
        '200':
          description: Record or null
        '400':
          description: INVALID_ISNI

  /artists-check-email:
    get:
      tags: [Artists]
      summary: Check if email is available for a new artist
      description: |
        Use before artist_info. Returns available true/false and existing_artist when taken.
      parameters:
        - name: email
          in: query
          required: true
          schema:
            type: string
            format: email
      responses:
        '200':
          description: Availability result
        '400':
          description: Invalid email
        '401':
          description: Missing or invalid credentials

  /countries:
    get:
      tags: [Artists]
      summary: Search platform countries (typeahead)
      description: |
        Same countries table as WasaWasa signup. Use for new-artist country autocomplete.
        Send artist_info.country as `code` only — invented / free-text names are rejected on publish.
        Full list also available on GET /catalog → countries[].
      parameters:
        - name: q
          in: query
          required: true
          schema:
            type: string
            minLength: 1
            example: mal
      responses:
        '200':
          description: Matching countries
        '400':
          description: Query missing
        '401':
          description: Missing or invalid credentials

  /assets:
    post:
      tags: [Upload]
      summary: Upload cover or audio file
      description: |
        `multipart/form-data` with fields `file` and `type` (`cover` or `audio`).
        Cover max 10 MB (JPEG/PNG/WebP). Audio max 50 MB (MP3/WAV/FLAC/M4A).
        Unused uploads are deleted after 48 hours; URLs used in a successful publish are kept.
        For `type=audio`, duration is detected server-side and returned (and stored for publish fill-in).
      requestBody:
        required: true
        content:
          multipart/form-data:
            schema:
              type: object
              required: [file, type]
              properties:
                file:
                  type: string
                  format: binary
                type:
                  type: string
                  enum: [cover, audio]
      responses:
        '200':
          description: Public URL for use in publish payload
          content:
            application/json:
              schema:
                type: object
                properties:
                  success:
                    type: boolean
                  url:
                    type: string
                    format: uri
                    description: Use as album.cover_url or tracks[].audio_url
                  file_name:
                    type: string
                  file_size:
                    type: integer
                  file_type:
                    type: string
                    description: MIME type
                  duration:
                    type: integer
                    nullable: true
                    description: Audio only — duration in whole seconds (auto-detected)
                  duration_seconds:
                    type: integer
                    nullable: true
                    description: Same as duration
                  message:
                    type: string
              examples:
                audio:
                  summary: Audio upload
                  value:
                    success: true
                    url: https://…/audio-tracks/…/….mp3
                    file_name: partner-id/audio/123-abc.mp3
                    file_size: 5242880
                    file_type: audio/mpeg
                    duration: 214
                    duration_seconds: 214
                    message: File uploaded successfully. duration is auto-detected (seconds); you may omit tracks[].duration on publish.
                cover:
                  summary: Cover upload
                  value:
                    success: true
                    url: https://…/album-covers/…/….jpg
                    file_name: partner-id/cover/123-abc.jpg
                    file_size: 2048576
                    file_type: image/jpeg
                    message: File uploaded successfully. Use this URL in your publish request.
        '400':
          description: Invalid file or type
        '401':
          description: Invalid or inactive API key

  /publish:
    post:
      tags: [Publish]
      summary: Publish album / single / EP
      description: |
        Creates the release (draft → tracks → publish for production keys; sandbox stays unpublished but still gets NIA).
        Hard stops before insert: missing cover/audio/genres/tracks, or music_type vs track count mismatch.
        Artist profiles are independent of labels. Delivering onto an existing `artist_id` does **not** require a
        `label_artists` roster row. The work is stored on `albums.label_id`. The first mapping of this partner to an
        existing artist holds the release in `pending_review` (stays draft, no `album.published` webhook until approved).
        `ARTIST_NOT_LINKED_TO_LABEL` and `ARTIST_NOT_ON_ROSTER` are not returned by this endpoint.
      requestBody:
        required: true
        content:
          application/json:
            schema:
              type: object
              required: [publication_mode, album]
              properties:
                artist_id:
                  type: string
                  format: uuid
                  description: WasaWasa artist UUID. No roster row required. First mapping of this partner to that artist is held for review.
                partner_artist_id:
                  type: string
                  description: Partner-scoped artist key. Auto-attaches if already mapped for this partner; upserted after success.
                artist_info:
                  type: object
                  required: [email, artist_name, country]
                  properties:
                    email:
                      type: string
                      format: email
                    artist_name:
                      type: string
                    country:
                      type: string
                      description: Platform country code (e.g. ML), not free text
                    bio:
                      type: string
                publication_mode:
                  type: string
                  enum: [independent, label]
                label_id:
                  type: string
                  format: uuid
                  description: Target label in the partner scope (parent or child). Required when several labels are in scope.
                label_name:
                  type: string
                  description: Validated alias only — never auto-matched by name. Maps via partner_label_aliases.
                revenue_share_percentage:
                  type: number
                  description: Legacy field. New artists are not inserted into `label_artists`. Platform revenue follows `publication_mode` and `albums.label_id`.
                album:
                  type: object
                  required: [title, release_date, music_type, cover_url, availability, tracks]
                  properties:
                    title:
                      type: string
                    description:
                      type: string
                    release_date:
                      type: string
                      format: date
                    music_type:
                      type: string
                      enum: [single, ep, album]
                      description: |
                        Must match tracks length: single = exactly 1, ep = 2–6, album = 7+.
                        Mismatch → 400 INVALID_TRACK_COUNT_FOR_MUSIC_TYPE.
                    cover_url:
                      type: string
                      format: uri
                    genres:
                      type: array
                      items:
                        type: string
                      description: Required for publish — catalog genres[].name
                    languages:
                      type: array
                      items:
                        type: string
                    explicit_content_level:
                      type: string
                      example: AAH1
                    main_instrument:
                      type: string
                    main_instrument_values:
                      type: array
                      items:
                        type: string
                      description: Multi-value instruments (catalog instruments[].value)
                    copyright_info:
                      type: string
                    upc:
                      type: string
                      description: UPC-A (12 digits). Stored as albums.gtin with ean/icpn/gtin.
                    ean:
                      type: string
                      description: EAN-13 (13 digits). Same column as upc/gtin.
                    gtin:
                      type: string
                      description: GTIN-12/13/14 digits. Preferred product barcode field.
                    icpn:
                      type: string
                      description: DDEX ICPN — same GTIN column.
                    content_type:
                      type: string
                    availability:
                      type: object
                      required: [mode]
                      properties:
                        mode:
                          type: string
                          enum: [streaming, free, purchase]
                        is_streaming:
                          type: boolean
                        is_free:
                          type: boolean
                        currency:
                          type: string
                          description: Required for purchase — catalog currency code; price converted to reference (EUR)
                        price:
                          type: number
                        vip_price:
                          type: number
                    tracks:
                      type: array
                      items:
                        type: object
                        required: [title, audio_url]
                        properties:
                          title:
                            type: string
                          track_number:
                            type: integer
                          audio_url:
                            type: string
                            format: uri
                            description: >
                              URL from POST /assets. Partners always send `audio_url`;
                              stored internally as tracks.file_url.
                          duration:
                            type: number
                            description: |
                              Optional seconds. If omitted or ≤0, filled from upload `duration_seconds`
                              for the matching audio_url.
                          is_vip_only:
                            type: boolean
                          track_genre:
                            type: string
                            description: Optional legacy single genre; prefer track_genre_values.
                          track_genre_values:
                            type: array
                            items:
                              type: string
                            description: Up to 5 — catalog genres[].name
                          track_ambiance_values:
                            type: array
                            items:
                              type: string
                            description: Up to 5 — catalog moods[].value (ambiances)
                          track_mood_values:
                            type: array
                            items:
                              type: string
                            description: Up to 5 — catalog moods[].value (moods)
                          mood:
                            type: string
                            description: Optional; prefer track_ambiance_values
                          voice_type:
                            type: string
                            description: Optional; catalog voice_types[].value (do not send mood values here)
                          track_type:
                            type: string
                            description: Optional; catalog track_types[].value
                          recording_date:
                            type: string
                            format: date
                          recording_studio:
                            type: string
                          lyrics:
                            type: string
                          isrc:
                            type: string
                            description: Track ISRC (12 characters). Stored on tracks.isrc.
                          iswc:
                            type: string
                            description: Musical work ISWC (T + 10 digits). Stored on tracks.iswc.
                          featuring:
                            type: array
                            description: Search via /artists-search; manual names allowed
                            items:
                              oneOf:
                                - type: string
                                - type: object
                                  properties:
                                    artist_id:
                                      type: string
                                      format: uuid
                                    display_name:
                                      type: string
                          instruments_used:
                            type: array
                            items:
                              type: string
                            description: Prefer catalog instruments[].value
                          production_credits:
                            description: Object map or array of { role, person }. Display credits only — not payable.
                            oneOf:
                              - type: object
                                additionalProperties:
                                  type: string
                              - type: array
                                items:
                                  type: object
                                  properties:
                                    role:
                                      type: string
                                    person:
                                      type: string
                          rights:
                            type: array
                            description: Payable splits. Require artist_id, rightholder_id, or email. Unknown email → invitation + pending (not paid). Name alone is rejected when percentage > 0.
                            items:
                              type: object
                              properties:
                                artist_id:
                                  type: string
                                  format: uuid
                                rightholder_id:
                                  type: string
                                  format: uuid
                                email:
                                  type: string
                                  format: email
                                display_name:
                                  type: string
                                role:
                                  type: string
                                percentage:
                                  type: number
                                  minimum: 0
                                  maximum: 100
      responses:
        '200':
          description: |
            Published. Aggregators must update release status from this sync response
            (`success: true`) and persist `nia_r_code`, `tracks[].nia_t_code`, `album_id`, and
            `artist_id` in their own database (mapped to their user/release) so returning users
            see the codes again. WasaWasa also stores the platform copy. `webhook.enqueued` means
            album.published was queued for HTTPS delivery (backup, not the only persistence path).
            Independent vs label does not change NIA return or local storage rules.
          content:
            application/json:
              schema:
                type: object
                properties:
                  success:
                    type: boolean
                  environment:
                    type: string
                    enum: [sandbox, production]
                  album_id:
                    type: string
                    format: uuid
                  artist_id:
                    type: string
                    format: uuid
                  nia_r_code:
                    type: string
                    nullable: true
                  tracks:
                    type: array
                    items:
                      type: object
                      properties:
                        title:
                          type: string
                        track_number:
                          type: integer
                        nia_t_code:
                          type: string
                          nullable: true
                  pending_invites:
                    type: array
                    description: Email-only splits waiting for a WasaWasa account. Not payable yet. Recipients get "WasaWasa — Invitation ayant droit" (same layout as claim emails).
                    items:
                      type: object
                      properties:
                        email:
                          type: string
                          format: email
                        track_number:
                          type: integer
                        percentage:
                          type: number
                        role:
                          type: string
                        status:
                          type: string
                          example: pending
                  message:
                    type: string
                  album_url:
                    type: string
                    nullable: true
                  webhook:
                    type: object
                    properties:
                      enqueued:
                        type: boolean
                      event:
                        type: string
                        example: album.published
        '400':
          description: |
            Validation error — e.g. INVALID_ARTIST_COUNTRY, MISSING_AUDIO_URL, MISSING_COVER_URL,
            INVALID_TRACK_COUNT_FOR_MUSIC_TYPE (single=1, ep=2–6, album=7+), INVALID_DATA,
            LABEL_ID_REQUIRED
        '409':
          description: ARTIST_EMAIL_TAKEN — use artist_id instead of artist_info. LABEL_NAME_UNMAPPED.
        '500':
          description: TRACK_INSERT_FAILED or server error
        '401':
          description: Missing or invalid API key
        '403':
          description: Permission error — PARTNER_LABEL_OUT_OF_SCOPE, LABEL_PENDING_VERIFICATION
        '429':
          description: Quota exceeded

  /analytics-streams:
    get:
      tags: [Analytics]
      summary: Streaming analytics (rich release payload)
      description: |
        Returns cover, titles, featuring, genres/moods (incl. multi-value arrays), streams, unique listeners,
        listen time, by country/day, and per-track stats for releases in partner scope.
        Requires `can_view_analytics`.
      parameters:
        - name: nia_r
          in: query
          schema:
            type: string
        - name: nia_t
          in: query
          schema:
            type: string
        - name: album_id
          in: query
          schema:
            type: string
            format: uuid
        - name: artist_id
          in: query
          schema:
            type: string
            format: uuid
        - name: period
          in: query
          schema:
            type: string
            enum: [today, week, month, year]
        - name: start_date
          in: query
          schema:
            type: string
        - name: end_date
          in: query
          schema:
            type: string
      responses:
        '200':
          description: Aggregated analytics with release metadata
        '401':
          description: Invalid API key
        '403':
          description: Analytics not allowed for this key

  /analytics-revenue:
    get:
      tags: [Revenue]
      summary: Revenue with FX conversion
      description: |
        Same rich release payload as analytics, plus earnings.
        Wallet `album_earnings` are stored in reference currency (usually EUR).
        Pass `currency` from `GET /catalog` → `currencies` (default XOF).
        Response includes converted `total_revenue`, `total_revenue_reference`, and `exchange_rate`.
        Unsupported currency → 400 `INVALID_CURRENCY`. Missing rate → 400 `MISSING_EXCHANGE_RATE`.
        Requires `can_view_revenue`.
      parameters:
        - name: nia_r
          in: query
          schema:
            type: string
        - name: nia_t
          in: query
          schema:
            type: string
        - name: album_id
          in: query
          schema:
            type: string
            format: uuid
        - name: artist_id
          in: query
          schema:
            type: string
            format: uuid
        - name: currency
          in: query
          schema:
            type: string
            default: XOF
          description: Platform currency code from catalog
        - name: period
          in: query
          schema:
            type: string
            enum: [today, week, month, year]
        - name: start_date
          in: query
          schema:
            type: string
        - name: end_date
          in: query
          schema:
            type: string
      responses:
        '200':
          description: Revenue data with FX fields
          content:
            application/json:
              schema:
                type: object
                properties:
                  success:
                    type: boolean
                  summary:
                    type: object
                    properties:
                      total_albums:
                        type: integer
                      total_revenue:
                        type: number
                      total_revenue_reference:
                        type: number
                      total_streams:
                        type: integer
                      currency:
                        type: string
                      reference_currency:
                        type: string
                  data:
                    type: array
                    items:
                      type: object
                      properties:
                        nia_r_code:
                          type: string
                        total_revenue:
                          type: number
                        total_revenue_reference:
                          type: number
                        currency:
                          type: string
                        reference_currency:
                          type: string
                        exchange_rate:
                          type: number
                          nullable: true
                        revenue:
                          $ref: '#/components/schemas/RevenueBlock'
        '400':
          description: INVALID_CURRENCY or MISSING_EXCHANGE_RATE
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/ErrorBody'
        '401':
          description: Invalid API key
        '403':
          description: Revenue not allowed for this key

  /analytics-reports:
    get:
      tags: [Reports]
      summary: Aggregated reports (streams + revenue over a window)
      description: |
        Releases cards + timeline. Revenue amounts converted like `/analytics-revenue`
        when `currency` is set (default XOF). Requires analytics and/or revenue permission.
      parameters:
        - name: nia_r
          in: query
          schema:
            type: string
        - name: nia_t
          in: query
          schema:
            type: string
        - name: album_id
          in: query
          schema:
            type: string
            format: uuid
        - name: artist_id
          in: query
          schema:
            type: string
            format: uuid
        - name: type
          in: query
          schema:
            type: string
            enum: [daily, monthly, current]
            default: monthly
        - name: year
          in: query
          schema:
            type: string
        - name: month
          in: query
          schema:
            type: string
          description: Required for daily report type (1–12)
        - name: currency
          in: query
          schema:
            type: string
            default: XOF
          description: Platform currency for FX conversion
      responses:
        '200':
          description: Report payload (releases[], timeline[], summary with currency + reference_currency)
        '400':
          description: INVALID_CURRENCY or MISSING_EXCHANGE_RATE
        '401':
          description: Invalid API key
        '403':
          description: Reports not allowed for this key

  /ddex-ern:
    post:
      tags: [DDEX]
      summary: Ingest a DDEX ERN 3.8.2 XML or ZIP package
      description: |
        DDEX ERN 3.8.2 NewReleaseMessage only (not takedown/purge). Namespace `http://ddex.net/xml/ern/382`.
        DDEX describes content; API key + `label_id` authorize the account.
        Managed: `label_id` required when several labels are in scope; label must be in partner scope.
        Independent: only when the key has no managed parent and no label context is sent.
        `partner_artist_id` is your artist id (namespaced by this API key). Optional `artist_id` (WasaWasa UUID).
        No roster row is required. First mapping of this partner to an existing artist is held for review.
        Email is last-resort lookup, not a primary identifier. Names never auto-attach.
        XML LabelName is imprint only. MessageSender / SentOnBehalfOf are not authorization.
        Same MessageId + open job → 409 DDEX_MESSAGE_EXISTS. Failed job may be retried with the same id.
        New MessageId = new delivery, not an in-place update. Poll GET /ddex-jobs?job_id=.
        ZIP 512 MB, XML 8 MB, audio 250 MB/track. Cover + audio required for the job to succeed.
      security:
        - ApiKey: []
          ApiSecret: []
      parameters:
        - name: x-wasawasa-label-id
          in: header
          required: false
          schema:
            type: string
            format: uuid
          description: Optional. Same as body `label_id`. Body or form field wins if both are sent.
      requestBody:
        required: true
        content:
          multipart/form-data:
            schema:
              type: object
              required: [file]
              properties:
                file:
                  type: string
                  format: binary
                label_id:
                  type: string
                  format: uuid
                  description: Target label in the partner scope. Required when several labels are in scope.
                label_name:
                  type: string
                  description: Validated alias only — never auto-matched from XML LabelName.
                artist_id:
                  type: string
                  format: uuid
                  description: WasaWasa artist UUID. No roster row required. First mapping of this partner to that artist is held for review.
                partner_artist_id:
                  type: string
                  description: Partner’s own artist id. Stored as partner_artist_aliases for this API key. Wins over XML PartyId.
                email:
                  type: string
                  description: Last-resort lookup only — not a primary identifier.
                publication_mode:
                  type: string
                  enum: [label, independent]
                  description: Ignored when the API key is tied to a parent label (forced to label).
          application/json:
            schema:
              type: object
              properties:
                xml:
                  type: string
                xml_url:
                  type: string
                  format: uri
                package_url:
                  type: string
                  format: uri
                label_id:
                  type: string
                  format: uuid
                  description: Target label in the partner scope. Required when several labels are in scope.
                label_name:
                  type: string
                  description: Validated alias only — never auto-matched from XML LabelName.
                artist_id:
                  type: string
                  format: uuid
                  description: WasaWasa artist UUID. No roster row required. First mapping of this partner to that artist is held for review.
                partner_artist_id:
                  type: string
                  description: Partner’s own artist id. Stored as partner_artist_aliases for this API key. Wins over XML PartyId.
                email:
                  type: string
                  description: Last-resort lookup only — not a primary identifier.
                publication_mode:
                  type: string
                  enum: [label, independent]
                  description: Ignored when the API key is tied to a parent label (forced to label).
      responses:
        '202':
          description: Job queued (`status: queued`). Poll GET /ddex-jobs?job_id=.
        '400':
          description: DDEX_PARSE_FAILED, MISSING_PACKAGE, LABEL_ID_REQUIRED
        '403':
          description: DDEX_INGEST_DISABLED, PERMISSION_DENIED, ARTIST_CREATION_DENIED, PARTNER_LABEL_OUT_OF_SCOPE
        '409':
          description: DDEX_MESSAGE_EXISTS, LABEL_NAME_UNMAPPED
        '413':
          description: PACKAGE_TOO_LARGE
        '429':
          description: RATE_LIMITED, QUOTA_EXCEEDED

  /ddex-jobs:
    get:
      tags: [DDEX]
      summary: List or fetch DDEX ingest jobs
      description: |
        Without query: last 100 jobs. With `job_id`, a single job (`{ job: {...} }`).
        Poll until `published`, `pending_review`, or `failed`.
      security:
        - ApiKey: []
          ApiSecret: []
      parameters:
        - name: job_id
          in: query
          required: false
          schema:
            type: string
            format: uuid
      responses:
        '200':
          description: `{ jobs: [...] }` or `{ job: {...} }`
        '404':
          description: JOB_NOT_FOUND
    post:
      tags: [DDEX]
      summary: List, approve, or reject a DDEX job
      description: |
        Body `{ "action": "list" }` or `{ "action": "approve"|"reject", "job_id": "<uuid>" }`.
      security:
        - ApiKey: []
          ApiSecret: []
      requestBody:
        content:
          application/json:
            schema:
              type: object
              properties:
                action:
                  type: string
                  enum: [list, approve, reject]
                job_id:
                  type: string
                  format: uuid
                reason:
                  type: string
      responses:
        '200':
          description: Job list or updated job
        '404':
          description: JOB_NOT_FOUND
        '409':
          description: JOB_NOT_PENDING

  /ddex-settings:
    get:
      tags: [DDEX]
      summary: Read DDEX ingest settings
      security:
        - ApiKey: []
          ApiSecret: []
      responses:
        '200':
          description: Current settings
    patch:
      tags: [DDEX]
      summary: Toggle partner auto-publish
      description: Body `{ "ddex_auto_publish": true|false }`. May be refused if the setting is locked.
      security:
        - ApiKey: []
          ApiSecret: []
      requestBody:
        required: true
        content:
          application/json:
            schema:
              type: object
              required: [ddex_auto_publish]
              properties:
                ddex_auto_publish:
                  type: boolean
      responses:
        '200':
          description: Updated flags
        '403':
          description: AUTO_PUBLISH_LOCKED

  /albums:
    get:
      tags: [Catalog]
      summary: List albums in your catalog
      security:
        - ApiKey: []
          ApiSecret: []
      parameters:
        - name: nia_r
          in: query
          schema: { type: string }
        - name: album_id
          in: query
          schema: { type: string, format: uuid }
        - name: artist_id
          in: query
          schema: { type: string, format: uuid }
        - name: partner_artist_id
          in: query
          schema: { type: string }
        - name: isrc
          in: query
          schema: { type: string }
        - name: iswc
          in: query
          schema: { type: string }
        - name: gtin
          in: query
          schema: { type: string }
        - name: upc
          in: query
          schema: { type: string }
        - name: ean
          in: query
          schema: { type: string }
        - name: music_type
          in: query
          schema: { type: string, enum: [single, ep, album] }
        - name: environment
          in: query
          schema: { type: string, enum: [all, production, sandbox], default: all }
      responses:
        '200':
          description: Album summaries (NIA-R) or fat album when id / nia_r is set

  /albums/{id}:
    get:
      tags: [Catalog]
      summary: Fat album (UUID, NIA-R, or GTIN / UPC / EAN)
      parameters:
        - name: id
          in: path
          required: true
          schema: { type: string }
      responses:
        '200':
          description: Full album
        '404':
          description: ALBUM_NOT_FOUND
    patch:
      tags: [Catalog]
      summary: Limited metadata update
      parameters:
        - name: id
          in: path
          required: true
          schema: { type: string }
      responses:
        '200':
          description: Updated; album.updated webhook

  /tracks:
    get:
      tags: [Catalog]
      summary: Flat track list in your catalog
      security:
        - ApiKey: []
          ApiSecret: []
      parameters:
        - name: nia_t
          in: query
          schema: { type: string }
        - name: nia_r
          in: query
          schema: { type: string }
        - name: album_id
          in: query
          schema: { type: string, format: uuid }
        - name: artist_id
          in: query
          schema: { type: string, format: uuid }
        - name: isrc
          in: query
          schema: { type: string }
        - name: iswc
          in: query
          schema: { type: string }
      responses:
        '200':
          description: Tracks with NIA-T

  /tracks/{id}:
    get:
      tags: [Catalog]
      summary: Fat track (UUID, NIA-T, ISRC, or ISWC)
      parameters:
        - name: id
          in: path
          required: true
          schema: { type: string }
      responses:
        '200':
          description: Full track
        '404':
          description: TRACK_NOT_FOUND

  /labels:
    get:
      tags: [Catalog]
      summary: List labels in your scope
      security:
        - ApiKey: []
          ApiSecret: []
      responses:
        '200':
          description: Parent and child labels linked to this partner (is_default on the default row)
    post:
      tags: [Catalog]
      summary: Create a child label
      security:
        - ApiKey: []
          ApiSecret: []
      requestBody:
        required: true
        content:
          application/json:
            schema:
              type: object
              required: [name]
              properties:
                name: { type: string }
                legal_name: { type: string }
                display_name: { type: string }
                country: { type: string }
                website: { type: string }
                labels:
                  type: array
                  items:
                    type: object
                    required: [name]
                    properties:
                      name: { type: string }
      responses:
        '200':
          description: created[] and errors[]
        '403':
          description: NOT_PARENT

  /royalties:
    get:
      tags: [Royalties]
      summary: Rights-holder earnings for your catalog
      security:
        - ApiKey: []
          ApiSecret: []
      parameters:
        - name: group
          in: query
          schema: { type: string, enum: [summary, artists, releases, tracks], default: summary }
        - name: nia_r
          in: query
          schema: { type: string }
        - name: nia_t
          in: query
          schema: { type: string }
        - name: artist_id
          in: query
          schema: { type: string, format: uuid }
        - name: currency
          in: query
          schema: { type: string }
          description: Display currency (default XOF). Reference amount is always in platform reference currency (EUR).
      responses:
        '200':
          description: Grouped earnings (amount + amount_reference)

  /analytics-countries:
    get:
      tags: [Analytics]
      summary: Streams by country
      security:
        - ApiKey: []
          ApiSecret: []
      responses:
        '200':
          description: Country rollup

  /analytics-platforms:
    get:
      tags: [Analytics]
      summary: Streams by device platform (ios / android / web)
      description: WasaWasa is the DSP. This is not Spotify/Apple delivery.
      security:
        - ApiKey: []
          ApiSecret: []
      responses:
        '200':
          description: Platform rollup

  /isrc:
    get:
      tags: [Catalog]
      summary: Look up an ISRC (MusicBrainz) and match your catalog
      security:
        - ApiKey: []
          ApiSecret: []
      parameters:
        - name: isrc
          in: query
          required: true
          schema:
            type: string
            example: USSM11204980
      responses:
        '200':
          description: recordings + catalog_tracks (duration empty tracks can be filled via POST)
        '400':
          description: INVALID_DATA — missing or malformed ISRC
    post:
      tags: [Catalog]
      summary: Fill empty duration on catalog tracks for this ISRC
      security:
        - ApiKey: []
          ApiSecret: []
      requestBody:
        required: true
        content:
          application/json:
            schema:
              type: object
              required: [isrc]
              properties:
                isrc:
                  type: string
                nia_t:
                  type: string
                apply:
                  type: boolean
                  example: true
      responses:
        '200':
          description: recordings, catalog_tracks, applied[]
        '403':
          description: PUBLISH_DENIED

  /rights:
    get:
      tags: [Rights]
      summary: List payable splits on your catalog
      security:
        - ApiKey: []
          ApiSecret: []
      responses:
        '200':
          description: content_rightshares rows plus pending_invites (email invitations, not payable)
          content:
            application/json:
              schema:
                type: object
                properties:
                  success:
                    type: boolean
                  rights:
                    type: array
                    items:
                      type: object
                  pending_invites:
                    type: array
                    items:
                      type: object
                      properties:
                        email:
                          type: string
                        percentage:
                          type: number
                        role:
                          type: string
                        status:
                          type: string
                        payable:
                          type: boolean
                          example: false
    post:
      tags: [Rights]
      summary: Replace splits on one track
      security:
        - ApiKey: []
          ApiSecret: []
      requestBody:
        required: true
        content:
          application/json:
            schema:
              type: object
              properties:
                nia_t:
                  type: string
                track_id:
                  type: string
                  format: uuid
                rights:
                  type: array
                  items:
                    type: object
      responses:
        '200':
          description: Updated splits and pending_invites
        '400':
          description: PARTY_NOT_RESOLVED / INVALID_PERCENTAGE
        '409':
          description: PARTY_AMBIGUOUS

  /licenses:
    get:
      tags: [Licenses]
      summary: Usage fields and territory windows
      security:
        - ApiKey: []
          ApiSecret: []
      responses:
        '200':
          description: Track license strings + label_rights_windows
    post:
      tags: [Licenses]
      summary: Update a track commercial_license / usage_rights
      security:
        - ApiKey: []
          ApiSecret: []
      requestBody:
        content:
          application/json:
            schema:
              type: object
              properties:
                nia_t:
                  type: string
                commercial_license:
                  type: string
                usage_rights:
                  type: array
                  items: { type: string }
      responses:
        '200':
          description: Updated fields

  /release-status:
    get:
      tags: [Catalog]
      summary: Publication status, delivery URLs, or ingest errors
      security:
        - ApiKey: []
          ApiSecret: []
      parameters:
        - name: id
          in: query
          schema: { type: string, format: uuid }
        - name: album_id
          in: query
          schema: { type: string, format: uuid }
        - name: nia_r
          in: query
          schema: { type: string }
        - name: view
          in: query
          schema: { type: string, enum: [status, delivery, errors], default: status }
      responses:
        '200':
          description: Status payload
        '404':
          description: RELEASE_NOT_IN_SCOPE

