Pular para conteúdo

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) — usa phoneNumber. 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

  1. Via tool transferir_atendimento, chamada pelo agente.
  2. Em caso de erro não-controlado no workflow (except Exception:).
  3. 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.