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

# FUGA distribution platform webhook

> Receives product delivery and status update events from the FUGA distribution platform.

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

## Description

This endpoint is called by FUGA, not by API users — configure it as the webhook URL in
your FUGA account as `https://api.royalti.io/webhook/fuga/{your-tenant-id}`.

**Authentication:** Requires `x-fuga-signature` header containing SHA3-256 hash (not HMAC).

**Important:** FUGA uses simple hash (SHA3-256) of `payload + salt`, NOT HMAC.

**Security Features:**

1. SHA3-256 signature verification with tenant-specific salt
2. Timestamp validation (rejects webhooks older than 5 minutes)
3. Duplicate message prevention using webhook ID + Redis caching (24-hour TTL)
4. Replay attack protection

**Workflow:**

1. Verifies SHA3-256 signature using tenant-specific salt
2. Validates webhook timestamp (must be within 5 minutes)
3. Checks for duplicate webhook ID in Redis cache
4. Finds DDEX delivery record by FUGA reference (or by product UPC for delivery events)
5. Updates delivery status based on event type
6. Updates product metadata with FUGA status
7. Sends provider-agnostic notifications for status changes

**Supported Event Types:**

* `product_state` - Product acceptance/rejection/processing status (also handles the
  legacy `product.accepted`, `product.rejected`, `product.processing`, `product.delivered`,
  `product.failed` values)
* `product_change` - Metadata updates in FUGA (informational only, no action taken)
* `delivery_completed` - Product delivered to a DSP
* `delivery_cancelled` - Delivery cancelled by FUGA
* `delivery_rejected` - Delivery rejected by a DSP
* `xml_ingestion` - Metadata/XML ingestion result
* `delivery.status` / `delivery.acknowledgment` - Legacy acknowledgment events, queued
  for async processing

Unrecognized `type` values are acknowledged with `200` and logged, not rejected.

## Code Examples

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

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

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

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

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

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

  ```
</CodeGroup>


## OpenAPI

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

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

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

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

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

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

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

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

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

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

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

      contract). Every aggregation returns the same uniform envelope:

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

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

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

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

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

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

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

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

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

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

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

      All endpoints require an active `publisher` addon on the tenant
      (`checkAddon('publisher')`) in addition to the stated role. Reads
      require the `user` role or higher; writes (create/update/delete and
      work associations) require `admin` or higher, enforced both by the
      global RBAC route table (`cwr.permissions.ts`) and, for this router,
      duplicated at the route level via `requireUser`/`requireAdmin`.
paths:
  /webhook/fuga/{tenantId}:
    post:
      tags:
        - Distribution Webhooks
      summary: FUGA distribution platform webhook
      description: >-
        Receives product delivery and status update events from the FUGA
        distribution platform.

        This endpoint is called by FUGA, not by API users — configure it as the
        webhook URL in

        your FUGA account as
        `https://api.royalti.io/webhook/fuga/{your-tenant-id}`.


        **Authentication:** Requires `x-fuga-signature` header containing
        SHA3-256 hash (not HMAC).


        **Important:** FUGA uses simple hash (SHA3-256) of `payload + salt`, NOT
        HMAC.


        **Security Features:**

        1. SHA3-256 signature verification with tenant-specific salt

        2. Timestamp validation (rejects webhooks older than 5 minutes)

        3. Duplicate message prevention using webhook ID + Redis caching
        (24-hour TTL)

        4. Replay attack protection


        **Workflow:**

        1. Verifies SHA3-256 signature using tenant-specific salt

        2. Validates webhook timestamp (must be within 5 minutes)

        3. Checks for duplicate webhook ID in Redis cache

        4. Finds DDEX delivery record by FUGA reference (or by product UPC for
        delivery events)

        5. Updates delivery status based on event type

        6. Updates product metadata with FUGA status

        7. Sends provider-agnostic notifications for status changes


        **Supported Event Types:**

        - `product_state` - Product acceptance/rejection/processing status (also
        handles the
          legacy `product.accepted`, `product.rejected`, `product.processing`, `product.delivered`,
          `product.failed` values)
        - `product_change` - Metadata updates in FUGA (informational only, no
        action taken)

        - `delivery_completed` - Product delivered to a DSP

        - `delivery_cancelled` - Delivery cancelled by FUGA

        - `delivery_rejected` - Delivery rejected by a DSP

        - `xml_ingestion` - Metadata/XML ingestion result

        - `delivery.status` / `delivery.acknowledgment` - Legacy acknowledgment
        events, queued
          for async processing

        Unrecognized `type` values are acknowledged with `200` and logged, not
        rejected.
      parameters:
        - name: tenantId
          in: path
          required: true
          schema:
            type: integer
          description: Tenant ID for webhook routing
          example: 123
        - name: x-fuga-webhook-id
          in: header
          required: false
          schema:
            type: string
          description: >-
            Unique webhook ID used for duplicate-delivery detection (not
            required, but recommended)
          example: webhook-uuid-123
      requestBody:
        required: true
        content:
          application/json:
            schema:
              $ref: '#/components/schemas/FugaWebhookPayload'
            examples:
              productDelivered:
                summary: Product Delivered Event (legacy type)
                value:
                  type: product.delivered
                  timestamp: '2025-01-22T14:30:00.000Z'
                  organization_id: 12345
                  data:
                    product_id: 789012
                    delivery_id: del_abc123
                    reference: ref_xyz789
                    status: delivered
                    product:
                      id: 789012
                      upc: '012345678905'
                    dsp:
                      id: 42
                      name: Spotify
              deliveryCompleted:
                summary: Delivery Completed Event
                value:
                  type: delivery_completed
                  timestamp: '2024-11-13T15:00:00Z'
                  organization_id: 12345
                  data:
                    product:
                      id: 789
                      upc: '012345678901'
                    dsp:
                      id: 456
                      name: Spotify
                    action: INSERT
              deliveryRejected:
                summary: Delivery Rejected Event
                value:
                  type: delivery_rejected
                  timestamp: '2024-11-13T15:00:00Z'
                  organization_id: 12345
                  data:
                    product:
                      id: 789
                      upc: '012345678901'
                    dsp:
                      id: 456
                      name: Apple Music
                    status: rejected
                    reason: Invalid metadata
              xmlIngestionCompleted:
                summary: XML Ingestion Completed
                value:
                  type: xml_ingestion
                  timestamp: '2025-01-22T14:30:00.000Z'
                  organization_id: 12345
                  data:
                    id: 555666
                    queued_at: '2025-01-22T14:25:00.000Z'
                    processed_at: '2025-01-22T14:30:00.000Z'
                    summary: Successfully processed 1 product, 3 tracks
      responses:
        '200':
          description: >
            Webhook processed. FUGA webhooks always return 200 (even for
            internal processing

            errors and unmatched deliveries) so the platform does not retry —
            check `status`

            in the response body to distinguish outcomes.
          content:
            application/json:
              schema:
                type: object
                properties:
                  status:
                    type: string
                    example: success
                  message:
                    type: string
                    example: Webhook processed
                  deliveryId:
                    type: string
                    description: Present when a matching DDEX delivery record was updated
                    example: delivery-uuid-456
                  jobId:
                    type: string
                    description: >-
                      Present for delivery.status/delivery.acknowledgment
                      events, which are queued for async processing
              examples:
                success:
                  value:
                    status: success
                    message: Webhook processed
                    deliveryId: delivery-uuid-456
                alreadyProcessed:
                  value:
                    status: success
                    message: Webhook already processed
                deliveryNotFound:
                  value:
                    status: acknowledged
                    message: Delivery not found
                internalProcessingError:
                  value:
                    status: error
                    message: Internal processing error
        '401':
          description: >-
            Unauthorized - Missing/invalid signature, expired timestamp, or no
            webhook salt configured for the tenant
          content:
            application/json:
              schema:
                type: object
                properties:
                  status:
                    type: string
                    example: error
                  message:
                    type: string
                    example: Invalid webhook signature
              examples:
                missingSignature:
                  value:
                    status: error
                    message: Missing FUGA webhook signature
                invalidSignature:
                  value:
                    status: error
                    message: Invalid webhook signature
                expiredTimestamp:
                  value:
                    status: error
                    message: Webhook timestamp expired
                missingSalt:
                  value:
                    status: error
                    message: FUGA webhook salt not configured
      security:
        - FugaWebhookAuth: []
components:
  schemas:
    FugaWebhookPayload:
      type: object
      required:
        - type
        - timestamp
        - organization_id
        - data
      properties:
        type:
          type: string
          enum:
            - product_state
            - product_change
            - delivery_completed
            - delivery_cancelled
            - delivery_rejected
            - xml_ingestion
            - product.accepted
            - product.rejected
            - product.processing
            - product.delivered
            - product.failed
            - delivery.status
            - delivery.acknowledgment
          description: >
            FUGA event type. `product_state`, `product_change`,
            `delivery_completed`,

            `delivery_cancelled`, `delivery_rejected`, and `xml_ingestion` are
            the current

            event types; the `product.*` and `delivery.*` dotted values are
            legacy types

            kept for backward compatibility.
          example: product_state
        timestamp:
          type: string
          format: date-time
          description: >-
            Event timestamp (ISO 8601). Webhooks older than 5 minutes are
            rejected.
          example: '2024-11-13T14:32:00Z'
        organization_id:
          type: integer
          description: FUGA organization ID
          example: 12345
        data:
          type: object
          description: Event-specific data payload
          properties:
            product_id:
              type: integer
              description: FUGA product ID
            delivery_id:
              type: string
              description: >-
                Delivery reference ID (present on delivery.status /
                delivery.acknowledgment events)
            reference:
              type: string
              description: FUGA reference string linking back to the DDEX delivery record
            status:
              type: string
              description: Status value
            reason:
              type: string
              description: Rejection/cancellation reason
            errors:
              type: array
              items:
                type: string
              description: Array of error messages
            triggers:
              type: array
              items:
                type: object
                properties:
                  type:
                    type: string
                  entity_id:
                    type: integer
                  entity_class:
                    type: string
                  user:
                    type: string
                  field_names:
                    type: array
                    items:
                      type: string
            action:
              type: string
              enum:
                - INSERT
                - UPDATE
                - TAKEDOWN
                - METADATA_ONLY
              description: Delivery action type
            product:
              type: object
              properties:
                id:
                  type: integer
                upc:
                  type: string
            dsp:
              type: object
              properties:
                id:
                  type: integer
                name:
                  type: string
            id:
              type: integer
              description: XML ingestion ID
            queued_at:
              type: string
              format: date-time
            processed_at:
              type: string
              format: date-time
            summary:
              type: string
              description: Processing summary (xml_ingestion events)
        signature:
          type: string
          description: >-
            SHA3-256 signature (not used — the signature is sent in the
            x-fuga-signature header, not the body)
  securitySchemes:
    bearerAuth:
      type: http
      scheme: bearer
      bearerFormat: JWT
      description: >-
        JWT Authorization header using the Bearer scheme. Format: "Bearer
        {token}"

````