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

URL base
https://atendimento.seuprovedor.com.br/api_publica.php?r=<recurso>/<acao>
RotaMétodoPermissão da chaveResumo
mensagens/enviarPOSTEnviar mensagensTexto livre para um telefone ou para uma conversa, com citação opcional.
mensagens/enviar_modeloPOSTEnviar mensagensModelo (template) aprovado pela Meta, com variáveis. Funciona fora da janela de 24 horas.
mensagens/enviar_arquivoPOSTEnviar mensagensImagem, vídeo, áudio ou documento, por upload (multipart) ou em base64 no JSON, com legenda.
mensagens/arquivoGET ou POSTConsultar conversasBaixa 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 autor igual a sistema no 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 status erro); conversa nova dispara conversa_criada; o encerramento automático do aviso dispara conversa_encerrada com motivo igual a aviso. 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:

FormaParâmetrosComo o destino é resolvido
Por telefonetelefone e, quando preciso, conexao_idA 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 conversaconversa_id ou protocoloConversa 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çãoResultado
conexao_id informado e conectadoUsa essa conexão.
conexao_id inexistente400 conexao_invalida.
conexao_id desconectado409 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 conectadoUsa esse número. É o caso de quem tem um WhatsApp só.
Sem conexao_id e nenhum número conectado400 sem_conexao.
Sem conexao_id e mais de um número conectadoUsa 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ê enviaA API usa
55449999900005544999990000
44999990000 ou (44) 99999-00005544999990000: sem o +, o número é brasileiro e ganha o 55.
+351 912 345 678351912345678: outro país entra sempre com o + na frente.
4499990000554499990000: fixo ou celular antigo, 10 dígitos com DDD.
44899990000400 telefone_invalido: com 11 dígitos, o terceiro precisa ser o 9 do celular.
12345400 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:

  1. O ChatISP cria a conversa, com protocolo novo, e envia a mensagem.
  2. 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_encerrada sai com motivo igual a aviso.
  3. 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 envioO que acontece
Nenhuma conversa aberta do telefone no númeroConversa 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/abrirA 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 encerradaEla 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úmeroFora da janela, um mensagens/enviar ou mensagens/enviar_arquivo...
Só QR CodeNã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.

resposta 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": "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

POST /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âmetroTipoObrigatórioDescrição
textostringSimDe 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).
telefonestringUm destinoTelefone do cliente. Veja Para quem a mensagem vai.
conexao_idinteiroCondicionalNú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 protocolointeiro / stringUm destinoEnvia para essa conversa, no lugar de telefone.
responder_ainteiroNãoid 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.
nomestringNãoNome 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.
terminal
# 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
<?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'] ?? ''));
}
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": "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:

resposta 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": "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"
  }
}
CampoDescrição
conversa_idConversa usada ou criada. Serve para conversas/consultar, conversas/mensagens e as rotas de gestão.
protocoloProtocolo da conversa, o mesmo que a equipe vê no painel.
mensagem_idIdentificador da mensagem registrada. É o id que aparece depois nos webhooks mensagem_status (entregue, lida).
mensagemA 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

POST /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âmetroTipoObrigatórioDescrição
modelo_idinteiroUm dos doisIdentificador do modelo no ChatISP (campo id de modelos/listar). Tem prioridade sobre modelo.
modelostringUm dos doisNome do modelo exatamente como cadastrado na Meta (por exemplo, lembrete_fatura).
idiomastringNãoCó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.
variaveislista de stringsSe o modelo tiverValores 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_idUm destinoComo 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 protocoloUm destinoA conversa precisa ser de um número que tenha API oficial (sozinha ou gêmea do QR Code).
nomestringNãoNome do cliente para uma conversa nova (até 190 caracteres).

Quais modelos podem ser enviados

  • Aprovado pela Meta (status igual a APPROVED) e da conta da mesma conexão. Modelo de outro número, pendente ou rejeitado: 400 modelo_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.

terminal
# 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
<?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;
}
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())

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.

variáveis faltando (HTTP 400)
{"ok": false, "error": "variaveis_invalidas", "esperadas": 3}
conexão sem API oficial (HTTP 400)
{"ok": false, "error": "conexao_invalida", "tipo_esperado": "meta"}
faltou modelo e modelo_id (HTTP 400)
{"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

POST /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âmetroTipoObrigatórioDescrição
arquivoarquivo (multipart)Um dos doisO arquivo, num envio multipart/form-data.
arquivo_base64stringUm dos doisO 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_arquivostringRecomendado no base64Nome 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>.
legendastringNãoTexto que acompanha o arquivo, até 1024 caracteres.
telefone + conexao_id, ou conversa_id / protocoloUm destinoVeja Para quem a mensagem vai. conexao_id é dispensado quando a chave tem Número de origem.
nomestringNãoNome 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 detectadoSai no WhatsApp comotipo na mensagem
JPEG, PNG, WebP, GIFImagemimagem
MP4, 3GPVídeovideo
OGG, MP3, M4A/MP4 de áudio, AAC, WAV, WebM de áudioÁudioaudio
Qualquer outro (PDF, DOCX, XLSX, CSV, TXT, ZIP...)Documento, com o nome do arquivodocumento
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 tipo com 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 em detalhe.
  • 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.
terminal
# 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
<?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);
Python
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):

resposta 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": "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"
  }
}
arquivo grande demais (HTTP 413)
{"ok": false, "error": "arquivo_grande", "maximo_bytes": 47185920}
tipo recusado (HTTP 415)
{"ok": false, "error": "tipo_nao_suportado", "mimetype": "application/x-dosexec"}
errorHTTPCausa
parametro_obrigatorio400Nem arquivo nem arquivo_base64 vieram (campo igual a arquivo|arquivo_base64).
arquivo_invalido400Base64 malformado, arquivo vazio ou upload interrompido.
arquivo_grande413Acima 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_suportado415Executável ou script.
legenda_invalida400Legenda com mais de 1024 caracteres.
arquivo500O 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

GET POST /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âmetroTipoObrigatórioDescrição
idinteiroSimid 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çalhoValor
Content-TypeO 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-Dispositioninline para imagens e PDF, attachment para o resto, sempre com filename="..." igual ao nome original do arquivo.
Content-LengthTamanho em bytes.
Cache-Controlprivate, no-store: não guarde em caches compartilhados.
X-Content-Type-Optionsnosniff.

Nos erros, a resposta volta a ser JSON (Content-Type: application/json). Confira o status HTTP antes de gravar o corpo como arquivo.

terminal
# -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
<?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');
}
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("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)
errorHTTPCausa
parametro_obrigatorio400Faltou id.
parametro_invalido400id não é um número inteiro.
nao_encontrado404A mensagem não existe, não tem arquivo, é uma nota interna ou foi apagada.
arquivo_indisponivel410A 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.

HTTPCorpoSignificado
200ok: true, conversa_id, protocolo, mensagem_id, mensagemO canal aceitou a mensagem. Entrega e leitura chegam depois, pelo webhook mensagem_status ou consultando a conversa.
409error: janela_fechada + os mesmos camposA API oficial recusou por estar fora da janela de 24 horas. A mensagem fica com status: erro.
502error: envio + os mesmos campos + detalheO 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.
4xxerror sem os campos da conversaRecusa na validação: nada foi registrado nem enviado.
falha de envio (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": "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:

errorHTTPenviarenviar_modeloenviar_arquivoarquivo
metodo405simsimsim
texto_invalido400sim
parametro_obrigatorio400simsimsim
arquivo_invalido, legenda_invalida400sim
arquivo_grande413sim
tipo_nao_suportado415sim
conexao_invalida400simsimsim
sem_conexao400simsimsim
conexao_id_obrigatorio400simsimsim
conexao_desconectada409simsimsim
telefone_invalido400simsimsim
identificador_obrigatorio400simsimsim
nao_encontrado404simsimsimsim
modelo_invalido, modelo_com_midia, variaveis_invalidas400sim
janela_fechada409simsim
envio502simsimsim
arquivo500sim
arquivo_indisponivel410sim

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.