# Track Processing Webhook | AudioDN Docs

> The Track Processing webhook notifies your application when a track reaches a final outcome — ready, incomplete, or error — and when its full set of files is complete.

Source: https://audiodeliverynetwork.com/docs/webhooks/track-processing/

---

# Track Processing Webhook

The Track Processing webhook lets your application react to a track's outcome without polling. AudioDN sends an HTTP `POST` request to the webhook URL configured for your organization when a track reaches a terminal status or when its full file set is complete. For per-file progress as each variant is produced, use the [Track File webhook](/docs/webhooks/track-files) instead.

#### Configured in the dashboard

The webhook URL is set in your **Settings → Webhook** panel in the dashboard, not via the API. Leave it blank to disable webhooks. For per-file events see the [Track File webhook](/docs/webhooks/track-files), and for collection create/update/delete events see the [Collection Sync webhook](/docs/webhooks/collection-sync).

## When it fires

To avoid noise, this webhook does **not** relay transitional statuses (`processing`, `fallback`, `fallback_processing`) or the initial `initialized` state. It is delivered only on events of note:

-   **Terminal status** — the track reaches `ready`, `incomplete`, `error`, or `init_error`.
-   **Files complete** — `track.files_completed_at` is stamped once every variant (including optional and after-the-fact ones) has been attempted. This can arrive after an earlier `ready`, so the same track may deliver twice: once when it becomes playable, and once when the full file set is done.

A typical track delivers **once** (status settles to `ready` and files complete in the same step) or **twice** (early `ready` from required variants, then a later files-complete event when optional variants finish).

-   Sent as an HTTP `POST` with `Content-Type: application/json`.
-   Includes an `X-ADN-Event: track.status_changed` header.
-   Delivery is fire-and-forget / at-least-once — design your handler to be idempotent.
-   Event ordering is not guaranteed; use the `status`, `previous_status`, and `track.files_completed_at` fields rather than assuming arrival order.
-   Requests are currently unsigned. Treat the webhook URL as a secret and, if needed, verify the `track_id` against the API.
-   Respond promptly with a `2xx` status code to acknowledge receipt.

## Track statuses

The `status` field reflects the track's `track_status_id` at the time of the event. The possible values are:

| Status | Terminal | Meaning |
| --- | --- | --- |
| initialized | No | The track record was created and is waiting for its file to finish uploading. This is the first event you receive. |
| processing | No | The upload completed and AudioDN is transcoding, generating waveforms, extracting metadata, and building variants. |
| fallback | No | The primary processor could not handle the file, so it has been queued for the fallback processor. |
| fallback\_processing | No | The fallback processor is actively working on the track. |
| ready | Yes | Processing finished successfully. All track files are available and the track is ready for playback. |
| incomplete | Yes | Processing finished with a playable track, but one or more non-essential variants failed. The track is usable. |
| error | Yes | Processing failed and no playable track could be produced. |
| init\_error | Yes | The track could not be initialized (for example, the upload was invalid or unreadable). No processing occurred. |

**Terminal** statuses (`ready`, `incomplete`, `error`, `init_error`) are end states — a track will not change status again after reaching one. The remaining statuses are transitional.

## Payload schema

Every event delivers the same envelope. Because deliveries are limited to terminal outcomes and the files-complete event, the `files` array is normally populated. `track.files_completed_at` is `null` on an early `ready` (optional variants still running) and a timestamp once the full set is done. Each file includes `is_success` and a nested `variant` (recipe) object; a failed variant appears with `is_success: false`, no `url`, and the reason in `status_text`.

```
{
  "event": "track.status_changed",
  "status": "ready | incomplete | error | init_error",
  "previous_status": "string | null",
  "organization_id": "uuid",
  "creator_id": "uuid | null",
  "collection_id": "uuid",
  "track_id": "uuid",
  "upload_session_id": "uuid | null",
  "track": {
    "id": "uuid",
    "track_status_id": "string",
    "upload_session_id": "uuid | null",
    "index": "string",
    "organization_index": "string | null",
    "file_name": "string | null",
    "file_name_original": "string | null",
    "player_title": "string | null",
    "player_subtitle": "string | null",
    "player_color": "string | null",
    "player_color_light": "string | null",
    "player_color_dark": "string | null",
    "order": "number",
    "format": "object | null",
    "metadata": "object | null",
    "duration": "number | null",
    "size": "number | null",
    "files_completed_at": "string | null",
    "cloudflare_image_id": "uuid | null",
    "cover_image": {
      "icon": { "type": "icon", "width": 80, "height": 80, "url": "string" },
      "small": { "type": "small", "width": 200, "height": 200, "url": "string" },
      "regular": { "type": "regular", "width": 400, "height": 400, "url": "string" },
      "large": { "type": "large", "width": 800, "height": 800, "url": "string" }
    },
    "theme": "array | null"
  },
  "files": [
    {
      "id": "uuid",
      "path": "string",
      "url": "string | null",
      "size": "number | null",
      "content_type": "string | null",
      "file_name": "string",
      "is_public": "boolean",
      "is_success": "boolean",
      "status_text": "string | null",
      "data": "object | null",
      "props": "object | null",
      "variant": {
        "id": "uuid",
        "index": "string",
        "variant_type": {
          "id": "string",
          "viewer_id": "string",
          "title": "string"
        }
      }
    }
  ]
}
```

## Cover images

When a track has cover art, `track.cloudflare_image_id` holds the image ID and `track.cover_image` provides ready-to-use URLs at each available size. Both are `null` until a cover is available (covers are extracted during processing, so early events such as `initialized` may not include one yet).

If you only have the `cloudflare_image_id` (for example, stored from a previous event), you can construct any size yourself. Each URL follows the pattern below, where `<variant>` is one of the named sizes:

```
https://imagedelivery.net/iVHDyjXAr_lt5UUJaLbk1Q/{cloudflare_image_id}/{variant}
```

| Variant | Dimensions | Example URL |
| --- | --- | --- |
| icon | 80 x 80 | .../{cloudflare\_image\_id}/icon |
| small | 200 x 200 | .../{cloudflare\_image\_id}/small |
| regular | 400 x 400 | .../{cloudflare\_image\_id}/regular |
| large | 800 x 800 | .../{cloudflare\_image\_id}/large |

The same scheme applies to collection cover art delivered by the [Collection Sync webhook](/docs/webhooks/collection-sync).

When a cover is processed, ADN also extracts a color palette from it, delivered as `track.theme` — an array of color objects with `hex`, `area`, `lightness`, and `saturation` values (`null` until a cover has been processed). This is the same `theme` field returned by the [Tracks API](/docs/api/tracks).

## Example payloads

### Early ready (required variants done)

Sent as soon as the required variants succeed and the track is playable, while optional variants are still processing. `files_completed_at` is `null` because the full file set is not done yet. A track with no optional variants skips straight to the settled event below and never sends this.

```
{
  "event": "track.status_changed",
  "status": "ready",
  "previous_status": "processing",
  "organization_id": "11111111-1111-1111-1111-111111111111",
  "creator_id": null,
  "collection_id": "22222222-2222-2222-2222-222222222222",
  "track_id": "33333333-3333-3333-3333-333333333333",
  "upload_session_id": "44444444-4444-4444-4444-444444444444",
  "track": {
    "id": "33333333-3333-3333-3333-333333333333",
    "track_status_id": "ready",
    "upload_session_id": "44444444-4444-4444-4444-444444444444",
    "index": "track-index-001",
    "organization_index": "your-track-id-001",
    "file_name": "interview.mp3",
    "file_name_original": "Interview Final.mp3",
    "player_title": "Episode 12",
    "player_subtitle": "The Interview",
    "player_color": "#1DB954",
    "player_color_light": "#1BAA4D",
    "player_color_dark": "#1DB954",
    "order": 0,
    "format": { "codec": "aac", "channels": 2, "sample_rate": 44100 },
    "metadata": null,
    "duration": 1832.5,
    "size": 18234123,
    "files_completed_at": null
  },
  "files": [ { "..." : "playable transcode file(s) so far" } ]
}
```

### Ready and files complete (settled)

Sent once every variant (required and optional) has been attempted. `files_completed_at` is stamped and the full file set is present. For a track with no optional variants this is the single delivery you receive.

```
{
  "event": "track.status_changed",
  "status": "ready",
  "previous_status": "processing",
  "organization_id": "11111111-1111-1111-1111-111111111111",
  "creator_id": null,
  "collection_id": "22222222-2222-2222-2222-222222222222",
  "track_id": "33333333-3333-3333-3333-333333333333",
  "upload_session_id": "44444444-4444-4444-4444-444444444444",
  "track": {
    "id": "33333333-3333-3333-3333-333333333333",
    "track_status_id": "ready",
    "upload_session_id": "44444444-4444-4444-4444-444444444444",
    "index": "track-index-001",
    "organization_index": "your-track-id-001",
    "file_name": "interview.mp3",
    "file_name_original": "Interview Final.mp3",
    "player_title": "Episode 12",
    "player_subtitle": "The Interview",
    "player_color": "#1DB954",
    "player_color_light": "#1BAA4D",
    "player_color_dark": "#1DB954",
    "order": 0,
    "format": { "codec": "aac", "channels": 2, "sample_rate": 44100 },
    "metadata": { "artist": "Acme Media" },
    "duration": 1832.5,
    "size": 18234123,
    "files_completed_at": "2026-01-01T12:00:05.000Z",
    "cloudflare_image_id": "77777777-7777-7777-7777-777777777777",
    "cover_image": {
      "icon": { "type": "icon", "width": 80, "height": 80, "url": "https://imagedelivery.net/iVHDyjXAr_lt5UUJaLbk1Q/77777777-7777-7777-7777-777777777777/icon" },
      "small": { "type": "small", "width": 200, "height": 200, "url": "https://imagedelivery.net/iVHDyjXAr_lt5UUJaLbk1Q/77777777-7777-7777-7777-777777777777/small" },
      "regular": { "type": "regular", "width": 400, "height": 400, "url": "https://imagedelivery.net/iVHDyjXAr_lt5UUJaLbk1Q/77777777-7777-7777-7777-777777777777/regular" },
      "large": { "type": "large", "width": 800, "height": 800, "url": "https://imagedelivery.net/iVHDyjXAr_lt5UUJaLbk1Q/77777777-7777-7777-7777-777777777777/large" }
    },
    "theme": [
      { "hex": "#1DB954", "area": 0.42, "lightness": 0.45, "saturation": 0.73 },
      { "hex": "#0E1320", "area": 0.31, "lightness": 0.08, "saturation": 0.35 }
    ]
  },
  "files": [
    {
      "id": "55555555-5555-5555-5555-555555555555",
      "path": "org/collection/track/transcode.aac",
      "url": "https://cdn.audiodn.com/org/collection/track/transcode.aac",
      "size": 17012344,
      "content_type": "audio/aac",
      "file_name": "transcode.aac",
      "is_public": true,
      "is_success": true,
      "status_text": "OK",
      "data": null,
      "props": null,
      "variant": {
        "id": "66666666-6666-6666-6666-666666666666",
        "index": "transcode-default",
        "variant_type": {
          "id": "transcode",
          "viewer_id": "audio",
          "title": "Transcoded Audio"
        }
      }
    }
  ]
}
```

### Incomplete (partial success)

Sent when a playable track was produced but a non-essential variant failed. The track is still usable.

```
{
  "event": "track.status_changed",
  "status": "incomplete",
  "previous_status": "processing",
  "organization_id": "11111111-1111-1111-1111-111111111111",
  "creator_id": null,
  "collection_id": "22222222-2222-2222-2222-222222222222",
  "track_id": "33333333-3333-3333-3333-333333333333",
  "upload_session_id": "44444444-4444-4444-4444-444444444444",
  "track": {
    "id": "33333333-3333-3333-3333-333333333333",
    "track_status_id": "incomplete",
    "upload_session_id": "44444444-4444-4444-4444-444444444444",
    "index": "track-index-001",
    "organization_index": "your-track-id-001",
    "file_name": "interview.mp3",
    "file_name_original": "Interview Final.mp3",
    "player_title": "Episode 12",
    "player_subtitle": "The Interview",
    "player_color": "#1DB954",
    "player_color_light": "#1BAA4D",
    "player_color_dark": "#1DB954",
    "order": 0,
    "format": { "codec": "aac", "channels": 2, "sample_rate": 44100 },
    "metadata": null,
    "duration": 1832.5,
    "size": 18234123,
    "files_completed_at": "2026-01-01T12:00:05.000Z"
  },
  "files": [
    {
      "id": "55555555-5555-5555-5555-555555555555",
      "path": "org/collection/track/transcode.aac",
      "url": "https://cdn.audiodn.com/org/collection/track/transcode.aac",
      "size": 17012344,
      "content_type": "audio/aac",
      "file_name": "transcode.aac",
      "is_public": true,
      "is_success": true,
      "status_text": "OK",
      "data": null,
      "props": null,
      "variant": {
        "id": "66666666-6666-6666-6666-666666666666",
        "index": "transcode-default",
        "variant_type": {
          "id": "transcode",
          "viewer_id": "audio",
          "title": "Transcoded Audio"
        }
      }
    },
    {
      "id": "99999999-9999-9999-9999-999999999999",
      "path": "org/collection/track/lossless.flac",
      "url": null,
      "size": null,
      "content_type": "audio/flac",
      "file_name": "lossless.flac",
      "is_public": false,
      "is_success": false,
      "status_text": "Source is lossy; cannot produce a lossless output",
      "data": null,
      "props": null,
      "variant": {
        "id": "88888888-8888-8888-8888-888888888888",
        "index": "lossless-download",
        "variant_type": {
          "id": "transcode",
          "viewer_id": "audio",
          "title": "Transcoded Audio"
        }
      }
    }
  ]
}
```

### Error (failure)

Sent when processing fails and no playable track could be produced. The `files` array is empty.

```
{
  "event": "track.status_changed",
  "status": "error",
  "previous_status": "processing",
  "organization_id": "11111111-1111-1111-1111-111111111111",
  "creator_id": null,
  "collection_id": "22222222-2222-2222-2222-222222222222",
  "track_id": "33333333-3333-3333-3333-333333333333",
  "upload_session_id": "44444444-4444-4444-4444-444444444444",
  "track": {
    "id": "33333333-3333-3333-3333-333333333333",
    "track_status_id": "error",
    "upload_session_id": "44444444-4444-4444-4444-444444444444",
    "index": "track-index-001",
    "organization_index": "your-track-id-001",
    "file_name": "interview.mp3",
    "file_name_original": "Interview Final.mp3",
    "player_title": "Episode 12",
    "player_subtitle": "The Interview",
    "player_color": "#1DB954",
    "player_color_light": "#1BAA4D",
    "player_color_dark": "#1DB954",
    "order": 0,
    "format": null,
    "metadata": null,
    "duration": null,
    "size": 18234123,
    "files_completed_at": "2026-01-01T12:00:05.000Z"
  },
  "files": []
}
```

#### Verifying delivery

Each delivery attempt is logged on AudioDN's side, including the HTTP status code returned by your endpoint. If you stop receiving events, confirm your endpoint returns a `2xx` response quickly and that the webhook URL in Settings is correct.

## Handling webhooks

A minimal handler reads the `status` field and reacts to terminal states. Because delivery is at-least-once and unordered, key your logic on `track_id` + `status` rather than on receipt order.

```
app.post('/webhooks/audiodn', (req, res) => {
  // Acknowledge quickly, then process asynchronously.
  res.sendStatus(200)

  const { event, status, track_id } = req.body
  if (event !== 'track.status_changed') return

  switch (status) {
    case 'ready':
      // Track is playable. track.files_completed_at === null means optional
      // variants are still running (a second delivery will follow once done).
      markTrackReady(track_id, req.body.files)
      break
    case 'incomplete':
      // Playable, but some variants failed. Inspect files[].is_success.
      markTrackReady(track_id, req.body.files)
      flagForReview(track_id)
      break
    case 'error':
    case 'init_error':
      // Processing failed; no playable track was produced.
      markTrackFailed(track_id)
      break
  }
  // Note: transitional statuses (processing / fallback*) are not delivered.
  // For per-file progress, use the Track File webhook instead.
})
```
