Webhooks API
The Webhooks API manages outbound HTTP integrations that deliver signed event notifications to external systems. All endpoints require admin authentication (Authorization: Bearer $ARBITEX_API_KEY).
Base URL: https://gateway.arbitex.ai
Endpoints summary
Section titled “Endpoints summary”| Method | Path | Description |
|---|---|---|
GET | /api/admin/webhooks/ | List all webhooks for the org |
POST | /api/admin/webhooks/ | Create a webhook |
GET | /api/admin/webhooks/{webhook_id} | Get webhook with last 20 delivery records |
PUT | /api/admin/webhooks/{webhook_id} | Update webhook (partial) |
DELETE | /api/admin/webhooks/{webhook_id} | Delete webhook |
POST | /api/admin/webhooks/{webhook_id}/test | Send a test delivery |
Webhook object
Section titled “Webhook object”| Field | Type | Description |
|---|---|---|
id | UUID | Webhook UUID |
name | string | Human-readable label |
url | string | HTTPS destination URL |
events | list[WebhookEventType] | Event types this webhook subscribes to |
secret | string | HMAC signing secret — write-only after create; omitted from all GET responses |
enabled | bool | Whether the webhook is active |
created_at | datetime | ISO 8601 creation timestamp |
updated_at | datetime | ISO 8601 last-modified timestamp |
WebhookEventType values
Section titled “WebhookEventType values”| Value | When it fires |
|---|---|
new_conversation | A new conversation is created on the gateway |
dlp_trigger | A DLP rule matches content in a conversation |
quota_exceeded | A user or group reaches or exceeds their usage quota |
bundle_state_change | A compliance bundle transitions to a new state |
List webhooks
Section titled “List webhooks”GET /api/admin/webhooks/Returns all webhook registrations for the tenant.
Request
curl -s https://gateway.arbitex.ai/api/admin/webhooks/ \ -H "Authorization: Bearer $ARBITEX_API_KEY"Response 200 OK
[ { "id": "3fa85f64-5717-4562-b3fc-2c963f66afa6", "name": "SIEM DLP pipeline", "url": "https://ingest.example.com/arbitex-events", "events": ["dlp_trigger", "quota_exceeded"], "enabled": true, "created_at": "2026-02-01T09:00:00Z", "updated_at": "2026-03-10T14:22:00Z" }, { "id": "9c0e1d2f-3b4a-5c6d-7e8f-9a0b1c2d3e4f", "name": "Compliance change notifier", "url": "https://compliance.internal.example.com/hooks", "events": ["bundle_state_change", "new_conversation"], "enabled": false, "created_at": "2026-01-15T11:30:00Z", "updated_at": "2026-01-15T11:30:00Z" }]Get webhook
Section titled “Get webhook”GET /api/admin/webhooks/{webhook_id}Returns the webhook registration plus the last 20 delivery records in reverse-chronological order.
Path parameters
| Parameter | Type | Description |
|---|---|---|
webhook_id | UUID | The webhook UUID |
Request
curl -s https://gateway.arbitex.ai/api/admin/webhooks/3fa85f64-5717-4562-b3fc-2c963f66afa6 \ -H "Authorization: Bearer $ARBITEX_API_KEY"Response 200 OK — WebhookDetailResponse
{ "id": "3fa85f64-5717-4562-b3fc-2c963f66afa6", "name": "SIEM DLP pipeline", "url": "https://ingest.example.com/arbitex-events", "events": ["dlp_trigger", "quota_exceeded"], "enabled": true, "created_at": "2026-02-01T09:00:00Z", "updated_at": "2026-03-10T14:22:00Z", "recent_deliveries": [ { "id": "del-a1b2c3d4-...", "webhook_id": "3fa85f64-5717-4562-b3fc-2c963f66afa6", "event_type": "dlp_trigger", "payload": { "event_type": "dlp_trigger", "payload": { "request_id": "req_01abc123", "user_id": "usr_01def456", "rule_id": "credit-card-block", "entity_type": "credit_card", "action_taken": "BLOCK", "timestamp": "2026-03-12T10:15:00Z" }, "timestamp": "2026-03-12T10:15:00.482Z" }, "response_status": 200, "response_body": "OK", "success": true, "attempt_count": 1, "delivered_at": "2026-03-12T10:15:01.103Z", "created_at": "2026-03-12T10:15:00.482Z" }, { "id": "del-b2c3d4e5-...", "webhook_id": "3fa85f64-5717-4562-b3fc-2c963f66afa6", "event_type": "quota_exceeded", "payload": { "event_type": "quota_exceeded", "payload": { "..." }, "timestamp": "2026-03-11T23:59:58Z" }, "response_status": null, "response_body": null, "success": false, "attempt_count": 3, "delivered_at": null, "created_at": "2026-03-11T23:59:58Z" } ]}Create webhook
Section titled “Create webhook”POST /api/admin/webhooks/Creates a new webhook registration. The secret field is returned only in the 201 response — it is never returned by subsequent GET requests. Store it securely.
Request body
| Field | Type | Required | Description |
|---|---|---|---|
name | string | yes | Human-readable label |
url | string | yes | HTTPS destination URL |
events | list[string] | yes | One or more WebhookEventType values |
secret | string | yes | HMAC signing secret, minimum 8 characters |
enabled | bool | no | Default true |
Request
curl -s -X POST https://gateway.arbitex.ai/api/admin/webhooks/ \ -H "Authorization: Bearer $ARBITEX_API_KEY" \ -H "Content-Type: application/json" \ -d '{ "name": "PagerDuty DLP alerts", "url": "https://events.pagerduty.com/v2/enqueue", "events": ["dlp_trigger", "quota_exceeded"], "secret": "s3cr3t-hmac-key-min8", "enabled": true }'Response 201 Created
{ "id": "7b8c9d0e-1f2a-3b4c-5d6e-7f8a9b0c1d2e", "name": "PagerDuty DLP alerts", "url": "https://events.pagerduty.com/v2/enqueue", "events": ["dlp_trigger", "quota_exceeded"], "secret": "s3cr3t-hmac-key-min8", "enabled": true, "created_at": "2026-03-12T15:00:00Z", "updated_at": "2026-03-12T15:00:00Z"}Update webhook
Section titled “Update webhook”PUT /api/admin/webhooks/{webhook_id}Partial update — only the fields included in the request body are modified. Omit secret to leave the signing key unchanged.
Path parameters
| Parameter | Type | Description |
|---|---|---|
webhook_id | UUID | The webhook UUID |
Request — disable a webhook
curl -s -X PUT \ https://gateway.arbitex.ai/api/admin/webhooks/7b8c9d0e-1f2a-3b4c-5d6e-7f8a9b0c1d2e \ -H "Authorization: Bearer $ARBITEX_API_KEY" \ -H "Content-Type: application/json" \ -d '{"enabled": false}'Request — rotate the signing secret
curl -s -X PUT \ https://gateway.arbitex.ai/api/admin/webhooks/7b8c9d0e-1f2a-3b4c-5d6e-7f8a9b0c1d2e \ -H "Authorization: Bearer $ARBITEX_API_KEY" \ -H "Content-Type: application/json" \ -d '{"secret": "new-signing-secret-rotated"}'Request — add an event type
curl -s -X PUT \ https://gateway.arbitex.ai/api/admin/webhooks/7b8c9d0e-1f2a-3b4c-5d6e-7f8a9b0c1d2e \ -H "Authorization: Bearer $ARBITEX_API_KEY" \ -H "Content-Type: application/json" \ -d '{"events": ["dlp_trigger", "quota_exceeded", "bundle_state_change"]}'Response 200 OK — the updated webhook object (without secret).
Response 404 Not Found — webhook ID does not exist within the tenant.
Delete webhook
Section titled “Delete webhook”DELETE /api/admin/webhooks/{webhook_id}Permanently removes the webhook registration and stops all future deliveries.
Request
curl -s -X DELETE \ https://gateway.arbitex.ai/api/admin/webhooks/7b8c9d0e-1f2a-3b4c-5d6e-7f8a9b0c1d2e \ -H "Authorization: Bearer $ARBITEX_API_KEY"Response 204 No Content
Send test delivery
Section titled “Send test delivery”POST /api/admin/webhooks/{webhook_id}/testSends a synthetic webhook_test event to the webhook URL immediately. Use this after initial registration, after rotating the signing secret, or after infrastructure changes that may affect reachability.
Request
curl -s -X POST \ https://gateway.arbitex.ai/api/admin/webhooks/7b8c9d0e-1f2a-3b4c-5d6e-7f8a9b0c1d2e/test \ -H "Authorization: Bearer $ARBITEX_API_KEY"Response 200 OK
{ "success": true, "status_code": 200, "latency_ms": 84, "error": null}On failure:
{ "success": false, "status_code": null, "latency_ms": 10003, "error": "Connection timed out after 10000ms"}| Field | Type | Description |
|---|---|---|
success | bool | Whether the endpoint returned a 2xx response |
status_code | int | null | HTTP status code from the endpoint, or null on connection failure |
latency_ms | int | null | Round-trip time in milliseconds, or null on connection failure |
error | string | null | Human-readable error description, or null on success |
Delivery record schema
Section titled “Delivery record schema”Each entry in recent_deliveries has the following fields:
| Field | Type | Description |
|---|---|---|
id | UUID | Delivery attempt UUID |
webhook_id | UUID | Parent webhook UUID |
event_type | string | The WebhookEventType that triggered the delivery |
payload | object | Full JSON payload sent to the endpoint |
response_status | int | null | HTTP status code returned by the endpoint, or null on connection failure |
response_body | string | null | First 1,000 characters of the endpoint response body |
success | bool | true if any attempt received a 2xx response |
attempt_count | int | Number of delivery attempts made (1–3) |
delivered_at | datetime | null | Timestamp of first successful delivery, or null if not yet delivered |
created_at | datetime | Timestamp of the initial delivery attempt |
Webhook payload format
Section titled “Webhook payload format”The gateway POSTs JSON to the webhook URL. Each delivery includes three request headers:
| Header | Description |
|---|---|
X-Arbitex-Webhook-Id | The UUID of the webhook registration |
X-Arbitex-Delivery-Id | The UUID of this specific delivery attempt |
X-Arbitex-Signature | sha256=<HMAC-SHA256 hex digest of the raw request body> |
Payload envelope
Section titled “Payload envelope”All events share the same outer envelope:
{ "event_type": "dlp_trigger", "payload": { "request_id": "req_01abc123", "user_id": "usr_01def456", "rule_id": "ssn-redact", "entity_type": "ssn", "action_taken": "REDACT", "timestamp": "2026-03-12T10:15:00Z" }, "timestamp": "2026-03-12T10:15:00.482Z"}The outer timestamp is the delivery time. payload.timestamp (when present) is the event time. They may differ slightly due to processing latency.
new_conversation example:
{ "event_type": "new_conversation", "payload": { "conversation_id": "conv_01abc123", "user_id": "usr_01def456", "provider": "anthropic", "model": "claude-sonnet-4-20250514", "created_at": "2026-03-12T10:00:00Z" }, "timestamp": "2026-03-12T10:00:00.198Z"}quota_exceeded example:
{ "event_type": "quota_exceeded", "payload": { "user_id": "usr_01def456", "group_id": null, "limit_type": "monthly_token_limit", "limit_value": 2000000, "current_usage": 2001450, "reset_at": "2026-04-01T00:00:00Z" }, "timestamp": "2026-03-12T11:42:07.321Z"}bundle_state_change example:
{ "event_type": "bundle_state_change", "payload": { "bundle_id": "bundle-uuid-...", "bundle_name": "PCI-DSS Baseline", "previous_state": "active", "new_state": "archived", "changed_by": "admin-uuid-...", "changed_at": "2026-03-12T09:00:00Z" }, "timestamp": "2026-03-12T09:00:00.544Z"}Signature verification
Section titled “Signature verification”The X-Arbitex-Signature header value is sha256= followed by the hex-encoded HMAC-SHA256 of the raw request body bytes using the webhook’s secret.
Always verify the signature on raw body bytes before parsing the JSON. Parsing first can allow a forged payload to pass verification if your JSON library normalizes the input.
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. """ prefix = "sha256=" if not signature_header.startswith(prefix): return False
received_digest = signature_header[len(prefix):] expected_digest = hmac.new( key=secret.encode("utf-8"), msg=raw_body, digestmod=hashlib.sha256, ).hexdigest()
# Use compare_digest to prevent timing-based attacks. return hmac.compare_digest(expected_digest, received_digest)
# Example: Flask handlerfrom flask import Flask, request, abort
app = Flask(__name__)WEBHOOK_SECRET = "s3cr3t-hmac-key-min8"
@app.route("/arbitex-events", methods=["POST"])def handle_webhook(): sig = request.headers.get("X-Arbitex-Signature", "") webhook_id = request.headers.get("X-Arbitex-Webhook-Id", "") delivery_id = request.headers.get("X-Arbitex-Delivery-Id", "")
if not verify_arbitex_signature(request.get_data(), WEBHOOK_SECRET, sig): abort(403)
event = request.get_json() print(f"[{delivery_id}] webhook={webhook_id} event={event['event_type']}") return "", 200Use hmac.compare_digest rather than == for the final comparison — constant-time comparison prevents timing-based attacks that could leak the expected digest.
Error codes
Section titled “Error codes”| Status | Condition |
|---|---|
400 Bad Request | Invalid request body, unsupported event_type, or secret shorter than 8 characters |
403 Forbidden | Caller is not an admin |
404 Not Found | Webhook ID does not exist within the tenant |
See also
Section titled “See also”- Webhooks admin guide — retry policy, dead letter queue, delivery monitoring, and operational tips
- Alert configuration — threshold-based rules that can deliver to a webhook URL
- DLP rules API — the rules that produce
dlp_triggerevents - Quotas API — quota configuration behind
quota_exceededevents - Compliance bundles — bundles that produce
bundle_state_changeevents