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

URL base
https://atendimento.seuprovedor.com.br/api_publica.php?r=<recurso>/<acao>
RutaMétodoPermiso de la claveResumen
mensagens/enviarPOSTEnviar mensajesTexto libre para un teléfono o para una conversación, con cita opcional.
mensagens/enviar_modeloPOSTEnviar mensajesPlantilla (template) aprobada por Meta, con variables. Funciona fuera de la ventana de 24 horas.
mensagens/enviar_arquivoPOSTEnviar mensajesImagen, video, audio o documento, por carga (multipart) o en base64 dentro del JSON, con leyenda.
mensagens/arquivoGET o POSTConsultar conversacionesDescarga 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 autor igual a sistema en 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 status erro); una conversación nueva dispara conversa_criada; el cierre automático del aviso dispara conversa_encerrada con motivo igual a aviso. 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:

FormaParámetrosCómo se resuelve el destino
Por teléfonotelefone y, cuando sea necesario, conexao_idLa 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ónconversa_id o protocoloConversació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ónResultado
conexao_id informado y conectadoUsa esa conexión.
conexao_id inexistente400 conexao_invalida.
conexao_id desconectado409 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 conectadoUsa ese número. Es el caso de quien tiene un solo WhatsApp.
Sin conexao_id y ningún número conectado400 sem_conexao.
Sin conexao_id y más de un número conectadoUsa 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íaLa API usa
55449999900005544999990000
44999990000 o (44) 99999-00005544999990000: sin el +, el número es brasileño y recibe el 55.
+351 912 345 678351912345678: otro país entra siempre con el + adelante.
4499990000554499990000: fijo o celular antiguo, 10 dígitos con código de área.
44899990000400 telefone_invalido: con 11 dígitos, el tercero debe ser el 9 del celular.
12345400 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:

  1. ChatISP crea la conversación, con protocolo nuevo, y envía el mensaje.
  2. 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_encerrada sale con motivo igual a aviso.
  3. 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íoQué sucede
Ninguna conversación abierta del teléfono en el númeroConversació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/abrirEl 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 cerradaNo 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úmeroFuera de la ventana, un mensagens/enviar o mensagens/enviar_arquivo...
Solo código QRNo hay ventana: el envío sigue normalmente.
Solo API oficialEs 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.

respuesta JSON (HTTP 409)
{
  "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

POST /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ámetroTipoObligatorioDescripción
textostringSí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).
telefonestringUn destinoTeléfono del cliente. Vea A quién va el mensaje.
conexao_identeroCondicionalNú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 protocoloentero / stringUn destinoEnvía a esa conversación, en lugar de telefone.
responder_aenteroNoid 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.
nomestringNoNombre 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.
terminal
# 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
<?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'] ?? ''));
}
Python
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:

respuesta JSON (HTTP 200)
{
  "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"
  }
}
CampoDescripción
conversa_idConversación usada o creada. Sirve para conversas/consultar, conversas/mensagens y las rutas de gestión.
protocoloProtocolo de la conversación, el mismo que el equipo ve en el panel.
mensagem_idIdentificador del mensaje registrado. Es el id que aparece después en los webhooks mensagem_status (entregado, leído).
mensagemEl 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

POST /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ámetroTipoObligatorioDescripción
modelo_identeroUno de los dosIdentificador de la plantilla en ChatISP (campo id de modelos/listar). Tiene prioridad sobre modelo.
modelostringUno de los dosNombre de la plantilla exactamente como está registrada en Meta (por ejemplo, lembrete_fatura).
idiomastringNoCó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.
variaveislista de stringsSi la plantilla las tieneValores 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_idUn destinoComo 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 protocoloUn destinoLa conversación debe ser de un número que tenga API oficial (sola o gemela del código QR).
nomestringNoNombre del cliente para una conversación nueva (hasta 190 caracteres).

Qué plantillas se pueden enviar

  • Aprobada por Meta (status igual a APPROVED) y de la cuenta de la misma conexión. Plantilla de otro número, pendiente o rechazada: 400 modelo_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.

terminal
# 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
<?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;
}
Python
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.

variables faltantes (HTTP 400)
{"ok": false, "error": "variaveis_invalidas", "esperadas": 3}
conexión sin API oficial (HTTP 400)
{"ok": false, "error": "conexao_invalida", "tipo_esperado": "meta"}
faltaron modelo y modelo_id (HTTP 400)
{"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

POST /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ámetroTipoObligatorioDescripción
arquivoarchivo (multipart)Uno de los dosEl archivo, en un envío multipart/form-data.
arquivo_base64stringUno de los dosEl 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_arquivostringRecomendado en base64Nombre 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>.
legendastringNoTexto que acompaña al archivo, hasta 1024 caracteres.
telefone + conexao_id, o conversa_id / protocoloUn destinoVea A quién va el mensaje. conexao_id se omite cuando la clave tiene Número de origen.
nomestringNoNombre 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 detectadoSale en WhatsApp comotipo en el mensaje
JPEG, PNG, WebP, GIFImagenimagem
MP4, 3GPVideovideo
OGG, MP3, M4A/MP4 de audio, AAC, WAV, WebM de audioAudioaudio
Cualquier otro (PDF, DOCX, XLSX, CSV, TXT, ZIP...)Documento, con el nombre del archivodocumento
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 tipo con 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 en detalhe.
  • 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.
terminal
# 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
<?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);
Python
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):

respuesta JSON (HTTP 200)
{
  "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"
  }
}
archivo demasiado grande (HTTP 413)
{"ok": false, "error": "arquivo_grande", "maximo_bytes": 47185920}
tipo rechazado (HTTP 415)
{"ok": false, "error": "tipo_nao_suportado", "mimetype": "application/x-dosexec"}
errorHTTPCausa
parametro_obrigatorio400No vino ni arquivo ni arquivo_base64 (campo igual a arquivo|arquivo_base64).
arquivo_invalido400Base64 mal formado, archivo vacío o carga interrumpida.
arquivo_grande413Má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_suportado415Ejecutable o script.
legenda_invalida400Leyenda con más de 1024 caracteres.
arquivo500El 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

GET POST /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ámetroTipoObligatorioDescripción
identeroSí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:

EncabezadoValor
Content-TypeEl 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-Dispositioninline para imágenes y PDF, attachment para el resto, siempre con filename="..." igual al nombre original del archivo.
Content-LengthTamaño en bytes.
Cache-Controlprivate, no-store: no lo guarde en cachés compartidas.
X-Content-Type-Optionsnosniff.

En los errores, la respuesta vuelve a ser JSON (Content-Type: application/json). Verifique el status HTTP antes de guardar el cuerpo como archivo.

terminal
# -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
<?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');
}
Python
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)
errorHTTPCausa
parametro_obrigatorio400Faltó id.
parametro_invalido400id no es un número entero.
nao_encontrado404El mensaje no existe, no tiene archivo, es una nota interna o fue eliminado.
arquivo_indisponivel410El 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.

HTTPCuerpoSignificado
200ok: true, conversa_id, protocolo, mensagem_id, mensagemEl canal aceptó el mensaje. La entrega y la lectura llegan después, por el webhook mensagem_status o consultando la conversación.
409error: janela_fechada + los mismos camposLa API oficial lo rechazó por estar fuera de la ventana de 24 horas. El mensaje queda con status: erro.
502error: envio + los mismos campos + detalheEl 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.
4xxerror sin los campos de la conversaciónRechazo en la validación: no se registró ni se envió nada.
falla de envío (HTTP 502)
{
  "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:

errorHTTPenviarenviar_modeloenviar_arquivoarquivo
metodo405sísísí
texto_invalido400sí
parametro_obrigatorio400sísísí
arquivo_invalido, legenda_invalida400sí
arquivo_grande413sí
tipo_nao_suportado415sí
conexao_invalida400sísísí
sem_conexao400sísísí
conexao_id_obrigatorio400sísísí
conexao_desconectada409sísísí
telefone_invalido400sísísí
identificador_obrigatorio400sísísí
nao_encontrado404sísísísí
modelo_invalido, modelo_com_midia, variaveis_invalidas400sí
janela_fechada409sísí
envio502sísísí
arquivo500sí
arquivo_indisponivel410sí

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.