Overview
Webhook Hub
The framework's metadata-driven webhook hub: it delivers application events to
external systems (outbound) and receives callbacks from external providers on a
single endpoint (inbound). All configuration lives in metadata DB tables —
no per-provider hardcoding, no deploy needed to add an integration.
Screenshot reference (user manual)
webhook-hub__webhook-hub-endpoints__desktop.png: webhook endpoint list (_wuic_webhook_endpoints/list) with direction, target URL, retry policy and pause state.
When to use it (use cases)
- Notify an external ERP/back-office system when something happens in the app
(order.created, invoice.paid): the ERP receives a signed POST without polling.
- Receive callbacks from a provider (payment gateway, digital signature,
courier): a single public endpoint, with routing to the application logic
configured via metadata.
- Resilient integrations: automatic retry with backoff, inspectable
dead-letter, manual replay — an external endpoint down for an hour loses no events.
- Integration auditing: every attempt (in/out) is tracked with
correlationId, outcome, HTTP code and latency.
Architecture in brief
Outbound flow (app → external):
- the application producer enqueues the event in the outbox (
_wuic_webhook_outbox, statusqueued); - the scheduled task
webhook_outbox_pump(schedulertable,action_type = 3,
every minute) picks up the due deliveries, signs the envelope with HMAC-SHA256 and
sends the POST to the target_url of each subscribed endpoint;
- outcome:
sent, orretryingwithnext_attempt_atcomputed from the
endpoint's retry policy, or dead_letter once attempts/window are exhausted.
Inbound flow (external → app):
- the provider calls
POST /api/webhooks/inbound(single endpoint, anonymous by design:
security is the HMAC signature);
- the hub resolves the first matching routing rule (
_wuic_webhook_inbound_routes,
by priority), verifies signature and anti-replay, then executes the configured
handler (sql | http | method);
- standard response:
202 accepted,200 duplicate,400/401/404for invalid
payload/signature/routing, 500 for a transient handler error (the provider may retry).
Data model (metadata DB)
| Table | Content |
|---|---|
_wuic_webhook_endpoints | outbound/inbound endpoints: URL, secret, signature header, timeout, retry policy, pause |
_wuic_webhook_events | catalog of application events (order.created, ...) |
_wuic_webhook_subscriptions | event → outbound endpoint association |
_wuic_webhook_inbound_routes | routing rules of the single inbound endpoint |
_wuic_webhook_outbox | outbound queue with delivery status and next_attempt_at |
_wuic_webhook_logs | bidirectional audit log (correlationId, outcome, http code, latency) |
_wuic_webhook_notifications | in-app notification policies with cooldown |
No manual installation. The physical schema self-applies at runtime
(idempotent, cross-DBMS: mssql/mysql/postgres/oracle), and on the first menu
load the framework registers the rest by itself:
- the administrative routes of the seven tables (column scaffolding
included);
- the menu entries, gathered in the Webhook Hub submenu under
Administration;
- the scheduled task
webhook_outbox_pump, which drains the queue every
minute — without it outbound deliveries stay queued forever.
This holds on fresh installations and on installations upgraded from an earlier
version. Reconciliation is insert-only: if you renamed, moved, disabled or hid
one of these entries, your change stays — it is not restored on every start.
Envelope and signature
Every outbound delivery (and every signed inbound call) carries the standard envelope:
{
"eventId": "b9d9c1e4f0a34c...",
"eventType": "order.created",
"occurredAt": "2026-07-22T10:30:00.000Z",
"source": "erp",
"data": { "orderId": 42, "total": 199.90 }
}The signature travels in the header configured on the endpoint (default X-Wuic-Signature):
X-Wuic-Signature: t=1784111400,v1=<hmac_sha256_hex(secret, "<t>.<body>")>The timestamp t is inside the signature: a replay beyond the tolerance
(timestamp_tolerance_seconds, default 300) is rejected even without server
state. Receiver-side verification example (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 step by step (complete example)
Goal: notify https://erp.example.com/hooks/wuic on every order.created.
1) Endpoint (route _wuic_webhook_endpoints/list, or 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,
With exponential retries land at 60s, 120s, 240s, 480s... (6h cap); with
fixed always at retry_base_seconds. Once retry_max_attempts is reached — or
retry_window_minutes has expired — the delivery goes to dead_letter.
2) Event + subscription: a row in _wuic_webhook_events
(event_type = "order.created") and a row in _wuic_webhook_subscriptions
linking event and endpoint.
3) Publish the event from application code (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,
or via REST (for external modules or tests):
curl -s -X POST http://localhost:5000/api/webhooks/admin/enqueue \
-H "Content-Type: application/json" \
-H "Cookie: k-user=<admin-session-cookie>" \
-d '{"eventType":"order.created","source":"erp","data":{"orderId":42}}'4) Delivery: within a minute the scheduled task pumps the queue. To force
a cycle immediately (or in tests): POST /api/webhooks/admin/process-outbox.
Enqueue is idempotent per (eventId, endpoint) pair: republishing the same
eventId does not duplicate the delivery.
Inbound step by step (complete example)
Goal: the payment gateway calls our single endpoint once a payment completes.
1) Inbound endpoint (for signature verification): a row in _wuic_webhook_endpoints
with direction = "inbound" and a secret shared with the provider.
2) Ingress route 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 supports header (name/value of a header), query (query string
parameter) and body_key (top-level JSON key). The first enabled rule that
matches (ordered by priority) wins. Available handlers:
sql— statement/stored procedure on the data DB; the raw body is available as bind@p_payload(:p_payloadon Oracle);http—[VERB] URLforwarding (default POST) to an internal service;method— static .NET methodNamespace.Type.Method(string payload).
3) Provider call (curl example with signature):
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"Responses:
| Code | status | Meaning |
|---|---|---|
202 | accepted | routing + signature ok, handler executed |
200 | duplicate | eventId already processed (anti-replay, idempotent) |
400 | rejected_payload | body is not JSON |
401 | rejected_signature | signature missing, invalid or expired |
404 | no_route | no routing rule matches |
500 | handler_error | handler failed: the provider may retry |
Operational notifications
The policies in _wuic_webhook_notifications generate in-app notifications (bell)
when critical conditions occur, with anti-spam cooldown:
trigger_kind | Fires when |
|---|---|
first_failure | first failure of a delivery |
dead_letter | a delivery reaches the dead-letter |
failure_rate | ≥ threshold retries within window_minutes |
signature_spike | ≥ threshold inbound signatures rejected within window_minutes |
Example: notify users 5 and 12 on every dead-letter, at most once every
15 minutes → a row with trigger_kind = "dead_letter", user_ids = "5,12",
cooldown_minutes = 15. With role_ids the notification goes to every user of the role.
Operations and troubleshooting
- Logs: route
_wuic_webhook_logs/list— filter bycorrelation_idto follow
the whole history of an event; direction + status for error rates.
- Replay of a single delivery:
POST /api/webhooks/admin/replay/{outboxId}. - Bulk dead-letter requeue:
POST /api/webhooks/admin/requeue-dead-letter
(resets attempts = 0, the retry policy starts over).
- Test an outbound endpoint without dirtying the outbox:
POST /api/webhooks/admin/test-endpoint/{endpointId} (synthetic event webhook.test).
- Temporary pause of an endpoint: flag
paused = 1— deliveries stay in the
queue and resume on unpause (unlike enabled = 0, which also excludes the
endpoint from the fan-out of new events).
- Payload in logs: off by default; with
store_payload = 1the payload is stored
in the logs with the keys listed in redact_fields (csv) masked ("iban" → "***").
Limits (v1)
- Notification channel: in-app only (no dedicated email/realtime; email remains
available via the standard scheduler/mailing list).
- The inbound endpoint is single: no per-provider dedicated paths (routing is
entirely metadata-driven).
- Inbound security: HMAC + timestamp tolerance + anti-replay on
eventId
(no mTLS/OAuth in v1).
- The outbound pump processes batches of 50 deliveries per scheduler cycle.
- The
api/webhooks/admin/*APIs (enqueue, process-outbox, replay, requeue-dead-letter,
test-endpoint) are reserved to administrators since 1.7.13: without a session with the
superadmin role they answer 401 errors.auth.unauthenticated. The inbound endpoint (HMAC
signature) and the scheduled task pump do not change.
Screenshot
