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.

RotaMétodoPermissão da chaveResumo
conversas/listarGET ou POSTConsultar conversasLista paginada com filtros por estado, departamento, atendente, conexão, telefone, etiqueta e datas.
conversas/consultarGET ou POSTConsultar conversasUma conversa (por id, protocolo ou telefone) com etiquetas e as últimas mensagens.
conversas/mensagensGET ou POSTConsultar conversasHistórico completo, paginado por cursor.
conversas/transferirPOSTGerenciar conversasPara um departamento, para um atendente ou para os dois.
conversas/encerrarPOSTGerenciar conversasEncerra com motivo, com ou sem a mensagem de encerramento e a pesquisa.
conversas/etiquetasPOSTGerenciar conversasDefine, adiciona ou remove etiquetas.
conversas/notaPOSTGerenciar conversasNota interna na linha do tempo (a equipe vê, o cliente nunca).
atendimentos/abrirPOSTAbrir atendimentosColoca 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 (ou id) ou protocolo. Sem nenhum deles: 400 identificador_obrigatorio. Inexistente: 404 nao_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:

ValorNa telaSignificado
autoAutomaçãoConduzida por um fluxo de atendimento automático (menu, horário, etapas). Sai do fluxo quando ele transfere, aciona a IA ou encerra.
iaCom a IASendo atendida pela IA. Sai dela quando a IA transfere para a equipe ou quando um atendente assume.
filaNa filaAguardando 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.
humanoEm atendimentoCom um atendente da equipe (atendente_id).
encerradaEncerradaAtendimento 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):

conversa
{
  "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" }
  ]
}
CampoTipoDescrição
idinteiroIdentificador da conversa (o conversa_id das outras rotas e dos webhooks).
protocolostringProtocolo do atendimento, o mesmo que a equipe e o cliente veem.
conexao_idinteiro ou nullNúmero de WhatsApp da conversa. null quando a conexão foi excluída do sistema (o histórico é preservado).
telefonestringTelefone do cliente, só dígitos, com o código do país.
nomestringNome do cliente: o salvo pela equipe ou, sem ele, o nome do perfil do WhatsApp. Vazio quando desconhecido.
estadostringauto, ia, fila, humano ou encerrada. Veja Estados da conversa.
aguardando_clientebooleanotrue 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, departamentointeiro ou null, stringDepartamento atual (id e nome). null e vazio na fila geral.
atendente_id, atendenteinteiro ou null, stringAtendente responsável (id e nome). Numa conversa encerrada, quem estava com ela no fim.
nao_lidasinteiroMensagens do cliente que o responsável ainda não abriu no painel.
motivo_encerramentostringNome do motivo de encerramento escolhido. Vazio quando a conversa está aberta ou foi encerrada sem motivo.
resumoobjeto ou nullResumo 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_emdata e horaAbertura da conversa.
ultima_mensagem_emdata e hora ou nullÚltima mensagem trocada (do cliente ou para ele).
encerrada_emdata e hora ou nullEncerramento; null se aberta.
etiquetaslistaEtiquetas 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:

mensagem
{
  "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"
}
CampoTipoDescrição
idinteiroIdentificador da mensagem. Cresce com o tempo: serve de cursor em conversas/mensagens.
conversa_idinteiroConversa a que a mensagem pertence.
direcaostringrecebida (do cliente) ou enviada (do provedor).
autorstringcliente, 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_nomestringNome do atendente quando autor é atendente; vazio nos demais casos.
tipostringtexto, imagem, audio, video, documento, localizacao ou contato (cartão de contato compartilhado).
conteudostringTexto da mensagem. Em mídias, a legenda (pode vir vazio). Vazio também quando a mensagem foi apagada.
transcricaostring ou nullTranscrição de um áudio, quando a transcrição de áudios está ligada e já terminou. null nos demais casos.
arquivoobjeto ou nullMensagens 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.
statusstringpendente (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).
errostring ou nullMotivo da falha quando status é erro.
responde_ainteiro ou nullid da mensagem citada (respondida) por esta.
editadabooleanoA mensagem foi editada depois de enviada; conteudo traz a versão atual.
apagadabooleanoA mensagem foi apagada para todos. O registro continua (para o histórico ficar coerente), sem conteúdo nem arquivo.
criada_emdata e horaMomento da mensagem.

Listar conversas

GET POST /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âmetroTipoDescrição
estadolistaUm 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.
abertasbooleano1: só conversas abertas (qualquer estado menos encerrada). Aceita 1/0, true/false, sim/nao.
departamento_idinteiroSó desse departamento. 0: só as que estão sem departamento (fila geral).
atendente_idinteiroSó desse atendente. 0: só as que estão sem atendente.
conexao_idinteiroSó desse número de WhatsApp. Num número gêmeo, traz as conversas dos dois canais.
telefonestringSó desse cliente, com e sem o nono dígito. Mesma regra de telefone das outras rotas (telefone_invalido se não fizer sentido).
etiqueta_idinteiroSó as que têm essa etiqueta.
criadas_desde, criadas_atedataAbertura 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_desdedataEncerradas a partir desse momento.
atualizadas_desdedataConversas abertas, encerradas ou com mensagem nova a partir desse momento. É o filtro da sincronização incremental.
ordemstringrecentes (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, limiteinteiroPaginaçã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.

terminal
# 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
<?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']);
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"], "conversas com o atendente 7")
for c in dados["conversas"]:
    print(c["protocolo"], c["nome"], c["nao_lidas"])
resposta JSON (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": "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).

  1. Na primeira carga, liste tudo (sem filtro de data, ordem=criacao) e guarde as conversas pelo id.
  2. Antes de cada rodada, anote o horário atual do servidor (campo servidor.agora de chave/info, ou o seu relógio sincronizado). Esse será o marco da próxima rodada.
  3. Chame conversas/listar com atualizadas_desde igual ao marco da rodada anterior, menos uma pequena folga (um ou dois minutos), ordem=antigas e limite=100, percorrendo as páginas.
  4. Grave cada conversa por id (atualiza se já existe, insere se é nova). A folga faz algumas conversas virem repetidas; gravar por id torna isso inofensivo.
  5. Para as mensagens de cada conversa alterada, use conversas/mensagens com depois_de igual ao último id de 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.

rodada incremental (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 por c["id"] no seu banco
        if pagina >= r["paginacao"]["paginas"]:
            break
        pagina += 1
    return agora                      # marco da próxima rodada

Consultar conversa

GET POST /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âmetroTipoObrigatórioDescrição
id ou conversa_idinteiroUm dos trêsIdentificador da conversa.
protocolostringUm dos trêsProtocolo, por exemplo 20261001-0042.
telefonestringUm dos trêsDevolve 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.
mensagensinteiroNãoQuantas 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.

terminal
# 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
<?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;
}
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("Protocolo não existe")
else:
    for m in r.json()["mensagens"]:
        print(m["criada_em"], m["autor"], m["conteudo"])
resposta JSON (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": "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

GET POST /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âmetroTipoObrigatórioDescrição
conversa_id (ou id) ou protocoloSimA conversa.
limiteinteiroNãoMensagens por página, de 1 a 100, padrão 50.
antes_deinteiroNãoRolar para trás: traz as mensagens anteriores a esse id (as mais próximas dele).
depois_deinteiroNãoBuscar o que chegou: traz as mensagens posteriores a esse id, a partir da mais antiga. Tem prioridade sobre antes_de.
Campo da respostaDescrição
conversa_idA conversa lida.
mensagensLista de mensagens, em ordem cronológica.
tem_maistrue 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_deid da primeira mensagem da página: passe em antes_de para a página anterior.
cursor.depois_deid 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_mais for true, chame de novo com antes_de igual a cursor.antes_de.
  • Acompanhar o que chega (espelhar a conversa no seu sistema): guarde o cursor.depois_de e, na próxima vez, chame com depois_de; enquanto tem_mais for true, continue com o novo cursor.depois_de.
terminal
# 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
<?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']);
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):
    """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
resposta JSON (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 }
}

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

POST /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âmetroTipoObrigatórioDescrição
conversa_id (ou id) ou protocoloSimA conversa. Precisa estar aberta.
departamento_idinteiroAo menos um dos doisDepartamento de destino, ativo.
atendente_idinteiroAo menos um dos doisAtendente de destino, ativo.
motivostringNãoTexto 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ê enviaResultado
Só departamento_idA 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_idA 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 doisA 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 (e conversa_atribuida quando a distribuição automática entrega a conversa a alguém).
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": "cliente inadimplente"}'
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' => 'cliente corporativo']),
]);
$r = json_decode(curl_exec($ch), true);
curl_close($ch);
echo $r['conversa']['estado'], ' com ', $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"])

A resposta traz a conversa já atualizada, com etiquetas:

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

POST /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âmetroTipoObrigatórioDescrição
conversa_id (ou id) ou protocoloSimA conversa. Precisa estar aberta.
motivo_idinteiroNãoMotivo 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_mensagembooleanoNãoPadrã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:

  1. 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.
  2. 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 resumo do 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, com motivo igual ao nome do motivo escolhido (ou à identificação da chave, sem motivo).
terminal
# 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
<?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
}
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"])
resposta JSON (HTTP 200, resumida)
{
  "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

POST /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âmetroTipoDescrição
conversa_id (ou id) ou protocoloA conversa (obrigatório).
etiquetaslista de idsSubstitui todas as etiquetas pela lista. Lista vazia ([] no JSON ou etiquetas= no formulário) remove todas.
adicionarlista de idsAcrescenta essas etiquetas, sem mexer nas demais.
removerlista de idsTira 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.

terminal
# 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
<?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"]])
resposta JSON (HTTP 200, resumida)
{
  "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" }
    ]
  }
}
etiqueta inexistente (HTTP 400)
{"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

POST /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âmetroTipoObrigatórioDescrição
conversa_id (ou id) ou protocoloSimA conversa (aberta ou encerrada).
textostringSimDe 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_id se precisar referenciá-la.
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": "Rompimento de fibra no bairro Jardim Alvorada, previsão 14h."}'
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' => 'Pagamento compensado às 10h12.']),
]);
$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": "Pagamento compensado às 10h12."}, timeout=30)
print(r.json())
resposta JSON (HTTP 200)
{"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

POST /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âmetroTipoObrigatórioDescrição
telefonestringSimTelefone do cliente (mesma regra das rotas de envio).
conexao_idinteiroCondicionalNú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_idinteiroNãoDepartamento 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_idinteiroNãoEntrega a conversa direto a esse atendente, ativo.
notastringNãoAté 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.
nomestringNãoNome do cliente para uma conversa nova (até 190 caracteres). O nome salvo pela equipe para o contato prevalece.

Como a chamada se comporta

  1. 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).
  2. A nota, se veio, é gravada na linha do tempo, junto com o registro da chave.
  3. Com atendente_id: a conversa vai direto para esse atendente (estado humano), qualquer que seja o estado em que estava, e também para o departamento_id, se veio. Sai da IA ou do fluxo, se estava neles. O atendente é avisado.
  4. Sem atendente_id, com a conversa na fila: ela vai para a fila do departamento_id pedido (ou continua na do departamento atual), e a distribuição automática tenta entregá-la na hora. Havendo atendente disponível, ela já sai humano; senão fica fila e a equipe recebe o aviso.
  5. 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.

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": "Pediu pelo portal a renegociação da fatura de setembro."
  }'
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'            => '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;
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": "Retorno de ligação agendado pelo NOC.",
    }, timeout=30)
corpo = r.json()
print(corpo["protocolo"], corpo["estado"], corpo["ja_existia"])
resposta JSON (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": "Financeiro",
    "...": "demais campos do objeto conversa",
    "etiquetas": []
  }
}
CampoDescrição
conversa_id, protocoloIdentificação do atendimento.
estadoEstado 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_existiatrue quando o telefone já tinha uma conversa aberta nesse número (em qualquer estado); false quando esta chamada criou o atendimento.
conversaO 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:

errorHTTPRotasCausa
metodo405transferir, encerrar, etiquetas, nota, abrirChamada com GET numa rota de escrita (esperado: POST).
identificador_obrigatorio400todas menos listar e abrirFaltou conversa_id/id ou protocolo (ou telefone, em consultar).
nao_encontrado404todas menos listar e abrirA conversa não existe.
parametro_obrigatorio400transferir, etiquetasFaltou o destino ou a lista; campo diz qual.
data_invalida400listarData que não pôde ser interpretada; campo diz qual.
telefone_invalido400listar, consultar, abrirTelefone que não faz sentido.
conversa_encerrada409transferir, encerrarA conversa já está encerrada.
departamento_invalido400transferir, abrirDepartamento inexistente ou inativo.
atendente_invalido400transferir, abrirAtendente inexistente, inativo ou excluído.
motivo_invalido400encerrarMotivo inexistente ou inativo.
etiqueta_invalida400etiquetasEtiqueta inexistente; campo diz em qual lista.
texto_invalido400nota, abrirTexto da nota vazio ou acima de 900 caracteres.
conexao_invalida, sem_conexao, conexao_id_obrigatorio400abrirEscolha do número de WhatsApp (veja Para quem a mensagem vai).
conexao_desconectada409abrirO número de WhatsApp escolhido está desconectado.