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
| Header | Required | Description |
|---|---|---|
Authorization | Yes | Bearer token with admin role |
Content-Type | Yes | Must 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
| Parameter | Type | Required | Description |
|---|---|---|---|
name | string | Yes | Webhook name |
description | string | No | Human-readable description |
target_url | string | Yes | HTTPS URL to receive webhooks |
event_types | array | Yes | Events to subscribe to |
event_filters | object | No | Filter events by attributes |
custom_headers | object | No | Custom headers to include |
retry_config | object | No | Retry configuration |
rate_limit_per_minute | integer | No | Max 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 Type | Description | Category |
|---|---|---|
action.submitted | Agent action submitted for evaluation | Actions |
action.approved | Action approved (auto or manual) | Actions |
action.denied | Action denied by policy or reviewer | Actions |
action.pending_approval | Action requires human approval | Actions |
alert.created | Security alert created | Alerts |
alert.acknowledged | Alert acknowledged by user | Alerts |
alert.resolved | Alert marked as resolved | Alerts |
agent.registered | New agent registered | Agents |
agent.activated | Agent activated | Agents |
agent.suspended | Agent suspended | Agents |
agent.blocked | Agent blocked via kill-switch | Agents |
policy.created | New policy created | Policies |
policy.violated | Policy violation detected | Policies |
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
| Field | Type | Description |
|---|---|---|
id | string | Unique event ID |
type | string | Event type |
timestamp | string | ISO 8601 timestamp |
organization_id | integer | Your organization ID |
data | object | Event-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:
| Parameter | Type | Default | Description |
|---|---|---|---|
status | string | (none) | Filter by status: success, failed, pending |
limit | integer | 50 | Max results (1-100) |
offset | integer | 0 | Pagination 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:
| Parameter | Type | Default | Description |
|---|---|---|---|
resolved | boolean | false | Include resolved entries |
limit | integer | 50 | Max 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
- Always verify signatures - Never process webhooks without signature verification
- Use HTTPS - Webhook URLs must use HTTPS
- Rotate secrets regularly - Rotate webhook secrets every 90 days
- Validate event types - Only process expected event types
Reliability
- Respond quickly - Return 200 within 30 seconds
- Process asynchronously - Queue events for background processing
- Implement idempotency - Handle duplicate deliveries gracefully
- 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)
Related
- Integrations Overview - Webhook integrations