openapi: 3.1.0
info:
  title: Kasty Music Delivery API
  version: 1.0.0
  description: |
    Versioned partner and Studio delivery contract for Kasty Music catalog V2.
    Sender, catalog account, and channel are derived from the bearer credential;
    clients must not put them in a manifest. A successful submit only accepts
    durable work and never transcodes media in the HTTP request. A machine
    credential represents its configured organization for delivery, but it
    cannot accept Partner Terms or record the separate human payment-regime
    acknowledgement.
  contact:
    name: Kasty Music delivery operations
    url: https://kasty.uk
  license:
    name: Proprietary
    identifier: LicenseRef-Kasty-Proprietary
servers:
  - url: https://api.kasty.app/api/v1/music-deliveries
    description: Production
security:
  - musicDeliveryBearer: []
tags:
  - name: Delivery
    description: Validate, submit, and inspect durable delivery batches.
  - name: Uploads
    description: Direct-to-object-storage upload lifecycle.
  - name: Operations
    description: Explicit retries of retryable failed work.
paths:
  /validate:
    post:
      tags: [Delivery]
      operationId: validateMusicDelivery
      summary: Validate a manifest without writing data
      description: |
        Requires `deliveries.validate`. This is a strict, write-free dry run.
        The response uses stable issue codes and JSONPath locations. When the
        publication-attestation rollout applies, a valid response also returns
        the exact public account/sender binding, normalized business-manifest
        SHA-256, territories, and resolved video-use scope needed to construct
        `rightsDeclaration`. Validation creates no acceptance evidence.
      requestBody:
        required: true
        content:
          application/json:
            schema:
              $ref: '#/components/schemas/DeliveryManifestV1'
            examples:
              single:
                $ref: '#/components/examples/SingleManifest'
      responses:
        '200':
          description: Validation completed, including invalid manifests.
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/ValidationResponse'
              examples:
                valid:
                  value:
                    valid: true
                    manifestSha256: aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa
                    itemCount: 7
                    issues: []
                    publicationAttestation:
                      required: true
                      declarationVersion: KASTY_PARTNER_DELIVERY_RIGHTS_V1
                      catalogAccountPublicId: catalog_public_1
                      senderPublicId: sender_public_1
                      channel: PARTNER_API
                      scope:
                        releaseCount: 1
                        recordingCount: 1
                        assetCount: 2
                        territories: [WORLDWIDE]
                        videoUse:
                          modes: { UNDECIDED: 0, DISABLED: 1, FULL: 0, CLIP: 0 }
                          commercialUseRecordingCount: 0
        '401':
          $ref: '#/components/responses/Unauthorized'
        '403':
          $ref: '#/components/responses/Forbidden'
        '413':
          $ref: '#/components/responses/ManifestTooLarge'
        '428':
          $ref: '#/components/responses/PublicationPreconditionRequired'
        '429':
          $ref: '#/components/responses/RateLimited'
  /:
    post:
      tags: [Delivery]
      operationId: submitMusicDelivery
      summary: Submit a durable delivery message
      description: |
        Requires `deliveries.write`. `NEW_RELEASE`, `METADATA_UPDATE`,
        `ASSET_UPDATE`, `DEAL_UPDATE`, and `TAKEDOWN` use this endpoint and
        append history; updates never overwrite prior deliveries. The pair
        `(authenticated sender, message.id)` is the idempotency key. Replaying
        the same normalized SHA-256 and complete evidence SHA-256 returns the
        existing batch. Different content or publication evidence with the
        same key returns a conflict. Partner API/bulk submission requires a
        declaration bound to the validation hash; the server independently
        resolves the current agreement acceptance, payment regime, declaration,
        and video-use scope in the acceptance transaction.
      requestBody:
        required: true
        content:
          application/json:
            schema:
              $ref: '#/components/schemas/DeliveryManifestV1'
            examples:
              newRelease:
                $ref: '#/components/examples/SingleManifest'
              metadataUpdate:
                value:
                  schemaVersion: kasty.music.delivery.v1
                  message:
                    id: label-2026-0002
                    type: METADATA_UPDATE
                    createdAt: '2026-07-14T13:00:00.000Z'
                  parties: []
                  recordings: []
                  releases: []
                  assets: []
                  deals: []
              takedown:
                value:
                  schemaVersion: kasty.music.delivery.v1
                  message:
                    id: label-2026-0003
                    type: TAKEDOWN
                    createdAt: '2026-07-14T14:00:00.000Z'
                  parties: []
                  recordings: []
                  releases: []
                  assets: []
                  deals: []
      responses:
        '202':
          description: Delivery accepted or idempotently replayed.
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/SubmitResponse'
              example:
                batchPublicId: 4b0588eb-2bfd-4db5-9d20-218bf131940a
                status: ACCEPTED
                replayed: false
        '400':
          $ref: '#/components/responses/ManifestInvalid'
        '401':
          $ref: '#/components/responses/Unauthorized'
        '403':
          $ref: '#/components/responses/Forbidden'
        '409':
          $ref: '#/components/responses/IdempotencyConflict'
        '428':
          $ref: '#/components/responses/PublicationPreconditionRequired'
        '413':
          $ref: '#/components/responses/ManifestTooLarge'
        '429':
          $ref: '#/components/responses/RateLimited'
  /{batchPublicId}:
    parameters:
      - $ref: '#/components/parameters/BatchPublicId'
    get:
      tags: [Delivery]
      operationId: getMusicDeliveryBatch
      summary: Read batch status and counters
      description: Requires `deliveries.read`. Reads are scoped to both sender and catalog account.
      responses:
        '200':
          description: Current durable batch state.
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/Batch'
        '401':
          $ref: '#/components/responses/Unauthorized'
        '403':
          $ref: '#/components/responses/Forbidden'
        '404':
          $ref: '#/components/responses/NotFound'
        '429':
          $ref: '#/components/responses/RateLimited'
  /{batchPublicId}/items:
    parameters:
      - $ref: '#/components/parameters/BatchPublicId'
      - $ref: '#/components/parameters/Cursor'
      - $ref: '#/components/parameters/PageLimit'
    get:
      tags: [Delivery]
      operationId: listMusicDeliveryItems
      summary: List independently progressing delivery items
      description: Requires `deliveries.read`. Cursor order is stable by creation time and opaque ID.
      responses:
        '200':
          description: One item page.
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/ItemPage'
        '400':
          $ref: '#/components/responses/BadCursor'
        '401':
          $ref: '#/components/responses/Unauthorized'
        '403':
          $ref: '#/components/responses/Forbidden'
        '404':
          $ref: '#/components/responses/NotFound'
        '429':
          $ref: '#/components/responses/RateLimited'
  /{batchPublicId}/issues:
    parameters:
      - $ref: '#/components/parameters/BatchPublicId'
      - $ref: '#/components/parameters/Cursor'
      - $ref: '#/components/parameters/PageLimit'
    get:
      tags: [Delivery]
      operationId: listMusicDeliveryIssues
      summary: List stable machine-readable issues
      description: Requires `deliveries.read`. Blocking, retryable, and resolved state are explicit.
      responses:
        '200':
          description: One issue page.
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/IssuePage'
        '400':
          $ref: '#/components/responses/BadCursor'
        '401':
          $ref: '#/components/responses/Unauthorized'
        '403':
          $ref: '#/components/responses/Forbidden'
        '404':
          $ref: '#/components/responses/NotFound'
        '429':
          $ref: '#/components/responses/RateLimited'
  /{batchPublicId}/uploads/intents:
    parameters:
      - $ref: '#/components/parameters/BatchPublicId'
    post:
      tags: [Uploads]
      operationId: createMusicDeliveryUploadIntent
      summary: Create or renew a scoped object upload intent
      description: |
        Requires `assets.upload`. The server derives the object key from the
        authenticated account and batch; arbitrary bucket or object keys are
        never accepted.
      requestBody:
        required: true
        content:
          application/json:
            schema:
              $ref: '#/components/schemas/UploadIntentRequest'
      responses:
        '201':
          description: Upload intent and a short-lived object-scoped write URL.
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/UploadIntent'
        '400':
          $ref: '#/components/responses/ManifestInvalid'
        '401':
          $ref: '#/components/responses/Unauthorized'
        '403':
          $ref: '#/components/responses/Forbidden'
        '404':
          $ref: '#/components/responses/NotFound'
        '428':
          $ref: '#/components/responses/PublicationPreconditionRequired'
        '429':
          $ref: '#/components/responses/RateLimited'
  /{batchPublicId}/uploads/{intentPublicId}/complete:
    parameters:
      - $ref: '#/components/parameters/BatchPublicId'
      - name: intentPublicId
        in: path
        required: true
        schema:
          type: string
          format: uuid
    post:
      tags: [Uploads]
      operationId: completeMusicDeliveryUpload
      summary: Verify and complete an uploaded asset
      description: Requires `assets.upload`; object size, media type, and checksum are verified server-side.
      responses:
        '202':
          description: Verification work accepted.
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/UploadCompletion'
        '400':
          $ref: '#/components/responses/ManifestInvalid'
        '401':
          $ref: '#/components/responses/Unauthorized'
        '403':
          $ref: '#/components/responses/Forbidden'
        '404':
          $ref: '#/components/responses/NotFound'
        '410':
          description: Upload URL expired; renew the scoped intent before retrying.
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/Error'
        '429':
          $ref: '#/components/responses/RateLimited'
  /{batchPublicId}/retry:
    parameters:
      - $ref: '#/components/parameters/BatchPublicId'
    post:
      x-kasty-implementation-status: planned
      tags: [Operations]
      operationId: retryMusicDeliveryBatch
      summary: Retry retryable failed items
      description: Requires `deliveries.write`. Permanent validation failures cannot be retried.
      requestBody:
        required: false
        content:
          application/json:
            schema:
              type: object
              additionalProperties: false
              properties:
                itemPublicIds:
                  type: array
                  maxItems: 500
                  uniqueItems: true
                  items:
                    type: string
                    format: uuid
      responses:
        '202':
          description: Retry jobs accepted.
          content:
            application/json:
              schema:
                type: object
                additionalProperties: false
                required: [batchPublicId, acceptedJobs]
                properties:
                  batchPublicId:
                    type: string
                    format: uuid
                  acceptedJobs:
                    type: integer
                    minimum: 0
        '400':
          $ref: '#/components/responses/ManifestInvalid'
        '401':
          $ref: '#/components/responses/Unauthorized'
        '403':
          $ref: '#/components/responses/Forbidden'
        '404':
          $ref: '#/components/responses/NotFound'
        '429':
          $ref: '#/components/responses/RateLimited'
webhooks:
  deliveryStatus:
    post:
      operationId: receiveMusicDeliveryStatusWebhook
      summary: Signed delivery status event
      description: |
        Kasty signs the exact request bytes using HMAC-SHA256. Consumers must
        check `Kasty-Webhook-Timestamp`, reject stale requests, recompute the
        signature, and deduplicate `eventId` before acknowledging.
      parameters:
        - name: Kasty-Webhook-Id
          in: header
          required: true
          schema:
            type: string
        - name: Kasty-Webhook-Timestamp
          in: header
          required: true
          schema:
            type: integer
            format: int64
        - name: Kasty-Webhook-Signature
          in: header
          required: true
          description: "v1= followed by the lowercase HMAC-SHA256 hex digest."
          schema:
            type: string
            pattern: '^v1=[a-f0-9]{64}$'
      requestBody:
        required: true
        content:
          application/json:
            schema:
              $ref: '#/components/schemas/WebhookEvent'
      responses:
        '204':
          description: Event accepted or already processed.
components:
  securitySchemes:
    musicDeliveryBearer:
      type: http
      scheme: bearer
      bearerFormat: kmd_publicId.secret
      description: High-entropy credential scoped to one sender and catalog account.
  parameters:
    BatchPublicId:
      name: batchPublicId
      in: path
      required: true
      schema:
        type: string
        format: uuid
    Cursor:
      name: cursor
      in: query
      required: false
      description: Opaque cursor returned by the previous page.
      schema:
        type: string
        minLength: 1
    PageLimit:
      name: limit
      in: query
      required: false
      schema:
        type: integer
        minimum: 1
        maximum: 500
        default: 100
  schemas:
    Sha256:
      type: string
      pattern: '^[a-f0-9]{64}$'
    Message:
      type: object
      additionalProperties: false
      required: [id, type, createdAt]
      properties:
        id:
          type: string
          minLength: 1
          maxLength: 200
        type:
          type: string
          enum: [NEW_RELEASE, METADATA_UPDATE, ASSET_UPDATE, DEAL_UPDATE, TAKEDOWN]
        createdAt:
          type: string
          format: date-time
    VideoUse:
      oneOf:
        - type: object
          additionalProperties: false
          required: [mode]
          properties:
            mode:
              const: DISABLED
        - type: object
          additionalProperties: false
          required: [mode]
          properties:
            mode:
              const: FULL
            commercialUseAllowed:
              type: boolean
        - type: object
          additionalProperties: false
          required: [mode, startMs, durationMs]
          properties:
            mode:
              const: CLIP
            startMs:
              type: integer
              minimum: 0
            durationMs:
              type: integer
              minimum: 1
            commercialUseAllowed:
              type: boolean
    RightsDeclarationV1:
      type: object
      additionalProperties: false
      required:
        - version
        - catalogAccountPublicId
        - senderPublicId
        - messageId
        - manifestSha256
        - rights
      properties:
        version:
          const: KASTY_PARTNER_DELIVERY_RIGHTS_V1
        catalogAccountPublicId:
          type: string
          minLength: 1
          maxLength: 200
        senderPublicId:
          type: string
          minLength: 1
          maxLength: 200
        messageId:
          type: string
          minLength: 1
          maxLength: 200
        manifestSha256:
          $ref: '#/components/schemas/Sha256'
        rights:
          type: object
          additionalProperties: false
          required:
            - soundRecordingAndMaster
            - compositionAndLyrics
            - performersProducersAndNeighboringRights
            - artworkAndMetadata
            - distributionTerritories
            - videoUseAuthorized
            - commercialVideoUseAuthorized
          properties:
            soundRecordingAndMaster:
              type: boolean
            compositionAndLyrics:
              type: boolean
            performersProducersAndNeighboringRights:
              type: boolean
            artworkAndMetadata:
              type: boolean
            distributionTerritories:
              type: boolean
            videoUseAuthorized:
              type: boolean
            commercialVideoUseAuthorized:
              type: boolean
    PublicationAttestationRequirement:
      type: object
      additionalProperties: false
      required:
        - required
        - declarationVersion
        - catalogAccountPublicId
        - senderPublicId
        - channel
        - scope
      properties:
        required:
          const: true
        declarationVersion:
          const: KASTY_PARTNER_DELIVERY_RIGHTS_V1
        catalogAccountPublicId:
          type: string
        senderPublicId:
          type: string
        channel:
          type: string
          enum: [PARTNER_API, PARTNER_BULK, DDEX]
        scope:
          type: object
          additionalProperties: false
          required: [releaseCount, recordingCount, assetCount, territories, videoUse]
          properties:
            releaseCount:
              type: integer
              minimum: 0
            recordingCount:
              type: integer
              minimum: 0
            assetCount:
              type: integer
              minimum: 0
            territories:
              type: array
              uniqueItems: true
              items:
                type: string
            videoUse:
              type: object
              additionalProperties: false
              required: [modes, commercialUseRecordingCount]
              properties:
                modes:
                  type: object
                  additionalProperties: false
                  required: [UNDECIDED, DISABLED, FULL, CLIP]
                  properties:
                    UNDECIDED: { type: integer, minimum: 0 }
                    DISABLED: { type: integer, minimum: 0 }
                    FULL: { type: integer, minimum: 0 }
                    CLIP: { type: integer, minimum: 0 }
                commercialUseRecordingCount:
                  type: integer
                  minimum: 0
    Party:
      type: object
      additionalProperties: false
      required: [ref, kind, displayName]
      properties:
        ref:
          type: string
          minLength: 1
        kind:
          type: string
          enum: [PERSON, ORGANIZATION]
        displayName:
          type: string
          minLength: 1
        externalIds:
          type: object
          additionalProperties:
            type: string
    Recording:
      type: object
      additionalProperties: false
      required: [ref, title, artistRefs, identifiers, masterAssetRef]
      properties:
        ref:
          type: string
        title:
          type: string
          minLength: 1
        versionTitle:
          type: [string, 'null']
        artistRefs:
          type: array
          minItems: 1
          uniqueItems: true
          items:
            type: string
        identifiers:
          type: object
          additionalProperties: false
          properties:
            isrc:
              type: [string, 'null']
              pattern: '^[A-Za-z0-9]{12}$'
            senderRecordingId:
              type: [string, 'null']
        masterAssetRef:
          type: string
        explicit:
          type: boolean
          default: false
        videoUse:
          $ref: '#/components/schemas/VideoUse'
    ReleaseTrack:
      type: object
      additionalProperties: false
      required: [recordingRef, discNumber, trackNumber, sequence]
      properties:
        recordingRef:
          type: string
        discNumber:
          type: integer
          minimum: 1
        trackNumber:
          type: integer
          minimum: 1
        sequence:
          type: integer
          minimum: 1
        displayTitle:
          type: [string, 'null']
    Release:
      type: object
      additionalProperties: false
      required: [ref, title, type, primaryArtistRef, tracks, identifiers, dealRefs]
      properties:
        ref:
          type: string
        title:
          type: string
          minLength: 1
        type:
          type: string
          enum: [SINGLE, EP, ALBUM, DELUXE, COMPILATION, OTHER]
        primaryArtistRef:
          type: string
        coverAssetRef:
          type: string
        tracks:
          type: array
          minItems: 1
          items:
            $ref: '#/components/schemas/ReleaseTrack'
        identifiers:
          type: object
          additionalProperties: false
          properties:
            upc:
              type: [string, 'null']
            ean:
              type: [string, 'null']
        labelName:
          type: [string, 'null']
        releaseDate:
          type: [string, 'null']
          format: date
        dealRefs:
          type: array
          uniqueItems: true
          items:
            type: string
        videoUse:
          $ref: '#/components/schemas/VideoUse'
    Asset:
      type: object
      additionalProperties: false
      required: [ref, fileName, contentType, sizeBytes, checksumSha256]
      properties:
        ref:
          type: string
        fileName:
          type: string
          minLength: 1
        contentType:
          type: string
          minLength: 1
        sizeBytes:
          type: integer
          minimum: 1
          maximum: 5368709120
        checksumSha256:
          $ref: '#/components/schemas/Sha256'
    Deal:
      type: object
      additionalProperties: false
      required: [ref, use, territories, startsAt, endsAt, recordingRefs, releaseRefs]
      properties:
        ref:
          type: string
        use:
          type: string
          enum: [LISTENING, KASTY_VIDEO, KASTY_COMMERCIAL_VIDEO]
        territories:
          type: array
          minItems: 1
          uniqueItems: true
          items:
            type: string
            pattern: '^(WORLDWIDE|[A-Za-z]{2})$'
        startsAt:
          type: [string, 'null']
          format: date-time
        endsAt:
          type: [string, 'null']
          format: date-time
        recordingRefs:
          type: array
          uniqueItems: true
          items:
            type: string
        releaseRefs:
          type: array
          uniqueItems: true
          items:
            type: string
    DeliveryManifestV1:
      type: object
      additionalProperties: false
      required: [schemaVersion, message, parties, recordings, releases, assets, deals]
      properties:
        schemaVersion:
          const: kasty.music.delivery.v1
        message:
          $ref: '#/components/schemas/Message'
        defaults:
          type: object
          additionalProperties: false
          properties:
            videoUse:
              $ref: '#/components/schemas/VideoUse'
        rightsDeclaration:
          $ref: '#/components/schemas/RightsDeclarationV1'
        parties:
          type: array
          items:
            $ref: '#/components/schemas/Party'
        recordings:
          type: array
          items:
            $ref: '#/components/schemas/Recording'
        releases:
          type: array
          items:
            $ref: '#/components/schemas/Release'
        assets:
          type: array
          items:
            $ref: '#/components/schemas/Asset'
        deals:
          type: array
          items:
            $ref: '#/components/schemas/Deal'
    Issue:
      type: object
      additionalProperties: false
      required:
        - code
        - severity
        - retryable
        - message
        - suggestedAction
        - entityType
        - externalRef
        - jsonPath
        - fileRef
      properties:
        issuePublicId:
          type: string
          format: uuid
        code:
          type: string
        severity:
          type: string
          enum: [WARNING, ERROR, BLOCKING]
        retryable:
          type: boolean
        message:
          type: string
        suggestedAction:
          type: [string, 'null']
        entityType:
          type: [string, 'null']
        externalRef:
          type: [string, 'null']
        jsonPath:
          type: [string, 'null']
        fileRef:
          type: [string, 'null']
        createdAt:
          type: string
          format: date-time
        resolvedAt:
          type: [string, 'null']
          format: date-time
    ValidationResponse:
      type: object
      additionalProperties: false
      required: [valid, manifestSha256, itemCount, issues, publicationAttestation]
      properties:
        valid:
          type: boolean
        manifestSha256:
          $ref: '#/components/schemas/Sha256'
        itemCount:
          type: integer
          minimum: 0
        issues:
          type: array
          items:
            $ref: '#/components/schemas/Issue'
        publicationAttestation:
          oneOf:
            - $ref: '#/components/schemas/PublicationAttestationRequirement'
            - type: 'null'
    SubmitResponse:
      type: object
      additionalProperties: false
      required: [batchPublicId, status, replayed]
      properties:
        batchPublicId:
          type: string
          format: uuid
        status:
          $ref: '#/components/schemas/BatchStatus'
        replayed:
          type: boolean
    BatchStatus:
      type: string
      enum:
        - RECEIVED
        - VALIDATING
        - ACCEPTED
        - PROCESSING
        - PARTIALLY_SUCCEEDED
        - SUCCEEDED
        - FAILED
        - REJECTED
        - CANCELLED
    PublicationEvidenceSummary:
      type: object
      additionalProperties: false
      required: [evidencePublicId, evidenceSha256, agreement, payment, rights, videoUse, provenance]
      properties:
        evidencePublicId:
          type: string
        evidenceSha256:
          $ref: '#/components/schemas/Sha256'
        agreement:
          type: object
          additionalProperties: false
          required: [programCode, acceptanceEpoch, packagePublicId, acceptanceReceiptPublicId]
          properties:
            programCode:
              const: KASTY_PARTNERS
            acceptanceEpoch:
              type: integer
              minimum: 1
            packagePublicId:
              type: string
            acceptanceReceiptPublicId:
              type: string
        payment:
          type: object
          additionalProperties: false
          required: [regime, acknowledgementPublicId, commercialAddendumPublicId]
          properties:
            regime:
              type: string
              enum: [PUBLIC_TERMS_NO_PARTNER_PAYOUT, COMMERCIAL_ADDENDUM]
            acknowledgementPublicId:
              type: [string, 'null']
            commercialAddendumPublicId:
              type: [string, 'null']
        rights:
          type: object
          additionalProperties: false
          required: [declarationVersion, declarationSha256]
          properties:
            declarationVersion:
              const: KASTY_PARTNER_DELIVERY_RIGHTS_V1
            declarationSha256:
              $ref: '#/components/schemas/Sha256'
        videoUse:
          type: object
          additionalProperties: false
          required: [version, sha256, recordingCount, modes, commercialUseRecordingCount]
          properties:
            version:
              const: KASTY_PARTNER_VIDEO_USE_V1
            sha256:
              $ref: '#/components/schemas/Sha256'
            recordingCount:
              type: integer
              minimum: 0
            modes:
              type: object
              additionalProperties: false
              required: [UNDECIDED, DISABLED, FULL, CLIP]
              properties:
                UNDECIDED: { type: integer, minimum: 0 }
                DISABLED: { type: integer, minimum: 0 }
                FULL: { type: integer, minimum: 0 }
                CLIP: { type: integer, minimum: 0 }
            commercialUseRecordingCount:
              type: integer
              minimum: 0
        provenance:
          type: string
          enum: [PARTNER_CONSOLE, PARTNER_API, PARTNER_BULK, DDEX]
    Batch:
      type: object
      additionalProperties: false
      required:
        - batchPublicId
        - messageId
        - messageType
        - deliveryVersion
        - channel
        - status
        - manifestSha256
        - publicationEvidence
        - counts
        - createdAt
        - updatedAt
        - completedAt
      properties:
        batchPublicId:
          type: string
          format: uuid
        messageId:
          type: string
        messageType:
          type: string
          enum: [NEW_RELEASE, METADATA_UPDATE, ASSET_UPDATE, DEAL_UPDATE, TAKEDOWN]
        deliveryVersion:
          type: integer
          minimum: 1
        channel:
          type: string
          enum: [STUDIO_SELF_SERVICE, PARTNER_API, PARTNER_BULK, DDEX, KASTY_INTERNAL, UNKNOWN]
        status:
          $ref: '#/components/schemas/BatchStatus'
        manifestSha256:
          $ref: '#/components/schemas/Sha256'
        publicationEvidence:
          oneOf:
            - $ref: '#/components/schemas/PublicationEvidenceSummary'
            - type: 'null'
        counts:
          type: object
          additionalProperties: false
          required: [total, accepted, failed]
          properties:
            total:
              type: integer
              minimum: 0
            accepted:
              type: integer
              minimum: 0
            failed:
              type: integer
              minimum: 0
        createdAt:
          type: string
          format: date-time
        updatedAt:
          type: string
          format: date-time
        completedAt:
          type: [string, 'null']
          format: date-time
    Item:
      type: object
      additionalProperties: false
      required: [itemPublicId, entityType, externalRef, status, revision, createdAt, updatedAt]
      properties:
        itemPublicId:
          type: string
          format: uuid
        entityType:
          type: string
          enum: [PARTY, RECORDING, RELEASE, RELEASE_TRACK, ASSET, DEAL]
        externalRef:
          type: string
        status:
          type: string
          enum: [RECEIVED, VALIDATING, ACCEPTED, WAITING_FOR_UPLOAD, PROCESSING, READY, PUBLISHED, FAILED, REJECTED, TAKEN_DOWN]
        revision:
          type: integer
          minimum: 1
        createdAt:
          type: string
          format: date-time
        updatedAt:
          type: string
          format: date-time
    ItemPage:
      type: object
      additionalProperties: false
      required: [data, nextCursor]
      properties:
        data:
          type: array
          items:
            $ref: '#/components/schemas/Item'
        nextCursor:
          type: [string, 'null']
    IssuePage:
      type: object
      additionalProperties: false
      required: [data, nextCursor]
      properties:
        data:
          type: array
          items:
            $ref: '#/components/schemas/Issue'
        nextCursor:
          type: [string, 'null']
    UploadIntentRequest:
      type: object
      additionalProperties: false
      required: [assetRef, filename, contentType, sizeBytes, checksumSha256]
      properties:
        assetRef:
          type: string
        filename:
          type: string
          minLength: 1
        contentType:
          type: string
          minLength: 1
        sizeBytes:
          type: integer
          minimum: 1
          maximum: 5368709120
        checksumSha256:
          $ref: '#/components/schemas/Sha256'
    UploadIntent:
      type: object
      additionalProperties: false
      required: [intentPublicId, provider, uploadUrl, httpMethod, requiredHeaders, expiresAt]
      properties:
        intentPublicId:
          type: string
          format: uuid
        provider:
          type: string
          const: OCI
        uploadUrl:
          type: string
          format: uri
        httpMethod:
          type: string
          const: PUT
        requiredHeaders:
          type: object
          additionalProperties:
            type: string
        expiresAt:
          type: string
          format: date-time
    UploadCompletion:
      type: object
      additionalProperties: false
      required: [intentPublicId, status, jobPublicId, jobType, eTag, replayed]
      properties:
        intentPublicId:
          type: string
          format: uuid
        status:
          type: string
          const: COMPLETED
        jobPublicId:
          type: string
          format: uuid
        jobType:
          type: string
          enum: [MASTER_INGEST, COVER_BUILD]
        eTag:
          type: [string, 'null']
        replayed:
          type: boolean
    Error:
      type: object
      additionalProperties: false
      required: [code, message]
      properties:
        code:
          type: string
        message:
          type: string
        currentEpoch:
          type: [integer, 'null']
          minimum: 1
        actionUrl:
          type: [string, 'null']
          format: uri
        suggestedAction:
          type: [string, 'null']
        issues:
          type: array
          items:
            $ref: '#/components/schemas/Issue'
    WebhookEvent:
      type: object
      additionalProperties: false
      required: [eventId, eventType, createdAt, batchPublicId, status]
      properties:
        eventId:
          type: string
          format: uuid
        eventType:
          type: string
          enum: [delivery.batch.updated, delivery.item.updated, delivery.issue.created]
        createdAt:
          type: string
          format: date-time
        batchPublicId:
          type: string
          format: uuid
        itemPublicId:
          type: string
          format: uuid
        status:
          type: string
  responses:
    Unauthorized:
      description: Credential is missing, expired, revoked, or invalid.
      content:
        application/json:
          schema:
            $ref: '#/components/schemas/Error'
          example:
            code: MUSIC_DELIVERY_AUTH_INVALID
            message: Invalid music delivery credential
    Forbidden:
      description: Credential lacks the operation scope.
      content:
        application/json:
          schema:
            $ref: '#/components/schemas/Error'
          example:
            code: MUSIC_DELIVERY_SCOPE_REQUIRED
            message: Credential does not grant the required scope
    ManifestInvalid:
      description: Manifest or requested operation is invalid.
      content:
        application/json:
          schema:
            $ref: '#/components/schemas/Error'
          example:
            code: MUSIC_DELIVERY_MANIFEST_INVALID
            message: Delivery manifest is invalid
            issues:
              - code: REQUIRED
                severity: BLOCKING
                retryable: false
                message: Required value is missing
                suggestedAction: Correct the manifest and validate again.
                entityType: RELEASE
                externalRef: release:single-42
                jsonPath: $.releases[0].title
                fileRef: null
    BadCursor:
      description: Cursor or requested page size is invalid.
      content:
        application/json:
          schema:
            $ref: '#/components/schemas/Error'
          example:
            code: MUSIC_DELIVERY_CURSOR_INVALID
            message: Cursor or page limit is invalid
    NotFound:
      description: Batch does not exist in the authenticated sender/account scope.
      content:
        application/json:
          schema:
            $ref: '#/components/schemas/Error'
          example:
            code: MUSIC_DELIVERY_BATCH_NOT_FOUND
            message: Delivery batch was not found
    IdempotencyConflict:
      description: |
        Sender/message identity conflicts with prior content/evidence, or the
        authenticated channel is not activated for the requested delivery.
      content:
        application/json:
          schema:
            $ref: '#/components/schemas/Error'
          examples:
            contentChanged:
              value:
                code: MUSIC_DELIVERY_IDEMPOTENCY_PAYLOAD_MISMATCH
                message: This sender and message ID were already used with different content
            evidenceChanged:
              value:
                code: MUSIC_DELIVERY_IDEMPOTENCY_EVIDENCE_MISMATCH
                message: This sender and message ID were already used with different publication evidence
            ddexMappingMissing:
              value:
                code: MUSIC_DELIVERY_DDEX_MAPPING_REQUIRED
                message: DDEX delivery requires an activated sender-specific bilateral mapping
    PublicationPreconditionRequired:
      description: |
        Human Partner agreement/payment state or this delivery's exact rights
        declaration is missing. A machine credential cannot create a human
        acceptance or payment acknowledgement. Follow `actionUrl` where present,
        then validate and submit the unchanged draft again.
      content:
        application/json:
          schema:
            $ref: '#/components/schemas/Error'
          examples:
            paymentAcknowledgement:
              value:
                code: PARTNER_PAYMENT_ACKNOWLEDGEMENT_REQUIRED
                message: Confirm the current Partner payment terms before delivering new releases
                currentEpoch: 3
                actionUrl: https://agreements.kasty.uk/programs/KASTY_PARTNERS/payment-acknowledgement?catalogAccountId=catalog_public_1
            rightsDeclaration:
              value:
                code: DELIVERY_RIGHTS_ATTESTATION_REQUIRED
                message: Confirm that the represented organization controls the rights required for this delivery
    ManifestTooLarge:
      description: Manifest bytes or entity count exceed the credential/channel quota.
      content:
        application/json:
          schema:
            $ref: '#/components/schemas/Error'
          example:
            code: MUSIC_DELIVERY_MANIFEST_LIMIT_EXCEEDED
            message: Delivery manifest exceeds the accepted limit
    RateLimited:
      description: Explicit endpoint bucket is exhausted.
      headers:
        Retry-After:
          description: Minimum seconds before a retry.
          schema:
            type: integer
            minimum: 1
      content:
        application/json:
          schema:
            $ref: '#/components/schemas/Error'
          example:
            code: MUSIC_DELIVERY_RATE_LIMITED
            message: Music delivery request rate exceeded; retry later
  examples:
    SingleManifest:
      summary: One recording delivered as a single
      value:
        schemaVersion: kasty.music.delivery.v1
        message:
          id: label-2026-0001
          type: NEW_RELEASE
          createdAt: '2026-07-14T12:00:00.000Z'
        defaults:
          videoUse:
            mode: DISABLED
        parties:
          - ref: artist:dire-straits
            kind: ORGANIZATION
            displayName: Dire Straits
        recordings:
          - ref: recording:you-and-your-friend
            title: You and Your Friend
            artistRefs: ['artist:dire-straits']
            identifiers:
              isrc: GBAAA9300001
              senderRecordingId: track-42
            masterAssetRef: asset:master-42
        releases:
          - ref: release:single-42
            title: You and Your Friend
            type: SINGLE
            primaryArtistRef: artist:dire-straits
            coverAssetRef: asset:cover-42
            tracks:
              - recordingRef: recording:you-and-your-friend
                discNumber: 1
                trackNumber: 1
                sequence: 1
            identifiers:
              upc: '123456789012'
            dealRefs: ['deal:listening-worldwide']
        assets:
          - ref: asset:master-42
            fileName: You and Your Friend.flac
            contentType: audio/flac
            sizeBytes: 29000000
            checksumSha256: aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa
          - ref: asset:cover-42
            fileName: cover.jpg
            contentType: image/jpeg
            sizeBytes: 2000000
            checksumSha256: bbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbb
        deals:
          - ref: deal:listening-worldwide
            use: LISTENING
            territories: [WORLDWIDE]
            startsAt: null
            endsAt: null
            recordingRefs: ['recording:you-and-your-friend']
            releaseRefs: ['release:single-42']
