Pular para o conteúdo
interage.ai

Configuração

API de chat e webhooks

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.

Vídeo: API de chat e webhooks de chat (3 min 26 s)

Para o widget pronto e a API pública dele, veja API pública do widget do site.

Criar o canal e a chave

  1. Em Integrações › Live Chat › API de chat, crie um canal e escolha o agente que vai atender.
  2. 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.
  3. 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étodoRotaO que faz
POST/conversationsAbre a conversa do usuário, ou devolve a que já existe
GET/conversations?externalUserId=Busca a conversa de um usuário
GET/conversations/:idEstado da conversa: modo, atendente e se foi finalizada
GET/conversations/:id/messagesLê as mensagens
POST/conversations/:id/messagesEnvia uma mensagem do usuário
POST/conversations/:id/attachmentsSobe um anexo antes de enviá-lo na mensagem
PATCH/conversations/:id/contextAtualiza o contexto sem enviar mensagem
PATCH/conversations/:id/userAtualiza nome, telefone e e-mail do usuário
POST/conversations/:id/client-tokenEmite 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:

CampoObrigatórioDescrição
user.externalIdSimId do usuário no seu sistema, até 128 caracteres, sem espaços
user.nameNãoNome
user.phoneNãoTelefone
user.emailNãoE-mail
contextNãoDados do seu sistema para o agente (veja Contexto)
messageNãoPrimeira 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:

CampoDescrição
idId da conversa, usado nas outras rotas
externalUserIdO externalId do usuário
statusactive, capped (passou para a equipe por limite) ou lead
modeautomatico (agente), transferido ou humano (equipe) e resolvido
attendant{ name } do atendente, ou null
resolvedtrue quando a conversa foi finalizada
createdAtData ISO de criação

Enviar mensagem

POST /conversations/:id/messages recebe:

CampoObrigatórioDescrição
clientMessageIdSimId que você gera para a mensagem: de 8 a 64 caracteres, só letras, números, - e _ (um UUID serve)
textNãoTexto da mensagem
attachmentsNãoLista de { objectKey } devolvidos pela rota de anexos, até 10
contextNãoContexto 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:

  1. O externalId: o mesmo usuário cai sempre na mesma conversa do canal.
  2. O telefone: se existe contato com ele, a conversa é ligada a esse contato.
  3. O e-mail: só quando o telefone não encontrou ninguém.
  4. 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étodoRotaO que faz
GET/sessions/:id/messagesLê a conversa
POST/sessions/:id/messagesEnvia mensagem (com clientMessageId)
POST/sessions/:id/attachmentsSobe anexo
POST/sessions/:id/readMarca como lida
GET/sessions/:id/eventsEventos 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ódigoHTTPQuando acontece
unauthorized401Chave ausente, inválida ou revogada, ou canal desativado
browser_forbidden403Pedido feito de navegador. Use o token de cliente
not_found404Conversa inexistente ou de outro canal
conflict409clientMessageId repetido com outro conteúdo
invalid_input422Dado inválido: externalId, telefone, e-mail, contexto, anexo ou corpo fora do formato
rate_limited429Passou de um limite. Espere os segundos do cabeçalho Retry-After
forward_failed503A mensagem não pôde ser entregue agora. Tente de novo com o mesmo clientMessageId
unavailable503A 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.

LimitePadrãoFaixaAo atingir
Pedidos por minuto, por chave60060 a 600429 rate_limited
Mensagens por minuto, em cada conversa202 a 120429 rate_limited no envio
Mensagens no total, em cada conversa50020 a 5.000A conversa passa para a equipe e a IA deixa de responder; o envio devolve capped: true
Conversas novas por dia, no canal20010 a 50.000As 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

EventoQuandodata
conversation.startedConversa nova num canal de chat{}
message.createdMensagem do usuário, do agente, da equipe ou do sistemamessageId, role (user, agent, attendant, system), text, attachments e clientMessageId quando houver
typing.started / typing.stoppedO agente ou a equipe começou ou parou de digitar{} / { reason }
conversation.handoffPassou para a equipe ou voltou para o agenteto (human ou agent), department, attendantName
conversation.closedA conversa foi finalizadaoutcome 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çalhoConteúdo
Interage-Event-IdId do evento, igual ao id do corpo
Interage-Event-TypeTipo do evento
Interage-Signaturet=<unix>,v1=<assinatura>

Conferir a assinatura

A assinatura é o HMAC-SHA256, em hexadecimal, de t + "." + corpo, com o segredo whsec_.... Para conferir:

  1. Use o corpo cru, exatamente como chegou, antes de qualquer JSON.parse.
  2. Recuse se t estiver a mais de 5 minutos do seu relógio.
  3. Calcule a assinatura e compare com cada v1 do 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 2xx conta 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 como dead. 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 Gone desativa 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.

  1. 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.
  2. No seu servidor, gere um JWT HS256 assinado com esse segredo para o usuário logado.
  3. Na página, entregue o token ao widget com Interage.identify.
ClaimObrigatóriaDescrição
subSimId do usuário no seu sistema, até 200 caracteres
iatSimMomento da emissão, em segundos (unix)
expSimValidade, no máximo 24 horas depois de iat
nameNãoNome, até 200 caracteres
emailNãoE-mail
phoneNãoTelefone
contextNãoObjeto 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.

Atualizado em