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.

RouteMethodKey permissionSummary
conversas/listarGET or POSTLook up conversationsPaginated list with filters by state, department, agent, connection, phone number, tag and dates.
conversas/consultarGET or POSTLook up conversationsOne conversation (by id, protocol number or phone number) with tags and its latest messages.
conversas/mensagensGET or POSTLook up conversationsFull history, paginated by cursor.
conversas/transferirPOSTManage conversationsTo a department, to an agent or to both.
conversas/encerrarPOSTManage conversationsCloses with a reason, with or without the closing message and the survey.
conversas/etiquetasPOSTManage conversationsSets, adds or removes tags.
conversas/notaPOSTManage conversationsInternal note in the timeline (the team sees it, the customer never does).
atendimentos/abrirPOSTOpen service requestsPuts 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 (or id) or protocolo. With neither: 400 identificador_obrigatorio. Not found: 404 nao_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:

ValueOn screenMeaning
autoAutomationDriven 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.
iaWith AIBeing handled by the AI. Leaves it when the AI transfers to the team or when an agent takes over.
filaIn queueWaiting 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.
humanoIn serviceWith an agent from the team (atendente_id).
encerradaClosedService 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):

conversation
{
  "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" }
  ]
}
FieldTypeDescription
idintegerConversation identifier (the conversa_id of the other routes and of the webhooks).
protocolostringService protocol number, the same one the team and the customer see.
conexao_idinteger or nullWhatsApp number of the conversation. null when the connection was deleted from the system (the history is preserved).
telefonestringCustomer phone number, digits only, with the country code.
nomestringCustomer name: the one saved by the team or, without it, the WhatsApp profile name. Empty when unknown.
estadostringauto, ia, fila, humano or encerrada. See Conversation states.
aguardando_clientebooleantrue 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, departamentointeger or null, stringCurrent department (id and name). null and empty in the general queue.
atendente_id, atendenteinteger or null, stringAgent in charge (id and name). In a closed conversation, whoever had it at the end.
nao_lidasintegerCustomer messages the person in charge has not opened in the panel yet.
motivo_encerramentostringName of the chosen closing reason. Empty when the conversation is open or was closed without a reason.
resumoobject or nullSummary 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_emdate and timeWhen the conversation was opened.
ultima_mensagem_emdate and time or nullLast message exchanged (from the customer or to them).
encerrada_emdate and time or nullWhen it was closed; null if open.
etiquetaslistConversation 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:

message
{
  "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"
}
FieldTypeDescription
idintegerMessage identifier. It grows over time: it serves as the cursor in conversas/mensagens.
conversa_idintegerConversation the message belongs to.
direcaostringrecebida (from the customer) or enviada (from the ISP).
autorstringcliente, 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_nomestringAgent name when autor is atendente; empty otherwise.
tipostringtexto, imagem, audio, video, documento, localizacao or contato (shared contact card).
conteudostringMessage text. In media, the caption (may be empty). Also empty when the message was deleted.
transcricaostring or nullTranscription of an audio message, when audio transcription is on and has finished. null otherwise.
arquivoobject or nullMedia 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.
statusstringpendente (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).
errostring or nullReason for the failure when status is erro.
responde_ainteger or nullid of the message quoted (replied to) by this one.
editadabooleanThe message was edited after being sent; conteudo holds the current version.
apagadabooleanThe message was deleted for everyone. The record remains (so the history stays consistent), with no content and no file.
criada_emdate and timeTime of the message.

List conversations

GET POST /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.

ParameterTypeDescription
estadolistOne 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.
abertasboolean1: open conversations only (any state except encerrada). Accepts 1/0, true/false, sim/nao.
departamento_idintegerOnly this department. 0: only those with no department (general queue).
atendente_idintegerOnly this agent. 0: only those with no agent.
conexao_idintegerOnly this WhatsApp number. On a twin number, returns the conversations of both channels.
telefonestringOnly this customer, with and without the ninth digit. Same phone number rule as the other routes (telefone_invalido if it makes no sense).
etiqueta_idintegerOnly those with this tag.
criadas_desde, criadas_atedateOpened 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_desdedateClosed from this moment on.
atualizadas_desdedateConversations opened, closed or with a new message from this moment on. This is the incremental sync filter.
ordemstringrecentes (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, limiteintegerPagination: 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.

terminal
# 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
<?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']);
Python
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"])
JSON response (HTTP 200)
{
  "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).

  1. On the first load, list everything (no date filter, ordem=criacao) and store the conversations by id.
  2. Before each run, write down the current server time (field servidor.agora of chave/info, or your own synchronized clock). That will be the checkpoint for the next run.
  3. Call conversas/listar with atualizadas_desde set to the checkpoint of the previous run minus a small margin (one or two minutes), ordem=antigas and limite=100, walking through the pages.
  4. 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 by id makes that harmless.
  5. For the messages of each changed conversation, use conversas/mensagens with depois_de set to the last message id you 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.

incremental run (Python)
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

GET POST /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.

ParameterTypeRequiredDescription
id or conversa_idintegerOne of the threeConversation identifier.
protocolostringOne of the threeProtocol number, for example 20261001-0042.
telefonestringOne of the threeReturns the most recent conversation of this phone number, on any WhatsApp number and in any state (including closed), with and without the ninth digit.
mensagensintegerNoHow 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.

terminal
# 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
<?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;
}
Python
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"])
JSON response (HTTP 200)
{
  "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

GET POST /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.

ParameterTypeRequiredDescription
conversa_id (or id) or protocoloYesThe conversation.
limiteintegerNoMessages per page, from 1 to 100, default 50.
antes_deintegerNoScroll back: returns the messages before this id (the ones closest to it).
depois_deintegerNoFetch what arrived: returns the messages after this id, starting from the oldest. Takes precedence over antes_de.
Response fieldDescription
conversa_idThe conversation read.
mensagensList of messages, in chronological order.
tem_maistrue 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_deid of the first message on the page: pass it in antes_de for the previous page.
cursor.depois_deid 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_mais is true, call again with antes_de set to cursor.antes_de.
  • Follow what arrives (mirror the conversation in your system): store cursor.depois_de and, next time, call with depois_de; while tem_mais is true, continue with the new cursor.depois_de.
terminal
# 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
<?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']);
Python
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
JSON response (HTTP 200, limite=2)
{
  "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

POST /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.

ParameterTypeRequiredDescription
conversa_id (or id) or protocoloYesThe conversation. It must be open.
departamento_idintegerAt least one of the twoTarget department, active.
atendente_idintegerAt least one of the twoTarget agent, active.
motivostringNoFree text, up to 300 characters, shown in the timeline next to the key name (for example, API: ERP (customer past due)).

What happens

You sendResult
departamento_id onlyThe 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 onlyThe 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.
BothThe 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_transferida webhook fires (and conversa_atribuida when automatic distribution delivers the conversation to someone).
terminal
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
<?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;
Python
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:

JSON response (HTTP 200, abridged)
{
  "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

POST /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.

ParameterTypeRequiredDescription
conversa_id (or id) or protocoloYesThe conversation. It must be open.
motivo_idintegerNoActive 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_mensagembooleanNoDefault 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:

  1. 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.
  2. 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 resumo in 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_encerrada webhook fires, with motivo set to the name of the chosen reason (or to the key identification, without a reason).
terminal
# 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
<?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
}
Python
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"])
JSON response (HTTP 200, abridged)
{
  "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

POST /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.

ParameterTypeDescription
conversa_id (or id) or protocoloThe conversation (required).
etiquetaslist of idsReplaces all tags with the list. An empty list ([] in JSON or etiquetas= in a form) removes them all.
adicionarlist of idsAdds these tags, leaving the others untouched.
removerlist of idsRemoves 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.

terminal
# 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
<?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;
Python
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"]])
JSON response (HTTP 200, abridged)
{
  "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" }
    ]
  }
}
tag not found (HTTP 400)
{"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

POST /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.

ParameterTypeRequiredDescription
conversa_id (or id) or protocoloYesThe conversation (open or closed).
textostringYesFrom 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_id if you need to reference it.
terminal
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
<?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);
Python
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())
JSON response (HTTP 200)
{"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

POST /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.

ParameterTypeRequiredDescription
telefonestringYesCustomer phone number (same rule as the sending routes).
conexao_idintegerConditionalWhatsApp 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_idintegerNoTarget 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_idintegerNoDelivers the conversation straight to this agent, active.
notastringNoUp 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.
nomestringNoCustomer name for a new conversation (up to 190 characters). The name the team saved for the contact takes precedence.

How the call behaves

  1. 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_criada webhook fires).
  2. The nota, if sent, is written to the timeline, along with the key record.
  3. With atendente_id: the conversation goes straight to that agent (state humano), whatever state it was in, and also to the departamento_id, if sent. It leaves the AI or the flow, if it was in them. The agent is notified.
  4. Without atendente_id, with the conversation in the queue: it goes to the queue of the requested departamento_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 as humano; otherwise it stays fila and the team gets the alert.
  5. 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.

terminal
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
<?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;
Python
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"])
JSON response (HTTP 200)
{
  "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": []
  }
}
FieldDescription
conversa_id, protocoloService request identification.
estadoState 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_existiatrue when the phone number already had an open conversation on that number (in any state); false when this call created the service request.
conversaThe 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:

errorHTTPRoutesCause
metodo405transferir, encerrar, etiquetas, nota, abrirGET call on a write route (esperado: POST).
identificador_obrigatorio400all except listar and abrirMissing conversa_id/id or protocolo (or telefone, in consultar).
nao_encontrado404all except listar and abrirThe conversation does not exist.
parametro_obrigatorio400transferir, etiquetasMissing target or list; campo says which.
data_invalida400listarDate that could not be parsed; campo says which.
telefone_invalido400listar, consultar, abrirPhone number that makes no sense.
conversa_encerrada409transferir, encerrarThe conversation is already closed.
departamento_invalido400transferir, abrirDepartment not found or inactive.
atendente_invalido400transferir, abrirAgent not found, inactive or deleted.
motivo_invalido400encerrarReason not found or inactive.
etiqueta_invalida400etiquetasTag not found; campo says in which list.
texto_invalido400nota, abrirNote text empty or over 900 characters.
conexao_invalida, sem_conexao, conexao_id_obrigatorio400abrirChoice of the WhatsApp number (see Who the message goes to).
conexao_desconectada409abrirThe chosen WhatsApp number is disconnected.