Skip to content

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/admin/webhooks.


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-Signature header 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.


Send a POST request to create a new webhook registration.

POST /api/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:

Terminal window
curl -s -X POST https://api.arbitex.ai/api/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:

FieldRequiredDescription
nameYesHuman-readable label for this webhook
urlYesHTTPS endpoint that will receive POST requests
eventsYesList of event types to subscribe to — at least one required
secretYesSigning secret for HMAC-SHA256 signature verification — 8 to 255 characters
enabledNoWhether the webhook is active; defaults to true

The response includes the assigned webhook_id, which you use for all subsequent operations on this registration.


Subscribe to any combination of the following event types. Pass them as string values in the events array.

Platform events

Event typeWhen it fires
new_conversationA new conversation is created on the platform
dlp_triggerA DLP policy rule matches content in a conversation
quota_exceededA user or group reaches or exceeds their configured usage quota
bundle_state_changeA compliance bundle transitions to a new state (e.g., active, archived)

Billing events (cloud-0020)

Event typeWhen it fires
quota_exceededCurrent monthly request count reaches the plan limit
usage_thresholdUsage reaches 80%, 90%, or 100% of the monthly limit
invoice_generatedA new invoice is created for the organization

A single webhook can subscribe to multiple event types simultaneously.


Returns all webhook registrations for the tenant.

GET /api/admin/webhooks/
Terminal window
curl -s https://api.arbitex.ai/api/admin/webhooks/ \
-H "Authorization: Bearer $ADMIN_TOKEN"

Returns the webhook registration details along with the last 20 delivery records.

GET /api/admin/webhooks/{webhook_id}
Terminal window
curl -s https://api.arbitex.ai/api/admin/webhooks/wh_01abc123 \
-H "Authorization: Bearer $ADMIN_TOKEN"

Use a PUT request to update any combination of fields. All fields are optional — only the fields you include are changed.

PUT /api/admin/webhooks/{webhook_id}
Terminal window
# Disable a webhook temporarily
curl -s -X PUT https://api.arbitex.ai/api/admin/webhooks/wh_01abc123 \
-H "Authorization: Bearer $ADMIN_TOKEN" \
-H "Content-Type: application/json" \
-d '{"enabled": false}'
# Update the URL and add an event type
curl -s -X PUT https://api.arbitex.ai/api/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 secret
curl -s -X PUT https://api.arbitex.ai/api/admin/webhooks/wh_01abc123 \
-H "Authorization: Bearer $ADMIN_TOKEN" \
-H "Content-Type: application/json" \
-d '{"secret": "new-signing-secret-min8"}'

Updatable fields: name, url, events, secret, enabled.

Permanently removes the webhook registration and stops all future deliveries.

DELETE /api/admin/webhooks/{webhook_id}
Terminal window
curl -s -X DELETE https://api.arbitex.ai/api/admin/webhooks/wh_01abc123 \
-H "Authorization: Bearer $ADMIN_TOKEN"

When a delivery attempt fails (non-2xx response, connection error, or 10-second timeout), Arbitex retries up to two additional times using exponential backoff:

AttemptDelay before attempt
1 (initial)Immediate
21 second
35 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.

If all three attempts fail, the delivery record status is set to dead_letter. Dead letter deliveries are not retried automatically. To re-deliver:

  1. Fix the issue at your endpoint (verify URL reachability, check response codes).
  2. Use the delivery log to identify the delivery ID.
  3. 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.


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.

import hmac
import 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 handler
from flask import Flask, request, abort
app = Flask(__name__)
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 "", 200

Always use hmac.compare_digest for the final comparison to prevent timing-based attacks.

Terminal window
# Compute the expected digest from the raw body and secret
BODY='{"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.


Send a synthetic webhook_test event to verify that your endpoint is reachable and that your signature verification logic is working correctly.

POST /api/admin/webhooks/{webhook_id}/test
Terminal window
curl -s -X POST https://api.arbitex.ai/api/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.


Every delivery attempt is written to the webhook delivery log. The GET /api/admin/webhooks/{webhook_id} response includes a deliveries array containing the last 20 delivery records.

Delivery record fields:

FieldDescription
idUnique delivery log ID
webhook_idID of the webhook registration
event_typeThe event type that triggered this delivery
statusCurrent delivery status (see below)
attempt_countNumber of attempts made so far (1–3)
last_attempt_atTimestamp of the most recent attempt
next_retry_atScheduled time for the next retry, or null if terminal
response_codeHTTP status code from the last attempt, or null on connection failure
error_messageError detail from the last failed attempt, or null on success
payloadThe JSON body sent (truncated to 10,000 characters)

Status values:

StatusDescription
pendingDelivery is in progress (first attempt)
deliveredAt least one attempt received a 2xx response
failedAn attempt failed; a retry is scheduled (next_retry_at is set)
dead_letterAll three attempts failed; no further retries will occur

Debugging failed deliveries:

If you see failed or dead_letter status:

  1. Check response_code — a 4xx response from your endpoint indicates a configuration issue (wrong URL, authentication failure).
  2. Check error_message"Request timed out" means your endpoint did not respond within 10 seconds.
  3. Verify the webhook URL is publicly reachable from Arbitex infrastructure.
  4. Verify your endpoint returns a 2xx status code synchronously (do not defer to a background job without responding first).
  5. 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:

Terminal window
POST /api/admin/alert-rules
{
"name": "webhook-dead-letter-alert",
"condition": {
"metric": "webhook_dead_letter_count",
"threshold": 5,
"window_minutes": 60
},
"action": {
"type": "email",
"recipients": ["ops@example.com"]
}
}

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"
}