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, statoqueued); - lo scheduled task
webhook_outbox_pump(tabellascheduler,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, oppureretryingconnext_attempt_atcalcolato 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/404per payload/firma/routing
invalidi, 500 per errore transiente dell'handler (il provider può ritentare).
Modello dati (DB metadati)
| Tabella | Contenuto |
|---|---|
_wuic_webhook_endpoints | endpoint outbound/inbound: URL, secret, header firma, timeout, retry policy, pause |
_wuic_webhook_events | catalogo eventi applicativi (order.created, ...) |
_wuic_webhook_subscriptions | associazione evento → endpoint outbound |
_wuic_webhook_inbound_routes | regole di routing dell'endpoint inbound unico |
_wuic_webhook_outbox | coda outbound con stato delivery e next_attempt_at |
_wuic_webhook_logs | audit log bidirezionale (correlationId, esito, http code, latenza) |
_wuic_webhook_notifications | policy 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:
{
"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):
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):
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):
{
"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#):
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):
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:
{
"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_payloadsu Oracle);http— inoltro[VERB] URL(default POST) verso un servizio interno;method— metodo statico .NETNamespace.Type.Method(string payload).
3) Chiamata del provider (esempio curl con firma):
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:
| Codice | status | Significato |
|---|---|---|
202 | accepted | routing + firma ok, handler eseguito |
200 | duplicate | eventId già processato (anti-replay, idempotente) |
400 | rejected_payload | body non JSON |
401 | rejected_signature | firma assente, invalida o scaduta |
404 | no_route | nessuna regola di routing matcha |
500 | handler_error | handler 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_kind | Scatta quando |
|---|---|
first_failure | prima failure di una consegna |
dead_letter | una consegna raggiunge il dead-letter |
failure_rate | ≥ threshold retry in window_minutes |
signature_spike | ≥ threshold 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 percorrelation_idper 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 = 1il 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
