Webhooks Overview
Receive real-time notifications when actions are approved, denied, or require attention using ASCEND webhooks.
What are Webhooks?
Webhooks are HTTP callbacks that ASCEND sends to your server when specific events occur. Instead of polling the API for updates, your application receives instant notifications.
Supported Events
| Event | Description | Trigger |
|---|---|---|
action.approved | Action was approved | Manual approval or auto-approval |
action.denied | Action was denied | Policy violation or manual denial |
action.pending | Action requires approval | High-risk action submitted |
policy.violation | Policy was violated | Risk threshold exceeded |
agent.trust_changed | Agent trust level changed | Behavioral analysis update |
approval.timeout | Approval request expired | TTL exceeded |
Quick Start
1. Configure Webhook (Console)
- Navigate to Settings > Webhooks in the ASCEND Console
- Click Add Webhook
- Enter your HTTPS endpoint URL
- Select events to subscribe to
- Copy the signing secret
2. Configure Webhook (SDK)
from ascend import AscendClient
client = AscendClient(
api_key="owkai_your_api_key",
agent_id="my-agent",
agent_name="My Agent"
)
webhook = client.configure_webhook(
url="https://your-app.com/webhooks/ascend",
events=["action.approved", "action.denied", "policy.violation"],
secret="your_signing_secret"
)
print(f"Webhook ID: {webhook['webhook_id']}")
3. Configure Webhook (API)
curl -X POST https://pilot.owkai.app/api/webhooks \
-H "Authorization: Bearer owkai_your_api_key" \
-H "Content-Type: application/json" \
-d '{
"url": "https://your-app.com/webhooks/ascend",
"events": ["action.approved", "action.denied"],
"secret": "your_signing_secret"
}'
Webhook Payload
Standard Format
All webhooks follow this format:
{
"id": "evt_abc123def456",
"event": "action.approved",
"created_at": "2024-01-15T10:30:00Z",
"data": {
// Event-specific data
}
}
Event Payloads
action.approved
{
"id": "evt_abc123",
"event": "action.approved",
"created_at": "2024-01-15T10:30:00Z",
"data": {
"action_id": "act_xyz789",
"agent_id": "my-ai-agent",
"agent_name": "My AI Agent",
"action_type": "database.query",
"resource": "production_db",
"risk_score": 35,
"risk_level": "low",
"approved_by": "user@example.com",
"approved_at": "2024-01-15T10:30:00Z",
"comments": "Approved for production use",
"auto_approved": false
}
}
action.denied
{
"id": "evt_abc123",
"event": "action.denied",
"created_at": "2024-01-15T10:30:00Z",
"data": {
"action_id": "act_xyz789",
"agent_id": "my-ai-agent",
"agent_name": "My AI Agent",
"action_type": "database.delete",
"resource": "production_db",
"risk_score": 85,
"risk_level": "critical",
"reason": "Critical risk action not allowed for this agent",
"policy_violations": [
"policy_max_risk_exceeded",
"policy_require_approval"
],
"denied_by": "system",
"denied_at": "2024-01-15T10:30:00Z"
}
}
action.pending
{
"id": "evt_abc123",
"event": "action.pending",
"created_at": "2024-01-15T10:30:00Z",
"data": {
"action_id": "act_xyz789",
"agent_id": "my-ai-agent",
"agent_name": "My AI Agent",
"action_type": "financial.refund",
"resource": "payment_api",
"risk_score": 72,
"risk_level": "high",
"approval_request_id": "apr_def456",
"required_approvers": ["manager@example.com", "security@example.com"],
"expires_at": "2024-01-15T11:30:00Z",
"approval_url": "https://pilot.owkai.app/approvals/apr_def456"
}
}
policy.violation
{
"id": "evt_abc123",
"event": "policy.violation",
"created_at": "2024-01-15T10:30:00Z",
"data": {
"agent_id": "my-ai-agent",
"agent_name": "My AI Agent",
"action_id": "act_xyz789",
"violations": [
{
"policy_id": "pol_abc123",
"policy_name": "Max Risk Threshold",
"description": "Risk score exceeded maximum allowed threshold",
"severity": "high"
},
{
"policy_id": "pol_def456",
"policy_name": "Require Human Approval",
"description": "Action type requires human approval",
"severity": "medium"
}
],
"action_type": "database.delete",
"resource": "production_db",
"risk_score": 92
}
}
agent.trust_changed
{
"id": "evt_abc123",
"event": "agent.trust_changed",
"created_at": "2024-01-15T10:30:00Z",
"data": {
"agent_id": "my-ai-agent",
"agent_name": "My AI Agent",
"old_trust_level": "standard",
"new_trust_level": "elevated",
"reason": "Consistent compliance over 30 days",
"effective_at": "2024-01-15T10:30:00Z",
"permissions_changed": {
"added": ["high_risk_actions"],
"removed": []
}
}
}
Signature Verification
All webhooks are signed with HMAC-SHA256 for security.
Headers
| Header | Description |
|---|---|
X-Signature | HMAC-SHA256 signature |
X-Timestamp | Unix timestamp of request |
X-Webhook-Id | Webhook configuration ID |
Verification Algorithm
import hmac
import hashlib
def verify_webhook(payload: bytes, timestamp: str, signature: str, secret: str) -> bool:
"""Verify webhook signature."""
# Construct signature base
signature_base = f"{timestamp}.{payload.decode('utf-8')}"
# Calculate expected signature
expected = hmac.new(
secret.encode('utf-8'),
signature_base.encode('utf-8'),
hashlib.sha256
).hexdigest()
# Compare (timing-safe)
return hmac.compare_digest(f"v1={expected}", signature)
Example Implementations
Python (Flask)
from flask import Flask, request, jsonify
import hmac
import hashlib
app = Flask(__name__)
WEBHOOK_SECRET = "whsec_your_secret_here"
@app.route("/webhooks/ascend", methods=["POST"])
def handle_webhook():
# Get headers
signature = request.headers.get("X-Signature", "")
timestamp = request.headers.get("X-Timestamp", "")
# Verify signature
if not verify_signature(request.data, timestamp, signature):
return jsonify({"error": "Invalid signature"}), 401
# Parse payload
payload = request.json
event = payload["event"]
data = payload["data"]
# Handle event
if event == "action.approved":
handle_approval(data)
elif event == "action.denied":
handle_denial(data)
elif event == "action.pending":
handle_pending(data)
return jsonify({"status": "received"}), 200
def verify_signature(payload: bytes, timestamp: str, signature: str) -> bool:
expected = hmac.new(
WEBHOOK_SECRET.encode(),
f"{timestamp}.{payload.decode()}".encode(),
hashlib.sha256
).hexdigest()
return hmac.compare_digest(f"v1={expected}", signature)
def handle_approval(data):
print(f"Action {data['action_id']} approved")
def handle_denial(data):
print(f"Action {data['action_id']} denied: {data['reason']}")
def handle_pending(data):
print(f"Action {data['action_id']} pending approval")
Node.js (Express)
import express from 'express';
import crypto from 'crypto';
const app = express();
const WEBHOOK_SECRET = 'whsec_your_secret_here';
app.use('/webhooks/ascend', express.raw({ type: 'application/json' }));
app.post('/webhooks/ascend', (req, res) => {
const signature = req.headers['x-signature'] as string;
const timestamp = req.headers['x-timestamp'] as string;
if (!verifySignature(req.body, timestamp, signature)) {
return res.status(401).json({ error: 'Invalid signature' });
}
const payload = JSON.parse(req.body.toString());
const { event, data } = payload;
switch (event) {
case 'action.approved':
handleApproval(data);
break;
case 'action.denied':
handleDenial(data);
break;
case 'action.pending':
handlePending(data);
break;
}
res.json({ status: 'received' });
});
function verifySignature(
payload: Buffer,
timestamp: string,
signature: string
): boolean {
const expected = crypto
.createHmac('sha256', WEBHOOK_SECRET)
.update(`${timestamp}.${payload.toString()}`)
.digest('hex');
return crypto.timingSafeEqual(
Buffer.from(`v1=${expected}`),
Buffer.from(signature)
);
}
Best Practices
1. Always Verify Signatures
Never process webhooks without verifying the signature:
if not verify_signature(payload, timestamp, signature):
return {"error": "Unauthorized"}, 401
2. Respond Quickly
Respond with 200 within 5 seconds. Process asynchronously if needed:
@app.route("/webhooks/ascend", methods=["POST"])
def webhook():
# Verify signature
verify_signature(...)
# Queue for async processing
queue.enqueue(process_webhook, request.json)
# Respond immediately
return {"status": "received"}, 200
3. Handle Retries
ASCEND retries failed webhooks with exponential backoff. Use idempotency keys:
def process_webhook(payload):
event_id = payload["id"]
# Check if already processed
if redis.exists(f"webhook:{event_id}"):
return # Already processed
# Process event
handle_event(payload)
# Mark as processed (expire after 24h)
redis.setex(f"webhook:{event_id}", 86400, "1")
4. Store Events
Store webhook events for debugging and audit:
def process_webhook(payload):
# Store event
db.webhooks.insert({
"event_id": payload["id"],
"event_type": payload["event"],
"data": payload["data"],
"received_at": datetime.utcnow()
})
# Process event
handle_event(payload)
5. Monitor Webhook Health
Track webhook delivery success:
def process_webhook(payload):
start = time.time()
try:
handle_event(payload)
metrics.increment("webhook.success", tags={"event": payload["event"]})
except Exception as e:
metrics.increment("webhook.error", tags={"event": payload["event"]})
raise
finally:
metrics.timing("webhook.duration", time.time() - start)
Retry Policy
ASCEND retries failed webhooks with exponential backoff:
| Attempt | Delay |
|---|---|
| 1 | Immediate |
| 2 | 1 minute |
| 3 | 5 minutes |
| 4 | 30 minutes |
| 5 | 2 hours |
After 5 failed attempts, the webhook is marked as failed and an alert is sent.
Retry Conditions
- HTTP status >= 500
- Connection timeout
- No response within 5 seconds
Non-Retryable
- HTTP status 4xx (except 429)
- Invalid response format
Managing Webhooks
List Webhooks
curl https://pilot.owkai.app/api/webhooks \
-H "Authorization: Bearer owkai_your_api_key"
Update Webhook
curl -X PUT https://pilot.owkai.app/api/webhooks/whk_abc123 \
-H "Authorization: Bearer owkai_your_api_key" \
-H "Content-Type: application/json" \
-d '{
"events": ["action.approved", "action.denied", "policy.violation"],
"enabled": true
}'
Delete Webhook
curl -X DELETE https://pilot.owkai.app/api/webhooks/whk_abc123 \
-H "Authorization: Bearer owkai_your_api_key"
Test Webhook
curl -X POST https://pilot.owkai.app/api/webhooks/whk_abc123/test \
-H "Authorization: Bearer owkai_your_api_key"
Webhook Logs
View webhook delivery history in the Console:
- Navigate to Settings > Webhooks
- Click on a webhook
- View Delivery History
Each delivery shows:
- Timestamp
- Event type
- Response status
- Response time
- Retry count
Troubleshooting
Not Receiving Webhooks
- Check URL is HTTPS - Webhooks require HTTPS endpoints
- Check firewall rules - Allow inbound from ASCEND IPs
- Verify webhook is enabled - Check Console settings
- Check event subscriptions - Ensure correct events selected
Signature Verification Failing
- Check secret - Use exact secret from Console
- Check timestamp - Use X-Timestamp header value
- Check payload - Use raw body, not parsed JSON
- Check encoding - Ensure UTF-8 encoding
High Latency
- Process asynchronously - Queue webhook for background processing
- Optimize handlers - Reduce processing time
- Use connection pooling - For database operations
Security
IP Allowlist
ASCEND webhooks originate from these IP ranges:
52.70.0.0/16
54.152.0.0/16
Configure firewall rules accordingly.
Secret Rotation
Rotate webhook secrets periodically:
- Generate new secret in Console
- Update your server to accept both old and new signatures
- After verification, remove old secret handling
Next Steps
- Python SDK - SDK webhook configuration
- Node.js SDK - TypeScript integration
- API Reference - Webhook API documentation
Support
- Documentation: https://docs.owkai.app
- Support: support@owkai.app
- GitHub Issues: https://github.com/owkai/ascend-platform/issues