# Enviar y recibir archivos por WhatsApp (/whatsapp)











## Cómo funciona [#cómo-funciona]

**Enviar.** Mandas la URL pública del archivo y Meta lo descarga desde ahí.

<img alt="Flujo de envío" src="__img0" />

**Recibir.** Pontiun guarda el archivo y después llama a tu webhook con un link listo para abrir.

<img alt="Flujo de recepción" src="__img1" />

## Antes de enviar [#antes-de-enviar]

* Autentica con `Authorization: Bearer pk_live_…`.
* `Idempotency-Key` es **obligatoria**. Si reintentas con la misma key, no se manda un duplicado.
* Destino: `conversationId` (del webhook) o `pageId` + `recipientId` (teléfono con código de país).
* Mandas texto (`reply`) **o** un adjunto (`attachment`), nunca los dos.
* `caption` es el pie de foto. Funciona en imagen, video y archivo; en audio da `400`. Máximo 1024 caracteres.
* La URL debe ser `https` y pública. Meta la cachea unos 10 minutos, así que no uses links que venzan en segundos.
* Si el contacto no te escribió en las últimas 24 h, la respuesta es `409` con `requiresTemplate: true`. En ese caso usa [una plantilla](/api/sendWhatsappTemplate).

## Imagen [#imagen]

**Enviar**

```bash
curl -X POST https://pontiun.com/api/meta/whatsapp/send \
  -H "Authorization: Bearer pk_live_<secreto>" \
  -H "Content-Type: application/json" \
  -H "Idempotency-Key: pedido-8841-foto" \
  -d '{
    "conversationId": "6f0e5a2c-8a5e-4a3d-9c2b-1f2e3d4c5b6a",
    "attachment": {
      "type": "image",
      "url": "https://cdn.example.com/pedidos/8841.jpg",
      "caption": "Así quedó tu pedido #8841"
    }
  }'
```

**Recibir.** El pie de foto llega en `message.text`.

```json
"message": {
  "id": "cce03202-3f6d-4379-b85e-6cc11063ccd5",
  "text": "¿La tienen en rojo?",
  "attachment": {
    "type": "image",
    "url": "https://pontiun.com/api/meta/whatsapp/media/cce03202-3f6d-4379-b85e-6cc11063ccd5?exp=1792526083&sig=1pQ2o3r4S5t6",
    "title": null,
    "details": {
      "status": "available",
      "mimeType": "image/jpeg",
      "sizeBytes": 32671,
      "urlExpiresAt": "2026-10-20T19:54:43.000Z"
    }
  }
}
```

## Video [#video]

**Enviar**

```bash
curl -X POST https://pontiun.com/api/meta/whatsapp/send \
  -H "Authorization: Bearer pk_live_<secreto>" \
  -H "Content-Type: application/json" \
  -H "Idempotency-Key: demo-producto-12" \
  -d '{
    "conversationId": "6f0e5a2c-8a5e-4a3d-9c2b-1f2e3d4c5b6a",
    "attachment": {
      "type": "video",
      "url": "https://cdn.example.com/videos/demo.mp4",
      "caption": "Así se arma"
    }
  }'
```

**Recibir**

```json
"attachment": {
  "type": "video",
  "url": "https://pontiun.com/api/meta/whatsapp/media/<id>?exp=…&sig=…",
  "details": {
    "status": "available",
    "mimeType": "video/mp4",
    "sizeBytes": 2483120,
    "urlExpiresAt": "2026-10-20T19:54:43.000Z"
  }
}
```

## Audio [#audio]

**Enviar.** No uses `caption`.

```bash
curl -X POST https://pontiun.com/api/meta/whatsapp/send \
  -H "Authorization: Bearer pk_live_<secreto>" \
  -H "Content-Type: application/json" \
  -H "Idempotency-Key: audio-bienvenida-77" \
  -d '{
    "conversationId": "6f0e5a2c-8a5e-4a3d-9c2b-1f2e3d4c5b6a",
    "attachment": {
      "type": "audio",
      "url": "https://cdn.example.com/audios/bienvenida.mp3"
    }
  }'
```

**Recibir.** `voice: true` indica que es una nota de voz.

```json
"attachment": {
  "type": "audio",
  "url": "https://pontiun.com/api/meta/whatsapp/media/<id>?exp=…&sig=…",
  "details": {
    "status": "available",
    "mimeType": "audio/ogg; codecs=opus",
    "voice": true,
    "sizeBytes": 18234,
    "urlExpiresAt": "2026-10-20T19:54:43.000Z"
  }
}
```

## Archivo (PDF y otros documentos) [#archivo-pdf-y-otros-documentos]

**Enviar.** Usa `type: "file"`; WhatsApp lo muestra como documento.

```bash
curl -X POST https://pontiun.com/api/meta/whatsapp/send \
  -H "Authorization: Bearer pk_live_<secreto>" \
  -H "Content-Type: application/json" \
  -H "Idempotency-Key: factura-8841" \
  -d '{
    "conversationId": "6f0e5a2c-8a5e-4a3d-9c2b-1f2e3d4c5b6a",
    "attachment": {
      "type": "file",
      "url": "https://cdn.example.com/facturas/8841.pdf",
      "caption": "Tu factura del pedido #8841"
    }
  }'
```

**Recibir.** `filename` es el nombre con el que lo mandó el contacto.

```json
"attachment": {
  "type": "file",
  "url": "https://pontiun.com/api/meta/whatsapp/media/<id>?exp=…&sig=…",
  "details": {
    "status": "available",
    "mimeType": "application/pdf",
    "filename": "comprobante.pdf",
    "sizeBytes": 84512,
    "urlExpiresAt": "2026-10-20T19:54:43.000Z"
  }
}
```

## Respuesta del envío [#respuesta-del-envío]

Es igual para los cuatro tipos:

```json
{
  "meta": { "messages": [{ "id": "wamid.HBgN…" }] },
  "resender": {
    "conversationId": "6f0e5a2c-8a5e-4a3d-9c2b-1f2e3d4c5b6a",
    "messageId": "0c9d2f6e-3b7a-4e1d-8f5c-2a1b3c4d5e6f",
    "status": "sent"
  }
}
```

## El link del archivo y su vencimiento [#el-link-del-archivo-y-su-vencimiento]

<img alt="Vencimiento del link" src="__img2" />

| Campo                  | Qué es                                                             |
| ---------------------- | ------------------------------------------------------------------ |
| `exp`                  | Vencimiento del link, en segundos epoch. Viene incluido en la URL. |
| `sig`                  | Firma del link. Si la modificas, el link deja de funcionar.        |
| `details.urlExpiresAt` | El mismo vencimiento en ISO 8601: 24 h después de la entrega.      |

* **Durante las primeras 24 h**, el link se abre tal cual, sin cabeceras: en el navegador, en un `<img>` o en n8n.
* **Después de las 24 h**, quita `exp` y `sig` y manda tu API key:

```bash
curl https://pontiun.com/api/meta/whatsapp/media/cce03202-3f6d-4379-b85e-6cc11063ccd5 \
  -H "Authorization: Bearer pk_live_<secreto>" \
  -o foto.jpg
```

* El archivo se guarda **180 días**. Después la ruta responde `409` con `status: deleted`.
* Quien tenga el link puede abrir el archivo hasta que vence, así que no lo publiques.
* Soporta `Range`: puedes adelantar un audio o un video en el reproductor.

<Callout title="No necesitas providerMediaId">
  El link ya apunta a tu archivo. No necesitas una app ni un token de Meta para descargarlo.
</Callout>

## Si el archivo no se pudo bajar [#si-el-archivo-no-se-pudo-bajar]

Pontiun espera hasta 2 minutos a que el archivo esté listo. Si no lo consigue, el evento llega igual, con `url: null`:

```json
"attachment": {
  "type": "file",
  "url": null,
  "details": { "status": "failed", "failureReason": "media_download_timeout" }
}
```

| `details.status` | `url`        | Significado                                         |
| ---------------- | ------------ | --------------------------------------------------- |
| `available`      | link firmado | Listo para descargar.                               |
| `failed`         | `null`       | No se pudo bajar de WhatsApp.                       |
| `unavailable`    | `null`       | WhatsApp no lo envió (historial de más de 14 días). |
| `deleted`        | `null`       | Pasaron los 180 días.                               |

## Orden de los mensajes [#orden-de-los-mensajes]

<img alt="Orden por conversación" src="__img3" />

* Los mensajes de una conversación llegan en el orden en que se escribieron. Un texto que llega después de una foto espera a que la foto esté lista.
* Las otras conversaciones no esperan.
* Si recibes un mensaje que Meta entregó tarde, ordena por `message.createdAt`.

## Más detalle [#más-detalle]

<Cards>
  <Card title="Enviar mensaje de WhatsApp" href="/api/sendWhatsappMessage" />

  <Card title="Evento de WhatsApp" href="/api/whatsappEvents" />

  <Card title="Descargar el adjunto" href="/api/downloadWhatsappMedia" />

  <Card title="Enviar plantilla" href="/api/sendWhatsappTemplate" />
</Cards>
