Skip to main content

Webhooks

Configure webhooks to receive real-time notifications when events occur in the Ascend platform. Webhooks enable you to build integrations, automate workflows, and respond to events as they happen.

Overview

Webhooks are HTTP POST requests sent to your server when specific events occur:

  • Agent actions submitted, approved, or denied
  • Security alerts triggered
  • Policy violations detected
  • Agent status changes

Create Webhook

Endpoint

POST /api/webhooks

Authentication

JWT Token required - Requires admin role.

Request

Headers

HeaderRequiredDescription
AuthorizationYesBearer token with admin role
Content-TypeYesMust be application/json

Body

{
"name": "Production Alert Webhook",
"description": "Receive notifications for high-risk actions",
"target_url": "https://your-server.com/webhooks/ascend",
"event_types": [
"action.submitted",
"action.approved",
"action.denied",
"alert.created"
],
"event_filters": {
"risk_level": ["high", "critical"],
"agent_id": ["production-agent-*"]
},
"custom_headers": {
"X-Custom-Header": "custom-value"
},
"retry_config": {
"max_retries": 3,
"retry_delay_seconds": 60
},
"rate_limit_per_minute": 100
}

Parameters

ParameterTypeRequiredDescription
namestringYesWebhook name
descriptionstringNoHuman-readable description
target_urlstringYesHTTPS URL to receive webhooks
event_typesarrayYesEvents to subscribe to
event_filtersobjectNoFilter events by attributes
custom_headersobjectNoCustom headers to include
retry_configobjectNoRetry configuration
rate_limit_per_minuteintegerNoMax events per minute (default: 100, max: 1000)

Response

Success (201 Created)

{
"id": 42,
"subscription_id": "a1b2c3d4-e5f6-7890-abcd-ef1234567890",
"name": "Production Alert Webhook",
"description": "Receive notifications for high-risk actions",
"target_url": "https://your-server.com/webhooks/ascend",
"event_types": ["action.submitted", "action.approved", "action.denied", "alert.created"],
"event_filters": {"risk_level": ["high", "critical"]},
"is_active": true,
"is_verified": false,
"rate_limit_per_minute": 100,
"created_at": "2026-01-20T14:30:00Z",
"updated_at": "2026-01-20T14:30:00Z",
"secret_key": "whsec_aBcDeFgHiJkLmNoPqRsTuVwXyZ123456789"
}

Important: The secret_key is only returned once at creation. Store it securely.


Event Types

Available Events

Event TypeDescriptionCategory
action.submittedAgent action submitted for evaluationActions
action.approvedAction approved (auto or manual)Actions
action.deniedAction denied by policy or reviewerActions
action.pending_approvalAction requires human approvalActions
alert.createdSecurity alert createdAlerts
alert.acknowledgedAlert acknowledged by userAlerts
alert.resolvedAlert marked as resolvedAlerts
agent.registeredNew agent registeredAgents
agent.activatedAgent activatedAgents
agent.suspendedAgent suspendedAgents
agent.blockedAgent blocked via kill-switchAgents
policy.createdNew policy createdPolicies
policy.violatedPolicy violation detectedPolicies

List Available Events

GET /api/webhooks/events

Response:

{
"events": [
{
"type": "action.submitted",
"description": "Fired when an agent action is submitted for evaluation",
"payload_schema": "ActionSubmittedPayload",
"category": "Actions"
}
],
"total": 12
}

Webhook Payload

All webhook deliveries follow this format:

{
"id": "evt_1234567890",
"type": "action.approved",
"timestamp": "2026-01-20T14:30:52Z",
"organization_id": 1,
"data": {
"action_id": 12345,
"agent_id": "my-production-agent",
"action_type": "database_query",
"risk_score": 35,
"risk_level": "low",
"status": "approved",
"approved_by": "admin@company.com",
"approved_at": "2026-01-20T14:30:52Z"
}
}

Payload Fields

FieldTypeDescription
idstringUnique event ID
typestringEvent type
timestampstringISO 8601 timestamp
organization_idintegerYour organization ID
dataobjectEvent-specific data

Signature Verification

All webhooks are signed with HMAC-SHA256 for security.

Headers

POST /your-webhook-endpoint HTTP/1.1
Content-Type: application/json
X-Webhook-Signature: sha256=a1b2c3d4e5f6...
X-Webhook-Timestamp: 1705762252
X-Webhook-Event: action.approved
X-Webhook-Delivery-Id: del_123456

Verification Code

Python:

import hmac
import hashlib

def verify_webhook_signature(payload: bytes, signature: str, secret: str) -> bool:
"""Verify webhook signature using HMAC-SHA256."""
expected = hmac.new(
secret.encode('utf-8'),
payload,
hashlib.sha256
).hexdigest()

# Remove 'sha256=' prefix if present
received = signature.replace('sha256=', '')

return hmac.compare_digest(expected, received)

# Usage in Flask
@app.route('/webhooks/ascend', methods=['POST'])
def handle_webhook():
signature = request.headers.get('X-Webhook-Signature')
payload = request.get_data()

if not verify_webhook_signature(payload, signature, WEBHOOK_SECRET):
return 'Invalid signature', 401

event = request.json
# Process event...
return 'OK', 200

Node.js:

const crypto = require('crypto');

function verifyWebhookSignature(payload, signature, secret) {
const expected = crypto
.createHmac('sha256', secret)
.update(payload)
.digest('hex');

const received = signature.replace('sha256=', '');

return crypto.timingSafeEqual(
Buffer.from(expected),
Buffer.from(received)
);
}

// Usage in Express
app.post('/webhooks/ascend', express.raw({type: 'application/json'}), (req, res) => {
const signature = req.headers['x-webhook-signature'];

if (!verifyWebhookSignature(req.body, signature, WEBHOOK_SECRET)) {
return res.status(401).send('Invalid signature');
}

const event = JSON.parse(req.body);
// Process event...
res.send('OK');
});

Test Webhook

Send a test event to verify your configuration.

POST /api/webhooks/{subscription_id}/test

Request Body (optional):

{
"event_type": "action.submitted",
"custom_payload": {
"test": true,
"message": "This is a test webhook"
}
}

Response:

{
"success": true,
"delivery_id": 456,
"response_status": 200,
"response_time_ms": 150,
"error_message": null,
"signature_header": "sha256=a1b2c3d4..."
}

Rotate Secret

Rotate the webhook signing secret for security.

POST /api/webhooks/{subscription_id}/rotate-secret

Response:

{
"subscription_id": 42,
"new_secret": "whsec_NewSecretKey123456789",
"message": "Secret rotated successfully. Update your webhook receiver with the new secret."
}

Important: Update your webhook receiver immediately after rotating the secret.


Delivery History

View webhook delivery history and troubleshoot failures.

GET /api/webhooks/{subscription_id}/deliveries

Query Parameters:

ParameterTypeDefaultDescription
statusstring(none)Filter by status: success, failed, pending
limitinteger50Max results (1-100)
offsetinteger0Pagination offset

Response:

[
{
"id": 789,
"event_id": "evt_1234567890",
"event_type": "action.approved",
"delivery_status": "success",
"attempt_count": 1,
"response_status": 200,
"response_time_ms": 145,
"error_message": null,
"created_at": "2026-01-20T14:30:52Z",
"delivered_at": "2026-01-20T14:30:52Z"
}
]

Dead Letter Queue

Failed webhooks after all retries go to the dead letter queue (DLQ).

GET /api/webhooks/dlq/entries

Query Parameters:

ParameterTypeDefaultDescription
resolvedbooleanfalseInclude resolved entries
limitinteger50Max results

Response:

[
{
"id": 101,
"event_id": "evt_9876543210",
"event_type": "alert.created",
"failure_reason": "Connection timeout after 30s",
"total_attempts": 3,
"last_attempt_at": "2026-01-20T14:35:00Z",
"resolved": false,
"created_at": "2026-01-20T14:30:00Z"
}
]

Examples

cURL - Create Webhook

curl -X POST https://pilot.owkai.app/api/webhooks \
-H "Authorization: Bearer eyJhbGciOiJSUzI1NiI..." \
-H "Content-Type: application/json" \
-d '{
"name": "Production Alert Webhook",
"target_url": "https://your-server.com/webhooks/ascend",
"event_types": ["action.submitted", "action.approved", "action.denied"],
"rate_limit_per_minute": 100
}'

cURL - List Webhooks

curl -X GET https://pilot.owkai.app/api/webhooks \
-H "Authorization: Bearer eyJhbGciOiJSUzI1NiI..."

Python

from ascend import AscendClient

client = AscendClient(access_token="eyJhbGciOiJSUzI1NiI...")

# Create webhook
webhook = client.webhooks.create(
name="Production Alert Webhook",
target_url="https://your-server.com/webhooks/ascend",
event_types=["action.submitted", "action.approved", "action.denied"],
event_filters={"risk_level": ["high", "critical"]}
)

# Store the secret securely!
print(f"Webhook Secret: {webhook.secret_key}")

# List webhooks
webhooks = client.webhooks.list()
for wh in webhooks:
print(f"- {wh.name}: {wh.target_url}")

# Test webhook
result = client.webhooks.test(webhook.id)
print(f"Test result: {'Success' if result.success else 'Failed'}")

Node.js

import { AscendClient } from '@anthropic/ascend-sdk';

const client = new AscendClient({ accessToken: 'eyJhbGciOiJSUzI1NiI...' });

// Create webhook
const webhook = await client.webhooks.create({
name: 'Production Alert Webhook',
targetUrl: 'https://your-server.com/webhooks/ascend',
eventTypes: ['action.submitted', 'action.approved', 'action.denied'],
eventFilters: { riskLevel: ['high', 'critical'] }
});

// Store the secret securely!
console.log(`Webhook Secret: ${webhook.secretKey}`);

// List webhooks
const webhooks = await client.webhooks.list();
webhooks.forEach(wh => {
console.log(`- ${wh.name}: ${wh.targetUrl}`);
});

Best Practices

Security

  1. Always verify signatures - Never process webhooks without signature verification
  2. Use HTTPS - Webhook URLs must use HTTPS
  3. Rotate secrets regularly - Rotate webhook secrets every 90 days
  4. Validate event types - Only process expected event types

Reliability

  1. Respond quickly - Return 200 within 30 seconds
  2. Process asynchronously - Queue events for background processing
  3. Implement idempotency - Handle duplicate deliveries gracefully
  4. Monitor failures - Check DLQ regularly for failed deliveries

Example Handler

from flask import Flask, request
import hashlib
import hmac
import json
from queue import Queue

app = Flask(__name__)
event_queue = Queue()

@app.route('/webhooks/ascend', methods=['POST'])
def handle_webhook():
# 1. Verify signature
signature = request.headers.get('X-Webhook-Signature')
if not verify_signature(request.get_data(), signature):
return 'Invalid signature', 401

# 2. Parse event
event = request.json

# 3. Check for duplicate (idempotency)
if is_duplicate(event['id']):
return 'OK', 200

# 4. Queue for async processing
event_queue.put(event)

# 5. Return quickly
return 'OK', 200

def process_events():
"""Background worker to process events."""
while True:
event = event_queue.get()
try:
if event['type'] == 'action.approved':
handle_action_approved(event['data'])
elif event['type'] == 'alert.created':
handle_alert_created(event['data'])
except Exception as e:
log_error(event, e)