Webhooks WTS Chat¶
O WTS Chat (FlowChat) é o canal de entrada e saída de mensagens WhatsApp. Toda mensagem do paciente chega via webhook HTTP.
Endpoints expostos¶
| Método | Path | Quem chama | Resposta esperada |
|---|---|---|---|
| POST | /webhook/message |
WTS (evento) | 202 {"status":"accepted"} |
| POST | /webhook/transfer |
WTS / interno | 200 {"status":"transferred"} |
| GET | /health |
load balancer | 200 {"status":"ok"} |
Payload /webhook/message¶
{
"eventType": "MESSAGE_RECEIVED",
"content": {
"sessionId": "abc-123",
"direction": "FROM_HUB",
"type": "TEXT",
"text": "Oi, queria agendar",
"details": {
"to": "+5599991220558",
"file": {
"publicUrl": "https://...",
"publicUrlDownload": "https://...",
"mimeType": "image/png"
}
}
}
}
Validações iniciais¶
Aceitar apenas o que interessa:
# server.py (ambos os projetos)
@app.post("/webhook/message")
async def webhook_message(request: Request):
body = await request.json()
if body.get("eventType") != "MESSAGE_RECEIVED":
return JSONResponse({"status": "ignored", "reason": "not_message_received"})
content = body.get("content") or {}
if content.get("direction") != "FROM_HUB":
return JSONResponse({"status": "ignored", "reason": "not_from_hub"})
# Thread separada para responder 202 rápido
threading.Thread(target=_run_workflow, args=(body,), daemon=True).start()
return JSONResponse({"status": "accepted"})
Por que responder 202 imediatamente
WTS faz retry se o webhook demorar > ~10s. Processar o LLM pode levar
5-15s. Responder 202 accepted imediatamente e rodar o workflow
em thread.
Filtros do workflow¶
Antes de dar trabalho ao LLM, filtrar:
# workflow.py (resumo)
def clinfeto_workflow(payload):
session = get_session_by_id(payload["content"]["sessionId"])
# 1. Não é WhatsApp? ignora (pode ser Instagram)
if session["platform"] != "WHATSAPP":
return "ignored: not whatsapp"
# 2. Não é a instância configurada?
if session["instanceNumber"] not in ALLOWED_INSTANCES:
return "ignored: wrong instance"
# 3. Não é a equipe "IA"?
if session["teamName"] != ALLOWED_TEAM:
return "ignored: wrong team"
# 4. Sessão fechada / já em atendimento humano?
if session["status"] not in ("Pendente", "IN_PROGRESS"):
return "ignored: wrong status"
# 5. Processa mídia e segue fluxo
...
Cliente WTS — funções essenciais¶
# wts_client.py
from config import WTS_API_TOKEN, WTS_BASE_URL, WTS_DEPARTMENT_HUMAN_ID
def _headers():
return {
"Authorization": f"Bearer {WTS_API_TOKEN}",
"Content-Type": "application/json",
}
def get_session_by_id(session_id: str) -> dict:
r = requests.get(f"{WTS_BASE_URL}/chat/v2/session/{session_id}",
headers=_headers(), timeout=30)
r.raise_for_status()
return r.json()
def send_message(phone: str, text: str) -> dict:
r = requests.post(
f"{WTS_BASE_URL}/chat/v1/message/send",
headers=_headers(),
json={"phoneNumber": phone, "message": text, "channelId": WTS_CHANNEL_ID},
timeout=30,
)
r.raise_for_status()
return r.json()
def send_message_in_session(session_id: str, text: str) -> dict:
r = requests.post(
f"{WTS_BASE_URL}/chat/v1/session/{session_id}/message/sync",
headers=_headers(),
json={"message": text},
timeout=30,
)
r.raise_for_status()
return r.json()
def transfer_session(session_id: str) -> dict:
r = requests.put(
f"{WTS_BASE_URL}/chat/v2/session/{session_id}/partial",
headers=_headers(),
json={"departmentId": WTS_DEPARTMENT_HUMAN_ID},
timeout=30,
)
r.raise_for_status()
return r.json()
def add_tags(phone: str, tags: list[str]) -> dict:
r = requests.post(
f"{WTS_BASE_URL}/core/v1/contact/phonenumber/{phone}/tags",
headers=_headers(),
json={"tags": tags},
timeout=30,
)
r.raise_for_status()
return r.json()
Transferência tem que ser PUT /chat/v2/session/{id}/partial
Existe um endpoint v1 antigo que parece funcionar mas move a sessão para estado inconsistente. Use v2 partial.
Split por \n\n — enviando a resposta¶
# workflow.py (resumo)
response_text = run_agent(...)
parts = [p.strip() for p in response_text.split("\n\n") if p.strip()]
for part in parts:
send_message_in_session(session_id, part)
# WTS ordena mensagens — não precisa sleep entre envios
Enviar por telefone vs por sessão¶
send_message(phone, text)— usaphoneNumber. Funciona sempre, mas cria nova conversa se a sessão atual foi fechada.send_message_in_session(session_id, text)— dentro da conversa ativa. Preferir esse no fluxo normal.
Use send_message apenas como fallback quando session_id falha ou
não está disponível (ex: resposta proativa).
Quando chamar transfer_session¶
- Via tool
transferir_atendimento, chamada pelo agente. - Em caso de erro não-controlado no workflow (
except Exception:). - Nunca chame por conta do usuário — o agente decide (ou o código confirma via erro).
Tag como sinal¶
Alguns fluxos aplicam tags (add_tags) para marcar o contato no CRM da
clínica:
"agendado"— paciente agendou"transferido"— subiu para humano"sem_interesse"— paciente desistiu
Isso é opcional e varia por clínica. Ver telemetria.