Mensagens
Referência das quatro rotas que falam com o cliente pelo WhatsApp: enviar texto, enviar um modelo aprovado da API oficial da Meta, enviar um arquivo (imagem, vídeo, áudio ou documento) e baixar o arquivo de qualquer mensagem. Com parâmetros, exemplos em curl, PHP e Python, respostas, erros e as regras que valem por trás de cada envio: aviso que nasce encerrado, janela de 24 horas e falha de entrega.
Visão geral
Todas as rotas usam o endereço único da API, com a chave no cabeçalho Authorization: Bearer e a rota no parâmetro r (detalhes em Autenticação):
https://atendimento.seuprovedor.com.br/api_publica.php?r=<recurso>/<acao>
| Rota | Método | Permissão da chave | Resumo |
|---|---|---|---|
mensagens/enviar | POST | Enviar mensagens | Texto livre para um telefone ou para uma conversa, com citação opcional. |
mensagens/enviar_modelo | POST | Enviar mensagens | Modelo (template) aprovado pela Meta, com variáveis. Funciona fora da janela de 24 horas. |
mensagens/enviar_arquivo | POST | Enviar mensagens | Imagem, vídeo, áudio ou documento, por upload (multipart) ou em base64 no JSON, com legenda. |
mensagens/arquivo | GET ou POST | Consultar conversas | Baixa o arquivo de uma mensagem recebida ou enviada. |
Com corpo JSON, o parâmetro r precisa ir na URL (como nos exemplos desta página); com formulário (-d/multipart), ele pode ir na URL ou no próprio formulário.
O que acontece por trás de um envio
- Tudo vira conversa do painel. O envio entra na conversa aberta do cliente naquele número de WhatsApp ou cria uma nova, com protocolo próprio. A equipe vê a mensagem na tela de atendimento, na mesma linha do tempo das outras.
- A mensagem sai como mensagem do sistema: sem o nome de atendente em negrito no topo, com
autorigual asistemano histórico. - Rastro da chave. Cada conversa tocada pela API ganha um registro na linha do tempo com o nome da chave usada, para a equipe saber qual integração agiu.
- Validação antes do protocolo. Texto, modelo, variáveis, arquivo, conexão e telefone são conferidos antes de qualquer conversa ser aberta: uma chamada recusada com 4xx não deixa protocolo vazio para trás.
- Webhooks continuam valendo. Todo envio dispara
mensagem_enviada(inclusive quando falha, com statuserro); conversa nova disparaconversa_criada; o encerramento automático do aviso disparaconversa_encerradacommotivoigual aaviso. Veja Webhooks.
Os números que estas rotas pedem (conexao_id, modelo_id, nomes e idiomas dos modelos) vêm das rotas de Catálogos, liberadas para qualquer chave ativa: conexoes/listar e modelos/listar.
Para quem a mensagem vai
As três rotas de envio aceitam duas formas de dizer o destino. Use uma delas:
| Forma | Parâmetros | Como o destino é resolvido |
|---|---|---|
| Por telefone | telefone e, quando preciso, conexao_id | A API escolhe o número de WhatsApp (regra abaixo), procura a conversa aberta desse telefone nele e, se não houver, cria uma nova. |
| Por conversa | conversa_id ou protocolo | Conversa aberta (qualquer estado diferente de encerrada): a mensagem entra nela, pelo número dela. Conversa encerrada: ela não é reaberta; a mensagem vai para o mesmo telefone pelo mesmo número de WhatsApp, entrando na conversa aberta que existir ou numa conversa nova. |
Quando conversa_id ou protocolo vem na chamada, telefone e conexao_id são ignorados. Conversa inexistente responde 404 nao_encontrado. Conversa cujo número de WhatsApp foi excluído do sistema responde 400 conexao_invalida. Se o número da conversa estiver desconectado (nenhum dos seus canais, QR Code ou API oficial, no ar; em mensagens/enviar_modelo, o canal da API oficial), a resposta é 409 conexao_desconectada, antes de qualquer tentativa de envio.
Escolha do número de WhatsApp (conexao_id)
| Situação | Resultado |
|---|---|
conexao_id informado e conectado | Usa essa conexão. |
conexao_id inexistente | 400 conexao_invalida. |
conexao_id desconectado | 409 conexao_desconectada. Num número com QR Code e API oficial ao mesmo tempo, basta um dos dois canais estar no ar. |
Sem conexao_id e um único número conectado | Usa esse número. É o caso de quem tem um WhatsApp só. |
Sem conexao_id e nenhum número conectado | 400 sem_conexao. |
Sem conexao_id e mais de um número conectado | Usa o Número de origem definido na chave de API (Integrações, Chaves de API, campo Número de origem). Chave sem Número de origem: 400 conexao_id_obrigatorio, a API não escolhe por você. |
Número com QR Code e API oficial ao mesmo tempo
Quando o mesmo telefone da empresa está ligado pelos dois canais (os números gêmeos, que o painel mostra como um canal só), a API também o conta como um número: sem conexao_id, ele é escolhido normalmente se for o único. Informar o id de qualquer um dos dois canais dá no mesmo: a conversa aberta do cliente é encontrada pelos dois, a conversa nova nasce no canal principal e cada mensagem sai pelo canal mais adequado no momento (a API oficial dentro da janela de 24 horas, o QR Code fora dela, com o outro como reserva). Em conexoes/listar, os dois canais aparecem ligados pelo campo gemea_id, e o principal vem com principal: true.
Telefone
Vale a mesma regra dos campos de telefone do painel (veja Formato das requisições):
| Você envia | A API usa |
|---|---|
5544999990000 | 5544999990000 |
44999990000 ou (44) 99999-0000 | 5544999990000: sem o +, o número é brasileiro e ganha o 55. |
+351 912 345 678 | 351912345678: outro país entra sempre com o + na frente. |
4499990000 | 554499990000: fixo ou celular antigo, 10 dígitos com DDD. |
44899990000 | 400 telefone_invalido: com 11 dígitos, o terceiro precisa ser o 9 do celular. |
12345 | 400 telefone_invalido. |
Para localizar a conversa aberta, as formas com e sem o nono dígito (5544999990000 e 554499990000) são a mesma pessoa: não é preciso adivinhar qual das duas o WhatsApp do cliente usa.
Aviso: conversa nova nasce encerrada
Um envio para quem não tinha conversa aberta naquele número é tratado como aviso, e não como atendimento. É a mesma regra da Automação de envios, e vale para as três rotas de envio:
- O ChatISP cria a conversa, com protocolo novo, e envia a mensagem.
- Logo em seguida, a conversa é marcada como encerrada. Ela não entra na fila, não é distribuída a ninguém e não ocupa nenhum atendente. O webhook
conversa_encerradasai commotivoigual aaviso. - Se o cliente responder, a resposta abre um atendimento normal, com protocolo próprio, que segue o fluxo, a IA e a fila configurados, como qualquer mensagem recebida.
Assim, um aviso de manutenção para 500 assinantes não enche a fila com 500 conversas paradas: só quem responde vira atendimento.
| Situação antes do envio | O que acontece |
|---|---|
| Nenhuma conversa aberta do telefone no número | Conversa nova, mensagem enviada, conversa encerrada. A resposta traz o protocolo novo. |
Conversa aberta em qualquer estado (fila, IA, fluxo, com atendente), inclusive aberta pouco antes por atendimentos/abrir | A mensagem entra nessa conversa, que continua no estado em que estava. O atendente vê a mensagem na tela. |
conversa_id ou protocolo de uma conversa encerrada | Ela não é reaberta. Se o telefone tiver outra conversa aberta no número, a mensagem entra nela; senão, vale a regra do aviso (conversa nova encerrada). |
Quer que a equipe acompanhe a resposta? Chame atendimentos/abrir antes do envio. A conversa passa a existir na fila (ou com o atendente escolhido) e o envio seguinte entra nela, sem ser tratado como aviso.
O encerramento automático do aviso não envia a mensagem de chamado encerrado nem a pesquisa de satisfação: essas mensagens só saem quando houve atendimento de verdade.
Janela de 24 horas da API oficial
Na conexão pela API oficial da Meta, texto livre e arquivos só são aceitos até 24 horas depois da última mensagem do cliente. Cada mensagem nova do cliente reabre a janela por mais 24 horas. Fora dela, a Meta só aceita modelos aprovados.
| Tipo de número | Fora da janela, um mensagens/enviar ou mensagens/enviar_arquivo... |
|---|---|
| Só QR Code | Não há janela: o envio segue normalmente. |
| Só API oficial | É recusado pela Meta. A resposta é 409 janela_fechada, e a mensagem fica registrada na conversa com status erro. |
| QR Code e API oficial (gêmeos) | Sai pelo QR Code. Só volta janela_fechada se o QR Code também não conseguir entregar. |
Ao receber janela_fechada, use mensagens/enviar_modelo para falar com o cliente. Quando ele responder, a janela abre de novo e o texto livre volta 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": "Sua visita técnica foi remarcada para sexta-feira.",
"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
Envia uma mensagem de texto ao cliente. Pode citar (responder a) uma mensagem da conversa, como o recurso de responder do WhatsApp.
Permissão: Enviar mensagens.
| Parâmetro | Tipo | Obrigatório | Descrição |
|---|---|---|---|
texto | string | Sim | De 1 a 4096 caracteres; espaços nas pontas são removidos. Aceita a formatação do WhatsApp (*negrito*, _itálico_, ~riscado~) e quebras de linha (\n no JSON). |
telefone | string | Um destino | Telefone do cliente. Veja Para quem a mensagem vai. |
conexao_id | inteiro | Condicional | Número de WhatsApp a usar, junto com telefone. Obrigatório quando há mais de um número conectado. Dispensado quando a chave tem Número de origem; se vier na chamada, vale o da chamada. |
conversa_id ou protocolo | inteiro / string | Um destino | Envia para essa conversa, no lugar de telefone. |
responder_a | inteiro | Não | id de uma mensagem da mesma conversa para citar. Se ela não for encontrada nessa conversa ou não tiver chegado ao WhatsApp (por exemplo, uma mensagem com erro), o texto sai normalmente, sem a citação. |
nome | string | Não | Nome do cliente, até 190 caracteres, usado só quando uma conversa nova é criada. Se a equipe já salvou um nome para esse contato (no painel ou por contatos/renomear), o nome salvo prevalece. |
# por telefone (com um único número de WhatsApp, conexao_id é dispensável) 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": "Olá, Maria! Sua instalação está confirmada para amanhã, entre 8h e 12h." }' # numa conversa aberta, citando a última mensagem do 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": "Recebemos a foto, obrigado!"}'
<?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' => 'Olá, Maria! Sua instalação está confirmada para amanhã, entre 8h e 12h.', ]), ]); $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'], ', mensagem ', $corpo['mensagem_id'], PHP_EOL; } elseif (($corpo['error'] ?? '') === 'janela_fechada') { // API oficial fora das 24 horas: mande um modelo aprovado } 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": "Olá, Maria! Sua instalação está confirmada para amanhã, entre 8h e 12h.", }, timeout=60) corpo = r.json() if corpo.get("ok"): print("Protocolo", corpo["protocolo"]) else: print(r.status_code, corpo.get("error"), corpo.get("detalhe", ""))
Resposta de sucesso:
{
"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": "Olá, Maria! Sua instalação está confirmada para amanhã, entre 8h e 12h.",
"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 | Descrição |
|---|---|
conversa_id | Conversa usada ou criada. Serve para conversas/consultar, conversas/mensagens e as rotas de gestão. |
protocolo | Protocolo da conversa, o mesmo que a equipe vê no painel. |
mensagem_id | Identificador da mensagem registrada. É o id que aparece depois nos webhooks mensagem_status (entregue, lida). |
mensagem | A mensagem completa, no formato de mensagem da API. Logo após o envio o status é enviada; entregue e lida chegam depois. |
Erros específicos: texto_invalido, janela_fechada, envio, além dos erros de destino (telefone_invalido, conexao_invalida, sem_conexao, conexao_id_obrigatorio, conexao_desconectada, nao_encontrado). Veja Erros por rota.
Enviar modelo da API oficial
/api_publica.php?r=mensagens/enviar_modelo
Envia um modelo de mensagem (template) aprovado pela Meta. É o único jeito de iniciar ou retomar a conversa pela API oficial fora da janela de 24 horas: lembrete de fatura, aviso de manutenção, confirmação de visita técnica. As regras de destino e de aviso são as mesmas de Enviar texto.
Permissão: Enviar mensagens. Só API oficial
| Parâmetro | Tipo | Obrigatório | Descrição |
|---|---|---|---|
modelo_id | inteiro | Um dos dois | Identificador do modelo no ChatISP (campo id de modelos/listar). Tem prioridade sobre modelo. |
modelo | string | Um dos dois | Nome do modelo exatamente como cadastrado na Meta (por exemplo, lembrete_fatura). |
idioma | string | Não | Código do idioma do modelo, como a Meta usa (por exemplo, pt_BR, es, en_US). Use quando o mesmo nome existe em mais de um idioma; sem ele, vale o primeiro sincronizado. |
variaveis | lista de strings | Se o modelo tiver | Valores de {{1}}, {{2}}... do corpo do modelo, na ordem. Nenhum pode ser vazio; cada valor é cortado em 500 caracteres; valores a mais são ignorados. |
telefone + conexao_id | Um destino | Como em Enviar texto, mas contando só os números que têm API oficial. conexao_id é dispensado quando a chave tem Número de origem. Pode ser o id de qualquer um dos canais de um número gêmeo: a API usa o canal oficial dele. | |
conversa_id ou protocolo | Um destino | A conversa precisa ser de um número que tenha API oficial (sozinha ou gêmea do QR Code). | |
nome | string | Não | Nome do cliente para uma conversa nova (até 190 caracteres). |
Quais modelos podem ser enviados
- Aprovado pela Meta (
statusigual aAPPROVED) e da conta da mesma conexão. Modelo de outro número, pendente ou rejeitado: 400modelo_invalido. - Sem cabeçalho de mídia. Modelos com cabeçalho de imagem, vídeo ou documento: 400
modelo_com_midia. Cabeçalho de texto fixo é aceito. - A API preenche só as variáveis do corpo. Variáveis no cabeçalho ou em botões não são preenchidas, e a Meta recusa o envio (502
envio).
Em modelos/listar, o campo enviavel já diz se o modelo serve para esta rota, e variaveis diz quantos valores ele pede. Os modelos são criados e aprovados no Gerenciador do WhatsApp da Meta; depois de aprovar um modelo novo, use Sincronizar com a Meta na tela Conexões para o ChatISP passar a conhecê-lo.
# Modelo "lembrete_fatura": "Olá, {{1}}! Sua fatura de {{2}} vence em {{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"] }' # o mesmo em formulário: listas com colchetes 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 'O modelo pede ', $corpo['esperadas'] ?? '?', ' variáveis', 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())
A resposta de sucesso tem o mesmo formato de mensagens/enviar. No histórico da conversa (e no campo mensagem.conteudo), o modelo aparece com o texto do corpo já preenchido com as variáveis.
{"ok": false, "error": "variaveis_invalidas", "esperadas": 3}{"ok": false, "error": "conexao_invalida", "tipo_esperado": "meta"}{"ok": false, "error": "parametro_obrigatorio", "campo": "modelo_id|modelo"}Erros específicos: parametro_obrigatorio, modelo_invalido, modelo_com_midia, variaveis_invalidas, envio, além dos erros de destino.
O envio de modelos pela API oficial é cobrado pela Meta, conforme a tabela dela. A cobrança e a franquia da API oficial são explicadas em Conexões.
Enviar arquivo
/api_publica.php?r=mensagens/enviar_arquivo
Envia uma imagem, vídeo, áudio ou documento (boleto em PDF, contrato, planilha, foto da instalação), com legenda opcional. O arquivo pode ir de dois jeitos: upload multipart (recomendado, principalmente para arquivos grandes) ou base64 dentro do JSON (prático quando o seu sistema já tem o arquivo em memória). As regras de destino, de aviso e da janela de 24 horas são as mesmas de Enviar texto.
Permissão: Enviar mensagens.
| Parâmetro | Tipo | Obrigatório | Descrição |
|---|---|---|---|
arquivo | arquivo (multipart) | Um dos dois | O arquivo, num envio multipart/form-data. |
arquivo_base64 | string | Um dos dois | O conteúdo em base64, num corpo JSON ou de formulário. Aceita também o formato data: (por exemplo, data:application/pdf;base64,JVBERi0...): o prefixo é descartado. |
nome_arquivo | string | Recomendado no base64 | Nome que o cliente vê no documento (até 200 caracteres), por exemplo boleto-outubro.pdf. No multipart, quando omitido, vale o nome do arquivo enviado. Sem nome, o arquivo sai como arquivo.<extensão>. |
legenda | string | Não | Texto que acompanha o arquivo, até 1024 caracteres. |
telefone + conexao_id, ou conversa_id / protocolo | Um destino | Veja Para quem a mensagem vai. conexao_id é dispensado quando a chave tem Número de origem. | |
nome | string | Não | Nome do cliente para uma conversa nova (até 190 caracteres). |
Tamanho e tipos aceitos
Limite: 45 MB por arquivo (47.185.920 bytes). O tipo é identificado pelo conteúdo real do arquivo, como no painel, e não pela extensão nem pelo Content-Type declarado:
| Conteúdo detectado | Sai no WhatsApp como | tipo na mensagem |
|---|---|---|
| JPEG, PNG, WebP, GIF | Imagem | imagem |
| MP4, 3GP | Vídeo | video |
| OGG, MP3, M4A/MP4 de áudio, AAC, WAV, WebM de áudio | Áudio | audio |
| Qualquer outro (PDF, DOCX, XLSX, CSV, TXT, ZIP...) | Documento, com o nome do arquivo | documento |
| Executáveis e scripts (EXE, MSI, binários, scripts de shell) | Recusado: 415 tipo_nao_suportado, com o tipo detectado em mimetype. | |
- Pelo QR Code, áudio que não seja OGG/Opus chega ao cliente como arquivo (documento), para não virar uma nota de voz quebrada. O registro da mensagem mostra o
tipocom que ele realmente saiu. - A API oficial da Meta tem limites próprios por tipo de mídia, bem menores que 45 MB para imagem, vídeo e áudio. Se o canal recusar o arquivo, a resposta é 502
envio, com o motivo emdetalhe. - No base64, o corpo da requisição fica cerca de um terço maior que o arquivo. Para arquivos de dezenas de megabytes, prefira o multipart.
# multipart: o campo do arquivo se chama "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=Segue o boleto de outubro. Vencimento em 10/10." \ -F "arquivo=@/tmp/boleto-outubro.pdf" # base64 no JSON (o base64 do Linux quebra linha: 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\": \"Segue o boleto de outubro.\", \"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 com CURLFile (não defina Content-Type: o curl monta o 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' => 'Segue o boleto de outubro.', 'arquivo' => new CURLFile('/tmp/boleto-outubro.pdf', 'application/pdf', 'boleto-outubro.pdf'), ], ]); $corpo = json_decode(curl_exec($ch), true); curl_close($ch); // 2) base64 no JSON (arquivo gerado em memória, sem tocar o disco) $pdf = gerarBoletoPdf(); // seu 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= monta o envio; não defina Content-Type à mão with open("/tmp/boleto-outubro.pdf", "rb") as f: r = requests.post(URL, params={"r": "mensagens/enviar_arquivo"}, headers=CAB, data={"telefone": "5544999990000", "legenda": "Segue o boleto de outubro."}, files={"arquivo": ("boleto-outubro.pdf", f, "application/pdf")}, timeout=120) print(r.status_code, r.json()) # 2) base64 no 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": "Instalação concluída.", "arquivo_base64": conteudo, }, timeout=120) print(r.status_code, r.json())
Resposta de sucesso (mesmo formato das outras rotas de envio; aqui a legenda vira o conteudo da mensagem e o bloco arquivo já traz o caminho de download):
{
"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": "Segue o boleto de outubro.",
"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 | Nem arquivo nem arquivo_base64 vieram (campo igual a arquivo|arquivo_base64). |
arquivo_invalido | 400 | Base64 malformado, arquivo vazio ou upload interrompido. |
arquivo_grande | 413 | Acima de 45 MB, ou acima do limite de upload do servidor no envio multipart. maximo_bytes traz o limite (no base64 recusado logo pelo tamanho do texto, o campo pode não vir). |
tipo_nao_suportado | 415 | Executável ou script. |
legenda_invalida | 400 | Legenda com mais de 1024 caracteres. |
arquivo | 500 | O servidor não conseguiu gravar o arquivo. Tente de novo; persistindo, fale com o suporte. |
O arquivo é validado antes do destino: um arquivo recusado nunca abre conversa. Depois vêm os erros de destino e os de envio (janela_fechada, envio).
Baixar o arquivo de uma mensagem
/api_publica.php?r=mensagens/arquivo&id=<mensagem_id>
Devolve o arquivo de uma mensagem: foto que o cliente mandou do roteador, áudio, comprovante de pagamento, documento enviado pela equipe. Vale para mensagens recebidas e enviadas; as notas internas da equipe nunca são entregues.
Permissão: Consultar conversas.
| Parâmetro | Tipo | Obrigatório | Descrição |
|---|---|---|---|
id | inteiro | Sim | id da mensagem. Toda mensagem com arquivo traz, no bloco arquivo, o campo download já pronto (mensagens/arquivo?id=402990): basta acrescentá-lo ao endereço da API como valor de r. |
Resposta
No sucesso, o corpo é o arquivo bruto, fora do envelope JSON, com HTTP 200 e estes cabeçalhos:
| Cabeçalho | Valor |
|---|---|
Content-Type | O tipo real para JPEG, PNG, GIF, WebP e PDF; application/octet-stream para os demais (o tipo original está em arquivo.mimetype da mensagem). |
Content-Disposition | inline para imagens e PDF, attachment para o resto, sempre com filename="..." igual ao nome original do arquivo. |
Content-Length | Tamanho em bytes. |
Cache-Control | private, no-store: não guarde em caches compartilhados. |
X-Content-Type-Options | nosniff. |
Nos erros, a resposta volta a ser JSON (Content-Type: application/json). Confira o status HTTP antes de gravar o corpo como arquivo.
# -f falha em erro HTTP (não grava o JSON de erro); -OJ usa o nome do 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) { // o arquivo gravado contém o JSON de erro (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("Erro:", 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 | Faltou id. |
parametro_invalido | 400 | id não é um número inteiro. |
nao_encontrado | 404 | A mensagem não existe, não tem arquivo, é uma nota interna ou foi apagada. |
arquivo_indisponivel | 410 | A mensagem existe, mas o arquivo não está mais disponível no servidor. |
Se a sua integração guarda os anexos (por exemplo, no prontuário do cliente no ERP), baixe o arquivo logo depois do webhook mensagem_recebida, em vez de depender de que ele continue no servidor para sempre.
Resultado do envio e falhas
As três rotas de envio respondem do mesmo jeito. A mensagem é registrada na conversa sempre que a chamada passa da validação, inclusive quando o WhatsApp recusa: assim a equipe vê no painel a tentativa com o aviso de erro, e o seu sistema recebe a mensagem completa para registrar.
| HTTP | Corpo | Significado |
|---|---|---|
| 200 | ok: true, conversa_id, protocolo, mensagem_id, mensagem | O canal aceitou a mensagem. Entrega e leitura chegam depois, pelo webhook mensagem_status ou consultando a conversa. |
| 409 | error: janela_fechada + os mesmos campos | A API oficial recusou por estar fora da janela de 24 horas. A mensagem fica com status: erro. |
| 502 | error: envio + os mesmos campos + detalhe | O canal recusou ou não respondeu (número sem WhatsApp, conexão caída no meio do envio, mídia recusada, modelo recusado pela Meta). O motivo vem em detalhe e em mensagem.erro; mensagem.status é erro. |
| 4xx | error sem os campos da conversa | Recusa na validação: nada foi registrado nem enviado. |
{
"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": "Olá, Maria! Sua instalação está confirmada para amanhã, entre 8h e 12h.",
"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"
}Mesmo com falha, a conversa nova de um aviso é encerrada como de costume: o protocolo devolvido continua válido para consulta.
Não repita um envio às cegas. Depois de um 502 ou de um erro de rede (tempo esgotado, conexão cortada), a mensagem pode ter chegado ao cliente mesmo sem a resposta ter chegado a você. Antes de reenviar, consulte a conversa (conversas/mensagens com o conversa_id, ou conversas/consultar pelo telefone) e confira o status da última mensagem.
Disparo em massa: respeite a cota por minuto da chave (cabeçalhos X-RateLimit-Limit e X-RateLimit-Remaining; 429 traz Retry-After) e mantenha um ritmo de envio parecido com o da Automação de envios. Veja Limites e o script de Exemplos.
Erros por rota
Além dos erros gerais (rota, autenticacao, chave_invalida, plano, cota_excedida, permissao, json_invalido, parametro_invalido), descritos em Códigos de erro:
error | HTTP | enviar | enviar_modelo | enviar_arquivo | arquivo |
|---|---|---|---|---|---|
metodo | 405 | sim | sim | sim | |
texto_invalido | 400 | sim | |||
parametro_obrigatorio | 400 | sim | sim | sim | |
arquivo_invalido, legenda_invalida | 400 | sim | |||
arquivo_grande | 413 | sim | |||
tipo_nao_suportado | 415 | sim | |||
conexao_invalida | 400 | sim | sim | sim | |
sem_conexao | 400 | sim | sim | sim | |
conexao_id_obrigatorio | 400 | sim | sim | sim | |
conexao_desconectada | 409 | sim | sim | sim | |
telefone_invalido | 400 | sim | sim | sim | |
identificador_obrigatorio | 400 | sim | sim | sim | |
nao_encontrado | 404 | sim | sim | sim | sim |
modelo_invalido, modelo_com_midia, variaveis_invalidas | 400 | sim | |||
janela_fechada | 409 | sim | sim | ||
envio | 502 | sim | sim | sim | |
arquivo | 500 | sim | |||
arquivo_indisponivel | 410 | sim |
A primeira falha encontrada é a que volta. Ordem de validação: texto (ou modelo e variáveis, ou arquivo), depois o destino (conversa, ou conexão e então telefone), e só então o envio.
parametro_obrigatorio, parametro_invalido e texto_invalido em outras rotas trazem campo com o nome do parâmetro.