Mensajes
Referencia de las cuatro rutas que hablan con el cliente por WhatsApp: enviar texto, enviar una plantilla aprobada de la API oficial de Meta, enviar un archivo (imagen, video, audio o documento) y descargar el archivo de cualquier mensaje. Con parámetros, ejemplos en curl, PHP y Python, respuestas, errores y las reglas que rigen cada envío: aviso que nace cerrado, ventana de 24 horas y falla de entrega.
Visión general
Todas las rutas usan la dirección única de la API, con la clave en el encabezado Authorization: Bearer y la ruta en el parámetro r (detalles en Autenticación):
https://atendimento.seuprovedor.com.br/api_publica.php?r=<recurso>/<acao>
| Ruta | Método | Permiso de la clave | Resumen |
|---|---|---|---|
mensagens/enviar | POST | Enviar mensajes | Texto libre para un teléfono o para una conversación, con cita opcional. |
mensagens/enviar_modelo | POST | Enviar mensajes | Plantilla (template) aprobada por Meta, con variables. Funciona fuera de la ventana de 24 horas. |
mensagens/enviar_arquivo | POST | Enviar mensajes | Imagen, video, audio o documento, por carga (multipart) o en base64 dentro del JSON, con leyenda. |
mensagens/arquivo | GET o POST | Consultar conversaciones | Descarga el archivo de un mensaje recibido o enviado. |
Con cuerpo JSON, el parámetro r debe ir en la URL (como en los ejemplos de esta página); con formulario (-d/multipart), puede ir en la URL o en el propio formulario.
Qué sucede detrás de un envío
- Todo se convierte en conversación del panel. El envío entra en la conversación abierta del cliente en ese número de WhatsApp o crea una nueva, con protocolo propio. El equipo ve el mensaje en la pantalla de atención, en la misma línea de tiempo que los demás.
- El mensaje sale como mensaje del sistema: sin el nombre del agente en negrita al inicio, con
autorigual asistemaen el historial. - Rastro de la clave. Cada conversación tocada por la API recibe un registro en la línea de tiempo con el nombre de la clave usada, para que el equipo sepa qué integración actuó.
- Validación antes del protocolo. Texto, plantilla, variables, archivo, conexión y teléfono se verifican antes de abrir cualquier conversación: una llamada rechazada con 4xx no deja un protocolo vacío.
- Los webhooks siguen vigentes. Todo envío dispara
mensagem_enviada(incluso cuando falla, con statuserro); una conversación nueva disparaconversa_criada; el cierre automático del aviso disparaconversa_encerradaconmotivoigual aaviso. Vea Webhooks.
Los números que piden estas rutas (conexao_id, modelo_id, nombres e idiomas de las plantillas) vienen de las rutas de Catálogos, disponibles para cualquier clave activa: conexoes/listar y modelos/listar.
A quién va el mensaje
Las tres rutas de envío aceptan dos formas de indicar el destino. Use una de ellas:
| Forma | Parámetros | Cómo se resuelve el destino |
|---|---|---|
| Por teléfono | telefone y, cuando sea necesario, conexao_id | La API elige el número de WhatsApp (regla abajo), busca en él la conversación abierta de ese teléfono y, si no la hay, crea una nueva. |
| Por conversación | conversa_id o protocolo | Conversación abierta (cualquier estado distinto de encerrada): el mensaje entra en ella, por su número. Conversación cerrada: no se reabre; el mensaje va al mismo teléfono por el mismo número de WhatsApp, y entra en la conversación abierta que exista o en una conversación nueva. |
Cuando conversa_id o protocolo viene en la llamada, telefone y conexao_id se ignoran. Una conversación inexistente responde 404 nao_encontrado. Una conversación cuyo número de WhatsApp fue eliminado del sistema responde 400 conexao_invalida. Si el número de la conversación está desconectado (ninguno de sus canales, código QR o API oficial, en línea; en mensagens/enviar_modelo, el canal de la API oficial), la respuesta es 409 conexao_desconectada, antes de cualquier intento de envío.
Elección del número de WhatsApp (conexao_id)
| Situación | Resultado |
|---|---|
conexao_id informado y conectado | Usa esa conexión. |
conexao_id inexistente | 400 conexao_invalida. |
conexao_id desconectado | 409 conexao_desconectada. En un número con código QR y API oficial al mismo tiempo, basta con que uno de los dos canales esté en línea. |
Sin conexao_id y un único número conectado | Usa ese número. Es el caso de quien tiene un solo WhatsApp. |
Sin conexao_id y ningún número conectado | 400 sem_conexao. |
Sin conexao_id y más de un número conectado | Usa el Número de origen definido en la clave de API (Integraciones, Claves de API, campo Número de origen). Clave sin Número de origen: 400 conexao_id_obrigatorio, la API no elige por usted. |
Número con código QR y API oficial al mismo tiempo
Cuando el mismo teléfono de la empresa está vinculado por los dos canales (los números gemelos, que el panel muestra como un solo canal), la API también lo cuenta como un número: sin conexao_id, se elige normalmente si es el único. Informar el id de cualquiera de los dos canales tiene el mismo efecto: la conversación abierta del cliente se encuentra por ambos, la conversación nueva nace en el canal principal y cada mensaje sale por el canal más adecuado en ese momento (la API oficial dentro de la ventana de 24 horas, el código QR fuera de ella, con el otro como respaldo). En conexoes/listar, los dos canales aparecen vinculados por el campo gemea_id, y el principal viene con principal: true.
Teléfono
Rige la misma regla de los campos de teléfono del panel (vea Formato de las solicitudes):
| Usted envía | La API usa |
|---|---|
5544999990000 | 5544999990000 |
44999990000 o (44) 99999-0000 | 5544999990000: sin el +, el número es brasileño y recibe el 55. |
+351 912 345 678 | 351912345678: otro país entra siempre con el + adelante. |
4499990000 | 554499990000: fijo o celular antiguo, 10 dígitos con código de área. |
44899990000 | 400 telefone_invalido: con 11 dígitos, el tercero debe ser el 9 del celular. |
12345 | 400 telefone_invalido. |
Para localizar la conversación abierta, las formas con y sin el noveno dígito (5544999990000 y 554499990000) son la misma persona: no hace falta adivinar cuál de las dos usa el WhatsApp del cliente.
Aviso: la conversación nueva nace cerrada
Un envío a quien no tenía conversación abierta en ese número se trata como aviso, y no como atención. Es la misma regla de la Automatización de envíos, y rige para las tres rutas de envío:
- ChatISP crea la conversación, con protocolo nuevo, y envía el mensaje.
- Enseguida, la conversación se marca como cerrada. No entra en la cola, no se distribuye a nadie y no ocupa a ningún agente. El webhook
conversa_encerradasale conmotivoigual aaviso. - Si el cliente responde, la respuesta abre una atención normal, con protocolo propio, que sigue el flujo, la IA y la cola configurados, como cualquier mensaje recibido.
Así, un aviso de mantenimiento para 500 suscriptores no llena la cola con 500 conversaciones paradas: solo quien responde se convierte en atención.
| Situación antes del envío | Qué sucede |
|---|---|
| Ninguna conversación abierta del teléfono en el número | Conversación nueva, mensaje enviado, conversación cerrada. La respuesta trae el protocolo nuevo. |
Conversación abierta en cualquier estado (cola, IA, flujo, con agente), incluso abierta poco antes por atendimentos/abrir | El mensaje entra en esa conversación, que sigue en el estado en que estaba. El agente ve el mensaje en la pantalla. |
conversa_id o protocolo de una conversación cerrada | No se reabre. Si el teléfono tiene otra conversación abierta en el número, el mensaje entra en ella; si no, rige la regla del aviso (conversación nueva cerrada). |
¿Quiere que el equipo dé seguimiento a la respuesta? Llame a atendimentos/abrir antes del envío. La conversación pasa a existir en la cola (o con el agente elegido) y el envío siguiente entra en ella, sin tratarse como aviso.
El cierre automático del aviso no envía el mensaje de atención cerrada ni la encuesta de satisfacción: esos mensajes solo salen cuando hubo atención de verdad.
Ventana de 24 horas de la API oficial
En la conexión por la API oficial de Meta, el texto libre y los archivos solo se aceptan hasta 24 horas después del último mensaje del cliente. Cada mensaje nuevo del cliente reabre la ventana por otras 24 horas. Fuera de ella, Meta solo acepta plantillas aprobadas.
| Tipo de número | Fuera de la ventana, un mensagens/enviar o mensagens/enviar_arquivo... |
|---|---|
| Solo código QR | No hay ventana: el envío sigue normalmente. |
| Solo API oficial | Es rechazado por Meta. La respuesta es 409 janela_fechada, y el mensaje queda registrado en la conversación con status erro. |
| Código QR y API oficial (gemelos) | Sale por el código QR. Solo devuelve janela_fechada si el código QR tampoco logra entregar. |
Al recibir janela_fechada, use mensagens/enviar_modelo para hablar con el cliente. Cuando responda, la ventana se abre de nuevo y el texto libre vuelve a funcionar.
{
"ok": false,
"error": "janela_fechada",
"conversa_id": 18350,
"protocolo": "20261001-0047",
"mensagem_id": 402917,
"mensagem": {
"id": 402917,
"conversa_id": 18350,
"direcao": "enviada",
"autor": "sistema",
"autor_nome": "",
"tipo": "texto",
"conteudo": "Su visita técnica fue reprogramada para el viernes.",
"transcricao": null,
"arquivo": null,
"status": "erro",
"erro": "Re-engagement message",
"responde_a": null,
"editada": false,
"apagada": false,
"criada_em": "2026-10-01T10:02:11-03:00"
}
}Enviar texto
/api_publica.php?r=mensagens/enviar
Envía un mensaje de texto al cliente. Puede citar (responder a) un mensaje de la conversación, como la función de responder de WhatsApp.
Permiso: Enviar mensajes.
| Parámetro | Tipo | Obligatorio | Descripción |
|---|---|---|---|
texto | string | Sí | De 1 a 4096 caracteres; los espacios de los extremos se eliminan. Acepta el formato de WhatsApp (*negrita*, _cursiva_, ~tachado~) y saltos de línea (\n en el JSON). |
telefone | string | Un destino | Teléfono del cliente. Vea A quién va el mensaje. |
conexao_id | entero | Condicional | Número de WhatsApp a usar, junto con telefone. Obligatorio cuando hay más de un número conectado. Se omite cuando la clave tiene Número de origen; si viene en la llamada, vale el de la llamada. |
conversa_id o protocolo | entero / string | Un destino | Envía a esa conversación, en lugar de telefone. |
responder_a | entero | No | id de un mensaje de la misma conversación para citar. Si no se encuentra en esa conversación o no llegó a WhatsApp (por ejemplo, un mensaje con error), el texto sale normalmente, sin la cita. |
nome | string | No | Nombre del cliente, hasta 190 caracteres, usado solo cuando se crea una conversación nueva. Si el equipo ya guardó un nombre para ese contacto (en el panel o por contatos/renomear), prevalece el nombre guardado. |
# por teléfono (con un único número de WhatsApp, conexao_id es prescindible) curl -s -X POST "https://atendimento.seuprovedor.com.br/api_publica.php?r=mensagens/enviar" \ -H "Authorization: Bearer $CHATISP_CHAVE" \ -H "Content-Type: application/json" \ -d '{ "telefone": "5544999990000", "nome": "Maria Souza", "texto": "¡Hola, Maria! Su instalación está confirmada para mañana, entre las 8 y las 12 h." }' # en una conversación abierta, citando el último mensaje del cliente curl -s -X POST "https://atendimento.seuprovedor.com.br/api_publica.php?r=mensagens/enviar" \ -H "Authorization: Bearer $CHATISP_CHAVE" \ -H "Content-Type: application/json" \ -d '{"protocolo": "20261001-0042", "responder_a": 402850, "texto": "Recibimos la foto, ¡gracias!"}'
<?php $ch = curl_init('https://atendimento.seuprovedor.com.br/api_publica.php?r=mensagens/enviar'); curl_setopt_array($ch, [ CURLOPT_POST => true, CURLOPT_RETURNTRANSFER => true, CURLOPT_TIMEOUT => 60, CURLOPT_HTTPHEADER => [ 'Authorization: Bearer ' . getenv('CHATISP_CHAVE'), 'Content-Type: application/json', ], CURLOPT_POSTFIELDS => json_encode([ 'telefone' => '5544999990000', 'nome' => 'Maria Souza', 'texto' => '¡Hola, Maria! Su instalación está confirmada para mañana, entre las 8 y las 12 h.', ]), ]); $corpo = json_decode(curl_exec($ch), true); $status = curl_getinfo($ch, CURLINFO_HTTP_CODE); curl_close($ch); if ($corpo['ok'] ?? false) { echo 'Protocolo ', $corpo['protocolo'], ', mensaje ', $corpo['mensagem_id'], PHP_EOL; } elseif (($corpo['error'] ?? '') === 'janela_fechada') { // API oficial fuera de las 24 horas: envíe una plantilla aprobada } else { error_log("ChatISP $status: " . ($corpo['error'] ?? '?') . ' ' . ($corpo['detalhe'] ?? '')); }
import os, requests r = requests.post("https://atendimento.seuprovedor.com.br/api_publica.php", params={"r": "mensagens/enviar"}, headers={"Authorization": "Bearer " + os.environ["CHATISP_CHAVE"]}, json={ "telefone": "5544999990000", "nome": "Maria Souza", "texto": "¡Hola, Maria! Su instalación está confirmada para mañana, entre las 8 y las 12 h.", }, timeout=60) corpo = r.json() if corpo.get("ok"): print("Protocolo", corpo["protocolo"]) else: print(r.status_code, corpo.get("error"), corpo.get("detalhe", ""))
Respuesta de éxito:
{
"ok": true,
"conversa_id": 18350,
"protocolo": "20261001-0047",
"mensagem_id": 402917,
"mensagem": {
"id": 402917,
"conversa_id": 18350,
"direcao": "enviada",
"autor": "sistema",
"autor_nome": "",
"tipo": "texto",
"conteudo": "¡Hola, Maria! Su instalación está confirmada para mañana, entre las 8 y las 12 h.",
"transcricao": null,
"arquivo": null,
"status": "enviada",
"erro": null,
"responde_a": null,
"editada": false,
"apagada": false,
"criada_em": "2026-10-01T10:02:11-03:00"
}
}| Campo | Descripción |
|---|---|
conversa_id | Conversación usada o creada. Sirve para conversas/consultar, conversas/mensagens y las rutas de gestión. |
protocolo | Protocolo de la conversación, el mismo que el equipo ve en el panel. |
mensagem_id | Identificador del mensaje registrado. Es el id que aparece después en los webhooks mensagem_status (entregado, leído). |
mensagem | El mensaje completo, en el formato de mensaje de la API. Justo después del envío el status es enviada; entregado y leído llegan después. |
Errores específicos: texto_invalido, janela_fechada, envio, además de los errores de destino (telefone_invalido, conexao_invalida, sem_conexao, conexao_id_obrigatorio, conexao_desconectada, nao_encontrado). Vea Errores por ruta.
Enviar plantilla de la API oficial
/api_publica.php?r=mensagens/enviar_modelo
Envía una plantilla de mensaje (template) aprobada por Meta. Es la única forma de iniciar o retomar la conversación por la API oficial fuera de la ventana de 24 horas: recordatorio de factura, aviso de mantenimiento, confirmación de visita técnica. Las reglas de destino y de aviso son las mismas de Enviar texto.
Permiso: Enviar mensajes. Solo API oficial
| Parámetro | Tipo | Obligatorio | Descripción |
|---|---|---|---|
modelo_id | entero | Uno de los dos | Identificador de la plantilla en ChatISP (campo id de modelos/listar). Tiene prioridad sobre modelo. |
modelo | string | Uno de los dos | Nombre de la plantilla exactamente como está registrada en Meta (por ejemplo, lembrete_fatura). |
idioma | string | No | Código del idioma de la plantilla, como lo usa Meta (por ejemplo, pt_BR, es, en_US). Úselo cuando el mismo nombre existe en más de un idioma; sin él, se usa el primero sincronizado. |
variaveis | lista de strings | Si la plantilla las tiene | Valores de {{1}}, {{2}}... del cuerpo de la plantilla, en orden. Ninguno puede estar vacío; cada valor se corta en 500 caracteres; los valores sobrantes se ignoran. |
telefone + conexao_id | Un destino | Como en Enviar texto, pero contando solo los números que tienen API oficial. conexao_id se omite cuando la clave tiene Número de origen. Puede ser el id de cualquiera de los canales de un número gemelo: la API usa su canal oficial. | |
conversa_id o protocolo | Un destino | La conversación debe ser de un número que tenga API oficial (sola o gemela del código QR). | |
nome | string | No | Nombre del cliente para una conversación nueva (hasta 190 caracteres). |
Qué plantillas se pueden enviar
- Aprobada por Meta (
statusigual aAPPROVED) y de la cuenta de la misma conexión. Plantilla de otro número, pendiente o rechazada: 400modelo_invalido. - Sin encabezado de medios. Plantillas con encabezado de imagen, video o documento: 400
modelo_com_midia. Se acepta el encabezado de texto fijo. - La API completa solo las variables del cuerpo. Las variables del encabezado o de los botones no se completan, y Meta rechaza el envío (502
envio).
En modelos/listar, el campo enviavel ya indica si la plantilla sirve para esta ruta, y variaveis indica cuántos valores pide. Las plantillas se crean y aprueban en el Administrador de WhatsApp de Meta; después de aprobar una plantilla nueva, use Sincronizar con Meta en la pantalla Conexiones para que ChatISP la conozca.
# Plantilla "lembrete_fatura": "¡Hola, {{1}}! Su factura de {{2}} vence el {{3}}." curl -s -X POST "https://atendimento.seuprovedor.com.br/api_publica.php?r=mensagens/enviar_modelo" \ -H "Authorization: Bearer $CHATISP_CHAVE" \ -H "Content-Type: application/json" \ -d '{ "telefone": "5544999990000", "modelo": "lembrete_fatura", "idioma": "pt_BR", "variaveis": ["Maria", "R$ 99,90", "10/10/2026"] }' # lo mismo en formulario: listas con corchetes curl -s -X POST "https://atendimento.seuprovedor.com.br/api_publica.php" \ -H "Authorization: Bearer $CHATISP_CHAVE" \ --data-urlencode "r=mensagens/enviar_modelo" \ --data-urlencode "telefone=5544999990000" \ --data-urlencode "modelo_id=12" \ --data-urlencode "variaveis[]=Maria" \ --data-urlencode "variaveis[]=R$ 99,90" \ --data-urlencode "variaveis[]=10/10/2026"
<?php $ch = curl_init('https://atendimento.seuprovedor.com.br/api_publica.php?r=mensagens/enviar_modelo'); curl_setopt_array($ch, [ CURLOPT_POST => true, CURLOPT_RETURNTRANSFER => true, CURLOPT_TIMEOUT => 60, CURLOPT_HTTPHEADER => [ 'Authorization: Bearer ' . getenv('CHATISP_CHAVE'), 'Content-Type: application/json', ], CURLOPT_POSTFIELDS => json_encode([ 'telefone' => '5544999990000', 'modelo' => 'lembrete_fatura', 'idioma' => 'pt_BR', 'variaveis' => ['Maria', 'R$ 99,90', '10/10/2026'], ]), ]); $corpo = json_decode(curl_exec($ch), true); curl_close($ch); if (($corpo['error'] ?? '') === 'variaveis_invalidas') { echo 'La plantilla pide ', $corpo['esperadas'] ?? '?', ' variables', PHP_EOL; }
import os, requests r = requests.post("https://atendimento.seuprovedor.com.br/api_publica.php", params={"r": "mensagens/enviar_modelo"}, headers={"Authorization": "Bearer " + os.environ["CHATISP_CHAVE"]}, json={ "telefone": "5544999990000", "modelo": "lembrete_fatura", "idioma": "pt_BR", "variaveis": ["Maria", "R$ 99,90", "10/10/2026"], }, timeout=60) print(r.status_code, r.json())
La respuesta de éxito tiene el mismo formato de mensagens/enviar. En el historial de la conversación (y en el campo mensagem.conteudo), la plantilla aparece con el texto del cuerpo ya completado con las variables.
{"ok": false, "error": "variaveis_invalidas", "esperadas": 3}{"ok": false, "error": "conexao_invalida", "tipo_esperado": "meta"}{"ok": false, "error": "parametro_obrigatorio", "campo": "modelo_id|modelo"}Errores específicos: parametro_obrigatorio, modelo_invalido, modelo_com_midia, variaveis_invalidas, envio, además de los errores de destino.
El envío de plantillas por la API oficial lo cobra Meta, según su tabla de precios. El cobro y la franquicia de la API oficial se explican en Conexiones.
Enviar archivo
/api_publica.php?r=mensagens/enviar_arquivo
Envía una imagen, video, audio o documento (boleto en PDF, contrato, planilla, foto de la instalación), con leyenda opcional. El archivo puede ir de dos formas: carga multipart (recomendada, sobre todo para archivos grandes) o base64 dentro del JSON (práctica cuando su sistema ya tiene el archivo en memoria). Las reglas de destino, de aviso y de la ventana de 24 horas son las mismas de Enviar texto.
Permiso: Enviar mensajes.
| Parámetro | Tipo | Obligatorio | Descripción |
|---|---|---|---|
arquivo | archivo (multipart) | Uno de los dos | El archivo, en un envío multipart/form-data. |
arquivo_base64 | string | Uno de los dos | El contenido en base64, en un cuerpo JSON o de formulario. También acepta el formato data: (por ejemplo, data:application/pdf;base64,JVBERi0...): el prefijo se descarta. |
nome_arquivo | string | Recomendado en base64 | Nombre que el cliente ve en el documento (hasta 200 caracteres), por ejemplo boleto-outubro.pdf. En multipart, cuando se omite, se usa el nombre del archivo enviado. Sin nombre, el archivo sale como arquivo.<extensión>. |
legenda | string | No | Texto que acompaña al archivo, hasta 1024 caracteres. |
telefone + conexao_id, o conversa_id / protocolo | Un destino | Vea A quién va el mensaje. conexao_id se omite cuando la clave tiene Número de origen. | |
nome | string | No | Nombre del cliente para una conversación nueva (hasta 190 caracteres). |
Tamaño y tipos aceptados
Límite: 45 MB por archivo (47.185.920 bytes). El tipo se identifica por el contenido real del archivo, como en el panel, y no por la extensión ni por el Content-Type declarado:
| Contenido detectado | Sale en WhatsApp como | tipo en el mensaje |
|---|---|---|
| JPEG, PNG, WebP, GIF | Imagen | imagem |
| MP4, 3GP | Video | video |
| OGG, MP3, M4A/MP4 de audio, AAC, WAV, WebM de audio | Audio | audio |
| Cualquier otro (PDF, DOCX, XLSX, CSV, TXT, ZIP...) | Documento, con el nombre del archivo | documento |
| Ejecutables y scripts (EXE, MSI, binarios, scripts de shell) | Rechazado: 415 tipo_nao_suportado, con el tipo detectado en mimetype. | |
- Por el código QR, el audio que no sea OGG/Opus llega al cliente como archivo (documento), para no convertirse en una nota de voz rota. El registro del mensaje muestra el
tipocon el que realmente salió. - La API oficial de Meta tiene límites propios por tipo de medio, mucho menores que 45 MB para imagen, video y audio. Si el canal rechaza el archivo, la respuesta es 502
envio, con el motivo endetalhe. - En base64, el cuerpo de la solicitud queda cerca de un tercio más grande que el archivo. Para archivos de decenas de megabytes, prefiera multipart.
# multipart: el campo del archivo se llama "arquivo" curl -s -X POST "https://atendimento.seuprovedor.com.br/api_publica.php?r=mensagens/enviar_arquivo" \ -H "Authorization: Bearer $CHATISP_CHAVE" \ -F "telefone=5544999990000" \ -F "legenda=Le enviamos el boleto de octubre. Vence el 10/10." \ -F "arquivo=@/tmp/boleto-outubro.pdf" # base64 en el JSON (el base64 de Linux corta líneas: use -w0) curl -s -X POST "https://atendimento.seuprovedor.com.br/api_publica.php?r=mensagens/enviar_arquivo" \ -H "Authorization: Bearer $CHATISP_CHAVE" \ -H "Content-Type: application/json" \ -d "{\"protocolo\": \"20261001-0042\", \"nome_arquivo\": \"boleto-outubro.pdf\", \"legenda\": \"Le enviamos el boleto de octubre.\", \"arquivo_base64\": \"$(base64 -w0 /tmp/boleto-outubro.pdf)\"}"
<?php $url = 'https://atendimento.seuprovedor.com.br/api_publica.php?r=mensagens/enviar_arquivo'; $chave = getenv('CHATISP_CHAVE'); // 1) multipart con CURLFile (no defina Content-Type: curl arma el boundary) $ch = curl_init($url); curl_setopt_array($ch, [ CURLOPT_POST => true, CURLOPT_RETURNTRANSFER => true, CURLOPT_TIMEOUT => 120, CURLOPT_HTTPHEADER => ['Authorization: Bearer ' . $chave], CURLOPT_POSTFIELDS => [ 'telefone' => '5544999990000', 'legenda' => 'Le enviamos el boleto de octubre.', 'arquivo' => new CURLFile('/tmp/boleto-outubro.pdf', 'application/pdf', 'boleto-outubro.pdf'), ], ]); $corpo = json_decode(curl_exec($ch), true); curl_close($ch); // 2) base64 en el JSON (archivo generado en memoria, sin tocar el disco) $pdf = gerarBoletoPdf(); // su código $ch = curl_init($url); curl_setopt_array($ch, [ CURLOPT_POST => true, CURLOPT_RETURNTRANSFER => true, CURLOPT_TIMEOUT => 120, CURLOPT_HTTPHEADER => ['Authorization: Bearer ' . $chave, 'Content-Type: application/json'], CURLOPT_POSTFIELDS => json_encode([ 'conversa_id' => 18342, 'nome_arquivo' => 'boleto-outubro.pdf', 'arquivo_base64' => base64_encode($pdf), ]), ]); $corpo = json_decode(curl_exec($ch), true); curl_close($ch);
import base64, os, requests URL = "https://atendimento.seuprovedor.com.br/api_publica.php" CAB = {"Authorization": "Bearer " + os.environ["CHATISP_CHAVE"]} # 1) multipart: files= arma el envío; no defina Content-Type a mano with open("/tmp/boleto-outubro.pdf", "rb") as f: r = requests.post(URL, params={"r": "mensagens/enviar_arquivo"}, headers=CAB, data={"telefone": "5544999990000", "legenda": "Le enviamos el boleto de octubre."}, files={"arquivo": ("boleto-outubro.pdf", f, "application/pdf")}, timeout=120) print(r.status_code, r.json()) # 2) base64 en el JSON with open("/tmp/foto-instalacao.jpg", "rb") as f: conteudo = base64.b64encode(f.read()).decode() r = requests.post(URL, params={"r": "mensagens/enviar_arquivo"}, headers=CAB, json={ "protocolo": "20261001-0042", "nome_arquivo": "foto-instalacao.jpg", "legenda": "Instalación concluida.", "arquivo_base64": conteudo, }, timeout=120) print(r.status_code, r.json())
Respuesta de éxito (mismo formato de las otras rutas de envío; aquí la legenda se convierte en el conteudo del mensaje y el bloque arquivo ya trae la ruta de descarga):
{
"ok": true,
"conversa_id": 18342,
"protocolo": "20261001-0042",
"mensagem_id": 402990,
"mensagem": {
"id": 402990,
"conversa_id": 18342,
"direcao": "enviada",
"autor": "sistema",
"autor_nome": "",
"tipo": "documento",
"conteudo": "Le enviamos el boleto de octubre.",
"transcricao": null,
"arquivo": {
"nome": "boleto-outubro.pdf",
"mimetype": "application/pdf",
"download": "mensagens/arquivo?id=402990"
},
"status": "enviada",
"erro": null,
"responde_a": null,
"editada": false,
"apagada": false,
"criada_em": "2026-10-01T10:15:40-03:00"
}
}{"ok": false, "error": "arquivo_grande", "maximo_bytes": 47185920}{"ok": false, "error": "tipo_nao_suportado", "mimetype": "application/x-dosexec"}error | HTTP | Causa |
|---|---|---|
parametro_obrigatorio | 400 | No vino ni arquivo ni arquivo_base64 (campo igual a arquivo|arquivo_base64). |
arquivo_invalido | 400 | Base64 mal formado, archivo vacío o carga interrumpida. |
arquivo_grande | 413 | Más de 45 MB, o más que el límite de carga del servidor en el envío multipart. maximo_bytes trae el límite (en el base64 rechazado de inmediato por el tamaño del texto, el campo puede no venir). |
tipo_nao_suportado | 415 | Ejecutable o script. |
legenda_invalida | 400 | Leyenda con más de 1024 caracteres. |
arquivo | 500 | El servidor no pudo guardar el archivo. Intente de nuevo; si persiste, contacte al soporte. |
El archivo se valida antes del destino: un archivo rechazado nunca abre conversación. Después vienen los errores de destino y los de envío (janela_fechada, envio).
Descargar el archivo de un mensaje
/api_publica.php?r=mensagens/arquivo&id=<mensagem_id>
Devuelve el archivo de un mensaje: foto del router que envió el cliente, audio, comprobante de pago, documento enviado por el equipo. Vale para mensajes recibidos y enviados; las notas internas del equipo nunca se entregan.
Permiso: Consultar conversaciones.
| Parámetro | Tipo | Obligatorio | Descripción |
|---|---|---|---|
id | entero | Sí | id del mensaje. Todo mensaje con archivo trae, en el bloque arquivo, el campo download ya listo (mensagens/arquivo?id=402990): basta con agregarlo a la dirección de la API como valor de r. |
Respuesta
En caso de éxito, el cuerpo es el archivo en bruto, fuera del sobre JSON, con HTTP 200 y estos encabezados:
| Encabezado | Valor |
|---|---|
Content-Type | El tipo real para JPEG, PNG, GIF, WebP y PDF; application/octet-stream para los demás (el tipo original está en arquivo.mimetype del mensaje). |
Content-Disposition | inline para imágenes y PDF, attachment para el resto, siempre con filename="..." igual al nombre original del archivo. |
Content-Length | Tamaño en bytes. |
Cache-Control | private, no-store: no lo guarde en cachés compartidas. |
X-Content-Type-Options | nosniff. |
En los errores, la respuesta vuelve a ser JSON (Content-Type: application/json). Verifique el status HTTP antes de guardar el cuerpo como archivo.
# -f falla en error HTTP (no guarda el JSON de error); -OJ usa el nombre del Content-Disposition
curl -sf -OJ "https://atendimento.seuprovedor.com.br/api_publica.php?r=mensagens/arquivo&id=402850" \
-H "Authorization: Bearer $CHATISP_CHAVE"<?php $destino = fopen('/var/anexos/402850.bin', 'wb'); $ch = curl_init('https://atendimento.seuprovedor.com.br/api_publica.php?r=mensagens/arquivo&id=402850'); curl_setopt_array($ch, [ CURLOPT_FILE => $destino, CURLOPT_TIMEOUT => 120, CURLOPT_HTTPHEADER => ['Authorization: Bearer ' . getenv('CHATISP_CHAVE')], ]); curl_exec($ch); $status = curl_getinfo($ch, CURLINFO_HTTP_CODE); curl_close($ch); fclose($destino); if ($status !== 200) { // el archivo guardado contiene el JSON de error (nao_encontrado, arquivo_indisponivel...) echo file_get_contents('/var/anexos/402850.bin'); unlink('/var/anexos/402850.bin'); }
import os, re, requests r = requests.get("https://atendimento.seuprovedor.com.br/api_publica.php", params={"r": "mensagens/arquivo", "id": 402850}, headers={"Authorization": "Bearer " + os.environ["CHATISP_CHAVE"]}, stream=True, timeout=120) if r.status_code != 200: print("Error:", r.json()) else: nome = re.search(r'filename="([^"]+)"', r.headers.get("Content-Disposition", "")) with open(nome.group(1) if nome else "arquivo.bin", "wb") as f: for pedaco in r.iter_content(65536): f.write(pedaco)
error | HTTP | Causa |
|---|---|---|
parametro_obrigatorio | 400 | Faltó id. |
parametro_invalido | 400 | id no es un número entero. |
nao_encontrado | 404 | El mensaje no existe, no tiene archivo, es una nota interna o fue eliminado. |
arquivo_indisponivel | 410 | El mensaje existe, pero el archivo ya no está disponible en el servidor. |
Si su integración guarda los adjuntos (por ejemplo, en la ficha del cliente en el ERP), descargue el archivo justo después del webhook mensagem_recebida, en lugar de depender de que siga en el servidor para siempre.
Resultado del envío y fallas
Las tres rutas de envío responden de la misma manera. El mensaje se registra en la conversación siempre que la llamada pasa la validación, incluso cuando WhatsApp lo rechaza: así el equipo ve en el panel el intento con el aviso de error, y su sistema recibe el mensaje completo para registrarlo.
| HTTP | Cuerpo | Significado |
|---|---|---|
| 200 | ok: true, conversa_id, protocolo, mensagem_id, mensagem | El canal aceptó el mensaje. La entrega y la lectura llegan después, por el webhook mensagem_status o consultando la conversación. |
| 409 | error: janela_fechada + los mismos campos | La API oficial lo rechazó por estar fuera de la ventana de 24 horas. El mensaje queda con status: erro. |
| 502 | error: envio + los mismos campos + detalhe | El canal lo rechazó o no respondió (número sin WhatsApp, conexión caída en medio del envío, medio rechazado, plantilla rechazada por Meta). El motivo viene en detalhe y en mensagem.erro; mensagem.status es erro. |
| 4xx | error sin los campos de la conversación | Rechazo en la validación: no se registró ni se envió nada. |
{
"ok": false,
"error": "envio",
"conversa_id": 18350,
"protocolo": "20261001-0047",
"mensagem_id": 402917,
"mensagem": {
"id": 402917,
"conversa_id": 18350,
"direcao": "enviada",
"autor": "sistema",
"autor_nome": "",
"tipo": "texto",
"conteudo": "¡Hola, Maria! Su instalación está confirmada para mañana, entre las 8 y las 12 h.",
"transcricao": null,
"arquivo": null,
"status": "erro",
"erro": "conexao",
"responde_a": null,
"editada": false,
"apagada": false,
"criada_em": "2026-10-01T10:02:11-03:00"
},
"detalhe": "conexao"
}Incluso con falla, la conversación nueva de un aviso se cierra como de costumbre: el protocolo devuelto sigue siendo válido para consulta.
No repita un envío a ciegas. Después de un 502 o de un error de red (tiempo agotado, conexión cortada), el mensaje puede haber llegado al cliente aunque la respuesta no le haya llegado a usted. Antes de reenviar, consulte la conversación (conversas/mensagens con el conversa_id, o conversas/consultar por el teléfono) y verifique el status del último mensaje.
Envío masivo: respete la cuota por minuto de la clave (encabezados X-RateLimit-Limit y X-RateLimit-Remaining; el 429 trae Retry-After) y mantenga un ritmo de envío parecido al de la Automatización de envíos. Vea Límites y el script de Ejemplos.
Errores por ruta
Además de los errores generales (rota, autenticacao, chave_invalida, plano, cota_excedida, permissao, json_invalido, parametro_invalido), descritos en Códigos de error:
error | HTTP | enviar | enviar_modelo | enviar_arquivo | arquivo |
|---|---|---|---|---|---|
metodo | 405 | sí | sí | sí | |
texto_invalido | 400 | sí | |||
parametro_obrigatorio | 400 | sí | sí | sí | |
arquivo_invalido, legenda_invalida | 400 | sí | |||
arquivo_grande | 413 | sí | |||
tipo_nao_suportado | 415 | sí | |||
conexao_invalida | 400 | sí | sí | sí | |
sem_conexao | 400 | sí | sí | sí | |
conexao_id_obrigatorio | 400 | sí | sí | sí | |
conexao_desconectada | 409 | sí | sí | sí | |
telefone_invalido | 400 | sí | sí | sí | |
identificador_obrigatorio | 400 | sí | sí | sí | |
nao_encontrado | 404 | sí | sí | sí | sí |
modelo_invalido, modelo_com_midia, variaveis_invalidas | 400 | sí | |||
janela_fechada | 409 | sí | sí | ||
envio | 502 | sí | sí | sí | |
arquivo | 500 | sí | |||
arquivo_indisponivel | 410 | sí |
Se devuelve la primera falla encontrada. Orden de validación: texto (o plantilla y variables, o archivo), después el destino (conversación, o conexión y luego teléfono), y solo entonces el envío.
parametro_obrigatorio, parametro_invalido y texto_invalido en otras rutas traen campo con el nombre del parámetro.