# Descargar el adjunto de un mensaje de WhatsApp (/api/downloadWhatsappMedia)

`GET /api/meta/whatsapp/media/{id}`

URL base: `https://resender.dev`

Devuelve el archivo (imagen, audio, vídeo, documento o sticker) de un
mensaje de WhatsApp, guardado en Resender. Es la URL que llega en
`message.attachment.url` del evento de WhatsApp.

**Dos formas de autenticar:**

- **Link firmado.** La URL del evento ya trae `exp` y `sig` y se abre
  tal cual, sin cabeceras: en el navegador, en un `<img>` o en un nodo
  HTTP de n8n. Vence a las 24 h (`details.urlExpiresAt`). Cualquiera
  que tenga el link puede abrirlo hasta entonces: no lo publiques.
- **API key.** La misma URL sin `exp` ni `sig`, con
  `Authorization: Bearer <api key>`. Sirve siempre, también después de
  que el link venció.

Soporta `Range` (`206`), así que un audio o un vídeo se pueden
adelantar en un reproductor. Los archivos se conservan 180 días desde
que llegó el mensaje.

## Autenticación

- **ApiKey**: Header `Authorization: Bearer <token>`. API key opaca del tenant, creada en **Settings → API Tokens**. Una key revocada, inexistente o malformada responde `401 {"error":"unauthorized"}`.

- También puede llegar sin autenticación.

## Parámetros

| Nombre | En | Tipo | Requerido | Descripción |
| --- | --- | --- | --- | --- |
| `id` | path | string (uuid) | Sí | `message.id` del evento (uuid de Resender, no el `wamid`). |
| `exp` | query | string | No | Vencimiento del link firmado, en epoch segundos. Viene en la URL del evento; no se arma a mano. (pattern: `^[0-9]+$`) |
| `sig` | query | string | No | Firma del link. Viene en la URL del evento; no se arma a mano. |
| `Range` | header | string | No |  |

## Ejemplo de petición

```bash
curl -X GET "https://resender.dev/api/meta/whatsapp/media/{id}" \
  -H "Authorization: Bearer <token>"
```

## Respuestas

### 200

El archivo completo. `content-type` es el MIME real del adjunto.

`*/*`

Tipo: string (binary)

### 206

El rango pedido con `Range`.

`*/*`

Tipo: string (binary)

### 401

Sin credencial válida: link vencido o alterado, y sin API key (o
con una inválida). Con un link vencido, repetí el pedido sin
`exp`/`sig` y con la API key.

`application/json`

| Campo | Tipo | Requerido | Descripción |
| --- | --- | --- | --- |
| `error` | string | Sí |  |
| `code` | string | No | Código estable, presente solo en algunos errores. |
| `message` | string | No | Explicación legible, presente en los bloqueos de plan y cuota. |
| `meta` | object \| null | No | Respuesta original de Graph cuando Meta rechazó el envío. |
| `resender` | object | No |  |

```json
{
  "error": "unauthorized"
}
```

### 404

El mensaje no existe, es de otro tenant o no tiene adjunto descargable.

`application/json`

| Campo | Tipo | Requerido | Descripción |
| --- | --- | --- | --- |
| `error` | string | Sí |  |
| `code` | string | No | Código estable, presente solo en algunos errores. |
| `message` | string | No | Explicación legible, presente en los bloqueos de plan y cuota. |
| `meta` | object \| null | No | Respuesta original de Graph cuando Meta rechazó el envío. |
| `resender` | object | No |  |

```json
{
  "error": "not_found"
}
```

### 409

El mensaje tiene adjunto pero el archivo no está disponible.
`status` dice por qué: `failed` (no se pudo bajar de WhatsApp),
`deleted` (pasaron los 180 días) o `unavailable` (WhatsApp nunca
lo ofreció, como en el historial de más de 14 días). En los tres
casos el evento ya llegó con `url: null`, así que no hace falta
reintentar.

`application/json`

| Campo | Tipo | Requerido | Descripción |
| --- | --- | --- | --- |
| `error` | `"attachment_not_available"` | Sí |  |
| `status` | `"available"` \| `"failed"` \| `"deleted"` \| `"unavailable"` \| `"pending"` | Sí | Solo WhatsApp. Estado del archivo del adjunto: - `available`: guardado en Resender; `url` lo descarga. - `failed`: no se pudo bajar de WhatsApp, o no estuvo listo en 2 minutos. - `deleted`: pasaron los 180 días de retención. - `unavailable`: WhatsApp no ofreció el archivo (historial de más de 14 días). - `pending`: todavía bajando. Los eventos ya no salen en este estado; solo lo puede devolver la descarga de un mensaje muy reciente. |

```json
{
  "error": "attachment_not_available",
  "status": "failed"
}
```