Pular para o conteúdo principal

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.

CampoDescrição
keySlug único por organização (ex.: erp-status).
publicIdIdentidade pública gerada; compõe a URL de ingestão.
authTypeAPI_KEY (token da org num header — mais fácil) ou HMAC (assina o corpo — mais seguro). ADR-028.
apiKeyHeaderHeader da API key quando authType=API_KEY (default x-api-key; aceita Authorization: Bearer).
secretSegredo HMAC compartilhado com o ERP (revelado uma vez) — modo HMAC.
algorithmsha256 (default), sha1, sha512 — modo HMAC.
signatureHeaderHeader que carrega a assinatura (default x-signature) — modo HMAC.
eventTypeTipo de evento de domínio emitido (default WebhookReceived).
enabledLiga/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:

  1. validate — o payload tem os campos esperados?
  2. transform — mapeia os campos específicos do ERP para os canônicos (ex.: copia payload.erpOrderNumber → erpOrderNumber, deriva o statusTag). É aqui que mora a configuração por ERP, sem deploy.
  3. 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.

ConfigDefaultDescrição
matchFielderpOrderNumberColuna do pedido p/ casar (erpOrderNumber/orderNumber/hashId).
matchFromerpOrderNumberDot-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=webhookCampos 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 fazConfig
run-connectorDispara um sync assíncrono do conector (persiste no canônico). Não espera.connectorKey (obrigatório), saveAs?
call-connectorExecuta 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étodoRotaPermissãoDescrição
GET/api/webhookswebhook:readLista endpoints (sem segredo).
POST/api/webhookswebhook:writeCria endpoint (revela o segredo uma vez).
PATCH/api/webhooks/:idwebhook:writeAtualiza (a key é imutável).
POST/api/webhooks/:id/rotate-secretwebhook:writeRotaciona o segredo.
DELETE/api/webhooks/:idwebhook:writeRemove (e as entregas).
GET/api/webhooks/deliverieswebhook:readEntregas (staging + resultado).
POST/api/webhooks/deliveries/:id/reprocesswebhook:writeRe-emite o evento a partir do staging.
POST/api/webhooks/ingest/:publicIdpú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 secret do 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-status grava fora da transação da execução do pipeline — use-o por último e com identifierFrom para idempotência no reprocessamento.