Webhooks de entrada (status do pedido)
Sistemas externos (o ERP do cliente) empurram dados para o SFA — ex.: atualizações de status do pedido (faturado, expedido, cancelado), ou um pedido de sincronização. A recepção é pública ao JWT mas autenticada por API key (mais fácil) ou HMAC (mais seguro) — escolhido por endpoint (ADR-028); o corpo bruto é gravado em staging e o processamento roda pela engine (o pipeline do gatilho) de forma assíncrona. Decisão original em ADR-021 (E-6.3); ações no pipeline em ADR-027.
Reusa toda a máquina do E-6.2: a entrega vira um evento WebhookReceived no outbox, o relay
publica, o worker roda o pipeline. O casamento com o pedido (por erpOrderNumber) e a escrita do
histórico são feitos por um nó configurável (apply-order-status) — não por código acoplado.
ERP --POST /api/webhooks/ingest/:publicId (API key OU assinado por HMAC)--> SFA
| autentica: valida a API key da org OU verifica o HMAC do corpo bruto
| grava WebhookDelivery (staging) + evento WebhookReceived (mesma transação)
| responde 202 { deliveryId }
v
outbox -> relay -> BullMQ -> worker --(WebhookReceived -> webhook.received)--> Pipeline Engine
| validate -> transform -> apply-order-status
v
OrderStatusHistory (pedido casado por erpOrderNumber)
1. Configurar um endpoint
POST /api/webhooks cria um endpoint por organização. Escolha o authType (API_KEY default ou
HMAC). No modo HMAC, o servidor gera (ou você informa) um segredo HMAC, revelado uma única
vez na resposta — depois write-only (rotacione com POST /api/webhooks/:id/rotate-secret). No modo
API key, não há segredo por endpoint: a auth usa as API keys da organização.
| Campo | Descrição |
|---|---|
key | Slug único por organização (ex.: erp-status). |
publicId | Identidade pública gerada; compõe a URL de ingestão. |
authType | API_KEY (token da org num header — mais fácil) ou HMAC (assina o corpo — mais seguro). ADR-028. |
apiKeyHeader | Header da API key quando authType=API_KEY (default x-api-key; aceita Authorization: Bearer). |
secret | Segredo HMAC compartilhado com o ERP (revelado uma vez) — modo HMAC. |
algorithm | sha256 (default), sha1, sha512 — modo HMAC. |
signatureHeader | Header que carrega a assinatura (default x-signature) — modo HMAC. |
eventType | Tipo de evento de domínio emitido (default WebhookReceived). |
enabled | Liga/desliga a recepção. |
No modo API_KEY, a autenticação usa uma API key da organização (qualquer key ativa da org). No modo HMAC, o cliente assina o corpo (ver Assinatura HMAC).
A resposta traz ingestPath = /api/webhooks/ingest/:publicId — a URL a configurar no ERP.
2. Autenticação
Cada endpoint escolhe um modo (authType). Em ambos, auth inválida ⇒ 401 (a entrega não é
gravada — evita DoS de staging). Detalhes/conceitos: API keys e
Assinatura HMAC.
API key (default — mais fácil)
O emissor envia uma API key da organização no header apiKeyHeader (default
x-api-key; aceita Authorization: Bearer <token>). Qualquer key ativa da org autentica; o uso é
registrado (contador/último uso/IP). O header da key não é gravado no staging.
curl -X POST "$API/api/webhooks/ingest/$PUBLIC_ID" \
-H "content-type: application/json" \
-H "x-api-key: sfa_ab12cd34_...seutoken..." \
-d '{"erpOrderNumber":"NF-123","status":"invoiced"}'
# -> 202 { "deliveryId": "..." } (key inválida/expirada/revogada -> 401)
HMAC (mais seguro)
O emissor calcula HMAC-<algorithm>(corpo_bruto, secret) em hex e envia no signatureHeader
(aceita o prefixo comum sha256=). O SFA recomputa e compara em tempo constante. É o padrão de
plataformas que enviam webhooks (Stripe, GitHub, Meta/WhatsApp — X-Hub-Signature-256).
BODY='{"erpOrderNumber":"NF-123","status":"invoiced"}'
SIG=$(printf '%s' "$BODY" | openssl dgst -sha256 -hmac "$SECRET" | awk '{print $2}')
curl -X POST "$API/api/webhooks/ingest/$PUBLIC_ID" \
-H "content-type: application/json" -H "x-signature: $SIG" -d "$BODY"
# -> 202 { "deliveryId": "..." }
3. Processamento — pipeline webhook.received
O worker mapeia WebhookReceived → webhook.received e roda o pipeline da organização. Um pipeline
típico:
validate— o payload tem os campos esperados?transform— mapeia os campos específicos do ERP para os canônicos (ex.: copiapayload.erpOrderNumber→erpOrderNumber, deriva ostatusTag). É aqui que mora a configuração por ERP, sem deploy.apply-order-status— casa o pedido e grava o status (ver abaixo).
Falha do pipeline ⇒ o worker relança ⇒ BullMQ retenta com backoff ⇒ DLQ. O resultado de cada
entrega é derivado da PipelineExecution (correlação = deliveryId) — visível em
GET /api/pipelines/:id/executions.
Nó apply-order-status
Casa um pedido por um código externo e grava uma entrada no histórico de status.
| Config | Default | Descrição |
|---|---|---|
matchField | erpOrderNumber | Coluna do pedido p/ casar (erpOrderNumber/orderNumber/hashId). |
matchFrom | erpOrderNumber | Dot-path do valor no contexto. |
statusTag | — | Tag do status (success/failed/…). Obrigatório. |
name | — | Nome legível do status. Obrigatório. |
message? / phase? / externalCode? | phase=webhook | Campos do histórico. |
identifierFrom? | — | Dot-path do identificador para idempotência (ex.: deliveryId). |
setOrderStatus? | — | Se informado, também atualiza o enum status do pedido. |
Sem pedido casado ⇒ segue a aresta default (entrega válida, porém unmatched). Com
identifierFrom, reprocessar a mesma entrega não duplica o histórico. Coloque este nó por
último no grafo (ele grava fora da transação da execução).
Ações: acionar conectores / chamar rotas (ADR-027)
O pipeline do webhook não serve só para status de pedido — é o lugar de orquestrar ações com o que
você recebeu. Além de http-request (chamar qualquer URL externa e tratar o retorno via saveAs),
há dois nós que ligam ao módulo de Integrações:
| Nó | O que faz | Config |
|---|---|---|
run-connector | Dispara um sync assíncrono do conector (persiste no canônico). Não espera. | connectorKey (obrigatório), saveAs? |
call-connector | Executa o conector síncrono (1 página, reusa auth/templating), com params do contexto, e grava a resposta ({ items, status }) no contexto. | connectorKey, params? (valores {{dot.path}} vêm do contexto), saveAs? (default connector) |
Exemplo — "um cliente aciona a gente para sincronizar um cliente": o webhook recebe { cnpj }, o
pipeline chama o conector do ERP com esse cnpj e encaminha o resultado adiante:
{ "id": "buscar", "type": "call-connector",
"config": { "connectorKey": "get-cliente", "params": { "cnpj": "{{payload.cnpj}}" }, "saveAs": "cliente" } }
Depois um transform/http-request usa {{cliente.items.0.…}}, ou um run-connector dispara o
sync completo. ("chamar outra API com os dados" é o nó http-request.)
Roteamento por endpoint
O eventType do endpoint é a key do pipeline: um trigger com ponto (ex.: webhook.sync-cliente)
roda o pipeline daquele trigger. Assim webhooks de entrada diferentes disparam fluxos de ações
diferentes. O default WebhookReceived mapeia para webhook.received.
UI
A aba Entrada em /dashboard/webhooks gerencia os endpoints (URL de ingestão + segredo write-only +
trigger do pipeline) e mostra as entregas recebidas. O fluxo de ações é montado em Pipelines com o
mesmo trigger.
4. Staging & reprocessamento
Toda entrega autenticada é gravada em WebhookDelivery (headers sanitizados + payload bruto). Isso
alimenta a auditoria e o reprocessamento:
| Método | Rota | Permissão | Descrição |
|---|---|---|---|
| GET | /api/webhooks | webhook:read | Lista endpoints (sem segredo). |
| POST | /api/webhooks | webhook:write | Cria endpoint (revela o segredo uma vez). |
| PATCH | /api/webhooks/:id | webhook:write | Atualiza (a key é imutável). |
| POST | /api/webhooks/:id/rotate-secret | webhook:write | Rotaciona o segredo. |
| DELETE | /api/webhooks/:id | webhook:write | Remove (e as entregas). |
| GET | /api/webhooks/deliveries | webhook:read | Entregas (staging + resultado). |
| POST | /api/webhooks/deliveries/:id/reprocess | webhook:write | Re-emite o evento a partir do staging. |
| POST | /api/webhooks/ingest/:publicId | público (API key/HMAC) | Recepção (usada pelo ERP). |
Schemas completos na Referência da API (tags Webhooks · Admin e Webhooks · Ingest).
Exemplo de pipeline webhook.received
{
"nodes": [
{ "id": "start", "type": "start" },
{ "id": "check", "type": "validate",
"config": { "conditions": { "field": "payload.erpOrderNumber", "op": "exists" },
"message": "webhook sem erpOrderNumber" }, "onError": "abort" },
{ "id": "map", "type": "transform",
"config": { "set": [
{ "target": "erpOrderNumber", "from": "payload.erpOrderNumber" },
{ "target": "statusTag", "value": "success" } ] } },
{ "id": "apply", "type": "apply-order-status",
"config": { "matchFrom": "erpOrderNumber", "statusTag": "success",
"name": "Faturado no ERP", "phase": "webhook", "identifierFrom": "deliveryId" } }
],
"edges": [
{ "from": "start", "to": "check" },
{ "from": "check", "to": "map", "branch": "success" },
{ "from": "map", "to": "apply" }
]
}
Segurança & limites
- API key vs HMAC: a API key é mais fácil (um header) e a gente guarda só o hash (não trafega o segredo em repouso); o HMAC não trafega o segredo no fio e valida a integridade do corpo. Sempre sobre HTTPS. Ver API keys e HMAC.
- O
secretdo HMAC é write-only na API mas fica em texto no banco (sem Vault/KMS ainda) — dívida registrada. A API key é guardada como hash (não recuperável). - Anti-replay (tolerância de timestamp/nonce) e rate-limit por API key são follow-ups; hoje o uso da key é rastreado, não bloqueado por limite.
apply-order-statusgrava fora da transação da execução do pipeline — use-o por último e comidentifierFrompara idempotência no reprocessamento.