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):
https://atendimento.seuprovedor.com.br/api_publica.php?r=<recurso>/<acao>
| Route | Method | Key permission | Summary |
|---|---|---|---|
mensagens/enviar | POST | Send messages | Free text to a phone number or to a conversation, with an optional quote. |
mensagens/enviar_modelo | POST | Send messages | Template approved by Meta, with variables. Works outside the 24-hour window. |
mensagens/enviar_arquivo | POST | Send messages | Image, video, audio or document, by upload (multipart) or as base64 in the JSON, with a caption. |
mensagens/arquivo | GET or POST | Look up conversations | Downloads 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
autorset tosistemain 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 statuserro); a new conversation firesconversa_criada; the automatic closing of a notice firesconversa_encerradawithmotivoset toaviso. 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:
| Way | Parameters | How the destination is resolved |
|---|---|---|
| By phone number | telefone and, when needed, conexao_id | The 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 conversation | conversa_id or protocolo | Open 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)
| Situation | Result |
|---|---|
conexao_id provided and connected | Uses that connection. |
conexao_id does not exist | 400 conexao_invalida. |
conexao_id disconnected | 409 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 number | Uses that number. This is the case for anyone with only one WhatsApp. |
No conexao_id and no connected number | 400 sem_conexao. |
No conexao_id and more than one connected number | Uses 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 send | The API uses |
|---|---|
5544999990000 | 5544999990000 |
44999990000 or (44) 99999-0000 | 5544999990000: without the +, the number is Brazilian and gets the 55. |
+351 912 345 678 | 351912345678: other countries always go with the + in front. |
4499990000 | 554499990000: landline or old mobile, 10 digits with area code. |
44899990000 | 400 telefone_invalido: with 11 digits, the third one must be the mobile 9. |
12345 | 400 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:
- ChatISP creates the conversation, with a new protocol number, and sends the message.
- 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_encerradawebhook goes out withmotivoset toaviso. - 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 send | What happens |
|---|---|
| No open conversation for the phone on the number | New 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/abrir | The 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 conversation | It 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 type | Outside the window, a mensagens/enviar or mensagens/enviar_arquivo... |
|---|---|
| QR code only | There is no window: the send proceeds normally. |
| Official API only | Is 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.
{
"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
/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.
| Parameter | Type | Required | Description |
|---|---|---|---|
texto | string | Yes | From 1 to 4096 characters; leading and trailing spaces are removed. Accepts WhatsApp formatting (*bold*, _italic_, ~strikethrough~) and line breaks (\n in the JSON). |
telefone | string | One destination | Customer's phone number. See Who the message goes to. |
conexao_id | integer | Conditional | WhatsApp 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 protocolo | integer / string | One destination | Sends to that conversation, instead of telefone. |
responder_a | integer | No | id 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. |
nome | string | No | Customer'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. |
# 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 $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'] ?? '')); }
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:
{
"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"
}
}| Field | Description |
|---|---|
conversa_id | Conversation used or created. Use it with conversas/consultar, conversas/mensagens and the management routes. |
protocolo | Conversation protocol number, the same one the team sees in the panel. |
mensagem_id | Identifier of the recorded message. It is the id that later appears in the mensagem_status webhooks (delivered, read). |
mensagem | The 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
/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
| Parameter | Type | Required | Description |
|---|---|---|---|
modelo_id | integer | One of the two | Template identifier in ChatISP (id field of modelos/listar). Takes priority over modelo. |
modelo | string | One of the two | Template name exactly as registered with Meta (for example, lembrete_fatura). |
idioma | string | No | Template 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. |
variaveis | list of strings | If the template has them | Values 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_id | One destination | As 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 protocolo | One destination | The conversation must belong to a number that has the official API (alone or twinned with the QR code). | |
nome | string | No | Customer's name for a new conversation (up to 190 characters). |
Which templates can be sent
- Approved by Meta (
statusequal toAPPROVED) and belonging to the same connection's account. A template from another number, pending or rejected: 400modelo_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.
# 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 $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; }
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.
{"ok": false, "error": "variaveis_invalidas", "esperadas": 3}{"ok": false, "error": "conexao_invalida", "tipo_esperado": "meta"}{"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
/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.
| Parameter | Type | Required | Description |
|---|---|---|---|
arquivo | file (multipart) | One of the two | The file, in a multipart/form-data request. |
arquivo_base64 | string | One of the two | The 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_arquivo | string | Recommended with base64 | Name 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>. |
legenda | string | No | Text that goes with the file, up to 1024 characters. |
telefone + conexao_id, or conversa_id / protocolo | One destination | See Who the message goes to. conexao_id is not needed when the key has a Sending number. | |
nome | string | No | Customer'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 content | Goes out on WhatsApp as | tipo in the message |
|---|---|---|
| JPEG, PNG, WebP, GIF | Image | imagem |
| MP4, 3GP | Video | video |
| OGG, MP3, audio M4A/MP4, AAC, WAV, audio WebM | Audio | audio |
| Anything else (PDF, DOCX, XLSX, CSV, TXT, ZIP...) | Document, with the file name | documento |
| 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
tipoit 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 indetalhe. - With base64, the request body is about one third larger than the file. For files of tens of megabytes, prefer multipart.
# 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 $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);
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):
{
"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"
}
}{"ok": false, "error": "arquivo_grande", "maximo_bytes": 47185920}{"ok": false, "error": "tipo_nao_suportado", "mimetype": "application/x-dosexec"}error | HTTP | Cause |
|---|---|---|
parametro_obrigatorio | 400 | Neither arquivo nor arquivo_base64 was sent (campo equal to arquivo|arquivo_base64). |
arquivo_invalido | 400 | Malformed base64, empty file or interrupted upload. |
arquivo_grande | 413 | Over 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_suportado | 415 | Executable or script. |
legenda_invalida | 400 | Caption longer than 1024 characters. |
arquivo | 500 | The 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
/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.
| Parameter | Type | Required | Description |
|---|---|---|---|
id | integer | Yes | Message 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:
| Header | Value |
|---|---|
Content-Type | The 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-Disposition | inline for images and PDF, attachment for the rest, always with filename="..." equal to the original file name. |
Content-Length | Size in bytes. |
Cache-Control | private, no-store: do not store it in shared caches. |
X-Content-Type-Options | nosniff. |
On errors, the response is JSON again (Content-Type: application/json). Check the HTTP status before saving the body as a file.
# -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 $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'); }
import os, re, requests r = requests.get("https://atendimento.seuprovedor.com.br/api_publica.php", params={"r": "mensagens/arquivo", "id": 402850}, headers={"Authorization": "Bearer " + os.environ["CHATISP_CHAVE"]}, stream=True, timeout=120) if r.status_code != 200: print("Error:", r.json()) else: nome = re.search(r'filename="([^"]+)"', r.headers.get("Content-Disposition", "")) with open(nome.group(1) if nome else "arquivo.bin", "wb") as f: for pedaco in r.iter_content(65536): f.write(pedaco)
error | HTTP | Cause |
|---|---|---|
parametro_obrigatorio | 400 | id is missing. |
parametro_invalido | 400 | id is not an integer. |
nao_encontrado | 404 | The message does not exist, has no file, is an internal note or was deleted. |
arquivo_indisponivel | 410 | The 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.
| HTTP | Body | Meaning |
|---|---|---|
| 200 | ok: true, conversa_id, protocolo, mensagem_id, mensagem | The channel accepted the message. Delivery and read come later, through the mensagem_status webhook or by looking up the conversation. |
| 409 | error: janela_fechada + the same fields | The official API rejected it for being outside the 24-hour window. The message gets status: erro. |
| 502 | error: envio + the same fields + detalhe | The 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. |
| 4xx | error without the conversation fields | Rejected at validation: nothing was recorded or sent. |
{
"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:
error | HTTP | enviar | enviar_modelo | enviar_arquivo | arquivo |
|---|---|---|---|---|---|
metodo | 405 | yes | yes | yes | |
texto_invalido | 400 | yes | |||
parametro_obrigatorio | 400 | yes | yes | yes | |
arquivo_invalido, legenda_invalida | 400 | yes | |||
arquivo_grande | 413 | yes | |||
tipo_nao_suportado | 415 | yes | |||
conexao_invalida | 400 | yes | yes | yes | |
sem_conexao | 400 | yes | yes | yes | |
conexao_id_obrigatorio | 400 | yes | yes | yes | |
conexao_desconectada | 409 | yes | yes | yes | |
telefone_invalido | 400 | yes | yes | yes | |
identificador_obrigatorio | 400 | yes | yes | yes | |
nao_encontrado | 404 | yes | yes | yes | yes |
modelo_invalido, modelo_com_midia, variaveis_invalidas | 400 | yes | |||
janela_fechada | 409 | yes | yes | ||
envio | 502 | yes | yes | yes | |
arquivo | 500 | yes | |||
arquivo_indisponivel | 410 | yes |
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.