Overview

Webhook Hub

Hub webhook metadata-driven del framework: consegna eventi applicativi a

sistemi esterni (outbound) e riceve callback da provider esterni su un endpoint

unico (inbound). Tutta la configurazione vive su tabelle del DB metadati —

nessun hardcode per provider, nessun deploy per aggiungere un'integrazione.

Screenshot reference (manuale utente)

  • webhook-hub__webhook-hub-endpoints__desktop.png: lista endpoint webhook (_wuic_webhook_endpoints/list) con direzione, URL target, retry policy e stato pause.

Quando usarlo (use-case)

  • Notificare un gestionale esterno quando nell'app succede qualcosa

(order.created, invoice.paid): l'ERP riceve un POST firmato senza fare polling.

  • Ricevere callback di un provider (gateway di pagamento, firma digitale,

corriere): un solo endpoint pubblico, il routing verso la logica applicativa

è configurato a metadata.

  • Integrazioni resilienti: retry automatico con backoff, dead-letter

ispezionabile, replay manuale — un endpoint esterno giù per un'ora non perde eventi.

  • Audit delle integrazioni: ogni tentativo (in/out) è tracciato con

correlationId, esito, HTTP code e latenza.

Architettura in breve

Flusso outbound (app → esterno):

  • il producer applicativo accoda l'evento nell'outbox (_wuic_webhook_outbox, stato queued);
  • lo scheduled task webhook_outbox_pump (tabella scheduler, action_type = 3,

ogni minuto) preleva le consegne dovute, firma l'envelope con HMAC-SHA256 e

invia il POST al target_url di ogni endpoint sottoscritto;

  • esito: sent, oppure retrying con next_attempt_at calcolato dalla retry

policy dell'endpoint, oppure dead_letter a tentativi/finestra esauriti.

Flusso inbound (esterno → app):

  • il provider chiama POST /api/webhooks/inbound (endpoint unico, anonimo by-design:

la sicurezza è la firma HMAC);

  • il hub risolve la prima regola di routing che matcha (_wuic_webhook_inbound_routes,

per priority), verifica firma e anti-replay, poi esegue l'handler configurato

(sql | http | method);

  • risposta standard: 202 accepted, 200 duplicate, 400/401/404 per payload/firma/routing

invalidi, 500 per errore transiente dell'handler (il provider può ritentare).

Modello dati (DB metadati)

TabellaContenuto
_wuic_webhook_endpointsendpoint outbound/inbound: URL, secret, header firma, timeout, retry policy, pause
_wuic_webhook_eventscatalogo eventi applicativi (order.created, ...)
_wuic_webhook_subscriptionsassociazione evento → endpoint outbound
_wuic_webhook_inbound_routesregole di routing dell'endpoint inbound unico
_wuic_webhook_outboxcoda outbound con stato delivery e next_attempt_at
_wuic_webhook_logsaudit log bidirezionale (correlationId, esito, http code, latenza)
_wuic_webhook_notificationspolicy di notifica in-app con cooldown

Nessuna installazione manuale. Lo schema fisico si auto-applica a runtime

(idempotente, cross-DBMS: mssql/mysql/postgres/oracle), e al primo caricamento

del menu il framework registra da solo anche il resto:

  • le route amministrative delle sette tabelle (scaffolding delle colonne

incluso);

  • le voci di menu, raccolte nel sottomenu Webhook Hub sotto

Amministrazione;

  • lo scheduled task webhook_outbox_pump, che smaltisce la coda ogni

minuto — senza di lui le consegne in uscita restano accodate per sempre.

Vale su installazioni nuove e su installazioni aggiornate da una versione

precedente. La riconciliazione e' di solo inserimento: se hai rinominato,

spostato, disabilitato o nascosto una di queste voci, la tua modifica resta —

non viene ripristinata ad ogni avvio.

Envelope e firma

Ogni delivery outbound (e ogni inbound firmato) trasporta l'envelope standard:

Snippet 1JSON
{
  "eventId": "b9d9c1e4f0a34c...",
  "eventType": "order.created",
  "occurredAt": "2026-07-22T10:30:00.000Z",
  "source": "erp",
  "data": { "orderId": 42, "total": 199.90 }
}

La firma viaggia nell'header configurato sull'endpoint (default X-Wuic-Signature):

Snippet 2text
X-Wuic-Signature: t=1784111400,v1=<hmac_sha256_hex(secret, "<t>.<body>")>

Il timestamp t è dentro la firma: un replay oltre la tolleranza

(timestamp_tolerance_seconds, default 300) viene respinto anche senza stato

server. Esempio di verifica lato ricevente (Node.js):

Snippet 3js
import { createHmac, timingSafeEqual } from 'node:crypto';

function verifyWuicSignature(secret, headerValue, rawBody, toleranceSec = 300) {
  const t = /t=(\d+)/.exec(headerValue)?.[1];
  const v1 = /v1=([0-9a-f]+)/.exec(headerValue)?.[1];
  if (!t || !v1) return false;
  if (Math.abs(Date.now() / 1000 - Number(t)) > toleranceSec) return false;

Outbound passo-passo (esempio completo)

Obiettivo: notificare https://erp.example.com/hooks/wuic ad ogni order.created.

1) Endpoint (route _wuic_webhook_endpoints/list, o via CRUD API):

Snippet 4JSON
{
  "we_name": "erp-principale",
  "direction": "outbound",
  "target_url": "https://erp.example.com/hooks/wuic",
  "secret": "s3gr3t0-condiviso",
  "timeout_seconds": 30,
  "retry_max_attempts": 5,

Con exponential i retry cadono a 60s, 120s, 240s, 480s... (cap 6h); con

fixed sempre a retry_base_seconds. A retry_max_attempts raggiunti — o a

retry_window_minutes scaduti — la consegna va in dead_letter.

2) Evento + sottoscrizione: riga in _wuic_webhook_events

(event_type = "order.created") e riga in _wuic_webhook_subscriptions che

collega evento ed endpoint.

3) Pubblicare l'evento dal codice applicativo (C#):

Snippet 5C#
using System.Text.Json;
using WuicCore.Services.Webhooks;

using JsonDocument doc = JsonDocument.Parse("{\"orderId\":42,\"total\":199.90}");
(string eventId, int enqueued) = WebhookHubService.EnqueueOutbound(
    eventType: "order.created",
    data: doc.RootElement,

oppure via REST (per moduli esterni o test):

Snippet 6Bash
curl -s -X POST http://localhost:5000/api/webhooks/admin/enqueue \
  -H "Content-Type: application/json" \
  -d '{"eventType":"order.created","source":"erp","data":{"orderId":42}}'

4) Consegna: entro un minuto lo scheduled task pompa la coda. Per forzare

subito un ciclo (o nei test): POST /api/webhooks/admin/process-outbox.

L'enqueue è idempotente per coppia (eventId, endpoint): ripubblicare lo

stesso eventId non duplica la consegna.

Inbound passo-passo (esempio completo)

Obiettivo: il gateway pagamenti chiama il nostro endpoint unico a pagamento avvenuto.

1) Endpoint inbound (per la verifica firma): riga in _wuic_webhook_endpoints

con direction = "inbound" e secret condiviso col provider.

2) Route di ingresso in _wuic_webhook_inbound_routes:

Snippet 7JSON
{
  "endpoint_id": 12,
  "match_kind": "body_key",
  "match_key": "eventType",
  "match_value": "payment.completed",
  "handler_kind": "sql",
  "handler_cmd": "EXEC dbo.registra_pagamento_webhook @p_payload",

match_kind supporta header (nome/valore di un header), query (parametro

query string) e body_key (chiave top-level del JSON). La prima regola abilitata

che matcha (ordinata per priority) vince. Handler disponibili:

  • sql — statement/stored sul DB dati; il body raw è disponibile come bind @p_payload (:p_payload su Oracle);
  • http — inoltro [VERB] URL (default POST) verso un servizio interno;
  • method — metodo statico .NET Namespace.Type.Method(string payload).

3) Chiamata del provider (esempio curl con firma):

Snippet 8Bash
BODY='{"eventId":"pg-789","eventType":"payment.completed","data":{"amount":199.90}}'
T=$(date +%s)
SIG=$(printf '%s.%s' "$T" "$BODY" | openssl dgst -sha256 -hmac 's3gr3t0-inbound' -hex | awk '{print $2}')
curl -s -X POST http://localhost:5000/api/webhooks/inbound \
  -H "Content-Type: application/json" \
  -H "X-Wuic-Signature: t=$T,v1=$SIG" \
  -d "$BODY"

Risposte:

CodicestatusSignificato
202acceptedrouting + firma ok, handler eseguito
200duplicateeventId già processato (anti-replay, idempotente)
400rejected_payloadbody non JSON
401rejected_signaturefirma assente, invalida o scaduta
404no_routenessuna regola di routing matcha
500handler_errorhandler fallito: il provider può ritentare

Notifiche operative

Le policy in _wuic_webhook_notifications generano notifiche in-app (campanello)

al verificarsi di condizioni critiche, con cooldown anti-spam:

trigger_kindScatta quando
first_failureprima failure di una consegna
dead_letteruna consegna raggiunge il dead-letter
failure_ratethreshold retry in window_minutes
signature_spikethreshold firme inbound respinte in window_minutes

Esempio: notificare gli utenti 5 e 12 su ogni dead-letter, non più di una volta

ogni 15 minuti → riga con trigger_kind = "dead_letter", user_ids = "5,12",

cooldown_minutes = 15. Con role_ids la notifica va a tutti gli utenti del ruolo.

Operatività e troubleshooting

  • Log: route _wuic_webhook_logs/list — filtra per correlation_id per seguire

tutta la storia di un evento; direction + status per i tassi di errore.

  • Replay di una singola consegna: POST /api/webhooks/admin/replay/{outboxId}.
  • Requeue dead-letter in blocco: POST /api/webhooks/admin/requeue-dead-letter

(rimette attempts = 0, riparte la policy retry).

  • Test endpoint outbound senza sporcare l'outbox:

POST /api/webhooks/admin/test-endpoint/{endpointId} (evento sintetico webhook.test).

  • Pausa temporanea di un endpoint: flag paused = 1 — le consegne restano in

coda e ripartono al resume (diverso da enabled = 0, che esclude l'endpoint

anche dal fan-out di nuovi eventi).

  • Payload nei log: default spento; con store_payload = 1 il payload è salvato

nei log con le chiavi elencate in redact_fields (csv) oscurate ("iban" → "***").

Limiti (v1)

  • Canale notifiche: solo in-app (niente email/realtime dedicati; l'email resta

disponibile via scheduler/mailing list standard).

  • L'endpoint inbound è unico: niente path dedicati per provider (il routing è

interamente metadata).

  • Sicurezza inbound: HMAC + tolleranza timestamp + anti-replay su eventId

(nessun mTLS/OAuth in v1).

  • La pompa outbound processa batch da 50 consegne per ciclo di scheduler.
  • Le API api/webhooks/admin/* sono pensate per il backoffice: esporle

pubblicamente richiede l'hardening ruoli previsto in roadmap.

Screenshot

webhook-hub / webhook-hub-endpoints
webhook-hub / webhook-hub-endpoints