Skip to content

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.

MétodoPathAuthDescripción
POST/platform/v1/mediaX-API-Key + upload:mediaSubí un binario a Meta. Acepta multipart/form-data o un body JSON con url-source.
GET/platform/v1/media/{media_id}/urlX-API-Key + upload:mediaDevuelve la metadata de Meta (url, mime_type, file_size). El token nunca se devuelve.
DELETE/platform/v1/media/{media_id}X-API-Key + upload:mediaBorra 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).

Mandá el binario directamente desde tu backend. Es el camino más simple cuando ya tenés el archivo en memoria.

Terminal window
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:

CampoTipoRequeridoDescripción
channel_idintId del canal de tu cuenta. 404 si el canal no es tuyo.
filebinaryEl 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.

Terminal window
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:

CampoTipoRequeridoDescripción
channel_idintId del canal de tu cuenta.
urlstring (1-2048)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_typestring (1-128)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.

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.

CategoríaLímiteTipos típicos
image5 MBJPEG, PNG (Meta rechaza WebP como image)
video16 MBMP4, 3GPP
audio16 MBAAC, MP4, MPEG, AMR, OGG/OPUS (voice notes)
document100 MBPDF, DOCX, XLSX, PPTX, TXT
sticker500 KBAnimado (cap superior); el sticker estático es ~100 KB

El bucket se infiere del prefijo principal del mime_type: image/* cae en image, video/* en video, audio/* en audio, y cualquier otra cosa en document. Si el archivo supera el cap, devolvemos 413 antes de subirlo a Meta.

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.

Terminal window
curl -G https://api-kalipto-test.delabs.pro/platform/v1/media/1234567890/url \
-H "X-API-Key: dlb_dev_..." \
--data-urlencode "channel_id=42"
{
"url": "https://lookaside.fbsbx.com/whatsapp_business/attachments/?mid=...",
"mime_type": "application/pdf",
"file_size": 73452,
"channel_id": 42
}
CampoDescripción
urlURL 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_typeMIME type devuelto por Meta. Puede ser null si Meta no lo devuelve.
file_sizeTamaño en bytes. Puede ser null.
channel_idEcho del request.

Si el media ya no existe en Meta (~30 días desde la subida): 404.

DELETE /platform/v1/media/{media_id}?channel_id=<id> borra el media en Meta con el token del canal.

Terminal window
curl -X DELETE https://api-kalipto-test.delabs.pro/platform/v1/media/1234567890 \
-H "X-API-Key: dlb_dev_..." \
--data-urlencode "channel_id=42"
{
"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>

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, exp o el media_id, el sig deja de coincidir y devolvemos 403.
  • Si la URL expira (exp ya 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.

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.

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ódigoTrigger
200 OKBinario devuelto como StreamingResponse con Content-Type igual al mime_type que reporta Meta.
403 Forbiddensig no coincide (URL manipulada).
410 GoneLa URL expiró (exp ya pasó).
404 Not FoundEl canal está suspendido o el media ya no existe en Meta.
502 Bad GatewayEl canal no tiene token decifrable, o Meta respondió con error.

Los 3 endpoints X-API-Key comparten el envelope estructurado de mensajes:

CódigoTriggerBody (detail)
400Validación dura no Pydantic (poco habitual en este pipeline).string
401 UnauthorizedKey inválida, inactiva o expirada.string
403 ForbiddenKey sin scope upload:media.string
404 Not FoundEl canal no es tuyo, está suspendido, o el media no existe en Meta.string opaco
413 Payload Too LargeEl binario supera el cap de tamaño para su categoría (ver tabla).string con la categoría y el cap.
415 Unsupported Media TypeEl Content-Type del request no es ni multipart/form-data ni application/json.string
422 Unprocessable EntityValidació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 RequestsRate limit por key (mismos headers que POST /messages).dict estándar.
502 Bad GatewayEl 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).

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

  • Enviar mensajes: usá el media_id que devuelve POST /platform/v1/media directamente como media.id en 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.