A API de chat liga o seu sistema ao agente sem passar pelo navegador. Seu servidor abre a conversa de um usuário, manda as mensagens dele e recebe as respostas, por consulta ou por webhook. As conversas aparecem em Conversas como as dos outros canais. Esta página também mostra como conferir os webhooks e como identificar o usuário logado no widget do site.
Para o widget pronto e a API pública dele, veja API pública do widget do site.
Criar o canal e a chave
- Em Integrações › Live Chat › API de chat, crie um canal e escolha o agente que vai atender.
- Na aba Receber pela API do canal, crie uma chave secreta. Ela começa com
sk_live_e aparece uma única vez. Copie e guarde no seu servidor. - Ajuste os limites e, se quiser, o mapeamento do contexto para campos do contato (veja Contexto).
Cada canal aceita até duas chaves ativas. Para trocar a chave sem parar o serviço, crie a segunda, troque no seu servidor e revogue a antiga. A revogação vale na hora e derruba todos os tokens de cliente do canal, inclusive os emitidos com a chave nova: depois dela, os apps pedem token novo.
Autenticação
Toda chamada leva a chave no cabeçalho Authorization:
Authorization: Bearer sk_live_...
A empresa e o canal saem da chave, nunca do corpo do pedido. A API recusa
pedidos feitos de navegador (com cabeçalho Origin): para conversar direto do
app ou do site, use o token de cliente.
Todas as rotas ficam em https://api.interage.ai/api/public/v1/chat.
| Método | Rota | O que faz |
|---|---|---|
POST | /conversations | Abre a conversa do usuário, ou devolve a que já existe |
GET | /conversations?externalUserId= | Busca a conversa de um usuário |
GET | /conversations/:id | Estado da conversa: modo, atendente e se foi finalizada |
GET | /conversations/:id/messages | Lê as mensagens |
POST | /conversations/:id/messages | Envia uma mensagem do usuário |
POST | /conversations/:id/attachments | Sobe um anexo antes de enviá-lo na mensagem |
PATCH | /conversations/:id/context | Atualiza o contexto sem enviar mensagem |
PATCH | /conversations/:id/user | Atualiza nome, telefone e e-mail do usuário |
POST | /conversations/:id/client-token | Emite o token de cliente para app ou site |
Abrir a conversa
POST /conversations recebe o usuário do seu sistema e, se quiser, o contexto
e a primeira mensagem:
| Campo | Obrigatório | Descrição |
|---|---|---|
user.externalId | Sim | Id do usuário no seu sistema, até 128 caracteres, sem espaços |
user.name | Não | Nome |
user.phone | Não | Telefone |
user.email | Não | |
context | Não | Dados do seu sistema para o agente (veja Contexto) |
message | Não | Primeira mensagem, no formato de Enviar mensagem |
Cada externalId tem uma conversa só neste canal. Chamar de novo com o mesmo
externalId devolve a mesma conversa, com created: false, e aplica os dados
novos. A resposta é { conversation, created, message }.
curl https://api.interage.ai/api/public/v1/chat/conversations \
-H "Authorization: Bearer $INTERAGE_CHAT_KEY" \
-H "Content-Type: application/json" \
-d '{
"user": { "externalId": "cliente-8812", "name": "Ana Souza", "phone": "+5561999990000" },
"context": { "plano": "premium", "pedido": "A-1043" },
"message": { "clientMessageId": "msg-0001", "text": "Quero mudar meu plano" }
}'
// Node 18+
const BASE = 'https://api.interage.ai/api/public/v1/chat'
const headers = { Authorization: `Bearer ${process.env.INTERAGE_CHAT_KEY}`, 'Content-Type': 'application/json' }
const res = await fetch(`${BASE}/conversations`, {
method: 'POST',
headers,
body: JSON.stringify({
user: { externalId: 'cliente-8812', name: 'Ana Souza', phone: '+5561999990000' },
context: { plano: 'premium', pedido: 'A-1043' },
message: { clientMessageId: 'msg-0001', text: 'Quero mudar meu plano' },
}),
})
if (!res.ok) throw new Error(`${res.status} ${(await res.json()).error}`)
const { conversation, created } = await res.json()
import os, requests
BASE = "https://api.interage.ai/api/public/v1/chat"
HEADERS = {"Authorization": f"Bearer {os.environ['INTERAGE_CHAT_KEY']}"}
res = requests.post(f"{BASE}/conversations", headers=HEADERS, json={
"user": {"externalId": "cliente-8812", "name": "Ana Souza", "phone": "+5561999990000"},
"context": {"plano": "premium", "pedido": "A-1043"},
"message": {"clientMessageId": "msg-0001", "text": "Quero mudar meu plano"},
}, timeout=15)
res.raise_for_status()
conversation = res.json()["conversation"]
A conversa volta assim:
| Campo | Descrição |
|---|---|
id | Id da conversa, usado nas outras rotas |
externalUserId | O externalId do usuário |
status | active, capped (passou para a equipe por limite) ou lead |
mode | automatico (agente), transferido ou humano (equipe) e resolvido |
attendant | { name } do atendente, ou null |
resolved | true quando a conversa foi finalizada |
createdAt | Data ISO de criação |
Enviar mensagem
POST /conversations/:id/messages recebe:
| Campo | Obrigatório | Descrição |
|---|---|---|
clientMessageId | Sim | Id que você gera para a mensagem: de 8 a 64 caracteres, só letras, números, - e _ (um UUID serve) |
text | Não | Texto da mensagem |
attachments | Não | Lista de { objectKey } devolvidos pela rota de anexos, até 10 |
context | Não | Contexto atualizado |
O clientMessageId torna o envio seguro para repetir. O mesmo id com o mesmo
conteúdo não duplica: a resposta volta com duplicate: true. O mesmo id com
conteúdo diferente responde 409 conflict. A resposta é
{ messageId, duplicate, capped }; capped: true quer dizer que a conversa
passou para a equipe por limite.
curl https://api.interage.ai/api/public/v1/chat/conversations/$CONVERSA/messages \
-H "Authorization: Bearer $INTERAGE_CHAT_KEY" \
-H "Content-Type: application/json" \
-d '{ "clientMessageId": "msg-0002", "text": "Pode ser o anual?" }'
await fetch(`${BASE}/conversations/${conversation.id}/messages`, {
method: 'POST',
headers,
body: JSON.stringify({ clientMessageId: 'msg-0002', text: 'Pode ser o anual?' }),
})
requests.post(f"{BASE}/conversations/{conversation['id']}/messages", headers=HEADERS,
json={"clientMessageId": "msg-0002", "text": "Pode ser o anual?"}, timeout=15)
A resposta do agente não vem nessa chamada. Ela chega pelo webhook
message.created ou pela leitura das mensagens.
Anexos
Suba o arquivo em POST /conversations/:id/attachments, como
multipart/form-data no campo file. A resposta traz o objectKey que vai em
attachments na mensagem. Os tipos e tamanhos aceitos são os mesmos do widget
do site.
curl https://api.interage.ai/api/public/v1/chat/conversations/$CONVERSA/attachments \
-H "Authorization: Bearer $INTERAGE_CHAT_KEY" \
-F "file=@comprovante.pdf"
Ler as mensagens
GET /conversations/:id/messages funciona como no widget. Sem after, devolve
as mensagens mais recentes em ordem cronológica. Com after (a data ISO at da
última mensagem que você já tem), devolve só as posteriores. limit vai até
200.
curl "https://api.interage.ai/api/public/v1/chat/conversations/$CONVERSA/messages?after=2026-10-01T14:03:11.000Z" \
-H "Authorization: Bearer $INTERAGE_CHAT_KEY"
const url = new URL(`${BASE}/conversations/${conversation.id}/messages`)
url.searchParams.set('after', ultimaData)
const { messages, state } = await (await fetch(url, { headers })).json()
res = requests.get(f"{BASE}/conversations/{conversation['id']}/messages",
headers=HEADERS, params={"after": ultima_data}, timeout=15)
messages = res.json()["messages"]
Cada mensagem traz id, direction (inbound do usuário, outbound do
agente ou da equipe), body, at e, quando houver, attachments com uma URL
temporária. A resposta também traz state com status e mode.
Contexto
O contexto é um objeto plano com o que o seu sistema sabe do usuário: plano,
pedido, página do app. O agente lê o contexto no turno seguinte. Ele segue as
regras do widget: até 30 chaves, nome com até 40 caracteres (letras, números,
_, . e -), valores texto, número ou verdadeiro/falso e até 4 KB no total.
O que sai dessas regras é descartado; se nada sobrar, a chamada responde
422 invalid_input.
curl -X PATCH https://api.interage.ai/api/public/v1/chat/conversations/$CONVERSA/context \
-H "Authorization: Bearer $INTERAGE_CHAT_KEY" \
-H "Content-Type: application/json" \
-d '{ "context": { "plano": "anual", "pedido": "A-1043" } }'
Como vem do seu servidor, o contexto da API é verificado. No canal, você
pode ligar uma chave do contexto a um campo personalizado do contato, como
plano → "Plano contratado". O valor é gravado no contato a cada atualização,
convertido pelo tipo do campo. Valor que não serve para o campo é ignorado e
não derruba a mensagem. Chave sem mapeamento fica só como informação para o
agente.
Atualizar o usuário
PATCH /conversations/:id/user recebe name, phone e email (ao menos um).
Use quando o usuário completa o cadastro no seu sistema.
curl -X PATCH https://api.interage.ai/api/public/v1/chat/conversations/$CONVERSA/user \
-H "Authorization: Bearer $INTERAGE_CHAT_KEY" \
-H "Content-Type: application/json" \
-d '{ "email": "ana@exemplo.com.br" }'
Identidade e deduplicação
Quem chama a API é o seu servidor, então a Interage confia nos dados do usuário: a conversa tem identidade verificada. Com isso, quando o telefone ou o e-mail já pertence a um contato do CRM, a conversa é ligada a esse contato. O agente passa a ver o histórico e os campos dessa pessoa, e o painel mostra o selo "Identidade verificada".
A busca segue esta ordem:
- O
externalId: o mesmo usuário cai sempre na mesma conversa do canal. - O telefone: se existe contato com ele, a conversa é ligada a esse contato.
- O e-mail: só quando o telefone não encontrou ninguém.
- Nenhum encontrado: os dados ficam no contato da própria conversa.
Algumas regras protegem o CRM:
- A resposta sai sempre por este canal, nunca pelo WhatsApp ou e-mail do contato encontrado.
- Um dado novo nunca apaga o que o contato já tem: só preenche campo vazio.
- Se o telefone aponta para um contato e o e-mail para outro, vale o telefone, e o painel avisa da divergência.
- Quando a ligação não é possível (por exemplo, os dois contatos têm WhatsApp), a conversa segue sem ligação, e a equipe pode mesclar pela tela.
No widget do site, os dados digitados pelo visitante no formulário inicial não são verificados: qualquer pessoa pode digitar o telefone de outra. Por isso, nesse caso o agente não vê o CRM da pessoa encontrada. Para áreas logadas, use a identificação no widget.
Token de cliente para app ou site
O token de cliente deixa o app nativo ou o site conversar direto com a Interage, em tempo real, sem expor a chave secreta. Seu servidor autentica o usuário, pede o token e entrega ao app:
curl -X POST https://api.interage.ai/api/public/v1/chat/conversations/$CONVERSA/client-token \
-H "Authorization: Bearer $INTERAGE_CHAT_KEY"
A resposta é { conversationId, token, expiresAt }. O token vale 24 horas, e
pedir um novo invalida o anterior. Revogar qualquer chave do canal derruba
todos os tokens de cliente do canal, mesmo os emitidos com outra chave; o app
pede um token novo ao seu servidor.
O app usa as rotas do widget, em https://api.interage.ai/api/public/v1/widget,
com o token no cabeçalho x-widget-token e o conversationId no lugar de
:id:
| Método | Rota | O que faz |
|---|---|---|
GET | /sessions/:id/messages | Lê a conversa |
POST | /sessions/:id/messages | Envia mensagem (com clientMessageId) |
POST | /sessions/:id/attachments | Sobe anexo |
POST | /sessions/:id/read | Marca como lida |
GET | /sessions/:id/events | Eventos em tempo real: mensagem nova, "digitando" e mudança de estado |
// No app, com o token entregue pelo seu servidor
await fetch(`https://api.interage.ai/api/public/v1/widget/sessions/${conversationId}/messages`, {
method: 'POST',
headers: { 'x-widget-token': token, 'Content-Type': 'application/json' },
body: JSON.stringify({ clientMessageId: crypto.randomUUID(), text: 'Oi!' }),
})
Nunca envie o token na URL. Os limites de mensagem do canal continuam valendo.
Erros e limites
Os erros voltam como { "error": "<código>", "message": "<texto>" }.
| Código | HTTP | Quando acontece |
|---|---|---|
unauthorized | 401 | Chave ausente, inválida ou revogada, ou canal desativado |
browser_forbidden | 403 | Pedido feito de navegador. Use o token de cliente |
not_found | 404 | Conversa inexistente ou de outro canal |
conflict | 409 | clientMessageId repetido com outro conteúdo |
invalid_input | 422 | Dado inválido: externalId, telefone, e-mail, contexto, anexo ou corpo fora do formato |
rate_limited | 429 | Passou de um limite. Espere os segundos do cabeçalho Retry-After |
forward_failed | 503 | A mensagem não pôde ser entregue agora. Tente de novo com o mesmo clientMessageId |
unavailable | 503 | A conversa não pôde ser aberta agora. Tente de novo |
Cada mensagem respondida pela IA é cobrada da sua empresa. Os limites protegem contra loop no seu servidor e contra custo inesperado. Você ajusta cada um no canal, dentro da faixa.
| Limite | Padrão | Faixa | Ao atingir |
|---|---|---|---|
| Pedidos por minuto, por chave | 600 | 60 a 600 | 429 rate_limited |
| Mensagens por minuto, em cada conversa | 20 | 2 a 120 | 429 rate_limited no envio |
| Mensagens no total, em cada conversa | 500 | 20 a 5.000 | A conversa passa para a equipe e a IA deixa de responder; o envio devolve capped: true |
| Conversas novas por dia, no canal | 200 | 10 a 50.000 | As conversas novas começam com a equipe, sem IA, e os administradores recebem um aviso por dia |
Webhooks
Os webhooks avisam o seu servidor do que acontece nas conversas do widget e da
API de chat. Cadastre a URL na aba Enviar por webhook do canal (o widget ou o canal
da API, em Integrações › Live Chat) e escolha os eventos. Cada webhook vale para o
canal em que foi cadastrado. O segredo
whsec_... aparece uma única vez, na criação.
A URL precisa ser https, na porta 443 e com endereço público. A Interage não
segue redirecionamentos e espera a resposta por até 10 segundos.
Eventos
| Evento | Quando | data |
|---|---|---|
conversation.started | Conversa nova num canal de chat | {} |
message.created | Mensagem do usuário, do agente, da equipe ou do sistema | messageId, role (user, agent, attendant, system), text, attachments e clientMessageId quando houver |
typing.started / typing.stopped | O agente ou a equipe começou ou parou de digitar | {} / { reason } |
conversation.handoff | Passou para a equipe ou voltou para o agente | to (human ou agent), department, attendantName |
conversation.closed | A conversa foi finalizada | outcome e reason, do catálogo de motivos da empresa |
Os anexos de message.created trazem type, mimeType, name, size, url
e expiresAt. A URL vale 1 hora a partir do envio; baixe o arquivo se precisar
guardá-lo.
Os eventos de "digitando" são de melhor esforço: uma tentativa só, sem registro, e descartados se atrasarem mais de 10 segundos. Os demais são garantidos, com retentativas.
Formato
O corpo é um JSON assim:
{
"id": "evt_6f1c...",
"type": "message.created",
"createdAt": "2026-10-01T14:03:11.204Z",
"companyId": "...",
"channel": { "id": "...", "type": "chat_api", "name": "App de clientes" },
"conversation": { "id": "...", "externalUserId": "cliente-8812", "contactId": "..." },
"data": {
"messageId": "...",
"role": "agent",
"text": "Claro! O plano anual sai por...",
"attachments": []
}
}
channel.type é chat_api ou web_widget. externalUserId só vem em
conversas com usuário identificado. Enviar evento de teste, no menu de
ações do webhook, manda um evento webhook.test com o mesmo formato e a
mesma assinatura. Com a lista vazia, o botão para começar é Cadastrar
webhook.
Cada envio leva estes cabeçalhos:
| Cabeçalho | Conteúdo |
|---|---|
Interage-Event-Id | Id do evento, igual ao id do corpo |
Interage-Event-Type | Tipo do evento |
Interage-Signature | t=<unix>,v1=<assinatura> |
Conferir a assinatura
A assinatura é o HMAC-SHA256, em hexadecimal, de t + "." + corpo, com o
segredo whsec_.... Para conferir:
- Use o corpo cru, exatamente como chegou, antes de qualquer
JSON.parse. - Recuse se
testiver a mais de 5 minutos do seu relógio. - Calcule a assinatura e compare com cada
v1do cabeçalho, em tempo constante. Basta uma bater.
Depois de girar o segredo, o anterior continua assinando por 24 horas. Nesse
período o cabeçalho traz dois v1, um por segredo, e o código abaixo aceita
qualquer um deles.
// Node + Express
import crypto from 'node:crypto'
import express from 'express'
const SECRET = process.env.INTERAGE_WEBHOOK_SECRET // whsec_...
const TOLERANCE_SECONDS = 5 * 60
function verifySignature(rawBody, header) {
const parts = (header ?? '').split(',').map((item) => item.trim().split('='))
const t = Number(parts.find(([k]) => k === 't')?.[1])
const signatures = parts.filter(([k]) => k === 'v1').map(([, v]) => v)
if (!Number.isFinite(t) || signatures.length === 0) return false
if (Math.abs(Date.now() / 1000 - t) > TOLERANCE_SECONDS) return false
const expected = crypto.createHmac('sha256', SECRET).update(`${t}.${rawBody}`).digest()
return signatures.some((sig) => {
const received = Buffer.from(sig, 'hex')
return received.length === expected.length && crypto.timingSafeEqual(received, expected)
})
}
const app = express()
app.post('/webhooks/interage', express.raw({ type: 'application/json' }), (req, res) => {
const rawBody = req.body.toString('utf8')
if (!verifySignature(rawBody, req.get('Interage-Signature'))) return res.sendStatus(400)
const event = JSON.parse(rawBody)
// Responda rápido e processe depois (fila, job etc.)
res.sendStatus(200)
handleEvent(event)
})
# Python + Flask
import hashlib, hmac, os, time
from flask import Flask, request
SECRET = os.environ["INTERAGE_WEBHOOK_SECRET"].encode() # whsec_...
TOLERANCE_SECONDS = 5 * 60
def verify_signature(raw_body: bytes, header: str) -> bool:
pairs = [item.strip().split("=", 1) for item in (header or "").split(",") if "=" in item]
t = next((v for k, v in pairs if k == "t"), None)
signatures = [v for k, v in pairs if k == "v1"]
if not t or not t.isdigit() or not signatures:
return False
if abs(time.time() - int(t)) > TOLERANCE_SECONDS:
return False
expected = hmac.new(SECRET, f"{t}.".encode() + raw_body, hashlib.sha256).hexdigest()
return any(hmac.compare_digest(expected, sig) for sig in signatures)
app = Flask(__name__)
@app.post("/webhooks/interage")
def interage_webhook():
raw_body = request.get_data()
if not verify_signature(raw_body, request.headers.get("Interage-Signature", "")):
return "", 400
event = request.get_json()
handle_event(event) # de preferência numa fila
return "", 200
Entrega, retentativas e ordem
- Qualquer resposta
2xxconta como entregue. Responda rápido e processe depois. - Sem
2xx, a Interage tenta de novo depois de 1 min, 5 min, 30 min, 2 h, 6 h e 24 h. Depois disso, a entrega fica comodead. Na tela de webhooks você vê o log e pode reenviar uma entrega ou todas as falhas das últimas 24 horas, exceto o evento de teste. - A resposta
410 Gonedesativa o webhook na hora. - 50 falhas seguidas, a primeira delas há mais de 24 horas, desativam o webhook, e os administradores recebem um aviso. Ao reativar, a contagem zera.
Por causa das retentativas, o mesmo evento pode chegar mais de uma vez. Guarde
o Interage-Event-Id dos eventos já processados e ignore os repetidos.
A ordem de chegada não é garantida: uma retentativa pode chegar depois de um
evento mais novo. Ordene pelo createdAt do corpo.
Identificar o usuário no widget
Em áreas logadas do site, o seu servidor pode provar quem é a pessoa que abre o widget. A conversa ganha identidade verificada, como na API, e o mesmo usuário volta à mesma conversa em qualquer navegador.
- Em Integrações › Live Chat › Widget do site, gere o segredo de identificação
(
idsec_...). Ele aparece uma única vez e fica só no seu servidor. - No seu servidor, gere um JWT
HS256assinado com esse segredo para o usuário logado. - Na página, entregue o token ao widget com
Interage.identify.
| Claim | Obrigatória | Descrição |
|---|---|---|
sub | Sim | Id do usuário no seu sistema, até 200 caracteres |
iat | Sim | Momento da emissão, em segundos (unix) |
exp | Sim | Validade, no máximo 24 horas depois de iat |
name | Não | Nome, até 200 caracteres |
email | Não | |
phone | Não | Telefone |
context | Não | Objeto plano, com as regras do contexto; vai para os campos mapeados do contato |
Só HS256 é aceito. A Interage tolera 60 segundos de diferença de relógio.
// Node, sem pacote extra
import crypto from 'node:crypto'
const b64url = (value) => Buffer.from(JSON.stringify(value)).toString('base64url')
export function interageIdentityToken(user) {
const now = Math.floor(Date.now() / 1000)
const header = b64url({ alg: 'HS256', typ: 'JWT' })
const payload = b64url({
sub: String(user.id),
name: user.name,
email: user.email,
phone: user.phone,
context: { plano: user.plan },
iat: now,
exp: now + 60 * 60, // 1 hora
})
const signature = crypto
.createHmac('sha256', process.env.INTERAGE_IDENTITY_SECRET) // idsec_...
.update(`${header}.${payload}`)
.digest('base64url')
return `${header}.${payload}.${signature}`
}
# Python, só com a biblioteca padrão
import base64, hashlib, hmac, json, os, time
def _b64url(data: bytes) -> str:
return base64.urlsafe_b64encode(data).rstrip(b"=").decode()
def interage_identity_token(user: dict) -> str:
now = int(time.time())
header = _b64url(json.dumps({"alg": "HS256", "typ": "JWT"}).encode())
payload = _b64url(json.dumps({
"sub": str(user["id"]),
"name": user.get("name"),
"email": user.get("email"),
"phone": user.get("phone"),
"context": {"plano": user.get("plan")},
"iat": now,
"exp": now + 60 * 60, # 1 hora
}).encode())
secret = os.environ["INTERAGE_IDENTITY_SECRET"].encode() # idsec_...
signature = _b64url(hmac.new(secret, f"{header}.{payload}".encode(), hashlib.sha256).digest())
return f"{header}.{payload}.{signature}"
Bibliotecas de JWT, como jsonwebtoken e PyJWT, também servem, desde que
assinem com HS256.
Na página, depois que a pessoa entra:
Interage.identify({ token: '<token gerado no seu servidor>' })
E, quando ela sai da conta, para que a próxima pessoa no mesmo navegador não veja a conversa:
Interage.reset()
Se o widget ainda não carregou, empilhe a chamada na fila:
Interage.q.push(['identify', { token }]).
Com Exigir identificação ligado, o botão do widget só aparece depois do
Interage.identify. Uma conversa já identificada que recebe o token de outro
usuário é descartada, e o widget abre a conversa do usuário novo.
Ao girar o segredo de identificação, o anterior continua valendo por 24 horas, tempo para trocar o segredo no seu servidor.