Skip to main content

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

EventDescriptionTrigger
action.approvedAction was approvedManual approval or auto-approval
action.deniedAction was deniedPolicy violation or manual denial
action.pendingAction requires approvalHigh-risk action submitted
policy.violationPolicy was violatedRisk threshold exceeded
agent.trust_changedAgent trust level changedBehavioral analysis update
approval.timeoutApproval request expiredTTL exceeded

Quick Start

1. Configure Webhook (Console)

  1. Navigate to Settings > Webhooks in the ASCEND Console
  2. Click Add Webhook
  3. Enter your HTTPS endpoint URL
  4. Select events to subscribe to
  5. 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

HeaderDescription
X-SignatureHMAC-SHA256 signature
X-TimestampUnix timestamp of request
X-Webhook-IdWebhook 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:

AttemptDelay
1Immediate
21 minute
35 minutes
430 minutes
52 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:

  1. Navigate to Settings > Webhooks
  2. Click on a webhook
  3. View Delivery History

Each delivery shows:

  • Timestamp
  • Event type
  • Response status
  • Response time
  • Retry count

Troubleshooting

Not Receiving Webhooks

  1. Check URL is HTTPS - Webhooks require HTTPS endpoints
  2. Check firewall rules - Allow inbound from ASCEND IPs
  3. Verify webhook is enabled - Check Console settings
  4. Check event subscriptions - Ensure correct events selected

Signature Verification Failing

  1. Check secret - Use exact secret from Console
  2. Check timestamp - Use X-Timestamp header value
  3. Check payload - Use raw body, not parsed JSON
  4. Check encoding - Ensure UTF-8 encoding

High Latency

  1. Process asynchronously - Queue webhook for background processing
  2. Optimize handlers - Reduce processing time
  3. 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:

  1. Generate new secret in Console
  2. Update your server to accept both old and new signatures
  3. After verification, remove old secret handling

Next Steps

Support