Launch Sale: Creator and Business plans are discounted for a limited time. View pricing

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 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, and for collection create/update/delete events see the Collection Sync webhook.

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

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.

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.
})