Conversas e atendimentos
Referência das rotas que leem e gerenciam os atendimentos: listar conversas com filtros e sincronização incremental, consultar uma conversa, percorrer o histórico de mensagens por cursor, transferir, encerrar, etiquetar, anotar e abrir um atendimento na fila sem falar com o cliente. Com os objetos conversa e mensagem campo a campo, exemplos em curl, PHP e Python e os erros de cada rota.
Visão geral
No ChatISP, cada conversa é um atendimento, com protocolo próprio no formato AAAAMMDD-NNNN. Uma conversa encerrada nunca é reaberta: quando o cliente volta a escrever, nasce outra conversa, com outro protocolo. O conjunto das conversas de um mesmo telefone forma o contato.
| Rota | Método | Permissão da chave | Resumo |
|---|---|---|---|
conversas/listar | GET ou POST | Consultar conversas | Lista paginada com filtros por estado, departamento, atendente, conexão, telefone, etiqueta e datas. |
conversas/consultar | GET ou POST | Consultar conversas | Uma conversa (por id, protocolo ou telefone) com etiquetas e as últimas mensagens. |
conversas/mensagens | GET ou POST | Consultar conversas | Histórico completo, paginado por cursor. |
conversas/transferir | POST | Gerenciar conversas | Para um departamento, para um atendente ou para os dois. |
conversas/encerrar | POST | Gerenciar conversas | Encerra com motivo, com ou sem a mensagem de encerramento e a pesquisa. |
conversas/etiquetas | POST | Gerenciar conversas | Define, adiciona ou remove etiquetas. |
conversas/nota | POST | Gerenciar conversas | Nota interna na linha do tempo (a equipe vê, o cliente nunca). |
atendimentos/abrir | POST | Abrir atendimentos | Coloca o telefone na fila de um departamento ou com um atendente, sem enviar nada ao cliente. |
- Visão da casa inteira. A chave enxerga todas as conversas, de todas as conexões e departamentos. As restrições de visibilidade dos perfis de atendente não se aplicam à API.
- Notas internas nunca saem. Mensagens internas da equipe e linhas do sistema ficam fora de todas as respostas e downloads.
- Rastro da chave. Toda ação de gestão feita pela API aparece na linha do tempo da conversa com o nome da chave (por exemplo, API: ERP), e o webhook correspondente é disparado como se a ação tivesse sido feita no painel.
- Identificar a conversa. As rotas de uma conversa aceitam
conversa_id(ouid) ouprotocolo. Sem nenhum deles: 400identificador_obrigatorio. Inexistente: 404nao_encontrado.
Os ids de departamento, atendente, etiqueta e motivo de encerramento vêm das rotas de Catálogos (departamentos/listar, atendentes/listar, etiquetas/listar, motivos/listar).
Estados da conversa
O campo estado diz quem está cuidando da conversa agora:
| Valor | Na tela | Significado |
|---|---|---|
auto | Automação | Conduzida por um fluxo de atendimento automático (menu, horário, etapas). Sai do fluxo quando ele transfere, aciona a IA ou encerra. |
ia | Com a IA | Sendo atendida pela IA. Sai dela quando a IA transfere para a equipe ou quando um atendente assume. |
fila | Na fila | Aguardando um atendente, na fila do departamento (departamento_id) ou na fila geral (sem departamento). É também o estado em que nasce uma conversa nova antes de o fluxo ou a IA assumirem. |
humano | Em atendimento | Com um atendente da equipe (atendente_id). |
encerrada | Encerrada | Atendimento terminado (pela equipe, pela IA, por inatividade, pela API) ou aviso enviado para quem não tinha conversa aberta. Não volta a abrir. |
Qualquer estado diferente de encerrada conta como conversa aberta (filtro abertas=1 de conversas/listar). Uma conversa marcada pelo atendente como Aguardando retorno do cliente continua humano, com aguardando_cliente: true.
Para acompanhar as mudanças de estado em tempo real, sem consultar em laço, use os webhooks conversa_criada, conversa_atribuida, conversa_transferida e conversa_encerrada. Veja Webhooks.
Objeto conversa
Formato usado por todas as rotas que devolvem conversas (listagens, consulta, gestão, abertura e contatos):
{
"id": 18342,
"protocolo": "20261001-0042",
"conexao_id": 1,
"telefone": "5544999990000",
"nome": "Maria Souza",
"estado": "encerrada",
"aguardando_cliente": false,
"departamento_id": 2,
"departamento": "Suporte técnico",
"atendente_id": 7,
"atendente": "Carlos Lima",
"nao_lidas": 0,
"motivo_encerramento": "Resolvido",
"resumo": {
"titulo": "Sem internet após queda de energia",
"texto": "A cliente relatou falta de internet desde a manhã. O atendente identificou a ONU sem sinal e agendou visita técnica para a tarde."
},
"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": "Visita técnica", "cor": "#2563eb" }
]
}| Campo | Tipo | Descrição |
|---|---|---|
id | inteiro | Identificador da conversa (o conversa_id das outras rotas e dos webhooks). |
protocolo | string | Protocolo do atendimento, o mesmo que a equipe e o cliente veem. |
conexao_id | inteiro ou null | Número de WhatsApp da conversa. null quando a conexão foi excluída do sistema (o histórico é preservado). |
telefone | string | Telefone do cliente, só dígitos, com o código do país. |
nome | string | Nome do cliente: o salvo pela equipe ou, sem ele, o nome do perfil do WhatsApp. Vazio quando desconhecido. |
estado | string | auto, ia, fila, humano ou encerrada. Veja Estados da conversa. |
aguardando_cliente | booleano | true enquanto o atendente marcou a conversa como aguardando retorno do cliente (essa espera não conta no tempo de atendimento). Volta a false quando o cliente responde ou a espera é cancelada. |
departamento_id, departamento | inteiro ou null, string | Departamento atual (id e nome). null e vazio na fila geral. |
atendente_id, atendente | inteiro ou null, string | Atendente responsável (id e nome). Numa conversa encerrada, quem estava com ela no fim. |
nao_lidas | inteiro | Mensagens do cliente que o responsável ainda não abriu no painel. |
motivo_encerramento | string | Nome do motivo de encerramento escolhido. Vazio quando a conversa está aberta ou foi encerrada sem motivo. |
resumo | objeto ou null | Resumo do chamado escrito pela IA depois do encerramento: titulo (assunto, até 70 caracteres) e texto (2 a 4 frases: o que o cliente trouxe, o que foi feito e o desfecho). Gerado em segundo plano, segundos ou minutos depois do encerramento, e só quando o cliente escreveu algo na conversa. null enquanto não existe. |
criada_em | data e hora | Abertura da conversa. |
ultima_mensagem_em | data e hora ou null | Última mensagem trocada (do cliente ou para ele). |
encerrada_em | data e hora ou null | Encerramento; null se aberta. |
etiquetas | lista | Etiquetas da conversa, cada uma com id, nome e cor, na ordem da tela. Vem em conversas/consultar, nas rotas de gestão e em atendimentos/abrir; não vem em conversas/listar nem em contatos/consultar. |
Datas em ISO 8601 com o fuso do servidor (o fuso aparece em chave/info, campo servidor.fuso_horario).
Objeto mensagem
Formato usado em conversas/consultar, conversas/mensagens e na resposta das rotas de envio:
{
"id": 402851,
"conversa_id": 18342,
"direcao": "recebida",
"autor": "cliente",
"autor_nome": "",
"tipo": "audio",
"conteudo": "",
"transcricao": "Boa tarde, minha internet caiu de novo depois da chuva.",
"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"
}| Campo | Tipo | Descrição |
|---|---|---|
id | inteiro | Identificador da mensagem. Cresce com o tempo: serve de cursor em conversas/mensagens. |
conversa_id | inteiro | Conversa a que a mensagem pertence. |
direcao | string | recebida (do cliente) ou enviada (do provedor). |
autor | string | cliente, ia, atendente (escrita por alguém da equipe no painel ou no aplicativo) ou sistema (enviada automaticamente: API, fluxos, automação de envios, mensagens padrão, pesquisa). |
autor_nome | string | Nome do atendente quando autor é atendente; vazio nos demais casos. |
tipo | string | texto, imagem, audio, video, documento, localizacao ou contato (cartão de contato compartilhado). |
conteudo | string | Texto da mensagem. Em mídias, a legenda (pode vir vazio). Vazio também quando a mensagem foi apagada. |
transcricao | string ou null | Transcrição de um áudio, quando a transcrição de áudios está ligada e já terminou. null nos demais casos. |
arquivo | objeto ou null | Mensagens com mídia: nome (nome original), mimetype e download, o valor de r que baixa o arquivo pela rota mensagens/arquivo. null em texto e em mensagem apagada. |
status | string | pendente (registrada, aguardando o canal), enviada (aceita pelo WhatsApp), entregue (chegou ao aparelho; as recebidas do cliente também aparecem assim), lida (o cliente leu, quando não desativou a confirmação de leitura) ou erro (o canal recusou). |
erro | string ou null | Motivo da falha quando status é erro. |
responde_a | inteiro ou null | id da mensagem citada (respondida) por esta. |
editada | booleano | A mensagem foi editada depois de enviada; conteudo traz a versão atual. |
apagada | booleano | A mensagem foi apagada para todos. O registro continua (para o histórico ficar coerente), sem conteúdo nem arquivo. |
criada_em | data e hora | Momento da mensagem. |
Listar conversas
/api_publica.php?r=conversas/listar
Lista paginada de conversas, com filtros combináveis (todos se somam com E). Sem filtro nenhum, devolve todas as conversas, das mais recentes para as mais antigas.
Permissão: Consultar conversas.
| Parâmetro | Tipo | Descrição |
|---|---|---|
estado | lista | Um ou mais estados: ia, fila, humano, encerrada, auto. Separados por vírgula (estado=fila,humano), em formulário (estado[]=fila&estado[]=humano) ou lista JSON. Valor desconhecido: 400 parametro_invalido com aceitos. |
abertas | booleano | 1: só conversas abertas (qualquer estado menos encerrada). Aceita 1/0, true/false, sim/nao. |
departamento_id | inteiro | Só desse departamento. 0: só as que estão sem departamento (fila geral). |
atendente_id | inteiro | Só desse atendente. 0: só as que estão sem atendente. |
conexao_id | inteiro | Só desse número de WhatsApp. Num número gêmeo, traz as conversas dos dois canais. |
telefone | string | Só desse cliente, com e sem o nono dígito. Mesma regra de telefone das outras rotas (telefone_invalido se não fizer sentido). |
etiqueta_id | inteiro | Só as que têm essa etiqueta. |
criadas_desde, criadas_ate | data | Abertura dentro do período. Só a data (2026-10-01) vale do início do dia, em criadas_desde, ao fim do dia, em criadas_ate. |
encerradas_desde | data | Encerradas a partir desse momento. |
atualizadas_desde | data | Conversas abertas, encerradas ou com mensagem nova a partir desse momento. É o filtro da sincronização incremental. |
ordem | string | recentes (padrão: última atividade, da mais nova para a mais antiga), antigas (última atividade, da mais antiga para a mais nova), criacao (ordem de abertura) ou criacao_desc (abertura, da mais nova para a mais antiga). A última atividade é a última mensagem ou, sem mensagens, a abertura. |
pagina, limite | inteiro | Paginação: pagina a partir de 1; limite de 1 a 100, padrão 50. |
Datas aceitam ISO 8601 (2026-10-01T08:00:00-03:00), AAAA-MM-DD ou AAAA-MM-DD HH:MM:SS; sem fuso, valem no fuso do servidor. Data que não faz sentido: 400 data_invalida com campo.
# fila do departamento 2 agora, as que esperam há mais tempo primeiro 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" # encerradas ontem com a etiqueta 3, página 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 // percorre todas as páginas das conversas abertas $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"], "conversas com o atendente 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": "Financeiro",
"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 }
}Erros específicos: parametro_invalido (estado, ordem, limite ou página fora do aceito, com campo), data_invalida, telefone_invalido.
Sincronização incremental
Para manter uma cópia das conversas no seu CRM, BI ou data warehouse sem baixar tudo de novo a cada rodada, use atualizadas_desde. Ele traz as conversas que, a partir do marco, foram abertas, encerradas ou tiveram mensagem nova (do cliente ou para ele).
- Na primeira carga, liste tudo (sem filtro de data,
ordem=criacao) e guarde as conversas peloid. - Antes de cada rodada, anote o horário atual do servidor (campo
servidor.agoradechave/info, ou o seu relógio sincronizado). Esse será o marco da próxima rodada. - Chame
conversas/listarcomatualizadas_desdeigual ao marco da rodada anterior, menos uma pequena folga (um ou dois minutos),ordem=antigaselimite=100, percorrendo as páginas. - Grave cada conversa por
id(atualiza se já existe, insere se é nova). A folga faz algumas conversas virem repetidas; gravar poridtorna isso inofensivo. - Para as mensagens de cada conversa alterada, use
conversas/mensagenscomdepois_deigual ao últimoidde mensagem que você já tem.
atualizadas_desde olha a abertura, o encerramento e a última mensagem. Mudanças que não envolvem mensagem (transferência, troca de etiquetas, o resumo da IA chegando depois do encerramento) não trazem a conversa de volta. Para elas, use os webhooks conversa_transferida e conversa_atribuida, e consulte de novo a conversa encerrada alguns minutos depois para pegar o 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 por c["id"] no seu banco if pagina >= r["paginacao"]["paginas"]: break pagina += 1 return agora # marco da próxima rodada
Consultar conversa
/api_publica.php?r=conversas/consultar
Devolve uma conversa completa (com etiquetas) e as últimas mensagens dela, em ordem cronológica. Serve para mostrar no CRM o andamento de um protocolo, conferir se o cliente respondeu a um aviso ou buscar o contexto depois de um webhook.
Permissão: Consultar conversas.
| Parâmetro | Tipo | Obrigatório | Descrição |
|---|---|---|---|
id ou conversa_id | inteiro | Um dos três | Identificador da conversa. |
protocolo | string | Um dos três | Protocolo, por exemplo 20261001-0042. |
telefone | string | Um dos três | Devolve a conversa mais recente desse telefone, em qualquer número de WhatsApp e em qualquer estado (inclusive encerrada), com e sem o nono dígito. |
mensagens | inteiro | Não | Quantas mensagens trazer, das mais recentes: de 0 a 100, padrão 20. Use 0 para só a conversa. Valores fora da faixa são ajustados ao limite mais próximo. |
Com mais de um identificador, vale o primeiro na ordem conversa_id/id, protocolo, telefone. Para mais de 100 mensagens, ou para paginar o histórico, use conversas/mensagens.
# por protocolo, com as 3 últimas mensagens curl -s "https://atendimento.seuprovedor.com.br/api_publica.php?r=conversas/consultar&protocolo=20261001-0042&mensagens=3" \ -H "Authorization: Bearer $CHATISP_CHAVE" # a conversa mais recente de um telefone, sem mensagens 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'], ' com ', $r['conversa']['atendente'] ?: 'ninguém', 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("Protocolo não existe") 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": "Suporte técnico",
"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": "Minha internet caiu desde cedo", "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": "Olá, Maria! Vou verificar a sua conexão agora mesmo.", "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, identifiquei a falha. Um técnico vai até você hoje à tarde.", "transcricao": null, "arquivo": null,
"status": "entregue", "erro": null, "responde_a": 402850, "editada": false, "apagada": false,
"criada_em": "2026-10-01T09:20:03-03:00"
}
]
}Erros específicos: identificador_obrigatorio (nenhum dos três identificadores), telefone_invalido, nao_encontrado (404: id ou protocolo inexistente, ou telefone sem nenhuma conversa).
Histórico de mensagens
/api_publica.php?r=conversas/mensagens
Percorre o histórico inteiro de uma conversa, em páginas, por cursor de id de mensagem. O cursor não pula nem repete mensagens quando chegam mensagens novas no meio da leitura, ao contrário de uma paginação numerada. As mensagens vêm sempre em ordem cronológica (da mais antiga para a mais nova) e sem as notas internas.
Permissão: Consultar conversas.
| Parâmetro | Tipo | Obrigatório | Descrição |
|---|---|---|---|
conversa_id (ou id) ou protocolo | Sim | A conversa. | |
limite | inteiro | Não | Mensagens por página, de 1 a 100, padrão 50. |
antes_de | inteiro | Não | Rolar para trás: traz as mensagens anteriores a esse id (as mais próximas dele). |
depois_de | inteiro | Não | Buscar o que chegou: traz as mensagens posteriores a esse id, a partir da mais antiga. Tem prioridade sobre antes_de. |
| Campo da resposta | Descrição |
|---|---|
conversa_id | A conversa lida. |
mensagens | Lista de mensagens, em ordem cronológica. |
tem_mais | true quando há mais mensagens na direção pedida: mais antigas (sem cursor ou com antes_de) ou mais novas além desta página (com depois_de). |
cursor.antes_de | id da primeira mensagem da página: passe em antes_de para a página anterior. |
cursor.depois_de | id da última mensagem da página: passe em depois_de para buscar o que chegar depois. Numa página vazia com depois_de, repete o valor enviado, para você continuar do mesmo ponto. |
Dois jeitos de usar
- Ler tudo de trás para a frente (exportar o histórico): primeira chamada sem cursor (as mais recentes); enquanto
tem_maisfortrue, chame de novo comantes_deigual acursor.antes_de. - Acompanhar o que chega (espelhar a conversa no seu sistema): guarde o
cursor.depois_dee, na próxima vez, chame comdepois_de; enquantotem_maisfortrue, continue com o novocursor.depois_de.
# as 50 mais recentes curl -s "https://atendimento.seuprovedor.com.br/api_publica.php?r=conversas/mensagens&conversa_id=18342" \ -H "Authorization: Bearer $CHATISP_CHAVE" # a página anterior curl -s "https://atendimento.seuprovedor.com.br/api_publica.php?r=conversas/mensagens&conversa_id=18342&antes_de=402850" \ -H "Authorization: Bearer $CHATISP_CHAVE" # o que chegou depois da mensagem 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 // exporta o histórico completo, do mais antigo para o mais novo 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): """Busca tudo o que chegou depois de ultimo_id e devolve o novo 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 }
}Erros específicos: identificador_obrigatorio, nao_encontrado, parametro_invalido (limite fora de 1 a 100, com maximo; cursor que não é número inteiro).
Transferir conversa
/api_publica.php?r=conversas/transferir
Leva uma conversa aberta para um departamento, para um atendente ou para os dois, exatamente como o botão Transferir do painel. Exemplos: o ERP detecta que o cliente está inadimplente e manda a conversa para o Financeiro; o CRM direciona clientes corporativos para o gerente de conta.
Permissão: Gerenciar conversas.
| Parâmetro | Tipo | Obrigatório | Descrição |
|---|---|---|---|
conversa_id (ou id) ou protocolo | Sim | A conversa. Precisa estar aberta. | |
departamento_id | inteiro | Ao menos um dos dois | Departamento de destino, ativo. |
atendente_id | inteiro | Ao menos um dos dois | Atendente de destino, ativo. |
motivo | string | Não | Texto livre, até 300 caracteres, que aparece na linha do tempo junto com o nome da chave (por exemplo, API: ERP (cliente inadimplente)). |
O que acontece
| Você envia | Resultado |
|---|---|
Só departamento_id | A conversa volta para a fila desse departamento, sem atendente. A distribuição automática do departamento tenta entregá-la na hora; se ninguém puder assumir, a equipe recebe o aviso de nova conversa na fila. |
Só atendente_id | A conversa vai direto para o atendente (humano), no departamento em que já estava. O atendente é avisado pelo aplicativo e, se tiver alertas no WhatsApp ligados, pelo WhatsApp. |
| Os dois | A conversa vai para o atendente e passa a constar nesse departamento. |
- Em todos os casos a conversa sai da automação: se estava com a IA ou num fluxo, eles param de responder.
- O compartilhamento da conversa com outros atendentes é desfeito; quem recebe decide com quem dividir.
- A API não confere se o atendente faz parte do departamento: a combinação é responsabilidade da integração.
- O cliente não recebe mensagem da transferência. A única exceção possível é o Aviso de tempo de espera, quando ele está ligado em Configurações e a conversa entra na fila com todos os atendentes ocupados.
- Sai o webhook
conversa_transferida(econversa_atribuidaquando a distribuição automática entrega a conversa a alguém).
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": "cliente inadimplente"}'<?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' => 'cliente corporativo']), ]); $r = json_decode(curl_exec($ch), true); curl_close($ch); echo $r['conversa']['estado'], ' com ', $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"])
A resposta traz a conversa já atualizada, com etiquetas:
{
"ok": true,
"conversa": {
"id": 18342,
"protocolo": "20261001-0042",
"estado": "fila",
"departamento_id": 2,
"departamento": "Financeiro",
"atendente_id": null,
"atendente": "",
"...": "demais campos do objeto conversa",
"etiquetas": []
}
}Se a distribuição automática entregar a conversa na hora, o estado já vem humano, com o atendente escolhido por ela.
Erros específicos: parametro_obrigatorio (sem departamento nem atendente; campo igual a departamento_id|atendente_id), departamento_invalido, atendente_invalido (inexistente, inativo ou excluído), conversa_encerrada (409), identificador_obrigatorio, nao_encontrado.
Encerrar conversa
/api_publica.php?r=conversas/encerrar
Encerra uma conversa aberta, como o botão Encerrar do painel. Exemplo: o sistema de ordens de serviço fecha a OS e encerra o chamado do WhatsApp com o motivo certo.
Permissão: Gerenciar conversas.
| Parâmetro | Tipo | Obrigatório | Descrição |
|---|---|---|---|
conversa_id (ou id) ou protocolo | Sim | A conversa. Precisa estar aberta. | |
motivo_id | inteiro | Não | Motivo de encerramento ativo (os motivos são cadastrados em Cadastros e listados por motivos/listar). Sem ele, a conversa fica sem motivo e a linha do tempo registra o nome da chave. |
enviar_mensagem | booleano | Não | Padrão true. Com false, nada é enviado ao cliente no encerramento. |
O que acontece com o cliente
Com enviar_mensagem verdadeiro (padrão), o encerramento pela API faz o mesmo que pelo painel:
- Mensagem de chamado encerrado: o texto configurado em Configurações (com
{protocolo}e{nome}preenchidos) é enviado, desde que tenha havido atendimento de verdade: o cliente participou da conversa, ou a IA ou um atendente falou com ele. Com o campo vazio nas Configurações, nada é enviado. - Pesquisa de satisfação: se Enviar pesquisa ao encerrar conversas atendidas estiver ligado (veja Pesquisa), a pergunta é enviada quando a IA ou um atendente respondeu ao cliente na conversa. A nota que o cliente responder entra nos relatórios.
Com enviar_mensagem=false, nenhuma das duas sai. Use, por exemplo, ao encerrar em lote conversas antigas ou de teste.
O que acontece por trás
- O estado passa a
encerrada, com o motivo; a conversa sai de fluxo e IA; uma espera de retorno aberta é fechada. - Se o cliente escreveu algo, a IA escreve o
resumodo chamado em segundo plano (consulte a conversa de novo alguns minutos depois). - OS abertas a partir desse chamado têm o resumo refeito cobrindo a conversa inteira.
- Sai o webhook
conversa_encerrada, commotivoigual ao nome do motivo escolhido (ou à identificação da chave, sem motivo).
# encerra com motivo, enviando mensagem de encerramento e pesquisa 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" # encerra em silêncio 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: já estava encerrada; trate como sucesso }
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": "Resolvido",
"resumo": null,
"encerrada_em": "2026-10-01T11:05:12-03:00",
"...": "demais campos do objeto conversa"
}
}Erros específicos: motivo_invalido (inexistente ou inativo), conversa_encerrada (409: já estava encerrada; em reprocessamentos, trate como sucesso), parametro_invalido (enviar_mensagem com valor que não é booleano), identificador_obrigatorio, nao_encontrado.
Etiquetas da conversa
/api_publica.php?r=conversas/etiquetas
Muda as etiquetas de uma conversa (aberta ou encerrada). As etiquetas são cadastradas em Cadastros e listadas por etiquetas/listar. Exemplos: marcar VIP quando o ERP identifica um plano empresarial; marcar Inadimplente para o Financeiro filtrar no kanban.
Permissão: Gerenciar conversas.
| Parâmetro | Tipo | Descrição |
|---|---|---|
conversa_id (ou id) ou protocolo | A conversa (obrigatório). | |
etiquetas | lista de ids | Substitui todas as etiquetas pela lista. Lista vazia ([] no JSON ou etiquetas= no formulário) remove todas. |
adicionar | lista de ids | Acrescenta essas etiquetas, sem mexer nas demais. |
remover | lista de ids | Tira essas etiquetas, sem mexer nas demais. |
Envie ao menos um dos três. Podem vir juntos: a API parte de etiquetas (ou das atuais, sem ele), soma adicionar e tira remover. As listas aceitam JSON ([1, 3]), formulário (adicionar[]=1&adicionar[]=3) ou texto separado por vírgula (adicionar=1,3). Ids repetidos são ignorados.
# acrescenta as etiquetas 1 e 3 e tira a 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]}' # limpa todas 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",
"...": "demais campos do objeto conversa",
"etiquetas": [
{ "id": 1, "nome": "VIP", "cor": "#f59e0b" },
{ "id": 3, "nome": "Visita técnica", "cor": "#2563eb" }
]
}
}{"ok": false, "error": "etiqueta_invalida", "campo": "adicionar"}Erros específicos: parametro_obrigatorio (nenhum dos três; campo igual a etiquetas|adicionar|remover), etiqueta_invalida (id inexistente; nada é alterado), parametro_invalido, identificador_obrigatorio, nao_encontrado.
Nota interna
/api_publica.php?r=conversas/nota
Grava uma nota na linha do tempo da conversa, visível só para a equipe. Exemplos: o NOC avisa que há um rompimento de fibra na região do cliente; o ERP registra que o pagamento foi compensado durante o atendimento.
Permissão: Gerenciar conversas.
| Parâmetro | Tipo | Obrigatório | Descrição |
|---|---|---|---|
conversa_id (ou id) ou protocolo | Sim | A conversa (aberta ou encerrada). | |
texto | string | Sim | De 1 a 900 caracteres. |
- A nota aparece no chat como linha do sistema, com o nome da chave na frente: API (ERP): Pagamento compensado às 10h12.
- Nunca é enviada ao cliente, não entra no histórico que a IA lê e não aparece em relatórios.
- Também não volta pelas rotas de leitura da API (que nunca devolvem conteúdo interno): guarde o
nota_idse precisar referenciá-la.
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": "Rompimento de fibra no bairro Jardim Alvorada, previsão 14h."}'<?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' => 'Pagamento compensado às 10h12.']), ]); $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": "Pagamento compensado às 10h12."}, timeout=30) print(r.json())
{"ok": true, "nota_id": 402990, "conversa_id": 18342}Erros específicos: texto_invalido (vazio ou acima de 900 caracteres), identificador_obrigatorio, nao_encontrado.
Abrir atendimento
/api_publica.php?r=atendimentos/abrir
Coloca um telefone na fila de atendimento (ou direto com um atendente) sem enviar nenhuma mensagem ao cliente. É o caminho para quando o pedido nasce fora do WhatsApp: formulário de contato do site, portal do assinante, alarme do sistema de monitoramento, retorno de ligação agendado. A equipe vê o atendimento no painel e fala com o cliente por ali.
Permissão: Abrir atendimentos.
| Parâmetro | Tipo | Obrigatório | Descrição |
|---|---|---|---|
telefone | string | Sim | Telefone do cliente (mesma regra das rotas de envio). |
conexao_id | inteiro | Condicional | Número de WhatsApp pelo qual a equipe vai atender. Obrigatório quando há mais de um número conectado. Dispensado quando a chave tem Número de origem; se vier na chamada, vale o da chamada. |
departamento_id | inteiro | Não | Departamento de destino, ativo. Sem ele, vale o departamento em que a conversa já está (numa conversa nova, o Departamento das novas conversas das Configurações, ou a fila geral). |
atendente_id | inteiro | Não | Entrega a conversa direto a esse atendente, ativo. |
nota | string | Não | Até 900 caracteres. Vira uma nota interna na conversa (API (Portal): Cliente pediu retorno sobre mudança de endereço.), para a equipe saber do que se trata antes de falar com o cliente. |
nome | string | Não | Nome do cliente para uma conversa nova (até 190 caracteres). O nome salvo pela equipe para o contato prevalece. |
Como a chamada se comporta
- O ChatISP procura uma conversa aberta desse telefone no número de WhatsApp (pelos dois canais de um número gêmeo, com e sem o nono dígito). Se não houver, cria uma nova, na fila, com protocolo próprio (sai o webhook
conversa_criada). - A
nota, se veio, é gravada na linha do tempo, junto com o registro da chave. - Com
atendente_id: a conversa vai direto para esse atendente (estadohumano), qualquer que seja o estado em que estava, e também para odepartamento_id, se veio. Sai da IA ou do fluxo, se estava neles. O atendente é avisado. - Sem
atendente_id, com a conversa na fila: ela vai para a fila dodepartamento_idpedido (ou continua na do departamento atual), e a distribuição automática tenta entregá-la na hora. Havendo atendente disponível, ela já saihumano; senão ficafilae a equipe recebe o aviso. - Sem
atendente_id, com a conversa com a IA, num fluxo ou já com um atendente: nada muda, nem o departamento. A API não puxa uma conversa que já está sendo conduzida.
Nada é enviado ao cliente. Ele só recebe algo quando a equipe escrever. A única exceção possível é o Aviso de tempo de espera, se estiver ligado em Configurações e a conversa ficar na fila com todos os atendentes ocupados. Para abrir e já mandar uma mensagem, chame mensagens/enviar logo depois com o conversa_id devolvido: como a conversa já está aberta, ela não é tratada como aviso e continua aberta.
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": "Pediu pelo portal a renegociação da fatura de setembro."
}'<?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' => 'Pediu pelo portal a renegociação da fatura de setembro.', ]), ]); $r = json_decode(curl_exec($ch), true); curl_close($ch); echo $r['ja_existia'] ? 'Já havia atendimento: ' : 'Atendimento aberto: ', $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": "Retorno de ligação agendado pelo 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": "Financeiro",
"...": "demais campos do objeto conversa",
"etiquetas": []
}
}| Campo | Descrição |
|---|---|
conversa_id, protocolo | Identificação do atendimento. |
estado | Estado depois da chamada: fila (aguardando atendente), humano (com o atendente pedido ou entregue pela distribuição automática) ou, numa conversa que já estava sendo conduzida, o estado em que ela estava (ia, auto, humano). |
ja_existia | true quando o telefone já tinha uma conversa aberta nesse número (em qualquer estado); false quando esta chamada criou o atendimento. |
conversa | O objeto conversa completo, com etiquetas. |
Chamada idempotente: chamar de novo para o mesmo telefone enquanto o atendimento está aberto não cria duplicata; devolve a mesma conversa com ja_existia: true (e grava a nova nota, se vier). Isso torna seguro repetir a chamada depois de um erro de rede.
Erros específicos: telefone_invalido, conexao_invalida, sem_conexao, conexao_id_obrigatorio, conexao_desconectada, departamento_invalido, atendente_invalido, texto_invalido (nota acima de 900 caracteres, com campo igual a nota).
Erros por rota
Além dos erros gerais (rota, autenticacao, chave_invalida, plano, cota_excedida, permissao, json_invalido, parametro_invalido), descritos em Códigos de erro:
error | HTTP | Rotas | Causa |
|---|---|---|---|
metodo | 405 | transferir, encerrar, etiquetas, nota, abrir | Chamada com GET numa rota de escrita (esperado: POST). |
identificador_obrigatorio | 400 | todas menos listar e abrir | Faltou conversa_id/id ou protocolo (ou telefone, em consultar). |
nao_encontrado | 404 | todas menos listar e abrir | A conversa não existe. |
parametro_obrigatorio | 400 | transferir, etiquetas | Faltou o destino ou a lista; campo diz qual. |
data_invalida | 400 | listar | Data que não pôde ser interpretada; campo diz qual. |
telefone_invalido | 400 | listar, consultar, abrir | Telefone que não faz sentido. |
conversa_encerrada | 409 | transferir, encerrar | A conversa já está encerrada. |
departamento_invalido | 400 | transferir, abrir | Departamento inexistente ou inativo. |
atendente_invalido | 400 | transferir, abrir | Atendente inexistente, inativo ou excluído. |
motivo_invalido | 400 | encerrar | Motivo inexistente ou inativo. |
etiqueta_invalida | 400 | etiquetas | Etiqueta inexistente; campo diz em qual lista. |
texto_invalido | 400 | nota, abrir | Texto da nota vazio ou acima de 900 caracteres. |
conexao_invalida, sem_conexao, conexao_id_obrigatorio | 400 | abrir | Escolha do número de WhatsApp (veja Para quem a mensagem vai). |
conexao_desconectada | 409 | abrir | O número de WhatsApp escolhido está desconectado. |