Messages

Reference for the four routes that talk to the customer over WhatsApp: send text, send a template approved through Meta's official API, send a file (image, video, audio or document) and download the file of any message. Includes parameters, examples in curl, PHP and Python, responses, errors and the rules behind every send: a notice that starts closed, the 24-hour window and delivery failures.

Overview

All routes use the single API address, with the key in the Authorization: Bearer header and the route in the r parameter (details in Authentication):

Base URL
https://atendimento.seuprovedor.com.br/api_publica.php?r=<recurso>/<acao>
RouteMethodKey permissionSummary
mensagens/enviarPOSTSend messagesFree text to a phone number or to a conversation, with an optional quote.
mensagens/enviar_modeloPOSTSend messagesTemplate approved by Meta, with variables. Works outside the 24-hour window.
mensagens/enviar_arquivoPOSTSend messagesImage, video, audio or document, by upload (multipart) or as base64 in the JSON, with a caption.
mensagens/arquivoGET or POSTLook up conversationsDownloads the file of a received or sent message.

With a JSON body, the r parameter must go in the URL (as in the examples on this page); with a form (-d/multipart), it can go in the URL or in the form itself.

What happens behind a send

  • Everything becomes a panel conversation. The send goes into the customer's open conversation on that WhatsApp number or creates a new one, with its own protocol number. The team sees the message on the service screen, in the same timeline as the others.
  • The message goes out as a system message: without an agent's name in bold at the top, and with autor set to sistema in the history.
  • Key trail. Every conversation touched by the API gets a timeline entry with the name of the key used, so the team knows which integration acted.
  • Validation before the protocol. Text, template, variables, file, connection and phone number are checked before any conversation is opened: a call rejected with a 4xx never leaves an empty protocol behind.
  • Webhooks still apply. Every send fires mensagem_enviada (even when it fails, with status erro); a new conversation fires conversa_criada; the automatic closing of a notice fires conversa_encerrada with motivo set to aviso. See Webhooks.

The numbers these routes ask for (conexao_id, modelo_id, template names and languages) come from the Catalogs routes, available to any active key: conexoes/listar and modelos/listar.


Who the message goes to

The three send routes accept two ways of stating the destination. Use one of them:

WayParametersHow the destination is resolved
By phone numbertelefone and, when needed, conexao_idThe API picks the WhatsApp number (rule below), looks for that phone's open conversation on it and, if there is none, creates a new one.
By conversationconversa_id or protocoloOpen conversation (any state other than encerrada): the message goes into it, through its number. Closed conversation: it is not reopened; the message goes to the same phone through the same WhatsApp number, into whatever open conversation exists or into a new conversation.

When conversa_id or protocolo comes in the call, telefone and conexao_id are ignored. A conversation that does not exist responds 404 nao_encontrado. A conversation whose WhatsApp number was deleted from the system responds 400 conexao_invalida. If the conversation's number is disconnected (none of its channels, QR code or official API, online; in mensagens/enviar_modelo, the official API channel), the response is 409 conexao_desconectada, before any send attempt.

Choosing the WhatsApp number (conexao_id)

SituationResult
conexao_id provided and connectedUses that connection.
conexao_id does not exist400 conexao_invalida.
conexao_id disconnected409 conexao_desconectada. On a number with QR code and official API at the same time, one of the two channels being online is enough.
No conexao_id and a single connected numberUses that number. This is the case for anyone with only one WhatsApp.
No conexao_id and no connected number400 sem_conexao.
No conexao_id and more than one connected numberUses the Sending number set on the API key (Integrations, API keys, Sending number field). Key without a Sending number: 400 conexao_id_obrigatorio, the API does not choose for you.

Number with QR code and official API at the same time

When the same company phone is linked through both channels (the twin numbers, which the panel shows as a single channel), the API also counts it as one number: without conexao_id, it is chosen normally if it is the only one. Providing the id of either channel has the same effect: the customer's open conversation is found through both, a new conversation starts on the main channel and each message goes out through the most suitable channel at that moment (the official API inside the 24-hour window, the QR code outside it, with the other as a fallback). In conexoes/listar, the two channels appear linked by the gemea_id field, and the main one comes with principal: true.

Phone number

The same rule as the panel's phone fields applies (see Request format):

You sendThe API uses
55449999900005544999990000
44999990000 or (44) 99999-00005544999990000: without the +, the number is Brazilian and gets the 55.
+351 912 345 678351912345678: other countries always go with the + in front.
4499990000554499990000: landline or old mobile, 10 digits with area code.
44899990000400 telefone_invalido: with 11 digits, the third one must be the mobile 9.
12345400 telefone_invalido.

To find the open conversation, the forms with and without the ninth digit (5544999990000 and 554499990000) are the same person: there is no need to guess which one the customer's WhatsApp uses.


Notice: a new conversation starts closed

A send to someone who had no open conversation on that number is treated as a notice, not as a service request. It is the same rule as Sending automation, and it applies to all three send routes:

  1. ChatISP creates the conversation, with a new protocol number, and sends the message.
  2. Right after that, the conversation is marked as closed. It does not enter the queue, is not assigned to anyone and does not take up any agent. The conversa_encerrada webhook goes out with motivo set to aviso.
  3. If the customer replies, the reply opens a normal service request, with its own protocol number, which follows the configured flow, AI and queue, like any received message.

This way, a maintenance notice to 500 subscribers does not fill the queue with 500 idle conversations: only those who reply become service requests.

Situation before the sendWhat happens
No open conversation for the phone on the numberNew conversation, message sent, conversation closed. The response carries the new protocol number.
Open conversation in any state (queue, AI, flow, with an agent), including one opened shortly before by atendimentos/abrirThe message goes into that conversation, which stays in the state it was in. The agent sees the message on screen.
conversa_id or protocolo of a closed conversationIt is not reopened. If the phone has another open conversation on the number, the message goes into it; otherwise, the notice rule applies (new closed conversation).

Want the team to follow up on the reply? Call atendimentos/abrir before the send. The conversation then exists in the queue (or with the chosen agent) and the following send goes into it, without being treated as a notice.

The automatic closing of a notice does not send the closed request message or the satisfaction survey: those messages only go out when there was real service.


Official API 24-hour window

On a connection through Meta's official API, free text and files are only accepted up to 24 hours after the customer's last message. Each new message from the customer reopens the window for another 24 hours. Outside it, Meta only accepts approved templates.

Number typeOutside the window, a mensagens/enviar or mensagens/enviar_arquivo...
QR code onlyThere is no window: the send proceeds normally.
Official API onlyIs rejected by Meta. The response is 409 janela_fechada, and the message is recorded in the conversation with status erro.
QR code and official API (twins)Goes out through the QR code. janela_fechada only comes back if the QR code also fails to deliver.

When you receive janela_fechada, use mensagens/enviar_modelo to reach the customer. When they reply, the window opens again and free text works once more.

JSON response (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": "Your technical visit has been rescheduled to Friday.",
    "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"
  }
}

Send text

POST /api_publica.php?r=mensagens/enviar

Sends a text message to the customer. It can quote (reply to) a message in the conversation, like WhatsApp's reply feature.

Permission: Send messages.

ParameterTypeRequiredDescription
textostringYesFrom 1 to 4096 characters; leading and trailing spaces are removed. Accepts WhatsApp formatting (*bold*, _italic_, ~strikethrough~) and line breaks (\n in the JSON).
telefonestringOne destinationCustomer's phone number. See Who the message goes to.
conexao_idintegerConditionalWhatsApp number to use, together with telefone. Required when more than one number is connected. Not needed when the key has a Sending number; if it comes in the call, the call's value wins.
conversa_id or protocolointeger / stringOne destinationSends to that conversation, instead of telefone.
responder_aintegerNoid of a message in the same conversation to quote. If it is not found in that conversation or never reached WhatsApp (for example, a message with an error), the text goes out normally, without the quote.
nomestringNoCustomer's name, up to 190 characters, used only when a new conversation is created. If the team has already saved a name for this contact (in the panel or through contatos/renomear), the saved name takes precedence.
terminal
# by phone number (with a single WhatsApp number, conexao_id can be omitted)
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": "Hi, Maria! Your installation is confirmed for tomorrow, between 8 AM and 12 PM."
  }'

# in an open conversation, quoting the customer's last message
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": "We got the photo, thank you!"}'
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'    => 'Hi, Maria! Your installation is confirmed for tomorrow, between 8 AM and 12 PM.',
    ]),
]);
$corpo  = json_decode(curl_exec($ch), true);
$status = curl_getinfo($ch, CURLINFO_HTTP_CODE);
curl_close($ch);

if ($corpo['ok'] ?? false) {
    echo 'Protocol ', $corpo['protocolo'], ', message ', $corpo['mensagem_id'], PHP_EOL;
} elseif (($corpo['error'] ?? '') === 'janela_fechada') {
    // official API outside the 24 hours: send an approved template
} 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": "Hi, Maria! Your installation is confirmed for tomorrow, between 8 AM and 12 PM.",
    }, timeout=60)

corpo = r.json()
if corpo.get("ok"):
    print("Protocol", corpo["protocolo"])
else:
    print(r.status_code, corpo.get("error"), corpo.get("detalhe", ""))

Success response:

JSON response (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": "Hi, Maria! Your installation is confirmed for tomorrow, between 8 AM and 12 PM.",
    "transcricao": null,
    "arquivo": null,
    "status": "enviada",
    "erro": null,
    "responde_a": null,
    "editada": false,
    "apagada": false,
    "criada_em": "2026-10-01T10:02:11-03:00"
  }
}
FieldDescription
conversa_idConversation used or created. Use it with conversas/consultar, conversas/mensagens and the management routes.
protocoloConversation protocol number, the same one the team sees in the panel.
mensagem_idIdentifier of the recorded message. It is the id that later appears in the mensagem_status webhooks (delivered, read).
mensagemThe full message, in the API's message format. Right after the send the status is enviada; delivered and read come later.

Specific errors: texto_invalido, janela_fechada, envio, plus the destination errors (telefone_invalido, conexao_invalida, sem_conexao, conexao_id_obrigatorio, conexao_desconectada, nao_encontrado). See Errors by route.


Send official API template

POST /api_publica.php?r=mensagens/enviar_modelo

Sends a message template approved by Meta. It is the only way to start or resume a conversation through the official API outside the 24-hour window: invoice reminder, maintenance notice, technical visit confirmation. The destination and notice rules are the same as in Send text.

Permission: Send messages. Official API only

ParameterTypeRequiredDescription
modelo_idintegerOne of the twoTemplate identifier in ChatISP (id field of modelos/listar). Takes priority over modelo.
modelostringOne of the twoTemplate name exactly as registered with Meta (for example, lembrete_fatura).
idiomastringNoTemplate language code, as Meta uses it (for example, pt_BR, es, en_US). Use it when the same name exists in more than one language; without it, the first one synced is used.
variaveislist of stringsIf the template has themValues for {{1}}, {{2}}... in the template body, in order. None can be empty; each value is truncated at 500 characters; extra values are ignored.
telefone + conexao_idOne destinationAs in Send text, but counting only the numbers that have the official API. conexao_id is not needed when the key has a Sending number. It can be the id of either channel of a twin number: the API uses its official channel.
conversa_id or protocoloOne destinationThe conversation must belong to a number that has the official API (alone or twinned with the QR code).
nomestringNoCustomer's name for a new conversation (up to 190 characters).

Which templates can be sent

  • Approved by Meta (status equal to APPROVED) and belonging to the same connection's account. A template from another number, pending or rejected: 400 modelo_invalido.
  • No media header. Templates with an image, video or document header: 400 modelo_com_midia. A fixed text header is accepted.
  • The API fills in only the body variables. Variables in the header or in buttons are not filled in, and Meta rejects the send (502 envio).

In modelos/listar, the enviavel field already says whether the template works for this route, and variaveis says how many values it needs. Templates are created and approved in Meta's WhatsApp Manager; after approving a new template, use Sync with Meta on the Connections screen so ChatISP learns about it.

terminal
# Template "lembrete_fatura": "Hi, {{1}}! Your {{2}} invoice is due on {{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"]
  }'

# the same as a form: lists with square brackets
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 'The template needs ', $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())

The success response has the same format as mensagens/enviar. In the conversation history (and in the mensagem.conteudo field), the template appears with the body text already filled in with the variables.

missing variables (HTTP 400)
{"ok": false, "error": "variaveis_invalidas", "esperadas": 3}
connection without official API (HTTP 400)
{"ok": false, "error": "conexao_invalida", "tipo_esperado": "meta"}
modelo and modelo_id both missing (HTTP 400)
{"ok": false, "error": "parametro_obrigatorio", "campo": "modelo_id|modelo"}

Specific errors: parametro_obrigatorio, modelo_invalido, modelo_com_midia, variaveis_invalidas, envio, plus the destination errors.

Sending templates through the official API is charged by Meta, according to its price list. Official API billing and allowance are explained in Connections.


Send file

POST /api_publica.php?r=mensagens/enviar_arquivo

Sends an image, video, audio or document (PDF payment slip, contract, spreadsheet, installation photo), with an optional caption. The file can be sent in two ways: multipart upload (recommended, especially for large files) or base64 inside the JSON (handy when your system already has the file in memory). The destination, notice and 24-hour window rules are the same as in Send text.

Permission: Send messages.

ParameterTypeRequiredDescription
arquivofile (multipart)One of the twoThe file, in a multipart/form-data request.
arquivo_base64stringOne of the twoThe content in base64, in a JSON or form body. Also accepts the data: format (for example, data:application/pdf;base64,JVBERi0...): the prefix is discarded.
nome_arquivostringRecommended with base64Name the customer sees on the document (up to 200 characters), for example boleto-outubro.pdf. In multipart, when omitted, the uploaded file's name is used. Without a name, the file goes out as arquivo.<extension>.
legendastringNoText that goes with the file, up to 1024 characters.
telefone + conexao_id, or conversa_id / protocoloOne destinationSee Who the message goes to. conexao_id is not needed when the key has a Sending number.
nomestringNoCustomer's name for a new conversation (up to 190 characters).

Size and accepted types

Limit: 45 MB per file (47,185,920 bytes). The type is identified by the file's actual content, as in the panel, and not by the extension or the declared Content-Type:

Detected contentGoes out on WhatsApp astipo in the message
JPEG, PNG, WebP, GIFImageimagem
MP4, 3GPVideovideo
OGG, MP3, audio M4A/MP4, AAC, WAV, audio WebMAudioaudio
Anything else (PDF, DOCX, XLSX, CSV, TXT, ZIP...)Document, with the file namedocumento
Executables and scripts (EXE, MSI, binaries, shell scripts)Rejected: 415 tipo_nao_suportado, with the detected type in mimetype.
  • Through the QR code, audio that is not OGG/Opus reaches the customer as a file (document), so it does not become a broken voice note. The message record shows the tipo it actually went out as.
  • Meta's official API has its own limits per media type, much smaller than 45 MB for image, video and audio. If the channel rejects the file, the response is 502 envio, with the reason in detalhe.
  • With base64, the request body is about one third larger than the file. For files of tens of megabytes, prefer multipart.
terminal
# multipart: the file field is called "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=Here is your October payment slip. Due on 10/10." \
  -F "arquivo=@/tmp/boleto-outubro.pdf"

# base64 in the JSON (Linux base64 wraps lines: 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\": \"Here is your October payment slip.\",
       \"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 with CURLFile (do not set Content-Type: curl builds the 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'  => 'Here is your October payment slip.',
        'arquivo'  => new CURLFile('/tmp/boleto-outubro.pdf', 'application/pdf', 'boleto-outubro.pdf'),
    ],
]);
$corpo = json_decode(curl_exec($ch), true);
curl_close($ch);

// 2) base64 in the JSON (file generated in memory, without touching the disk)
$pdf = gerarBoletoPdf(); // your code
$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= builds the request; do not set Content-Type by hand
with open("/tmp/boleto-outubro.pdf", "rb") as f:
    r = requests.post(URL, params={"r": "mensagens/enviar_arquivo"}, headers=CAB,
        data={"telefone": "5544999990000", "legenda": "Here is your October payment slip."},
        files={"arquivo": ("boleto-outubro.pdf", f, "application/pdf")},
        timeout=120)
print(r.status_code, r.json())

# 2) base64 in the 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": "Installation completed.",
    "arquivo_base64": conteudo,
}, timeout=120)
print(r.status_code, r.json())

Success response (same format as the other send routes; here the legenda becomes the message's conteudo and the arquivo block already carries the download path):

JSON response (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": "Here is your October payment slip.",
    "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"
  }
}
file too large (HTTP 413)
{"ok": false, "error": "arquivo_grande", "maximo_bytes": 47185920}
type rejected (HTTP 415)
{"ok": false, "error": "tipo_nao_suportado", "mimetype": "application/x-dosexec"}
errorHTTPCause
parametro_obrigatorio400Neither arquivo nor arquivo_base64 was sent (campo equal to arquivo|arquivo_base64).
arquivo_invalido400Malformed base64, empty file or interrupted upload.
arquivo_grande413Over 45 MB, or over the server's upload limit in a multipart request. maximo_bytes carries the limit (with base64 rejected right away because of the text size, the field may be absent).
tipo_nao_suportado415Executable or script.
legenda_invalida400Caption longer than 1024 characters.
arquivo500The server could not save the file. Try again; if it persists, contact support.

The file is validated before the destination: a rejected file never opens a conversation. Then come the destination errors and the send errors (janela_fechada, envio).


Download a message's file

GET POST /api_publica.php?r=mensagens/arquivo&id=<mensagem_id>

Returns the file of a message: a photo of the router sent by the customer, audio, proof of payment, a document sent by the team. It works for received and sent messages; the team's internal notes are never delivered.

Permission: Look up conversations.

ParameterTypeRequiredDescription
idintegerYesMessage id. Every message with a file carries, in the arquivo block, a ready-made download field (mensagens/arquivo?id=402990): just append it to the API address as the value of r.

Response

On success, the body is the raw file, outside the JSON envelope, with HTTP 200 and these headers:

HeaderValue
Content-TypeThe actual type for JPEG, PNG, GIF, WebP and PDF; application/octet-stream for the rest (the original type is in the message's arquivo.mimetype).
Content-Dispositioninline for images and PDF, attachment for the rest, always with filename="..." equal to the original file name.
Content-LengthSize in bytes.
Cache-Controlprivate, no-store: do not store it in shared caches.
X-Content-Type-Optionsnosniff.

On errors, the response is JSON again (Content-Type: application/json). Check the HTTP status before saving the body as a file.

terminal
# -f fails on HTTP errors (does not save the error JSON); -OJ uses the name from 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) {
    // the saved file contains the error JSON (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)
errorHTTPCause
parametro_obrigatorio400id is missing.
parametro_invalido400id is not an integer.
nao_encontrado404The message does not exist, has no file, is an internal note or was deleted.
arquivo_indisponivel410The message exists, but the file is no longer available on the server.

If your integration stores attachments (for example, in the customer record in the ERP), download the file right after the mensagem_recebida webhook, instead of relying on it staying on the server forever.


Send result and failures

The three send routes respond the same way. The message is recorded in the conversation whenever the call passes validation, even when WhatsApp rejects it: this way the team sees the attempt with the error warning in the panel, and your system receives the full message to record.

HTTPBodyMeaning
200ok: true, conversa_id, protocolo, mensagem_id, mensagemThe channel accepted the message. Delivery and read come later, through the mensagem_status webhook or by looking up the conversation.
409error: janela_fechada + the same fieldsThe official API rejected it for being outside the 24-hour window. The message gets status: erro.
502error: envio + the same fields + detalheThe channel rejected it or did not respond (number without WhatsApp, connection dropped during the send, media rejected, template rejected by Meta). The reason comes in detalhe and in mensagem.erro; mensagem.status is erro.
4xxerror without the conversation fieldsRejected at validation: nothing was recorded or sent.
send failure (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": "Hi, Maria! Your installation is confirmed for tomorrow, between 8 AM and 12 PM.",
    "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"
}

Even with a failure, the new conversation of a notice is closed as usual: the returned protocol number remains valid for lookups.

Do not repeat a send blindly. After a 502 or a network error (timeout, connection cut), the message may have reached the customer even though the response never reached you. Before resending, look up the conversation (conversas/mensagens with the conversa_id, or conversas/consultar by phone number) and check the status of the last message.

Bulk sending: respect the key's per-minute quota (headers X-RateLimit-Limit and X-RateLimit-Remaining; a 429 carries Retry-After) and keep a sending pace similar to Sending automation. See Limits and the script in Examples.


Errors by route

In addition to the general errors (rota, autenticacao, chave_invalida, plano, cota_excedida, permissao, json_invalido, parametro_invalido), described in Error codes:

errorHTTPenviarenviar_modeloenviar_arquivoarquivo
metodo405yesyesyes
texto_invalido400yes
parametro_obrigatorio400yesyesyes
arquivo_invalido, legenda_invalida400yes
arquivo_grande413yes
tipo_nao_suportado415yes
conexao_invalida400yesyesyes
sem_conexao400yesyesyes
conexao_id_obrigatorio400yesyesyes
conexao_desconectada409yesyesyes
telefone_invalido400yesyesyes
identificador_obrigatorio400yesyesyes
nao_encontrado404yesyesyesyes
modelo_invalido, modelo_com_midia, variaveis_invalidas400yes
janela_fechada409yesyes
envio502yesyesyes
arquivo500yes
arquivo_indisponivel410yes

The first failure found is the one returned. Validation order: text (or template and variables, or file), then the destination (conversation, or connection and then phone number), and only then the send.

parametro_obrigatorio, parametro_invalido and texto_invalido on other routes carry campo with the parameter name.