Multimedia
El pipeline de multimedia te permite subir un binario para enviarlo
como media.id desde POST /platform/v1/messages,
bajar el binario de un mensaje entrante a través de una URL firmada
de corta vida, y borrar un archivo en Meta. Auth: X-API-Key + scope
upload:media.
El pipeline es stateless: Kalipto no almacena el binario en disco ni en un bucket. Cada acceso al proxy de descarga hace un fetch en vivo a Meta usando el token del canal. El token nunca aparece en la URL del proxy, en un log, ni en la respuesta HTTP.
Endpoints
Section titled “Endpoints”| Método | Path | Auth | Descripción |
|---|---|---|---|
POST | /platform/v1/media | X-API-Key + upload:media | Subí un binario a Meta. Acepta multipart/form-data o un body JSON con url-source. |
GET | /platform/v1/media/{media_id}/url | X-API-Key + upload:media | Devuelve la metadata de Meta (url, mime_type, file_size). El token nunca se devuelve. |
DELETE | /platform/v1/media/{media_id} | X-API-Key + upload:media | Borra el media en Meta. |
GET | /platform/v1/media/inbound/{media_id} | HMAC en la URL (no X-API-Key) | Proxy de descarga del binario entrante. La URL firmada llega en el webhook entrante (ver más abajo). |
Subir un archivo
Section titled “Subir un archivo”Opción A: multipart/form-data
Section titled “Opción A: multipart/form-data”Mandá el binario directamente desde tu backend. Es el camino más simple cuando ya tenés el archivo en memoria.
curl -X POST https://api-kalipto-test.delabs.pro/platform/v1/media \ -H "X-API-Key: dlb_dev_..." \ -F "channel_id=42" \ -F "file=@./factura-1042.pdf;type=application/pdf"Form fields:
| Campo | Tipo | Requerido | Descripción |
|---|---|---|---|
channel_id | int | Sí | Id del canal de tu cuenta. 404 si el canal no es tuyo. |
file | binary | Sí | El binario. El Content-Type de la parte determina el mime_type que mandamos a Meta. |
Opción B: application/json con url-source
Section titled “Opción B: application/json con url-source”Cuando el archivo ya está en una URL pública, evitás bajarlo y volverlo a subir desde tu lado. Kalipto baja el archivo en tu nombre, con un guardia SSRF (HTTPS obligatorio, bloqueo de IPs privadas, loopback, link-local, reservadas y metadata), y lo sube a Meta.
curl -X POST https://api-kalipto-test.delabs.pro/platform/v1/media \ -H "X-API-Key: dlb_dev_..." \ -H "Content-Type: application/json" \ -d '{ "channel_id": 42, "url": "https://files.tu-app.com/factura-1042.pdf", "mime_type": "application/pdf" }'JSON body:
| Campo | Tipo | Requerido | Descripción |
|---|---|---|---|
channel_id | int | Sí | Id del canal de tu cuenta. |
url | string (1-2048) | Sí | URL HTTPS pública del archivo. Se rechaza con 422 si no es HTTPS, si la IP cae en una red privada (10.0.0.0/8, 172.16.0.0/12, 192.168.0.0/16), loopback (127.0.0.0/8, ::1), link-local (169.254.0.0/16), reservadas o la metadata de cloud (169.254.169.254). No seguimos redirects. |
mime_type | string (1-128) | Sí | MIME type del archivo. Usado para validar el límite de tamaño por categoría (ver la tabla abajo). |
El fetch usa DNS-pinning (
PinnedIPTransport): resolvemos el host, validamos la IP, y abrimos la conexión contra esa IP exacta. Eso cierra la ventana de DNS-rebind entre la validación y el fetch.
Response 200
Section titled “Response 200”Las dos variantes devuelven el mismo shape:
{ "media_id": "1234567890", "channel_id": 42}Usá el media_id directamente en
POST /platform/v1/messages como
media.id.
Límites de tamaño por tipo
Section titled “Límites de tamaño por tipo”| Categoría | Límite | Tipos típicos |
|---|---|---|
image | 5 MB | JPEG, PNG (Meta rechaza WebP como image) |
video | 16 MB | MP4, 3GPP |
audio | 16 MB | AAC, MP4, MPEG, AMR, OGG/OPUS (voice notes) |
document | 100 MB | PDF, DOCX, XLSX, PPTX, TXT |
sticker | 500 KB | Animado (cap superior); el sticker estático es ~100 KB |
El bucket se infiere del prefijo principal del
mime_type:image/*cae enimage,video/*envideo,audio/*enaudio, y cualquier otra cosa endocument. Si el archivo supera el cap, devolvemos 413 antes de subirlo a Meta.
Obtener la URL de un media (metadata)
Section titled “Obtener la URL de un media (metadata)”GET /platform/v1/media/{media_id}/url?channel_id=<id> devuelve la
metadata que Meta nos devuelve para un media_id. La URL de Meta es de
corta vida (alrededor de 5 minutos): no la guardes para más tarde,
volvé a pedirla cuando la necesites.
curl -G https://api-kalipto-test.delabs.pro/platform/v1/media/1234567890/url \ -H "X-API-Key: dlb_dev_..." \ --data-urlencode "channel_id=42"Response 200
Section titled “Response 200”{ "url": "https://lookaside.fbsbx.com/whatsapp_business/attachments/?mid=...", "mime_type": "application/pdf", "file_size": 73452, "channel_id": 42}| Campo | Descripción |
|---|---|
url | URL temporal de Meta. No incluye el token del canal. Para bajarla necesitás autorizar contra Meta con el token; preferí usar el proxy /media/inbound/{media_id} cuando se trate de un media entrante (Kalipto se encarga del token). |
mime_type | MIME type devuelto por Meta. Puede ser null si Meta no lo devuelve. |
file_size | Tamaño en bytes. Puede ser null. |
channel_id | Echo del request. |
Si el media ya no existe en Meta (~30 días desde la subida): 404.
Borrar un media
Section titled “Borrar un media”DELETE /platform/v1/media/{media_id}?channel_id=<id> borra el media en
Meta con el token del canal.
curl -X DELETE https://api-kalipto-test.delabs.pro/platform/v1/media/1234567890 \ -H "X-API-Key: dlb_dev_..." \ --data-urlencode "channel_id=42"Response 200
Section titled “Response 200”{ "deleted": true, "media_id": "1234567890", "result": {"success": true}}Borrar dos veces es idempotente en Meta. Si el media ya no existe: 404.
Proxy de descarga para un media entrante (URL firmada)
Section titled “Proxy de descarga para un media entrante (URL firmada)”Cuando recibís un webhook entrante (ver
Webhook entrante) con un mensaje que
trae un media_id (imagen, video, audio, documento, sticker), Kalipto
te incluye una URL firmada que apunta al endpoint
/platform/v1/media/inbound/{media_id}. Esa URL te entrega el binario
directamente, sin que vos manejes el token del canal.
https://api-kalipto-test.delabs.pro/platform/v1/media/inbound/<media_id> ?ch=<channel_id> &exp=<unix_epoch> &sig=<hmac_hex>Cómo se firma la URL
Section titled “Cómo se firma la URL”El sig es un HMAC-SHA256 (truncado a 32 hex) sobre el string
platmedia:<channel_id>:<media_id>:<exp> con el secret interno de
Kalipto. Cubrir los 3 campos en el HMAC significa:
- Si manipulás
ch,expo elmedia_id, elsigdeja de coincidir y devolvemos 403. - Si la URL expira (
expya pasó), devolvemos 410 sin bajar nada. - La verificación usa
hmac.compare_digest(tiempo constante).
La URL vive alrededor de 4 minutos (SIGNED_URL_TTL_SECONDS = 240),
deliberadamente más corta que los ~5 minutos de la URL de Meta: cuando el
HMAC todavía es válido, el binario todavía existe en Meta.
Stateless: cada acceso es un fetch fresco
Section titled “Stateless: cada acceso es un fetch fresco”El endpoint hace dos pasos: pide la metadata a Meta con el token del
canal, baja el binario, y te lo devuelve como StreamingResponse con
el mime_type correcto. No cacheamos el binario ni la URL de Meta.
Sin auth X-API-Key
Section titled “Sin auth X-API-Key”Este endpoint no usa la API key: la autorización es el HMAC en la URL. Pensá la URL como una credencial de un solo enlace: cualquier cliente con esa URL accede al binario hasta que expire.
Replay dentro de la ventana: si la misma URL firmada se accede más de una vez antes de expirar, permitimos el acceso (es stateless, no trackeamos consumo). Si necesitás un solo uso estricto, no compartas la URL fuera de tu backend.
Códigos de respuesta
Section titled “Códigos de respuesta”| Código | Trigger |
|---|---|
200 OK | Binario devuelto como StreamingResponse con Content-Type igual al mime_type que reporta Meta. |
403 Forbidden | sig no coincide (URL manipulada). |
410 Gone | La URL expiró (exp ya pasó). |
404 Not Found | El canal está suspendido o el media ya no existe en Meta. |
502 Bad Gateway | El canal no tiene token decifrable, o Meta respondió con error. |
Errores
Section titled “Errores”Los 3 endpoints X-API-Key comparten el envelope estructurado de
mensajes:
| Código | Trigger | Body (detail) |
|---|---|---|
400 | Validación dura no Pydantic (poco habitual en este pipeline). | string |
401 Unauthorized | Key inválida, inactiva o expirada. | string |
403 Forbidden | Key sin scope upload:media. | string |
404 Not Found | El canal no es tuyo, está suspendido, o el media no existe en Meta. | string opaco |
413 Payload Too Large | El binario supera el cap de tamaño para su categoría (ver tabla). | string con la categoría y el cap. |
415 Unsupported Media Type | El Content-Type del request no es ni multipart/form-data ni application/json. | string |
422 Unprocessable Entity | Validación Pydantic, multipart inválido, url no HTTPS o IP bloqueada por SSRF ({"error":"invalid_source_url","message":"<motivo>"}). | dict o string. |
429 Too Many Requests | Rate limit por key (mismos headers que POST /messages). | dict estándar. |
502 Bad Gateway | El canal no tiene token decifrable ({"error":"channel_token_unavailable", ...}), la URL del url-source no es alcanzable ({"error":"source_url_unreachable", ...}), o Meta respondió con error (envelope meta_send_failed). | dict (envelope de 6 claves). |
Scope upload:media
Section titled “Scope upload:media”El scope cubre los 4 endpoints (POST, GET, DELETE y el proxy
inbound). Para crear una key con este scope, andá al panel devs:
Configuración › API keys y marcá upload:media al generar la key.
El backend rechaza una key sin este scope con 403 en cualquiera de
los 3 endpoints autenticados; el proxy inbound no usa el scope (auth =
HMAC de la URL).
Próximo paso
Section titled “Próximo paso”- Enviar mensajes: usá el
media_idque devuelvePOST /platform/v1/mediadirectamente comomedia.iden el body del send. - Webhook entrante (forward): cómo recibís los webhooks crudos de Meta y dónde llega la URL firmada del proxy inbound.