Ir al contenido

Multimedia

La multimedia del motor es privada: el archivo vive en el bucket que el operador configura en Ajustes → Almacenamiento, y en la base solo quedan su identidad, su MIME real, su tamaño, su SHA-256, su estado y su retención. Nunca hay un enlace público ni un base64 guardado. Para mandar un archivo, primero lo subes y después lo citas por su media_id; un archivo que llega en un mensaje se descarga solo y te avisa con media.ready.

Operación Scope
POST /v1/instances/{id}/media/uploads (subir) media:write
DELETE /v1/media/{id} (borrar o cancelar una descarga) media:write
GET /v1/media/{id} (estado y permiso de descarga) media:read
GET /v1/media/{id}/content (descargar) media:read, o el permiso temporal ?permiso= sin API key
POST /v1/instances/{id}/messages con un tipo de multimedia messages:send

Un medio es de una instancia y de una app: otra app recibe 404, y un envío solo puede usar un media_id de su misma instancia.

POST /v1/instances/{id}/media/uploads recibe el archivo crudo en el cuerpo (no multipart ni base64), con Content-Type igual a su MIME. Parámetros opcionales: nombre (el nombre del archivo, para un documento; hasta 200 caracteres) y sha256 (el hash en hex: si no coincide, 422 digest).

El motor lo copia en streaming a su disco temporal, calcula su SHA-256, detecta el MIME real por el contenido (lo declarado puede precisar un contenedor genérico, como un ZIP que es un .docx, pero no contradecirlo: 422 mime_falso), revisa las cuotas y lo guarda en el bucket. Responde 201 con el media_id y un permiso temporal de descarga.

Ventana de terminal
curl -X POST "https://motor.ejemplo.com/v1/instances/INSTANCIA_ID/media/uploads?nombre=factura-1042.pdf" \
-H "Authorization: Bearer TU_API_KEY" \
-H "Content-Type: application/pdf" \
--data-binary @factura-1042.pdf
201 Created
{
"media_id": "0b9d6c7e-0000-4000-8000-0000000000f1",
"instance_id": "8a1d2c3e-0000-4000-8000-0000000000a1",
"origen": "subida",
"estado": "listo",
"mime": "application/pdf",
"mime_declarado": "application/pdf",
"bytes": 182044,
"sha256": "9f2c…",
"nombre_archivo": "factura-1042.pdf",
"metadatos": {},
"created_at": "2026-10-24T15:20:00Z",
"retencion_hasta": "2026-11-23T15:20:00Z",
"descarga": {
"url": "/v1/media/0b9d6c7e-0000-4000-8000-0000000000f1/content?permiso=mwp_…",
"expira_en": "2026-10-24T15:35:00Z"
}
}

Los límites son de Ajustes → Multimedia (los pone el operador):

Ajuste De fábrica Qué pasa al superarlo
multimedia.tamano_maximo_mb 100 MB por archivo (subir, importar o descargar) 413 demasiado_grande
multimedia.cuota_instancia_mb 2048 MB guardados por instancia 429 cuota_excedida
multimedia.cuota_app_mb 10 240 MB por app (cada app puede tener la suya) 429 cuota_excedida
multimedia.espacio_libre_minimo_mb 256 MB libres en el disco temporal 503 presion_disco con Retry-After

Se puede subir cualquier tipo de archivo: lo que cambia es con qué tipo se puede mandar.

tipo Formato que sirve (MIME real) Caption (texto)
image JPEG o PNG Sí, hasta 1024
video MP4 o 3GP Sí, hasta 1024
audio OGG/Opus, MP3, M4A, AAC o AMR No
document Cualquiera (medio.nombre_archivo es el nombre que ve el destinatario) Sí, hasta 1024

Un archivo que no sirve para ese tipo es 422 formato_incompatible: mándalo como document o usa un subtipo con conversión.

Cuatro subtipos se convierten antes de salir, con ffmpeg, como un trabajo durable acotado en procesos, tiempo y memoria:

tipo Sale como Se puede hacer de Caption
ptt Nota de voz: Opus mono en Ogg, con su duración y su forma de onda Un audio o un vídeo No
sticker WebP de 512 × 512: estático hasta 100 KB, animado hasta 6 s y 500 KB Una imagen (no HEIC, AVIF ni TIFF), un GIF o un vídeo No
ptv Vídeo circular: MP4 cuadrado de 480, hasta 60 s Un vídeo No
gif MP4 sin audio, en bucle, hasta 720 de ancho y 30 s Un vídeo o un GIF Sí

Mientras convierte, el comando sigue en_cola y su media.estado es pendiente. Un archivo que no sirve para el subtipo es 422 formato_no_convertible al aceptar. Si la conversión falla, el envío termina fallido con conversion_fallida, y el motivo (formato_no_convertible, tiempo_excedido o memoria_excedida) va en media.motivo del comando y en media.failed. Si el motor no tiene ffmpeg instalado, los subtipos responden 503 conversion_no_disponible.

Los límites de la conversión también son de Ajustes: multimedia.conversiones_simultaneas (2), multimedia.segundos_conversion (120) y multimedia.memoria_conversion_mb (1024).

POST /v1/instances/{id}/messages con el tipo y medio, que lleva exactamente uno de:

  • medio.media_id: una subida de esta instancia, en estado listo (si no, 409 no_listo).
  • medio.url: una URL http(s) cuyo host está en los destinos permitidos de los webhooks (si no, 422 url_no_permitida). El motor la importa como un trabajo durable y el envío espera a media.ready.

El archivo se sube a WhatsApp dentro del mismo envío durable: si esa subida falla, no salió nada y se reintenta. Puede llevar cita y menciones, como un texto (ver Mensajes).

Ventana de terminal
curl -X POST https://motor.ejemplo.com/v1/instances/INSTANCIA_ID/messages \
-H "Authorization: Bearer TU_API_KEY" \
-H "Content-Type: application/json" \
-H "Idempotency-Key: factura-1042" \
-d '{
"destino": {"telefono": "+NUMERO_DEL_CONTACTO"},
"tipo": "document",
"medio": {"media_id": "0b9d6c7e-0000-4000-8000-0000000000f1", "nombre_archivo": "factura-1042.pdf"},
"texto": "Tu factura del pedido 1042"
}'
202 Accepted
{
"command_id": "5c2e7a10-0000-4000-8000-0000000000c5",
"instance_id": "8a1d2c3e-0000-4000-8000-0000000000a1",
"idempotency_key": "factura-1042",
"type": "message.document",
"generation": 1,
"status": "en_cola",
"media": { "media_id": "0b9d6c7e-0000-4000-8000-0000000000f1", "estado": "listo" },
"created_at": "2026-10-24T15:21:00Z",
"updated_at": "2026-10-24T15:21:00Z"
}

El type del comando es message.image, message.video, message.document, message.audio, message.ptt, message.ptv, message.sticker o message.gif. El resultado llega como cualquier envío: message.sent cuando WhatsApp lo acepta y message.updated con entregado, leido o reproducido; o command.failed con medio_no_disponible (el medio falló o se borró antes de salir, por ejemplo una importación de URL que no se pudo hacer), medio_incompatible (lo importado no sirve para el tipo) o conversion_fallida.

Un mensaje con un archivo llega como message.received con contenido.medio. El archivo todavía no está: el motor lo descarga de WhatsApp como un trabajo durable y, mientras tanto, estado es pendiente. El media_id ya viene en el evento.

message.received · data (una nota de voz)
{
"chat": { "peer_ref": "4b7d9e01-0000-4000-8000-0000000000b1", "jid": "CONTACTO@lid", "clase": "usuario", "aliases": ["CONTACTO@s.whatsapp.net"] },
"provider_id": "PROVIDER_MESSAGE_ID",
"from_me": false,
"es_grupo": false,
"operacion": "mensaje",
"contenido": {
"tipo": "audio",
"medio": {
"media_id": "0b9d6c7e-0000-4000-8000-0000000000f2",
"estado": "pendiente",
"mime": "audio/ogg; codecs=opus",
"bytes": 20449,
"duracion_seg": 9,
"ptt": true,
"sha256": "4a1b…"
}
},
"momento": "2026-10-24T15:22:00Z"
}

medio puede traer además nombre (un documento), ancho y alto, y las banderas ptt (nota de voz), ptv (vídeo circular), gif y animado (sticker animado). El caption va en contenido.texto. Un mensaje importado del historial con la multimedia del historial apagada llega con estado: no_descargado y sin media_id: solo sus metadatos.

Cuando la descarga termina llega uno de dos eventos, con el provider_id del mensaje:

media.ready · data
{
"media_id": "0b9d6c7e-0000-4000-8000-0000000000f2",
"origen": "entrante",
"estado": "listo",
"mime": "audio/ogg",
"bytes": 20449,
"sha256": "4a1b…",
"provider_id": "PROVIDER_MESSAGE_ID"
}
media.failed · data
{
"media_id": "0b9d6c7e-0000-4000-8000-0000000000f3",
"origen": "entrante",
"estado": "fallido",
"motivo": "vencido_en_whatsapp",
"provider_id": "PROVIDER_MESSAGE_ID"
}

origen es entrante (un archivo recibido), url (una importación de medio.url, con el command_id del envío que la usa) o conversion. Motivos de media.failed: vencido_en_whatsapp (WhatsApp ya no lo tiene), invalido (no coincide con su hash), demasiado_grande, cuota_excedida, reintentos_agotados, url_no_permitida, url_rechazada, y los de la conversión. Una descarga que falla por algo pasajero se reintenta hasta 6 veces (a los 30 s, 2 min, 10 min, 30 min y 1 h) antes de reintentos_agotados; si el almacenamiento no responde o el disco está casi lleno, espera sin gastar intentos.

Hay dos maneras:

  1. Con la API key (scope media:read): GET /v1/media/{id}/content.
  2. Con un permiso temporal, sin API key: GET /v1/media/{id} devuelve, si el medio está listo, descarga.url (una ruta relativa con ?permiso=mwp_…) y descarga.expira_en. Vale hasta que vence (15 minutos de fábrica, multimedia.minutos_permiso) y solo para ese medio. Es el esquema de seguridad permisoMedia del OpenAPI: sirve para pasarle el archivo a un navegador o a otro servicio sin darle tu key. No es público ni permanente: no lo guardes.

El archivo sale en streaming, con su Content-Type real y Content-Disposition: attachment. Con encoding=base64, la respuesta es un JSON con data en base64, solo para archivos de hasta multimedia.base64_maximo_kb (5120 KB de fábrica); uno más grande es 422 demasiado_grande_para_base64.

Ventana de terminal
# Con la API key
curl -o nota.ogg https://motor.ejemplo.com/v1/media/MEDIA_ID/content \
-H "Authorization: Bearer TU_API_KEY"
# El permiso temporal (descarga.url), para usarlo sin key
curl https://motor.ejemplo.com/v1/media/MEDIA_ID \
-H "Authorization: Bearer TU_API_KEY"
curl -o nota.ogg "https://motor.ejemplo.com/v1/media/MEDIA_ID/content?permiso=mwp_…"
  • Retención: un medio listo trae retencion_hasta, a los multimedia.dias_retencion días (30 de fábrica). Después, el motor borra el archivo y el medio queda borrado con motivo: retencion, salvo que lo use un envío pendiente. Su descarga responde 410 no_disponible.
  • Mensajes temporales: cuando vence un mensaje temporal, el motor borra también sus medios (borrado, motivo: mensaje_vencido). Ver Mensajes.
  • Borrar ya: DELETE /v1/media/{id} (scope media:write) quita el archivo del bucket, o cancela su descarga o importación pendiente, y lo deja borrado (motivo: pedido). Si un envío pendiente lo usa, 409 en_uso. Repetirlo no hace nada.
  • En WhatsApp: un archivo viejo puede ya no estar en los servidores de WhatsApp; su descarga falla con vencido_en_whatsapp y el motor nunca inventa el contenido.

Los estados de un medio son pendiente, subiendo, listo, fallido y borrado. trabajo muestra el progreso de su descarga, importación o conversión (intentos, proximo_intento_en, motivo).

El código va al final de type (urn:motor-whatsapp:error:<código>). Ver Errores.

HTTP Código Cuándo
401 — El permiso temporal no es válido o venció: pide otro con GET /v1/media/{id}.
404 — El medio no existe o es de otra app.
409 no_listo El medio todavía no está listo: espera media.ready.
409 en_uso Borrar un medio que usa un envío pendiente.
410 no_disponible Falló, se borró o venció su retención.
413 demasiado_grande Supera multimedia.tamano_maximo_mb.
422 mime_falso / digest / vacio El MIME declarado contradice el contenido, el SHA-256 no coincide o el archivo está vacío.
422 formato_incompatible El formato no sirve para ese tipo de envío.
422 formato_no_convertible El archivo no se puede convertir a ese subtipo.
422 url_no_permitida medio.url fuera de los destinos permitidos.
404 medio_no_encontrado Un media_id que no existe en esta instancia (no existe, se borró o es de otra instancia o de otra app: la respuesta es la misma). El campo va en errors[].location.
422 — medio sin exactamente uno de media_id o url; un caption en audio, ptt, sticker o ptv.
422 demasiado_grande_para_base64 Pedir en base64 un archivo más grande que el máximo.
429 cuota_excedida Supera la cuota de la instancia o de la app.
503 multimedia_deshabilitada Falta configurar el almacenamiento.
503 almacen_no_disponible / presion_disco El bucket no responde o el disco temporal está casi lleno (con Retry-After).
503 conversion_no_disponible El motor no tiene ffmpeg para los subtipos.

Lo comprobado con números reales (octubre de 2026):

  • Ida y vuelta: imagen con caption, PDF con nombre, MP3 y vídeo salen con su vista previa, su texto y su nombre; lo que manda el teléfono llega con su media.ready en segundos y el archivo idéntico (mismo SHA-256) por la API, en Android e iOS (caso A18, 16 al 21/10/2026).
  • Almacenamiento caído: con el bucket apagado, los archivos recibidos quedan pendiente, sin media.failed, y al volver quedan listo (caso A19, 16/10/2026). Un archivo vencido en WhatsApp no se pudo provocar: el teléfono no deja reenviar uno que ya no tiene.
  • Nota de voz y stickers: la nota de voz convertida se ve y se escucha como nota de voz en Android e iOS (caso A20, 21/10/2026); los stickers estático y animado se ven y el animado se mueve (caso A21, 21/10/2026). Un sticker animado de la colección del teléfono llega en el formato nuevo de WhatsApp (application/was) y el motor lo guarda como sticker con animado: true.
  • PTV y GIF: no están garantizados en números no oficiales. Se vieron bien en Android, iOS y WhatsApp Web (caso A22, 16 y 18/10/2026), pero puede cambiar.
  • Reenviar un medio de un canal: salía enviado pero no llegaba, porque un medio de canal no va cifrado (falla F10, corregida y repetida el 20/10/2026).

Lo general de WhatsApp está en Limitaciones de WhatsApp.