# Research: Webhook VM112 → Ops Platform

**Date**: 2026-06-08  
**Feature**: 001-webhook-vm112-integration

## R1 — Estado do receptor Ops (VM122)

**Decisão**: Reutilizar endpoint MVP existente; estender com idempotência.

**Evidência**:
- `GET /health` → `{"status":"ok","service":"ligbox-ops-api","version":"0.1.0-mvp"}`
- `POST /api/v1/webhooks/onboard` aceita `WebhookPayload` + header `X-Webhook-Secret`
- Tabelas `webhook_events`, `tickets` já criadas em `init_db()`
- Tenant VM112 pré-registado (id=1)

**Alternativas rejeitadas**:
- Novo microserviço webhook → YAGNI, endpoint já existe
- Redis pub/sub directo → menos auditável que HTTP + SQLite

## R2 — Estado do emissor Portal (VM112)

**Decisão**: Novo módulo `ops_webhook.py`; hook em `create_account`.

**Evidência**:
- Portal health: `http://10.10.10.112:8090/api/onboarding/health` OK
- `create_account` retorna sem webhook actual
- `session_id` via `X-Onboarding-Session` header (`deps.bind_onboarding_session`)
- Notificações email existem (`notifications.py`) — webhook é canal adicional

**Alternativas rejeitadas**:
- Polling Ops → VM122 worker já faz poll health; não substitui eventos push
- ntfy como substituto → fora de scope; ops desk precisa tickets

## R3 — Autenticação

**Decisão**: Header `X-Webhook-Secret` partilhado; mesmo padrão do MVP.

**Evidência**: Receptor já valida `x_webhook_secret != WEBHOOK_SECRET` → 401

**Produção**: Gerar secret forte (`openssl rand -hex 32`); configurar em ambos `.env` no mesmo deploy.

## R4 — Idempotência

**Decisão**: Chave natural `(event_type, session_id, domain)` — consultar `webhook_events` antes de INSERT ticket.

**Alternativas rejeitadas**:
- UUID por evento no portal → mais complexo; session_id já existe
- UNIQUE constraint DB → requer migration; lookup é suficiente para MVP volume

## R5 — Non-blocking

**Decisão**: `BackgroundTasks` do FastAPI no portal para emitir webhook após response preparada, OU try/except inline com timeout curto (5s).

**Preferência**: `BackgroundTasks` — zero impacto na latência percebida pelo cliente.

## R6 — Rede

**Decisão**: URL fixa LAN `http://10.10.10.122:8080` — sem Traefik nesta fase.

**Constitution**: LAN-only ✅; API bindada a `10.10.10.122` no docker-compose ✅

## R7 — Código fonte

| VM | Path deploy | Path fonte (versionar) |
|----|-------------|------------------------|
| 112 | `/opt/ibytera-mail-portal/` | `obsidian-infra/carbonio/ibytera-mail-portal/` |
| 122 | `/opt/ligbox-ops-platform/` | `workspace/projects/ligbox-ops-platform/` |
