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
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, orinit_error. - Files complete —
track.files_completed_atis stamped once every variant (including optional and after-the-fact ones) has been attempted. This can arrive after an earlierready, 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
POSTwithContent-Type: application/json. - Includes an
X-ADN-Event: track.status_changedheader. - Delivery is fire-and-forget / at-least-once — design your handler to be idempotent.
- Event ordering is not guaranteed; use the
status,previous_status, andtrack.files_completed_atfields rather than assuming arrival order. - Requests are currently unsigned. Treat the webhook URL as a secret and, if needed, verify the
track_idagainst the API. - Respond promptly with a
2xxstatus 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
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.
})