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:
WebhookOutboxDispatcherin 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 exampleorders,fin_invoice)events: comma-separated events, for example"INSERT,UPDATE,DELETE"webhookUrl: target webhook URL (must be a valid URL)httpMethod: HTTP method (defaultPOST)headersJson: extra headers configuration, JSON string,Record<string,string>timeoutMs: request timeout in milliseconds (default3000)maxRetries: maximum retries (default3)backoffInitialSeconds: initial retry interval in seconds (default5)backoffFactor: exponential backoff factor (default2.0)enabled: whether enabled (defaulttrue)secret: signing secret (optional, used to sign payload)createdAt/updatedAt: timestampscreatedBy/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
eventsvalues:INSERT,UPDATE,UPSERT,DELETE, can be combined. headersJsonmust be valid JSON and parse into an object; otherwiseWEBHOOK_HEADERS_INVALIDis 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
enabledtofalse.
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 from1, default1pageSize/size: page size, default20entityName: filter by entity name (optional)sortBy: sort field, defaultcreatedAtsortDirection: sort direction, defaultDESC
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/internalpermissions.
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 IDtenant_id: tenant IDentity_name: entity nameoperation: operation type (INSERT/UPDATE/UPSERT/DELETE)request_id: request ID for idempotency and debuggingwebhook_config_id: referenced webhook configuration IDpayload_json: JSON payload of the changestatus:PENDING/RETRYING/DELIVERED/DEADretry_count/max_retries: retry counters and limitnext_retry_at: next attempt timelast_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_MISSINGis thrown and the record is markedDEADhttpMethod: parsed toHttpMethod; invalid values fall back toPOSTheadersJson: if non-empty, deserialized intoMap<String,String>and added as headers; parse errors throwWEBHOOK_HEADERS_INVALIDtimeoutMs: wired into HTTP client (via RestTemplate configuration)secret: used for signing (see below)
- Fixed headers:
Content-Type: application/jsonX-AIDAAS-Request-Id: <request_id>X-AIDAAS-Timestamp: <unix_timestamp_seconds>X-AIDAAS-Signatureis added ifsecretis 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.
- Mark outbox record as
- For non-2xx HTTP status codes:
408,429,5xx: treated as retryable, throwWEBHOOK_DELIVERY_RETRYABLE- Other codes: treated as non-retryable, throw
WEBHOOK_DELIVERY_NON_RETRYABLE
- Error handling:
- Non-retryable, signature, or configuration errors: mark
DEADand writelast_error - Retryable:
- If
retry_count + 1 >= max_retries: markDEAD - Otherwise set status to
RETRYINGand compute nextnext_retry_atusing backoff
- If
- Non-retryable, signature, or configuration errors: mark
Backoff strategy:
- Uses:
backoffInitialSeconds: base seconds, default5, values ≤ 0 fall back to5backoffFactor: factor, default2.0, values <1.0fall back to1.5
- Calculation:
backoffSeconds = base * factor^(currentRetry)
- Max backoff is capped at
3600seconds (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-TimestampX-AIDAAS-Signature
- Check timestamp is within a reasonable window (to prevent replay)
- Recompute signature with shared
secretand 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
secretand validate signatures and timestamps downstream. - Implement idempotency on receivers, for example by using
X-AIDAAS-Request-Idas idempotency key.