Webhooks — configuration and reliability
Arbitex webhooks push real-time event notifications to an HTTP endpoint of your choice. When a subscribed event occurs — a new conversation, a DLP rule trigger, a quota breach, a compliance bundle state change, or a billing event — Arbitex sends a signed JSON payload to your configured URL within seconds.
Common use cases include feeding Arbitex events into SIEM pipelines, triggering remediation workflows, updating compliance dashboards, and syncing audit records to external systems.
All webhook management endpoints require an admin role. The router prefix is /api/v1/admin/webhooks.
Overview
Section titled “Overview”Each webhook registration binds one URL to one or more event types. When a matching event fires, Arbitex delivers an HTTP POST request to your endpoint with:
- A JSON body describing the event and its payload
- An
X-Arbitex-Signatureheader containing an HMAC-SHA256 signature you can use to verify authenticity - Automatic retry logic on delivery failure
Delivery is best-effort with up to three attempts. See Retry policy for timing details.
Registering a webhook
Section titled “Registering a webhook”Send a POST request to create a new webhook registration.
POST /api/v1/admin/webhooks/Request body:
{ "name": "My SIEM Integration", "url": "https://ingest.example.com/arbitex-events", "events": ["new_conversation", "dlp_trigger"], "secret": "<your-signing-secret>", "enabled": true}Example:
curl -s -X POST https://api.arbitex.ai/api/v1/admin/webhooks/ \ -H "Authorization: Bearer $ADMIN_TOKEN" \ -H "Content-Type: application/json" \ -d '{ "name": "My SIEM Integration", "url": "https://ingest.example.com/arbitex-events", "events": ["new_conversation", "dlp_trigger"], "secret": "<your-signing-secret>", "enabled": true }'Field descriptions:
| Field | Required | Description |
|---|---|---|
name |
Yes | Human-readable label for this webhook |
url |
Yes | HTTPS endpoint that will receive POST requests |
events |
Yes | List of event types to subscribe to — at least one required |
secret |
Yes | Signing secret for HMAC-SHA256 signature verification — 8 to 255 characters |
enabled |
No | Whether the webhook is active; defaults to true |
The response includes the assigned webhook_id, which you use for all subsequent operations on this registration.
Supported event types
Section titled “Supported event types”Subscribe to any combination of the following event types. Pass them as string values in the events array.
Platform events
| Event type | When it fires |
|---|---|
new_conversation |
A new conversation is created on the platform |
dlp_trigger |
A DLP policy rule matches content in a conversation |
quota_exceeded |
A user or group reaches or exceeds their configured usage quota |
bundle_state_change |
A compliance bundle transitions to a new state (e.g., active, archived) |
Billing events (cloud-0020)
| Event type | When it fires |
|---|---|
quota_exceeded |
Current monthly request count reaches the plan limit |
usage_threshold |
Usage reaches 80%, 90%, or 100% of the monthly limit |
invoice_generated |
A new invoice is created for the organization |
A single webhook can subscribe to multiple event types simultaneously.
Managing webhooks
Section titled “Managing webhooks”List all webhooks
Section titled “List all webhooks”Returns all webhook registrations for the tenant.
GET /api/v1/admin/webhooks/curl -s https://api.arbitex.ai/api/v1/admin/webhooks/ \ -H "Authorization: Bearer $ADMIN_TOKEN"View a webhook and its delivery history
Section titled “View a webhook and its delivery history”Returns the webhook registration details along with the last 20 delivery records.
GET /api/v1/admin/webhooks/{webhook_id}curl -s https://api.arbitex.ai/api/v1/admin/webhooks/wh_01abc123 \ -H "Authorization: Bearer $ADMIN_TOKEN"Update a webhook
Section titled “Update a webhook”Use a PUT request to update any combination of fields. All fields are optional — only the fields you include are changed.
PUT /api/v1/admin/webhooks/{webhook_id}# Disable a webhook temporarilycurl -s -X PUT https://api.arbitex.ai/api/v1/admin/webhooks/wh_01abc123 \ -H "Authorization: Bearer $ADMIN_TOKEN" \ -H "Content-Type: application/json" \ -d '{"enabled": false}'
# Update the URL and add an event typecurl -s -X PUT https://api.arbitex.ai/api/v1/admin/webhooks/wh_01abc123 \ -H "Authorization: Bearer $ADMIN_TOKEN" \ -H "Content-Type: application/json" \ -d '{ "url": "https://ingest.example.com/arbitex-events-v2", "events": ["new_conversation", "dlp_trigger", "quota_exceeded"] }'
# Rotate the signing secretcurl -s -X PUT https://api.arbitex.ai/api/v1/admin/webhooks/wh_01abc123 \ -H "Authorization: Bearer $ADMIN_TOKEN" \ -H "Content-Type: application/json" \ -d '{"secret": "<your-new-signing-secret>"}'Updatable fields: name, url, events, secret, enabled.
Delete a webhook
Section titled “Delete a webhook”Permanently removes the webhook registration and stops all future deliveries.
DELETE /api/v1/admin/webhooks/{webhook_id}curl -s -X DELETE https://api.arbitex.ai/api/v1/admin/webhooks/wh_01abc123 \ -H "Authorization: Bearer $ADMIN_TOKEN"Retry policy
Section titled “Retry policy”When a delivery attempt fails (non-2xx response, connection error, or 10-second timeout), Arbitex retries up to two additional times using exponential backoff:
| Attempt | Delay before attempt |
|---|---|
| 1 (initial) | Immediate |
| 2 | 1 second |
| 3 | 5 seconds |
| (if 3 fails) | Delivery moves to dead letter queue after 30-second wait |
The request timeout per attempt is 10 seconds. Connections that do not complete within 10 seconds are treated as failures and trigger the retry sequence.
Dead letter queue
Section titled “Dead letter queue”If all three attempts fail, the delivery record status is set to dead_letter. Dead letter deliveries are not retried automatically. To re-deliver:
- Fix the issue at your endpoint (verify URL reachability, check response codes).
- Use the delivery log to identify the delivery ID.
- Re-trigger the original event (if available) or re-run from your SIEM pipeline using the delivery log payload.
Check the delivery log regularly for dead_letter entries. A pattern of dead letter deliveries indicates a persistent connectivity or endpoint configuration issue.
HMAC signature verification
Section titled “HMAC signature verification”Every delivery includes an X-Arbitex-Signature header. Verifying this signature confirms the request originated from Arbitex and that the body has not been tampered with in transit.
Header format:
X-Arbitex-Signature: sha256=<hex_digest>Algorithm: HMAC-SHA256, computed over the raw request body bytes using the secret you provided at webhook creation.
Python
Section titled “Python”import hmacimport hashlib
def verify_arbitex_signature(raw_body: bytes, secret: str, signature_header: str) -> bool: """ Verify an Arbitex webhook signature.
Args: raw_body: The raw, unmodified request body bytes. secret: The signing secret configured on the webhook. signature_header: The value of the X-Arbitex-Signature header.
Returns: True if the signature is valid, False otherwise. """ expected_prefix = "sha256=" if not signature_header.startswith(expected_prefix): return False
received_digest = signature_header[len(expected_prefix):] expected_digest = hmac.new( key=secret.encode("utf-8"), msg=raw_body, digestmod=hashlib.sha256, ).hexdigest()
return hmac.compare_digest(expected_digest, received_digest)
# Example usage in a Flask handlerfrom flask import Flask, request, abort
app = Flask(__name__)# Replace with the secret you registered; load it from your environment or# secret store rather than committing it to source.WEBHOOK_SECRET = "<your-signing-secret>"
@app.route("/arbitex-events", methods=["POST"])def handle_webhook(): signature = request.headers.get("X-Arbitex-Signature", "") if not verify_arbitex_signature(request.get_data(), WEBHOOK_SECRET, signature): abort(403)
event = request.get_json() print(f"Received event: {event['event_type']}") return "", 200Always use hmac.compare_digest for the final comparison to prevent timing-based attacks.
Shell (openssl)
Section titled “Shell (openssl)”# Compute the expected digest from the raw body and secretBODY='{"event_type":"dlp_trigger","payload":{}}'SECRET="<your-signing-secret>"
EXPECTED=$(echo -n "$BODY" | openssl dgst -sha256 -hmac "$SECRET" | awk '{print $2}')echo "sha256=$EXPECTED"Compare the output against the X-Arbitex-Signature header value. Both must match exactly (prefix included) for the delivery to be considered authentic.
Testing webhook delivery
Section titled “Testing webhook delivery”Send a synthetic webhook_test event to verify that your endpoint is reachable and that your signature verification logic is working correctly.
POST /api/v1/admin/webhooks/{webhook_id}/testcurl -s -X POST https://api.arbitex.ai/api/v1/admin/webhooks/wh_01abc123/test \ -H "Authorization: Bearer $ADMIN_TOKEN"Response:
{ "success": true, "status_code": 200, "error": null}If the delivery fails, success is false, status_code reflects the HTTP response code returned by your endpoint (or null on connection failure), and error contains a description of the failure.
Use the test endpoint after initial registration, after rotating the signing secret, and after making infrastructure changes that could affect reachability.
Delivery log and monitoring
Section titled “Delivery log and monitoring”Every delivery attempt is written to the webhook delivery log. The GET /api/v1/admin/webhooks/{webhook_id} response includes a deliveries array containing the last 20 delivery records.
Delivery record fields:
| Field | Description |
|---|---|
id |
Unique delivery log ID |
webhook_id |
ID of the webhook registration |
event_type |
The event type that triggered this delivery |
status |
Current delivery status (see below) |
attempt_count |
Number of attempts made so far (1–3) |
last_attempt_at |
Timestamp of the most recent attempt |
next_retry_at |
Scheduled time for the next retry, or null if terminal |
response_code |
HTTP status code from the last attempt, or null on connection failure |
error_message |
Error detail from the last failed attempt, or null on success |
payload |
The JSON body sent (truncated to 10,000 characters) |
Status values:
| Status | Description |
|---|---|
pending |
Delivery is in progress (first attempt) |
delivered |
At least one attempt received a 2xx response |
failed |
An attempt failed; a retry is scheduled (next_retry_at is set) |
dead_letter |
All three attempts failed; no further retries will occur |
Debugging failed deliveries:
If you see failed or dead_letter status:
- Check
response_code— a 4xx response from your endpoint indicates a configuration issue (wrong URL, authentication failure). - Check
error_message—"Request timed out"means your endpoint did not respond within 10 seconds. - Verify the webhook URL is publicly reachable from Arbitex infrastructure.
- Verify your endpoint returns a 2xx status code synchronously (do not defer to a background job without responding first).
- Verify the signing secret in your webhook registration matches what your endpoint expects.
Alert on failed deliveries:
Configure an alert rule to fire when dead_letter deliveries exceed a threshold:
POST /api/v1/admin/alert-rules{ "name": "webhook-dead-letter-alert", "condition": { "metric": "webhook_dead_letter_count", "threshold": 5, "window_minutes": 60 }, "action": { "type": "email", }}Delivery payload format
Section titled “Delivery payload format”All webhook deliveries use the same envelope format:
{ "event_type": "dlp_trigger", "payload": { "request_id": "req_01abc123", "user_id": "user_01def456", "rule_id": "finance-pii-block", "entity_type": "credit_card", "action_taken": "BLOCK", "timestamp": "2026-03-10T16:30:00Z" }, "timestamp": "2026-03-10T16:30:00.123456Z"}The outer timestamp is the delivery time. The inner payload.timestamp is the event time. They may differ slightly due to processing latency.
Billing event example:
{ "event_type": "usage_threshold", "payload": { "org_id": "org_01abc123", "threshold": 0.80, "current_requests": 8000, "monthly_limit": 10000, "period": "2026-03" }, "timestamp": "2026-03-10T16:30:00.123456Z"}See also
Section titled “See also”- Policy Engine admin guide — configure the DLP rules that produce
dlp_triggerevents - Admin operations — set user and group quotas that produce
quota_exceededevents - Compliance bundles — manage bundles that produce
bundle_state_changeevents - Audit log — full audit trail of admin actions including webhook management
SSRF protection
Section titled “SSRF protection”The delivery service validates each webhook URL before making any HTTP request. URLs that resolve to private, loopback, link-local, or reserved IP ranges are blocked immediately with no retry:
10.0.0.0/8,172.16.0.0/12,192.168.0.0/16127.0.0.0/8(localhost)169.254.0.0/16(link-local / cloud metadata endpoints such as169.254.169.254)- IPv6 private ranges:
::1,fe80::/10,fc00::/7
Blocked deliveries are recorded with status failed and last_error: "URL blocked by SSRF protection: target resolves to a private, loopback, or reserved IP address". They are not moved to the dead letter queue.
Cloud Portal webhook implementation
Section titled “Cloud Portal webhook implementation”The Cloud Portal has a separate notification surface for infrastructure health events, exposed at:
GET /v1/orgs/{org_id}/notificationsThis endpoint returns:
- Outpost heartbeat health alerts (Outposts not seen for more than 5 minutes)
- Webhook dead-letter notifications (deliveries that failed after all retries in cloud-connected orgs)
Portal-level notifications are distinct from the platform webhook subscriptions documented above. Platform webhooks fire on conversation/DLP/quota/compliance events; portal notifications surface infrastructure health for operators.
Custom retry policy
Section titled “Custom retry policy”Organizations can override the default retry policy through the Cloud Portal API. The custom policy applies to all webhooks in the organization.
Get current policy:
GET /v1/orgs/{org_id}/webhooks/retry-policyAuthorization: Bearer <org-token>{ "max_retries": 3, "backoff_base": 1, "timeout_seconds": 10, "is_custom": false}Set custom policy:
POST /v1/orgs/{org_id}/webhooks/retry-policyAuthorization: Bearer <org-token>Content-Type: application/json
{"max_retries": 5, "backoff_base": 2, "timeout_seconds": 30}| Field | Range | Default | Description |
|---|---|---|---|
max_retries |
1–10 | 3 | Maximum delivery attempts |
backoff_base |
1–60 s | 1 | Base delay in seconds (doubled each retry: base × 2^attempt) |
timeout_seconds |
5–120 s | 10 | HTTP timeout per attempt |
Setting a policy replaces all three values — partial updates are not supported. When is_custom is true, the org has overridden the global defaults.
Cloud Portal delivery dashboard
Section titled “Cloud Portal delivery dashboard”The delivery dashboard at Portal > Webhook Dashboard (/portal/webhook-dashboard) provides real-time visibility into webhook delivery health.
Summary cards
Section titled “Summary cards”| Card | Description |
|---|---|
| Total Deliveries | Lifetime count of all delivery attempts |
| Success Rate | (delivered / total) × 100 |
| Failed | Count of failed deliveries, including those currently retrying |
| Dead Letter | Count of permanently failed deliveries |
Success rate colour thresholds: ≥ 90% green, ≥ 70% amber, < 70% red.
Delivery history table columns
Section titled “Delivery history table columns”| Column | Description |
|---|---|
| Timestamp | When the delivery was first queued |
| Webhook ID | Target webhook |
| Event Type | The event type that triggered the delivery |
| Status | pending, delivered, failed, dead_letter, or retrying |
| Code | HTTP response code from the target |
| Attempts | Format: N/max |
| Error | Truncated error message (40 characters) |
| Action | Retry button for dead_letter entries |
The dashboard auto-refreshes every 30 seconds.
Delivery stats API
Section titled “Delivery stats API”GET /v1/orgs/{org_id}/webhooks/delivery-statsAuthorization: Bearer <org-token>{ "webhooks": [ { "webhook_id": "3f8a1b2c-...", "total_deliveries": 1250, "success_count": 1200, "failure_count": 47, "dead_letter_count": 3, "success_rate_pct": 96.0, "last_delivery_at": "2026-03-15T12:00:00Z" } ], "totals": { "delivered": 1200, "failed": 47, "dead_letter": 3, "pending": 0, "retrying": 0, "total": 1250 }}Dead letter queue — retrying via API
Section titled “Dead letter queue — retrying via API”Dead-lettered deliveries can be re-queued programmatically:
# Non-scoped retryPOST /v1/orgs/{org_id}/webhooks/deliveries/{delivery_id}/retry
# Webhook-scoped retry (enforces IDOR protection at org and webhook level)POST /v1/orgs/{org_id}/webhooks/{webhook_id}/deliveries/{delivery_id}/retryBoth endpoints: verify status: "dead_letter", reset to pending, clear next_retry_at and error_message, and re-enter the normal retry cycle. HTTP 409 is returned if the delivery is not in dead_letter status.
HMAC signature verification — Node.js and Go
Section titled “HMAC signature verification — Node.js and Go”Node.js
Section titled “Node.js”const crypto = require("crypto");
function verifyWebhook(bodyBuffer, secret, signature) { const expected = crypto .createHmac("sha256", secret) .update(bodyBuffer) .digest("hex"); return crypto.timingSafeEqual( Buffer.from(expected, "utf-8"), Buffer.from(signature, "utf-8") );}
// Express handler — use express.raw() to get raw body bytesapp.post("/webhook", express.raw({ type: "application/json" }), (req, res) => { const signature = req.headers["x-webhook-signature"] || ""; if (!verifyWebhook(req.body, process.env.WEBHOOK_SECRET, signature)) { return res.status(401).send("Invalid signature"); } const event = JSON.parse(req.body); res.sendStatus(200);});func verifyWebhook(body []byte, secret, signature string) bool { mac := hmac.New(sha256.New, []byte(secret)) mac.Write(body) expected := hex.EncodeToString(mac.Sum(nil)) return hmac.Equal([]byte(expected), []byte(signature))}Common signature mistakes
Section titled “Common signature mistakes”| Mistake | Fix |
|---|---|
| Verifying against re-serialized JSON | Always verify against the raw request body bytes as received |
| Using string comparison instead of constant-time | Use hmac.compare_digest (Python), crypto.timingSafeEqual (Node), or hmac.Equal (Go) |
| Encoding the secret incorrectly | The secret is UTF-8 encoded |
| Forgetting to handle missing header | Check for empty or missing signature header before comparing |
Outpost Webhook Emitter
Section titled “Outpost Webhook Emitter”Outpost instances can emit scan.complete events independently of the platform webhook system. Configure via environment variables:
| Variable | Description |
|---|---|
WEBHOOK_EMIT_ENABLED |
Enable outpost webhook emission (true/false) |
WEBHOOK_EMIT_URL |
Target URL for scan completion events |
WEBHOOK_EMIT_SECRET |
HMAC-SHA256 signing secret |
The outpost emitter uses the same retry policy (3 attempts, exponential backoff from 1 second) and the same HMAC-SHA256 signing scheme as the platform.
Cloud API reference (superseded)
Section titled “Cloud API reference (superseded)”All Cloud API endpoints required a Bearer token with webhook:write scope. Base URL was https://cloud.arbitex.ai/v1/orgs/{org_id}.
| Method | Path | Description |
|---|---|---|
| GET | /webhooks |
List webhooks (augmented with delivery stats) |
| POST | /webhooks |
Create webhook |
| PUT | /webhooks/{webhook_id} |
Update webhook |
| DELETE | /webhooks/{webhook_id} |
Delete webhook |
| POST | /webhooks/{webhook_id}/test |
Send test event |
| GET | /webhooks/{webhook_id}/health |
Probe target URL (HEAD request, 10 s timeout) |
| GET | /webhooks/deliveries |
List all deliveries (query: status, start_time, end_time, limit, offset) |
| GET | /webhooks/{webhook_id}/deliveries |
Per-webhook deliveries (paginated) |
| POST | /webhooks/deliveries/{delivery_id}/retry |
Retry dead-lettered delivery |
| POST | /webhooks/{webhook_id}/deliveries/{delivery_id}/retry |
Webhook-scoped retry |
| GET | /webhooks/dead-letters |
Dead letter archive (last 100) |
| GET | /webhooks/retry-policy |
Get retry policy |
| POST | /webhooks/retry-policy |
Set custom retry policy |
| GET | /webhooks/delivery-stats |
Aggregate delivery statistics |