# Webhook Track Processing | Docs de AudioDN

> El webhook Track Processing notifica a tu aplicación cuando una pista alcanza un resultado final — ready, incomplete o error — y cuando su conjunto completo de archivos está listo.

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

---

# Webhook Track Processing

El webhook Track Processing permite que tu aplicación reaccione al resultado de una pista sin hacer polling. AudioDN envía una solicitud HTTP `POST` a la URL de webhook configurada para tu organización cuando una pista alcanza un estado terminal o cuando su conjunto completo de archivos está listo. Para el progreso por archivo a medida que se produce cada variante, usa el [webhook Track File](/docs/webhooks/track-files) en su lugar.

#### Se configura en el panel

La URL del webhook se establece en el panel **Settings → Webhook** del dashboard, no vía la API. Déjala en blanco para desactivar los webhooks. Para eventos por archivo consulta el [webhook Track File](/docs/webhooks/track-files), y para eventos de create/update/delete de colección consulta el [webhook Collection Sync](/docs/webhooks/collection-sync).

## Cuándo se dispara

Para evitar ruido, este webhook **no** retransmite estados de transición (`processing`, `fallback`, `fallback_processing`) ni el estado inicial `initialized`. Solo se entrega en eventos relevantes:

-   **Estado terminal** — la pista alcanza `ready`, `incomplete`, `error` o `init_error`.
-   **Archivos completos** — se sella `track.files_completed_at` cuando se ha intentado cada variante (incluyendo las opcionales y las posteriores). Puede llegar después de un `ready` anterior, así que la misma pista puede entregarse dos veces: una cuando se vuelve reproducible y otra cuando el conjunto completo de archivos está listo.

Una pista típica se entrega **una vez** (el estado se asienta en `ready` y los archivos se completan en el mismo paso) o **dos veces** (`ready` temprano por las variantes requeridas, y luego un evento de archivos completos cuando terminan las variantes opcionales).

-   Se envía como un HTTP `POST` con `Content-Type: application/json`.
-   Incluye una cabecera `X-ADN-Event: track.status_changed`.
-   La entrega es fire-and-forget / al menos una vez — diseña tu manejador para que sea idempotente.
-   No se garantiza el orden de eventos; usa los campos `status`, `previous_status` y `track.files_completed_at` en lugar de asumir el orden de llegada.
-   Las solicitudes actualmente no están firmadas. Trata la URL del webhook como un secreto y, si hace falta, verifica el `track_id` contra la API.
-   Responde con prontitud con un código de estado `2xx` para acusar recibo.

## Estados de pista

El campo `status` refleja el `track_status_id` de la pista en el momento del evento. Los valores posibles son:

| Estado | Terminal | Significado |
| --- | --- | --- |
| initialized | No | Se creó el registro de la pista y está esperando a que termine de subirse su archivo. Este es el primer evento que recibes. |
| processing | No | La subida se completó y AudioDN está transcodificando, generando formas de onda, extrayendo metadatos y construyendo variantes. |
| fallback | No | El procesador principal no pudo manejar el archivo, así que se ha encolado para el procesador de respaldo. |
| fallback\_processing | No | El procesador de respaldo está trabajando activamente en la pista. |
| ready | Sí | El procesamiento terminó con éxito. Todos los track files están disponibles y la pista está lista para reproducción. |
| incomplete | Sí | El procesamiento terminó con una pista reproducible, pero una o más variantes no esenciales fallaron. La pista es usable. |
| error | Sí | El procesamiento falló y no se pudo producir una pista reproducible. |
| init\_error | Sí | No se pudo inicializar la pista (por ejemplo, la subida no era válida o no se podía leer). No hubo procesamiento. |

Los estados **terminales** (`ready`, `incomplete`, `error`, `init_error`) son estados finales — una pista no volverá a cambiar de estado después de alcanzar uno. Los demás estados son de transición.

## Esquema del payload

Cada evento entrega el mismo sobre. Como las entregas se limitan a resultados terminales y al evento de archivos completos, el array `files` normalmente está poblado. `track.files_completed_at` es `null` en un `ready` temprano (variantes opcionales aún en curso) y una marca de tiempo cuando el conjunto completo está listo. Cada archivo incluye `is_success` y un objeto anidado `variant` (receta); una variante fallida aparece con `is_success: false`, sin `url` y el motivo en `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"
      }
    }
  }
]
}
```

## Imágenes de portada

Cuando una pista tiene arte de portada, `track.cover_image` proporciona URLs listas para usar en cada tamaño disponible, y `track.cloudflare_image_id` almacena el ID de imagen subyacente si necesitas reconstruir una URL más tarde. Ambos son `null` hasta que hay una portada disponible (las portadas se extraen durante el procesamiento, así que eventos tempranos como `initialized` pueden no incluir una aún).

Prefiere las URLs `cover_image` ya preparadas. Si solo tienes el ID de imagen de un evento anterior, puedes construir cualquier tamaño tú mismo. Cada URL sigue el patrón de abajo, donde `<variant>` es uno de los tamaños con nombre:

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

| Variante | Dimensiones | URL de ejemplo |
| --- | --- | --- |
| 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 |

El mismo esquema se aplica al arte de portada de colección entregado por el [webhook Collection Sync](/docs/webhooks/collection-sync).

Cuando se procesa una portada, ADN también extrae una paleta de colores de ella, entregada como `track.theme` — un array de objetos de color con valores `hex`, `area`, `lightness` y `saturation` (`null` hasta que se haya procesado una portada). Es el mismo campo `theme` que devuelve la [Tracks API](/docs/api/tracks).

## Ejemplos de payload

### Ready temprano (variantes requeridas listas)

Se envía en cuanto las variantes requeridas tienen éxito y la pista es reproducible, mientras las variantes opcionales siguen procesándose. `files_completed_at` es `null` porque el conjunto completo de archivos aún no está listo. Una pista sin variantes opcionales pasa directamente al evento asentado de abajo y nunca envía este.

```
{
"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 y archivos completos (asentado)

Se envía cuando se ha intentado cada variante (requerida y opcional). Se sella `files_completed_at` y el conjunto completo de archivos está presente. Para una pista sin variantes opcionales, esta es la única entrega que recibes.

```
{
"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 (éxito parcial)

Se envía cuando se produjo una pista reproducible pero falló una variante no esencial. La pista sigue siendo 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 (fallo)

Se envía cuando el procesamiento falla y no se pudo producir una pista reproducible. El array `files` está vacío.

```
{
"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": []
}
```

#### Verificar la entrega

Cada intento de entrega se registra en el lado de AudioDN, incluyendo el código de estado HTTP devuelto por tu endpoint. Si dejas de recibir eventos, confirma que tu endpoint responde con `2xx` rápidamente y que la URL del webhook en Settings es correcta.

## Manejar webhooks

Un manejador mínimo lee el campo `status` y reacciona a los estados terminales. Como la entrega es al menos una vez y sin orden garantizado, basa tu lógica en `track_id` + `status` en lugar del orden de recepción.

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