Pular para o conteúdo

API — Conversas

Conversa é o fio de mensagens com um cliente, em qualquer canal. Para as mensagens dentro dela, veja Mensagens.

MétodoEndpointAção
GET/conversationsLista
GET/conversations/metaSó as contagens por status
POST/conversations/filterFiltro combinado
GET/conversations/searchBusca no texto das mensagens
POST/conversationsAbre conversa
GET/conversations/{id}Detalhes
PATCH/conversations/{id}Só muda a prioridade
DELETE/conversations/{id}Remove
POST/conversations/{id}/toggle_statusResolver, reabrir, adiar
POST/conversations/{id}/assignmentsAtribuir a pessoa ou a equipe
POST/conversations/{id}/labelsEtiquetas
POST/conversations/{id}/toggle_priorityPrioridade
POST/conversations/{id}/mute · /unmuteSilenciar
POST/conversations/{id}/toggle_botLiga e desliga o bot naquela conversa
POST/conversations/{id}/custom_attributesCampos personalizados
GET/conversations/{id}/attachmentsAnexos trocados
POST/conversations/{id}/transcriptManda a transcrição por e-mail

Todos sob /api/v1/accounts/{account_id}.

Esta é a parte que mais derruba integração nova, então vem antes dos exemplos:

// GET /conversations — tem a camada "data", e o meta vem VAZIO
{ "data": { "meta": {}, "payload": [ ... ] } }
// POST /conversations/filter — sem a camada "data"
{ "meta": {}, "payload": [ ... ] }
// GET /conversations/{id} — o objeto direto
{ "id": 8821, "status": "open", ... }

O total não vem em nenhum dos três. Para contar, existe endpoint separado:

Terminal window
curl "https://app.smart2.com.br/api/v1/accounts/1/conversations/meta?status=open" \
-H "api_access_token: SEU_TOKEN"
{
"id": 8821,
"account_id": 1,
"inbox_id": 3,
"uuid": "b3f1...",
"status": "open",
"priority": "high",
"labels": ["vip", "lead-novo"],
"custom_attributes": { "motivo": "duvida-comercial" },
"additional_attributes": {},
"can_reply": true,
"muted": false,
"bot_active": true,
"is_group": false,
"snoozed_until": null,
"sla_policy_id": null,
"unread_count": 1,
"meta": {
"sender": { "id": 4521, "name": "Maria Silva", "phone_number": "+5511999990000" },
"assignee": { "id": 7, "name": "João Souza", "email": "joao@..." },
"assignee_type": "User",
"team": { "id": 2, "name": "Vendas SP" },
"channel": "Channel::Whatsapp",
"queue_state": null,
"hmac_verified": false
},
"messages": [ { "id": 99012, "content": "Oi, queria um orçamento" } ],
"last_non_activity_message": { "...": "última mensagem de gente, ignorando eventos" },
"created_at": 1758542400,
"updated_at": 1758542400.0,
"last_activity_at": 1758542400,
"timestamp": 1758542400,
"waiting_since": 1758542400,
"first_reply_created_at": 0,
"messaging_window_expires_at": 1758628800,
"agent_last_seen_at": 1758542400,
"contact_last_seen_at": 1758542400
}

Cinco coisas que costumam surpreender:

  • id é o número da URL (o display_id), que conta a partir de 1 dentro da conta. Não é chave global.
  • Cliente, responsável, equipe e canal ficam dentro de meta, não no primeiro nível.
  • messages traz só a última. Para o histórico, GET /conversations/{id}/messages.
  • As datas são números — segundos desde 1970. updated_at vem com fração.
  • channel é o nome da classe: Channel::Whatsapp, Channel::WebWidget, Channel::FacebookPage, Channel::Email, Channel::Api.

messaging_window_expires_at é a janela de 24 horas do WhatsApp — passada ela, só modelo aprovado sai. can_reply já resume isso num booleano.

StatusSignificado
openEm andamento
pendingEsperando a primeira ação de gente
resolvedEncerrada
snoozedAdiada, volta na data marcada
Terminal window
curl "https://app.smart2.com.br/api/v1/accounts/1/conversations?status=open&inbox_id=3" \
-H "api_access_token: SEU_TOKEN"
ParâmetroFiltra por
statusopen, pending, resolved, snoozed ou all. Padrão: open
inbox_idCaixa
team_idEquipe
assignee_typeassigned, unassigned, me
labels[]Etiqueta — basta uma bater
qTexto dentro das mensagens
updated_withinMudou nos últimos N segundos
sort_bylast_activity_at_desc (padrão), created_at_asc, priority_desc, waiting_since_asc…
pagePágina. São 25 por página

Ordenação é um parâmetro só, com campo e direção juntos (created_at_desc), não dois. Valor desconhecido cai no padrão sem avisar.

Terminal window
curl -X POST "https://app.smart2.com.br/api/v1/accounts/1/conversations" \
-H "api_access_token: SEU_TOKEN" \
-H "Content-Type: application/json" \
-d '{
"inbox_id": 3,
"contact_id": 4521,
"status": "open",
"source_id": "ERP-8842"
}'
CampoNotas
inbox_idObrigatório
contact_idObrigatório (ou source_id de um contato já ligado àquela caixa)
statusPadrão open
source_idO identificador do seu lado
additional_attributesDados livres seus
messageJá cria a primeira mensagem junto
Terminal window
curl -X POST "https://app.smart2.com.br/api/v1/accounts/1/conversations/8821/toggle_status" \
-H "api_access_token: SEU_TOKEN" \
-H "Content-Type: application/json" \
-d '{ "status": "resolved" }'

Adiar leva a data de volta:

Terminal window
curl -X POST ".../conversations/8821/toggle_status" \
-d '{ "status": "snoozed", "snoozed_until": "2026-09-25T09:00:00Z" }'

Sem status, o endpoint alterna entre aberta e resolvida — daí o nome.

Terminal window
# a uma pessoa
curl -X POST ".../conversations/8821/assignments" \
-H "Content-Type: application/json" \
-d '{ "assignee_id": 7 }'
# a uma equipe
curl -X POST ".../conversations/8821/assignments" \
-d '{ "team_id": 2 }'

Para tirar o responsável, mande assignee_id: null.

Os números vêm de GET /agents e GET /teams, os dois sob a conta.

Terminal window
curl -X POST ".../conversations/8821/labels" \
-H "Content-Type: application/json" \
-d '{ "labels": ["vip", "urgente"] }'

Substitui a lista inteira. Para acrescentar sem perder o que já tinha, leia antes:

atuais = requests.get(f"{BASE}/conversations/8821/labels", headers=H).json()["payload"]
requests.post(f"{BASE}/conversations/8821/labels", headers=H,
json={"labels": list(set(atuais) | {"urgente"})})
Terminal window
curl -X POST ".../conversations/8821/toggle_priority" \
-d '{ "priority": "urgent" }'

Valores: low, medium, high, urgent, ou null para tirar. A resposta vem vazia, com 200.

Terminal window
curl -X POST "https://app.smart2.com.br/api/v1/accounts/1/conversations/filter" \
-H "api_access_token: SEU_TOKEN" \
-H "Content-Type: application/json" \
-d '{
"payload": [
{ "attribute_key": "status", "filter_operator": "equal_to", "values": ["open"] },
{ "attribute_key": "labels", "filter_operator": "contains",
"values": ["vip"], "query_operator": "and" },
{ "attribute_key": "last_activity_at", "filter_operator": "is_less_than",
"values": ["2026-09-15T00:00:00Z"], "query_operator": "and" }
]
}'

“Abertas, com etiqueta VIP, paradas há mais de uma semana.”

Repare no operador: is_less_than, com o prefixo is_. Sem ele a chamada é recusada. A lista completa está em convenções.

from datetime import datetime, timedelta
corte = (datetime.utcnow() - timedelta(days=30)).isoformat() + "Z"
paradas = requests.post(f"{BASE}/conversations/filter", headers=H, json={
"payload": [
{"attribute_key": "status", "filter_operator": "equal_to", "values": ["open"]},
{"attribute_key": "last_activity_at", "filter_operator": "is_less_than",
"values": [corte], "query_operator": "and"},
]
}).json()["payload"] # sem a camada "data" aqui
for c in paradas:
requests.post(f"{BASE}/conversations/{c['id']}/toggle_status",
headers=H, json={"status": "resolved"})

O uso mais comum de integração com ERP: nota interna na conversa.

requests.post(f"{BASE}/conversations/8821/messages", headers=H, json={
"content": "Pedido #8842 faturado. NF 1.294 emitida.",
"private": True,
})
pagina, todas = 1, []
while True:
r = requests.get(f"{BASE}/conversations", headers=H, params={
"status": "all", # sem isto, só vêm as abertas
"page": pagina,
}).json()
lote = r["data"]["payload"] # repare na camada "data"
if not lote:
break
todas.extend(lote)
pagina += 1