Skip to main content

Entity Webhooks

Gateway prefix: ${API_BASE}/metadata/entity-webhooks/...

Metadata entity webhooks send change events (insert/update/upsert/delete) to external HTTP endpoints when an entity changes. They are built on the data service’s mutation outbox table for reliable delivery and retry.

Backend components:

  • Configuration management: EntityWebhookConfigController (/api/v1/entity-webhooks)
  • Delivery execution: WebhookOutboxDispatcher in the data service (scheduled job)

Configuration object (WebhookConfigDto)

Request/response fields (see WebhookConfigDto and EntityWebhookConfig):

  • id: configuration ID (returned)
  • tenantId: tenant ID (written by server)
  • entityName: entity name (for example orders, fin_invoice)
  • events: comma-separated events, for example "INSERT,UPDATE,DELETE"
  • webhookUrl: target webhook URL (must be a valid URL)
  • httpMethod: HTTP method (default POST)
  • headersJson: extra headers configuration, JSON string, Record<string,string>
  • timeoutMs: request timeout in milliseconds (default 3000)
  • maxRetries: maximum retries (default 3)
  • backoffInitialSeconds: initial retry interval in seconds (default 5)
  • backoffFactor: exponential backoff factor (default 2.0)
  • enabled: whether enabled (default true)
  • secret: signing secret (optional, used to sign payload)
  • createdAt / updatedAt: timestamps
  • createdBy / updatedBy: audit fields

Frontend management page: EntityWebhookResource, which provides list/create/edit forms with validation (event types, URL format, headersJson JSON validity, etc.).

Configuration management APIs

Base path: /metadata/entity-webhooks (gateway to /api/v1/entity-webhooks).

Permissions: tenant:admin / admin.

Headers:

  • Authorization: Bearer <token>
  • X-Tenant-Id: <tenantId> (injected by gateway or caller)

Create configuration

  • POST /metadata/entity-webhooks

Example:

curl -X POST "${API_BASE}/metadata/entity-webhooks" \
-H "Authorization: Bearer <token>" \
-H "X-Tenant-Id: tenant-abc123" \
-H "Content-Type: application/json" \
-d '{
"entityName": "orders",
"events": "INSERT,UPDATE",
"webhookUrl": "https://example.com/hooks/order-events",
"httpMethod": "POST",
"headersJson": "{\"X-Source\":\"aidaas\"}",
"timeoutMs": 5000,
"maxRetries": 5,
"backoffInitialSeconds": 5,
"backoffFactor": 2.0,
"enabled": true,
"secret": "my-webhook-secret"
}'

Notes:

  • Supported events values: INSERT, UPDATE, UPSERT, DELETE, can be combined.
  • headersJson must be valid JSON and parse into an object; otherwise WEBHOOK_HEADERS_INVALID is returned.
  • Timeout and retry fields default when not explicitly provided.

Update configuration

  • PUT /metadata/entity-webhooks/{id}

Example:

curl -X PUT "${API_BASE}/metadata/entity-webhooks/<id>" \
-H "Authorization: Bearer <token>" \
-H "X-Tenant-Id: tenant-abc123" \
-H "Content-Type: application/json" \
-d '{
"entityName": "orders",
"events": "INSERT,UPDATE,DELETE",
"enabled": true
}'

Notes:

  • Updates overwrite specified fields; omitted fields remain unchanged.
  • To disable a configuration, set enabled to false.

Delete configuration

  • DELETE /metadata/entity-webhooks/{id}

Notes:

  • Soft delete (deleted = true); existing outbox records are not reclaimed, only future reads and deliveries are affected.

Get by ID

  • GET /metadata/entity-webhooks/{id}

Returns a single WebhookConfigDto, or error WEBHOOK_CONFIG_NOT_FOUND if missing.

Paginated query

  • GET /metadata/entity-webhooks?page=1&pageSize=20&entityName=orders&sortBy=createdAt&sortDirection=DESC

Query parameters:

  • page: page number, starting from 1, default 1
  • pageSize/size: page size, default 20
  • entityName: filter by entity name (optional)
  • sortBy: sort field, default createdAt
  • sortDirection: sort direction, default DESC

Response: PageResponse<WebhookConfigDto>.

Query by entity (internal)

  • GET /metadata/entity-webhooks/internal/by-entity/{entityName}

Notes:

  • Used by internal components (such as data service) to load all webhooks for an entity.
  • Requires tenant:admin / admin / internal permissions.

Change events and outbox model

When entities change, the data service writes a row to tenant table mutation_webhook_outbox:

  • Core fields:
    • id: outbox record ID
    • tenant_id: tenant ID
    • entity_name: entity name
    • operation: operation type (INSERT/UPDATE/UPSERT/DELETE)
    • request_id: request ID for idempotency and debugging
    • webhook_config_id: referenced webhook configuration ID
    • payload_json: JSON payload of the change
    • status: PENDING / RETRYING / DELIVERED / DEAD
    • retry_count / max_retries: retry counters and limit
    • next_retry_at: next attempt time
    • last_error: latest error message

WebhookOutboxDispatcher periodically scans each tenant’s outbox table, selects records based on status and next_retry_at, and sends HTTP requests according to the configuration.

Delivery and retry mechanism

Delivery logic (see WebhookOutboxDispatcher.sendOnce):

  • Parse WebhookConfigDto:
    • webhookUrl: target URL; if missing or empty, WEBHOOK_URL_MISSING is thrown and the record is marked DEAD
    • httpMethod: parsed to HttpMethod; invalid values fall back to POST
    • headersJson: if non-empty, deserialized into Map<String,String> and added as headers; parse errors throw WEBHOOK_HEADERS_INVALID
    • timeoutMs: wired into HTTP client (via RestTemplate configuration)
    • secret: used for signing (see below)
  • Fixed headers:
    • Content-Type: application/json
    • X-AIDAAS-Request-Id: <request_id>
    • X-AIDAAS-Timestamp: <unix_timestamp_seconds>
    • X-AIDAAS-Signature is added if secret is set
  • Request body: JSON text from payload_json

Retry and state machine:

  • If webhook configuration is missing or enabled = false:
    • Mark outbox record as DEAD, no more retries.
  • For non-2xx HTTP status codes:
    • 408, 429, 5xx: treated as retryable, throw WEBHOOK_DELIVERY_RETRYABLE
    • Other codes: treated as non-retryable, throw WEBHOOK_DELIVERY_NON_RETRYABLE
  • Error handling:
    • Non-retryable, signature, or configuration errors: mark DEAD and write last_error
    • Retryable:
      • If retry_count + 1 >= max_retries: mark DEAD
      • Otherwise set status to RETRYING and compute next next_retry_at using backoff

Backoff strategy:

  • Uses:
    • backoffInitialSeconds: base seconds, default 5, values ≤ 0 fall back to 5
    • backoffFactor: factor, default 2.0, values < 1.0 fall back to 1.5
  • Calculation:
backoffSeconds = base * factor^(currentRetry)
  • Max backoff is capped at 3600 seconds (1 hour).

Signing mechanism (X-AIDAAS-Signature)

When secret is configured, each delivery carries a signature header:

  • Headers:
    • X-AIDAAS-Timestamp: <timestamp> (Unix timestamp in seconds)
    • X-AIDAAS-Signature: v1=<base64(hmac_sha256(secret, "<timestamp>.<payload>"))>

Signing algorithm (see signPayload):

data = "<timestamp>.<payloadJson>"
mac = HMAC-SHA256(secret, data)
signature = "v1=" + Base64(mac)

Verification advice for downstream services:

  • Read headers:
    • X-AIDAAS-Timestamp
    • X-AIDAAS-Signature
  • Check timestamp is within a reasonable window (to prevent replay)
  • Recompute signature with shared secret and compare

Example downstream receiver (pseudo code)

Example verification using Node.js / TypeScript:

import crypto from "node:crypto";

function verifyWebhook(req: any, secret: string): boolean {
const timestamp = req.headers["x-aidaas-timestamp"];
const signature = req.headers["x-aidaas-signature"]; // like v1=...
if (!timestamp || !signature) return false;

const payload = JSON.stringify(req.body ?? {});
const data = `${timestamp}.${payload}`;
const mac = crypto.createHmac("sha256", secret).update(data, "utf8").digest("base64");
const expected = `v1=${mac}`;

return signature === expected;
}

Usage recommendations

  • Configure dedicated webhooks for important entities (orders, invoices, risk evaluation results, etc.) to push changes to risk, CRM, and audit systems.
  • Prefer separate webhook configurations per downstream system for easier isolation and debugging.
  • For stronger security, always configure secret and validate signatures and timestamps downstream.
  • Implement idempotency on receivers, for example by using X-AIDAAS-Request-Id as idempotency key.