Conversations and service requests
Reference for the routes that read and manage service requests: list conversations with filters and incremental sync, look up a conversation, walk through the message history with a cursor, transfer, close, tag, add notes and open a service request in the queue without messaging the customer. Includes the conversation and message objects field by field, examples in curl, PHP and Python, and the errors of each route.
Overview
In ChatISP, each conversation is a service request with its own protocol number in the format YYYYMMDD-NNNN. A closed conversation is never reopened: when the customer writes again, a new conversation starts, with a new protocol number. All the conversations of the same phone number make up the contact.
| Route | Method | Key permission | Summary |
|---|---|---|---|
conversas/listar | GET or POST | Look up conversations | Paginated list with filters by state, department, agent, connection, phone number, tag and dates. |
conversas/consultar | GET or POST | Look up conversations | One conversation (by id, protocol number or phone number) with tags and its latest messages. |
conversas/mensagens | GET or POST | Look up conversations | Full history, paginated by cursor. |
conversas/transferir | POST | Manage conversations | To a department, to an agent or to both. |
conversas/encerrar | POST | Manage conversations | Closes with a reason, with or without the closing message and the survey. |
conversas/etiquetas | POST | Manage conversations | Sets, adds or removes tags. |
conversas/nota | POST | Manage conversations | Internal note in the timeline (the team sees it, the customer never does). |
atendimentos/abrir | POST | Open service requests | Puts the phone number in a department queue or with an agent, without sending anything to the customer. |
- Company-wide view. The key sees every conversation, on every connection and in every department. The visibility restrictions of agent profiles do not apply to the API.
- Internal notes never leave. Internal team messages and system lines are left out of every response and download.
- Key trail. Every management action taken through the API shows up in the conversation timeline with the key name (for example, API: ERP), and the matching webhook fires as if the action had been taken in the panel.
- Identifying the conversation. The routes for a single conversation accept
conversa_id(orid) orprotocolo. With neither: 400identificador_obrigatorio. Not found: 404nao_encontrado.
The department, agent, tag and closing reason ids come from the Catalogs routes (departamentos/listar, atendentes/listar, etiquetas/listar, motivos/listar).
Conversation states
The estado field tells who is handling the conversation right now:
| Value | On screen | Meaning |
|---|---|---|
auto | Automation | Driven by an automatic service flow (menu, business hours, steps). Leaves the flow when the flow transfers it, hands it to the AI or closes it. |
ia | With AI | Being handled by the AI. Leaves it when the AI transfers to the team or when an agent takes over. |
fila | In queue | Waiting for an agent, in the department queue (departamento_id) or in the general queue (no department). It is also the state a new conversation is born in, before the flow or the AI takes over. |
humano | In service | With an agent from the team (atendente_id). |
encerrada | Closed | Service finished (by the team, the AI, inactivity or the API) or a notice sent to someone who had no open conversation. It is never reopened. |
Any state other than encerrada counts as an open conversation (filter abertas=1 in conversas/listar). A conversation the agent marked as Waiting for the customer to reply stays humano, with aguardando_cliente: true.
To follow state changes in real time, without polling in a loop, use the conversa_criada, conversa_atribuida, conversa_transferida and conversa_encerrada webhooks. See Webhooks.
Conversation object
Format used by every route that returns conversations (listings, lookup, management, opening and contacts):
{
"id": 18342,
"protocolo": "20261001-0042",
"conexao_id": 1,
"telefone": "5544999990000",
"nome": "Maria Souza",
"estado": "encerrada",
"aguardando_cliente": false,
"departamento_id": 2,
"departamento": "Technical support",
"atendente_id": 7,
"atendente": "Carlos Lima",
"nao_lidas": 0,
"motivo_encerramento": "Resolved",
"resumo": {
"titulo": "No internet after a power outage",
"texto": "The customer reported no internet since the morning. The agent found the ONU with no signal and scheduled a technician visit for the afternoon."
},
"criada_em": "2026-10-01T09:12:40-03:00",
"ultima_mensagem_em": "2026-10-01T09:41:03-03:00",
"encerrada_em": "2026-10-01T09:42:15-03:00",
"etiquetas": [
{ "id": 3, "nome": "Technician visit", "cor": "#2563eb" }
]
}| Field | Type | Description |
|---|---|---|
id | integer | Conversation identifier (the conversa_id of the other routes and of the webhooks). |
protocolo | string | Service protocol number, the same one the team and the customer see. |
conexao_id | integer or null | WhatsApp number of the conversation. null when the connection was deleted from the system (the history is preserved). |
telefone | string | Customer phone number, digits only, with the country code. |
nome | string | Customer name: the one saved by the team or, without it, the WhatsApp profile name. Empty when unknown. |
estado | string | auto, ia, fila, humano or encerrada. See Conversation states. |
aguardando_cliente | boolean | true while the agent has marked the conversation as waiting for the customer to reply (this wait does not count towards handling time). Goes back to false when the customer replies or the wait is cancelled. |
departamento_id, departamento | integer or null, string | Current department (id and name). null and empty in the general queue. |
atendente_id, atendente | integer or null, string | Agent in charge (id and name). In a closed conversation, whoever had it at the end. |
nao_lidas | integer | Customer messages the person in charge has not opened in the panel yet. |
motivo_encerramento | string | Name of the chosen closing reason. Empty when the conversation is open or was closed without a reason. |
resumo | object or null | Summary of the request written by the AI after closing: titulo (subject, up to 70 characters) and texto (2 to 4 sentences: what the customer brought up, what was done and the outcome). Generated in the background, seconds or minutes after closing, and only when the customer wrote something in the conversation. null while it does not exist. |
criada_em | date and time | When the conversation was opened. |
ultima_mensagem_em | date and time or null | Last message exchanged (from the customer or to them). |
encerrada_em | date and time or null | When it was closed; null if open. |
etiquetas | list | Conversation tags, each with id, nome and cor, in the on-screen order. Included in conversas/consultar, in the management routes and in atendimentos/abrir; not included in conversas/listar or contatos/consultar. |
Dates in ISO 8601 with the server time zone (the time zone is shown in chave/info, field servidor.fuso_horario).
Message object
Format used in conversas/consultar, conversas/mensagens and in the response of the sending routes:
{
"id": 402851,
"conversa_id": 18342,
"direcao": "recebida",
"autor": "cliente",
"autor_nome": "",
"tipo": "audio",
"conteudo": "",
"transcricao": "Good afternoon, my internet went down again after the rain.",
"arquivo": {
"nome": "audio.ogg",
"mimetype": "audio/ogg",
"download": "mensagens/arquivo?id=402851"
},
"status": "entregue",
"erro": null,
"responde_a": null,
"editada": false,
"apagada": false,
"criada_em": "2026-10-01T09:13:02-03:00"
}| Field | Type | Description |
|---|---|---|
id | integer | Message identifier. It grows over time: it serves as the cursor in conversas/mensagens. |
conversa_id | integer | Conversation the message belongs to. |
direcao | string | recebida (from the customer) or enviada (from the ISP). |
autor | string | cliente, ia, atendente (written by someone on the team in the panel or in the app) or sistema (sent automatically: API, flows, sending automation, standard messages, survey). |
autor_nome | string | Agent name when autor is atendente; empty otherwise. |
tipo | string | texto, imagem, audio, video, documento, localizacao or contato (shared contact card). |
conteudo | string | Message text. In media, the caption (may be empty). Also empty when the message was deleted. |
transcricao | string or null | Transcription of an audio message, when audio transcription is on and has finished. null otherwise. |
arquivo | object or null | Media messages: nome (original name), mimetype and download, the r value that downloads the file through the mensagens/arquivo route. null for text and for deleted messages. |
status | string | pendente (recorded, waiting for the channel), enviada (accepted by WhatsApp), entregue (reached the device; messages received from the customer also show up this way), lida (the customer read it, when read receipts are not turned off) or erro (the channel refused it). |
erro | string or null | Reason for the failure when status is erro. |
responde_a | integer or null | id of the message quoted (replied to) by this one. |
editada | boolean | The message was edited after being sent; conteudo holds the current version. |
apagada | boolean | The message was deleted for everyone. The record remains (so the history stays consistent), with no content and no file. |
criada_em | date and time | Time of the message. |
List conversations
/api_publica.php?r=conversas/listar
Paginated list of conversations, with filters that can be combined (all of them are joined with AND). With no filter at all, it returns every conversation, from newest to oldest.
Permission: Look up conversations.
| Parameter | Type | Description |
|---|---|---|
estado | list | One or more states: ia, fila, humano, encerrada, auto. Comma separated (estado=fila,humano), as form fields (estado[]=fila&estado[]=humano) or as a JSON list. Unknown value: 400 parametro_invalido with aceitos. |
abertas | boolean | 1: open conversations only (any state except encerrada). Accepts 1/0, true/false, sim/nao. |
departamento_id | integer | Only this department. 0: only those with no department (general queue). |
atendente_id | integer | Only this agent. 0: only those with no agent. |
conexao_id | integer | Only this WhatsApp number. On a twin number, returns the conversations of both channels. |
telefone | string | Only this customer, with and without the ninth digit. Same phone number rule as the other routes (telefone_invalido if it makes no sense). |
etiqueta_id | integer | Only those with this tag. |
criadas_desde, criadas_ate | date | Opened within the period. A date alone (2026-10-01) means the start of the day in criadas_desde and the end of the day in criadas_ate. |
encerradas_desde | date | Closed from this moment on. |
atualizadas_desde | date | Conversations opened, closed or with a new message from this moment on. This is the incremental sync filter. |
ordem | string | recentes (default: last activity, newest first), antigas (last activity, oldest first), criacao (opening order) or criacao_desc (opening, newest first). Last activity is the last message or, with no messages, the opening. |
pagina, limite | integer | Pagination: pagina starting at 1; limite from 1 to 100, default 50. |
Dates accept ISO 8601 (2026-10-01T08:00:00-03:00), YYYY-MM-DD or YYYY-MM-DD HH:MM:SS; without a time zone, they are read in the server time zone. A date that makes no sense: 400 data_invalida with campo.
# department 2 queue right now, longest waiting first curl -s "https://atendimento.seuprovedor.com.br/api_publica.php?r=conversas/listar&estado=fila&departamento_id=2&ordem=antigas" \ -H "Authorization: Bearer $CHATISP_CHAVE" # closed yesterday with tag 3, page 2 curl -s "https://atendimento.seuprovedor.com.br/api_publica.php?r=conversas/listar&estado=encerrada&etiqueta_id=3&encerradas_desde=2026-09-30&criadas_ate=2026-09-30&pagina=2" \ -H "Authorization: Bearer $CHATISP_CHAVE"
<?php // walks through every page of open conversations $pagina = 1; do { $url = 'https://atendimento.seuprovedor.com.br/api_publica.php?' . http_build_query([ 'r' => 'conversas/listar', 'abertas' => 1, 'limite' => 100, 'pagina' => $pagina, ]); $ch = curl_init($url); curl_setopt_array($ch, [ CURLOPT_RETURNTRANSFER => true, CURLOPT_HTTPHEADER => ['Authorization: Bearer ' . getenv('CHATISP_CHAVE')], ]); $r = json_decode(curl_exec($ch), true); curl_close($ch); foreach ($r['conversas'] as $c) { echo $c['protocolo'], ' ', $c['estado'], ' ', $c['atendente'], PHP_EOL; } $pagina++; } while ($pagina <= $r['paginacao']['paginas']);
import os, requests r = requests.get("https://atendimento.seuprovedor.com.br/api_publica.php", params={ "r": "conversas/listar", "estado": "fila,humano", "atendente_id": 7, "limite": 100, }, headers={"Authorization": "Bearer " + os.environ["CHATISP_CHAVE"]}, timeout=30) dados = r.json() print(dados["paginacao"]["total"], "conversations with agent 7") for c in dados["conversas"]: print(c["protocolo"], c["nome"], c["nao_lidas"])
{
"ok": true,
"conversas": [
{
"id": 18360,
"protocolo": "20261001-0055",
"conexao_id": 1,
"telefone": "5544988887777",
"nome": "João Pereira",
"estado": "fila",
"aguardando_cliente": false,
"departamento_id": 2,
"departamento": "Billing",
"atendente_id": null,
"atendente": "",
"nao_lidas": 2,
"motivo_encerramento": "",
"resumo": null,
"criada_em": "2026-10-01T10:31:08-03:00",
"ultima_mensagem_em": "2026-10-01T10:31:40-03:00",
"encerrada_em": null
}
],
"paginacao": { "pagina": 1, "limite": 50, "total": 1, "paginas": 1 }
}Route-specific errors: parametro_invalido (state, order, limit or page outside the accepted values, with campo), data_invalida, telefone_invalido.
Incremental sync
To keep a copy of the conversations in your CRM, BI or data warehouse without downloading everything again on every run, use atualizadas_desde. It returns the conversations that, from the checkpoint on, were opened, closed or got a new message (from the customer or to them).
- On the first load, list everything (no date filter,
ordem=criacao) and store the conversations byid. - Before each run, write down the current server time (field
servidor.agoraofchave/info, or your own synchronized clock). That will be the checkpoint for the next run. - Call
conversas/listarwithatualizadas_desdeset to the checkpoint of the previous run minus a small margin (one or two minutes),ordem=antigasandlimite=100, walking through the pages. - Save each conversation by
id(update it if it already exists, insert it if it is new). The margin makes some conversations come back repeated; saving byidmakes that harmless. - For the messages of each changed conversation, use
conversas/mensagenswithdepois_deset to the last messageidyou already have.
atualizadas_desde looks at the opening, the closing and the last message. Changes that do not involve a message (transfer, tag changes, the AI summary arriving after closing) do not bring the conversation back. For those, use the conversa_transferida and conversa_atribuida webhooks, and look up the closed conversation again a few minutes later to get the resumo.
import os, requests from datetime import datetime, timedelta URL = "https://atendimento.seuprovedor.com.br/api_publica.php" CAB = {"Authorization": "Bearer " + os.environ["CHATISP_CHAVE"]} def rodada(marco_anterior): agora = requests.get(URL, params={"r": "chave/info"}, headers=CAB, timeout=30).json()["servidor"]["agora"] desde = (datetime.fromisoformat(marco_anterior) - timedelta(minutes=2)).isoformat() pagina = 1 while True: r = requests.get(URL, headers=CAB, timeout=30, params={ "r": "conversas/listar", "atualizadas_desde": desde, "ordem": "antigas", "limite": 100, "pagina": pagina}).json() for c in r["conversas"]: gravar_conversa(c) # upsert by c["id"] in your database if pagina >= r["paginacao"]["paginas"]: break pagina += 1 return agora # checkpoint for the next run
Look up conversation
/api_publica.php?r=conversas/consultar
Returns a full conversation (with tags) and its latest messages, in chronological order. Use it to show the progress of a protocol number in the CRM, to check whether the customer replied to a notice or to fetch the context after a webhook.
Permission: Look up conversations.
| Parameter | Type | Required | Description |
|---|---|---|---|
id or conversa_id | integer | One of the three | Conversation identifier. |
protocolo | string | One of the three | Protocol number, for example 20261001-0042. |
telefone | string | One of the three | Returns the most recent conversation of this phone number, on any WhatsApp number and in any state (including closed), with and without the ninth digit. |
mensagens | integer | No | How many messages to return, newest ones: from 0 to 100, default 20. Use 0 for the conversation only. Values outside the range are adjusted to the nearest limit. |
With more than one identifier, the first one in the order conversa_id/id, protocolo, telefone wins. For more than 100 messages, or to paginate the history, use conversas/mensagens.
# by protocol number, with the 3 latest messages curl -s "https://atendimento.seuprovedor.com.br/api_publica.php?r=conversas/consultar&protocolo=20261001-0042&mensagens=3" \ -H "Authorization: Bearer $CHATISP_CHAVE" # the most recent conversation of a phone number, without messages curl -s "https://atendimento.seuprovedor.com.br/api_publica.php?r=conversas/consultar&telefone=44999990000&mensagens=0" \ -H "Authorization: Bearer $CHATISP_CHAVE"
<?php $url = 'https://atendimento.seuprovedor.com.br/api_publica.php?' . http_build_query([ 'r' => 'conversas/consultar', 'protocolo' => '20261001-0042', 'mensagens' => 3, ]); $ch = curl_init($url); curl_setopt_array($ch, [ CURLOPT_RETURNTRANSFER => true, CURLOPT_HTTPHEADER => ['Authorization: Bearer ' . getenv('CHATISP_CHAVE')], ]); $r = json_decode(curl_exec($ch), true); curl_close($ch); echo $r['conversa']['estado'], ' with ', $r['conversa']['atendente'] ?: 'nobody', PHP_EOL; foreach ($r['mensagens'] as $m) { echo $m['criada_em'], ' [', $m['autor'], '] ', $m['conteudo'] ?: ($m['transcricao'] ?? ''), PHP_EOL; }
import os, requests r = requests.get("https://atendimento.seuprovedor.com.br/api_publica.php", params={ "r": "conversas/consultar", "protocolo": "20261001-0042", "mensagens": 3, }, headers={"Authorization": "Bearer " + os.environ["CHATISP_CHAVE"]}, timeout=30) if r.status_code == 404: print("Protocol number does not exist") else: for m in r.json()["mensagens"]: print(m["criada_em"], m["autor"], m["conteudo"])
{
"ok": true,
"conversa": {
"id": 18342,
"protocolo": "20261001-0042",
"conexao_id": 1,
"telefone": "5544999990000",
"nome": "Maria Souza",
"estado": "humano",
"aguardando_cliente": false,
"departamento_id": 1,
"departamento": "Technical support",
"atendente_id": 7,
"atendente": "Carlos Lima",
"nao_lidas": 0,
"motivo_encerramento": "",
"resumo": null,
"criada_em": "2026-10-01T09:12:40-03:00",
"ultima_mensagem_em": "2026-10-01T09:20:03-03:00",
"encerrada_em": null,
"etiquetas": []
},
"mensagens": [
{
"id": 402850, "conversa_id": 18342, "direcao": "recebida", "autor": "cliente", "autor_nome": "",
"tipo": "texto", "conteudo": "My internet has been down since early this morning", "transcricao": null, "arquivo": null,
"status": "entregue", "erro": null, "responde_a": null, "editada": false, "apagada": false,
"criada_em": "2026-10-01T09:12:40-03:00"
},
{
"id": 402852, "conversa_id": 18342, "direcao": "enviada", "autor": "ia", "autor_nome": "",
"tipo": "texto", "conteudo": "Hi, Maria! I will check your connection right now.", "transcricao": null, "arquivo": null,
"status": "lida", "erro": null, "responde_a": null, "editada": false, "apagada": false,
"criada_em": "2026-10-01T09:12:52-03:00"
},
{
"id": 402866, "conversa_id": 18342, "direcao": "enviada", "autor": "atendente", "autor_nome": "Carlos Lima",
"tipo": "texto", "conteudo": "Maria, I found the problem. A technician will visit you this afternoon.", "transcricao": null, "arquivo": null,
"status": "entregue", "erro": null, "responde_a": 402850, "editada": false, "apagada": false,
"criada_em": "2026-10-01T09:20:03-03:00"
}
]
}Route-specific errors: identificador_obrigatorio (none of the three identifiers), telefone_invalido, nao_encontrado (404: id or protocol number not found, or phone number with no conversation).
Message history
/api_publica.php?r=conversas/mensagens
Walks through the whole history of a conversation, in pages, using a message id cursor. Unlike numbered pagination, the cursor neither skips nor repeats messages when new messages arrive in the middle of the reading. Messages always come in chronological order (oldest to newest) and without internal notes.
Permission: Look up conversations.
| Parameter | Type | Required | Description |
|---|---|---|---|
conversa_id (or id) or protocolo | Yes | The conversation. | |
limite | integer | No | Messages per page, from 1 to 100, default 50. |
antes_de | integer | No | Scroll back: returns the messages before this id (the ones closest to it). |
depois_de | integer | No | Fetch what arrived: returns the messages after this id, starting from the oldest. Takes precedence over antes_de. |
| Response field | Description |
|---|---|
conversa_id | The conversation read. |
mensagens | List of messages, in chronological order. |
tem_mais | true when there are more messages in the requested direction: older ones (no cursor or with antes_de) or newer ones beyond this page (with depois_de). |
cursor.antes_de | id of the first message on the page: pass it in antes_de for the previous page. |
cursor.depois_de | id of the last message on the page: pass it in depois_de to fetch what arrives later. On an empty page with depois_de, it repeats the value sent, so you can continue from the same point. |
Two ways to use it
- Read everything backwards (export the history): first call without a cursor (the newest messages); while
tem_maisistrue, call again withantes_deset tocursor.antes_de. - Follow what arrives (mirror the conversation in your system): store
cursor.depois_deand, next time, call withdepois_de; whiletem_maisistrue, continue with the newcursor.depois_de.
# the 50 most recent curl -s "https://atendimento.seuprovedor.com.br/api_publica.php?r=conversas/mensagens&conversa_id=18342" \ -H "Authorization: Bearer $CHATISP_CHAVE" # the previous page curl -s "https://atendimento.seuprovedor.com.br/api_publica.php?r=conversas/mensagens&conversa_id=18342&antes_de=402850" \ -H "Authorization: Bearer $CHATISP_CHAVE" # what arrived after message 402866 curl -s "https://atendimento.seuprovedor.com.br/api_publica.php?r=conversas/mensagens&protocolo=20261001-0042&depois_de=402866" \ -H "Authorization: Bearer $CHATISP_CHAVE"
<?php // exports the full history, from oldest to newest function chatisp(array $params): array { $ch = curl_init('https://atendimento.seuprovedor.com.br/api_publica.php?' . http_build_query($params)); curl_setopt_array($ch, [ CURLOPT_RETURNTRANSFER => true, CURLOPT_HTTPHEADER => ['Authorization: Bearer ' . getenv('CHATISP_CHAVE')], ]); $r = json_decode(curl_exec($ch), true); curl_close($ch); return $r; } $historico = []; $params = ['r' => 'conversas/mensagens', 'conversa_id' => 18342, 'limite' => 100]; do { $r = chatisp($params); $historico = array_merge($r['mensagens'], $historico); $params['antes_de'] = $r['cursor']['antes_de']; } while ($r['tem_mais']);
import os, requests URL = "https://atendimento.seuprovedor.com.br/api_publica.php" CAB = {"Authorization": "Bearer " + os.environ["CHATISP_CHAVE"]} def novas_mensagens(conversa_id, ultimo_id): """Fetches everything that arrived after ultimo_id and returns the new cursor.""" while True: r = requests.get(URL, headers=CAB, timeout=30, params={ "r": "conversas/mensagens", "conversa_id": conversa_id, "depois_de": ultimo_id, "limite": 100}).json() for m in r["mensagens"]: print(m["autor"], m["conteudo"]) ultimo_id = r["cursor"]["depois_de"] if not r["tem_mais"]: return ultimo_id
{
"ok": true,
"conversa_id": 18342,
"mensagens": [
{ "id": 402852, "conversa_id": 18342, "direcao": "enviada", "autor": "ia", "...": "..." },
{ "id": 402866, "conversa_id": 18342, "direcao": "enviada", "autor": "atendente", "...": "..." }
],
"tem_mais": true,
"cursor": { "antes_de": 402852, "depois_de": 402866 }
}Route-specific errors: identificador_obrigatorio, nao_encontrado, parametro_invalido (limite outside 1 to 100, with maximo; a cursor that is not an integer).
Transfer conversation
/api_publica.php?r=conversas/transferir
Moves an open conversation to a department, to an agent or to both, exactly like the Transfer button in the panel. Examples: the ERP detects that the customer is past due and sends the conversation to Billing; the CRM routes business customers to their account manager.
Permission: Manage conversations.
| Parameter | Type | Required | Description |
|---|---|---|---|
conversa_id (or id) or protocolo | Yes | The conversation. It must be open. | |
departamento_id | integer | At least one of the two | Target department, active. |
atendente_id | integer | At least one of the two | Target agent, active. |
motivo | string | No | Free text, up to 300 characters, shown in the timeline next to the key name (for example, API: ERP (customer past due)). |
What happens
| You send | Result |
|---|---|
departamento_id only | The conversation goes back to that department queue, with no agent. The department automatic distribution tries to deliver it right away; if nobody can take it, the team gets the new conversation in queue alert. |
atendente_id only | The conversation goes straight to the agent (humano), in the department it was already in. The agent is notified through the app and, if WhatsApp alerts are turned on, through WhatsApp. |
| Both | The conversation goes to the agent and is now listed under that department. |
- In every case the conversation leaves automation: if it was with the AI or in a flow, they stop replying.
- Sharing of the conversation with other agents is undone; the recipient decides who to share it with.
- The API does not check whether the agent belongs to the department: the combination is the integration's responsibility.
- The customer gets no message about the transfer. The only possible exception is the Waiting time notice, when it is turned on in Settings and the conversation enters the queue with every agent busy.
- The
conversa_transferidawebhook fires (andconversa_atribuidawhen automatic distribution delivers the conversation to someone).
curl -s -X POST "https://atendimento.seuprovedor.com.br/api_publica.php?r=conversas/transferir" \
-H "Authorization: Bearer $CHATISP_CHAVE" \
-H "Content-Type: application/json" \
-d '{"protocolo": "20261001-0042", "departamento_id": 2, "motivo": "customer past due"}'<?php $ch = curl_init('https://atendimento.seuprovedor.com.br/api_publica.php?r=conversas/transferir'); curl_setopt_array($ch, [ CURLOPT_POST => true, CURLOPT_RETURNTRANSFER => true, CURLOPT_HTTPHEADER => ['Authorization: Bearer ' . getenv('CHATISP_CHAVE'), 'Content-Type: application/json'], CURLOPT_POSTFIELDS => json_encode(['conversa_id' => 18342, 'atendente_id' => 7, 'motivo' => 'business customer']), ]); $r = json_decode(curl_exec($ch), true); curl_close($ch); echo $r['conversa']['estado'], ' with ', $r['conversa']['atendente'], PHP_EOL;
import os, requests r = requests.post("https://atendimento.seuprovedor.com.br/api_publica.php", params={"r": "conversas/transferir"}, headers={"Authorization": "Bearer " + os.environ["CHATISP_CHAVE"]}, json={"conversa_id": 18342, "departamento_id": 2, "atendente_id": 9}, timeout=30) print(r.status_code, r.json()["conversa"]["estado"])
The response returns the conversation already updated, with tags:
{
"ok": true,
"conversa": {
"id": 18342,
"protocolo": "20261001-0042",
"estado": "fila",
"departamento_id": 2,
"departamento": "Billing",
"atendente_id": null,
"atendente": "",
"...": "other fields of the conversation object",
"etiquetas": []
}
}If automatic distribution delivers the conversation right away, estado already comes back as humano, with the agent it picked.
Route-specific errors: parametro_obrigatorio (no department and no agent; campo set to departamento_id|atendente_id), departamento_invalido, atendente_invalido (not found, inactive or deleted), conversa_encerrada (409), identificador_obrigatorio, nao_encontrado.
Close conversation
/api_publica.php?r=conversas/encerrar
Closes an open conversation, like the End button in the panel. Example: the work order system closes the work order and closes the WhatsApp request with the right reason.
Permission: Manage conversations.
| Parameter | Type | Required | Description |
|---|---|---|---|
conversa_id (or id) or protocolo | Yes | The conversation. It must be open. | |
motivo_id | integer | No | Active closing reason (reasons are registered in Catalogs and listed by motivos/listar). Without it, the conversation has no reason and the timeline records the key name. |
enviar_mensagem | boolean | No | Default true. With false, nothing is sent to the customer on closing. |
What happens to the customer
With enviar_mensagem true (default), closing through the API does the same as closing through the panel:
- Closed ticket message: the text configured in Settings (with
{protocolo}and{nome}filled in) is sent, as long as there was real service: the customer took part in the conversation, or the AI or an agent talked to them. With the field empty in Settings, nothing is sent. - Satisfaction survey: if Send the survey when closing handled conversations is on (see Satisfaction survey), the question is sent when the AI or an agent replied to the customer in the conversation. The score the customer gives goes into the reports.
With enviar_mensagem=false, neither of them is sent. Use it, for example, when closing old or test conversations in bulk.
What happens behind the scenes
- The state becomes
encerrada, with the reason; the conversation leaves the flow and the AI; an open reply wait is ended. - If the customer wrote something, the AI writes the request
resumoin the background (look up the conversation again a few minutes later). - Service orders opened from this request have their summary rewritten to cover the whole conversation.
- The
conversa_encerradawebhook fires, withmotivoset to the name of the chosen reason (or to the key identification, without a reason).
# closes with a reason, sending the closing message and the survey curl -s -X POST "https://atendimento.seuprovedor.com.br/api_publica.php" \ -H "Authorization: Bearer $CHATISP_CHAVE" \ -d "r=conversas/encerrar" -d "protocolo=20261001-0042" -d "motivo_id=1" # closes silently curl -s -X POST "https://atendimento.seuprovedor.com.br/api_publica.php" \ -H "Authorization: Bearer $CHATISP_CHAVE" \ -d "r=conversas/encerrar" -d "conversa_id=18342" -d "enviar_mensagem=0"
<?php $ch = curl_init('https://atendimento.seuprovedor.com.br/api_publica.php?r=conversas/encerrar'); curl_setopt_array($ch, [ CURLOPT_POST => true, CURLOPT_RETURNTRANSFER => true, CURLOPT_HTTPHEADER => ['Authorization: Bearer ' . getenv('CHATISP_CHAVE'), 'Content-Type: application/json'], CURLOPT_POSTFIELDS => json_encode(['protocolo' => '20261001-0042', 'motivo_id' => 1]), ]); $r = json_decode(curl_exec($ch), true); $status = curl_getinfo($ch, CURLINFO_HTTP_CODE); curl_close($ch); if ($status === 409) { // conversa_encerrada: it was already closed; treat it as success }
import os, requests r = requests.post("https://atendimento.seuprovedor.com.br/api_publica.php", params={"r": "conversas/encerrar"}, headers={"Authorization": "Bearer " + os.environ["CHATISP_CHAVE"]}, json={"conversa_id": 18342, "motivo_id": 1, "enviar_mensagem": False}, timeout=30) c = r.json()["conversa"] print(c["estado"], c["motivo_encerramento"], c["encerrada_em"])
{
"ok": true,
"conversa": {
"id": 18342,
"protocolo": "20261001-0042",
"estado": "encerrada",
"motivo_encerramento": "Resolved",
"resumo": null,
"encerrada_em": "2026-10-01T11:05:12-03:00",
"...": "other fields of the conversation object"
}
}Route-specific errors: motivo_invalido (not found or inactive), conversa_encerrada (409: it was already closed; when reprocessing, treat it as success), parametro_invalido (enviar_mensagem with a value that is not boolean), identificador_obrigatorio, nao_encontrado.
Conversation tags
/api_publica.php?r=conversas/etiquetas
Changes the tags of a conversation (open or closed). Tags are registered in Catalogs and listed by etiquetas/listar. Examples: tag VIP when the ERP identifies a business plan; tag Past due so Billing can filter it on the kanban.
Permission: Manage conversations.
| Parameter | Type | Description |
|---|---|---|
conversa_id (or id) or protocolo | The conversation (required). | |
etiquetas | list of ids | Replaces all tags with the list. An empty list ([] in JSON or etiquetas= in a form) removes them all. |
adicionar | list of ids | Adds these tags, leaving the others untouched. |
remover | list of ids | Removes these tags, leaving the others untouched. |
Send at least one of the three. They can be sent together: the API starts from etiquetas (or from the current tags, without it), adds adicionar and removes remover. Lists accept JSON ([1, 3]), form fields (adicionar[]=1&adicionar[]=3) or comma separated text (adicionar=1,3). Repeated ids are ignored.
# adds tags 1 and 3 and removes tag 2 curl -s -X POST "https://atendimento.seuprovedor.com.br/api_publica.php?r=conversas/etiquetas" \ -H "Authorization: Bearer $CHATISP_CHAVE" \ -H "Content-Type: application/json" \ -d '{"conversa_id": 18342, "adicionar": [1, 3], "remover": [2]}' # clears them all curl -s -X POST "https://atendimento.seuprovedor.com.br/api_publica.php" \ -H "Authorization: Bearer $CHATISP_CHAVE" \ -d "r=conversas/etiquetas" -d "conversa_id=18342" -d "etiquetas="
<?php $ch = curl_init('https://atendimento.seuprovedor.com.br/api_publica.php?r=conversas/etiquetas'); curl_setopt_array($ch, [ CURLOPT_POST => true, CURLOPT_RETURNTRANSFER => true, CURLOPT_HTTPHEADER => ['Authorization: Bearer ' . getenv('CHATISP_CHAVE'), 'Content-Type: application/json'], CURLOPT_POSTFIELDS => json_encode(['protocolo' => '20261001-0042', 'etiquetas' => [1]]), ]); $r = json_decode(curl_exec($ch), true); curl_close($ch); echo implode(', ', array_column($r['conversa']['etiquetas'], 'nome')), PHP_EOL;
import os, requests r = requests.post("https://atendimento.seuprovedor.com.br/api_publica.php", params={"r": "conversas/etiquetas"}, headers={"Authorization": "Bearer " + os.environ["CHATISP_CHAVE"]}, json={"conversa_id": 18342, "adicionar": [3]}, timeout=30) print([e["nome"] for e in r.json()["conversa"]["etiquetas"]])
{
"ok": true,
"conversa": {
"id": 18342,
"protocolo": "20261001-0042",
"...": "other fields of the conversation object",
"etiquetas": [
{ "id": 1, "nome": "VIP", "cor": "#f59e0b" },
{ "id": 3, "nome": "Technician visit", "cor": "#2563eb" }
]
}
}{"ok": false, "error": "etiqueta_invalida", "campo": "adicionar"}Route-specific errors: parametro_obrigatorio (none of the three; campo set to etiquetas|adicionar|remover), etiqueta_invalida (id not found; nothing is changed), parametro_invalido, identificador_obrigatorio, nao_encontrado.
Internal note
/api_publica.php?r=conversas/nota
Writes a note in the conversation timeline, visible to the team only. Examples: the NOC reports a fiber cut in the customer's area; the ERP records that the payment cleared during the service.
Permission: Manage conversations.
| Parameter | Type | Required | Description |
|---|---|---|---|
conversa_id (or id) or protocolo | Yes | The conversation (open or closed). | |
texto | string | Yes | From 1 to 900 characters. |
- The note appears in the chat as a system line, with the key name in front: API (ERP): Payment cleared at 10:12 AM.
- It is never sent to the customer, is not part of the history the AI reads and does not appear in reports.
- It also does not come back through the API read routes (which never return internal content): store the
nota_idif you need to reference it.
curl -s -X POST "https://atendimento.seuprovedor.com.br/api_publica.php?r=conversas/nota" \
-H "Authorization: Bearer $CHATISP_CHAVE" \
-H "Content-Type: application/json" \
-d '{"protocolo": "20261001-0042", "texto": "Fiber cut in the Jardim Alvorada neighborhood, expected fix at 2 PM."}'<?php $ch = curl_init('https://atendimento.seuprovedor.com.br/api_publica.php?r=conversas/nota'); curl_setopt_array($ch, [ CURLOPT_POST => true, CURLOPT_RETURNTRANSFER => true, CURLOPT_HTTPHEADER => ['Authorization: Bearer ' . getenv('CHATISP_CHAVE'), 'Content-Type: application/json'], CURLOPT_POSTFIELDS => json_encode(['conversa_id' => 18342, 'texto' => 'Payment cleared at 10:12 AM.']), ]); $r = json_decode(curl_exec($ch), true); curl_close($ch);
import os, requests r = requests.post("https://atendimento.seuprovedor.com.br/api_publica.php", params={"r": "conversas/nota"}, headers={"Authorization": "Bearer " + os.environ["CHATISP_CHAVE"]}, json={"conversa_id": 18342, "texto": "Payment cleared at 10:12 AM."}, timeout=30) print(r.json())
{"ok": true, "nota_id": 402990, "conversa_id": 18342}Route-specific errors: texto_invalido (empty or over 900 characters), identificador_obrigatorio, nao_encontrado.
Open service request
/api_publica.php?r=atendimentos/abrir
Puts a phone number in the service queue (or straight with an agent) without sending any message to the customer. This is the way to go when the request starts outside WhatsApp: website contact form, subscriber portal, monitoring system alarm, scheduled callback. The team sees the service request in the panel and talks to the customer from there.
Permission: Open service requests.
| Parameter | Type | Required | Description |
|---|---|---|---|
telefone | string | Yes | Customer phone number (same rule as the sending routes). |
conexao_id | integer | Conditional | WhatsApp number the team will use to serve the customer. 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. |
departamento_id | integer | No | Target department, active. Without it, the department the conversation is already in applies (in a new conversation, the Department for new conversations from Settings, or the general queue). |
atendente_id | integer | No | Delivers the conversation straight to this agent, active. |
nota | string | No | Up to 900 characters. Becomes an internal note in the conversation (API (Portal): Customer asked for a callback about an address change.), so the team knows what it is about before talking to the customer. |
nome | string | No | Customer name for a new conversation (up to 190 characters). The name the team saved for the contact takes precedence. |
How the call behaves
- ChatISP looks for an open conversation of this phone number on the WhatsApp number (across both channels of a twin number, with and without the ninth digit). If there is none, it creates a new one, in the queue, with its own protocol number (the
conversa_criadawebhook fires). - The
nota, if sent, is written to the timeline, along with the key record. - With
atendente_id: the conversation goes straight to that agent (statehumano), whatever state it was in, and also to thedepartamento_id, if sent. It leaves the AI or the flow, if it was in them. The agent is notified. - Without
atendente_id, with the conversation in the queue: it goes to the queue of the requesteddepartamento_id(or stays in the current department queue), and automatic distribution tries to deliver it right away. If an agent is available, it already comes out ashumano; otherwise it staysfilaand the team gets the alert. - Without
atendente_id, with the conversation with the AI, in a flow or already with an agent: nothing changes, not even the department. The API does not pull a conversation that is already being handled.
Nothing is sent to the customer. They only receive something when the team writes. The only possible exception is the Waiting time notice, if it is turned on in Settings and the conversation stays in the queue with every agent busy. To open the request and send a message right away, call mensagens/enviar right after with the returned conversa_id: since the conversation is already open, it is not treated as a notice and stays open.
curl -s -X POST "https://atendimento.seuprovedor.com.br/api_publica.php?r=atendimentos/abrir" \
-H "Authorization: Bearer $CHATISP_CHAVE" \
-H "Content-Type: application/json" \
-d '{
"telefone": "5544988887777",
"nome": "João Pereira",
"departamento_id": 2,
"nota": "Asked through the portal to renegotiate the September invoice."
}'<?php $ch = curl_init('https://atendimento.seuprovedor.com.br/api_publica.php'); curl_setopt_array($ch, [ CURLOPT_POST => true, CURLOPT_RETURNTRANSFER => true, CURLOPT_HTTPHEADER => ['Authorization: Bearer ' . getenv('CHATISP_CHAVE')], CURLOPT_POSTFIELDS => http_build_query([ 'r' => 'atendimentos/abrir', 'telefone' => '5544988887777', 'nome' => 'João Pereira', 'departamento_id' => 2, 'nota' => 'Asked through the portal to renegotiate the September invoice.', ]), ]); $r = json_decode(curl_exec($ch), true); curl_close($ch); echo $r['ja_existia'] ? 'A service request already existed: ' : 'Service request opened: ', $r['protocolo'], PHP_EOL;
import os, requests r = requests.post("https://atendimento.seuprovedor.com.br/api_publica.php", params={"r": "atendimentos/abrir"}, headers={"Authorization": "Bearer " + os.environ["CHATISP_CHAVE"]}, json={ "telefone": "5544988887777", "atendente_id": 7, "nota": "Callback scheduled by the NOC.", }, timeout=30) corpo = r.json() print(corpo["protocolo"], corpo["estado"], corpo["ja_existia"])
{
"ok": true,
"conversa_id": 18361,
"protocolo": "20261001-0056",
"estado": "fila",
"ja_existia": false,
"conversa": {
"id": 18361,
"protocolo": "20261001-0056",
"conexao_id": 1,
"telefone": "5544988887777",
"nome": "João Pereira",
"estado": "fila",
"departamento_id": 2,
"departamento": "Billing",
"...": "other fields of the conversation object",
"etiquetas": []
}
}| Field | Description |
|---|---|
conversa_id, protocolo | Service request identification. |
estado | State after the call: fila (waiting for an agent), humano (with the requested agent or delivered by automatic distribution) or, in a conversation that was already being handled, the state it was in (ia, auto, humano). |
ja_existia | true when the phone number already had an open conversation on that number (in any state); false when this call created the service request. |
conversa | The full conversation object, with tags. |
Idempotent call: calling again for the same phone number while the service request is open does not create a duplicate; it returns the same conversation with ja_existia: true (and writes the new nota, if sent). This makes it safe to repeat the call after a network error.
Route-specific errors: telefone_invalido, conexao_invalida, sem_conexao, conexao_id_obrigatorio, conexao_desconectada, departamento_invalido, atendente_invalido, texto_invalido (note over 900 characters, with campo set to nota).
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 | Routes | Cause |
|---|---|---|---|
metodo | 405 | transferir, encerrar, etiquetas, nota, abrir | GET call on a write route (esperado: POST). |
identificador_obrigatorio | 400 | all except listar and abrir | Missing conversa_id/id or protocolo (or telefone, in consultar). |
nao_encontrado | 404 | all except listar and abrir | The conversation does not exist. |
parametro_obrigatorio | 400 | transferir, etiquetas | Missing target or list; campo says which. |
data_invalida | 400 | listar | Date that could not be parsed; campo says which. |
telefone_invalido | 400 | listar, consultar, abrir | Phone number that makes no sense. |
conversa_encerrada | 409 | transferir, encerrar | The conversation is already closed. |
departamento_invalido | 400 | transferir, abrir | Department not found or inactive. |
atendente_invalido | 400 | transferir, abrir | Agent not found, inactive or deleted. |
motivo_invalido | 400 | encerrar | Reason not found or inactive. |
etiqueta_invalida | 400 | etiquetas | Tag not found; campo says in which list. |
texto_invalido | 400 | nota, abrir | Note text empty or over 900 characters. |
conexao_invalida, sem_conexao, conexao_id_obrigatorio | 400 | abrir | Choice of the WhatsApp number (see Who the message goes to). |
conexao_desconectada | 409 | abrir | The chosen WhatsApp number is disconnected. |