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, status queued);
  • the scheduled task webhook_outbox_pump (scheduler table, 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, or retrying with next_attempt_at computed 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/404 for invalid

payload/signature/routing, 500 for a transient handler error (the provider may retry).

Data model (metadata DB)

TableContent
_wuic_webhook_endpointsoutbound/inbound endpoints: URL, secret, signature header, timeout, retry policy, pause
_wuic_webhook_eventscatalog of application events (order.created, ...)
_wuic_webhook_subscriptionsevent → outbound endpoint association
_wuic_webhook_inbound_routesrouting rules of the single inbound endpoint
_wuic_webhook_outboxoutbound queue with delivery status and next_attempt_at
_wuic_webhook_logsbidirectional audit log (correlationId, outcome, http code, latency)
_wuic_webhook_notificationsin-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:

Snippet 1JSON
{
  "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):

Snippet 2text
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):

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 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):

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,

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#):

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,

or via REST (for external modules or tests):

Snippet 6Bash
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:

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 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_payload on Oracle);
  • http — [VERB] URL forwarding (default POST) to an internal service;
  • method — static .NET method Namespace.Type.Method(string payload).

3) Provider call (curl example with signature):

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"

Responses:

CodestatusMeaning
202acceptedrouting + signature ok, handler executed
200duplicateeventId already processed (anti-replay, idempotent)
400rejected_payloadbody is not JSON
401rejected_signaturesignature missing, invalid or expired
404no_routeno routing rule matches
500handler_errorhandler 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_kindFires when
first_failurefirst failure of a delivery
dead_lettera 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 by correlation_id to 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 = 1 the 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

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