Webhooks
Webhooks allow you to receive real-time notifications about events in your VoiceRun agents. When an event occurs, VoiceRun sends an HTTP POST request to your configured endpoint with event details.
Overview#
Webhooks provide a push-based mechanism for receiving event notifications, eliminating the need to poll APIs for updates.
Use cases:
- Log session completions to your database
- Trigger automated workflows based on call outcomes
- Send notifications to team members
- Update external CRM or analytics systems
- Monitor agent performance in real-time
Setup & Configuration#
Via Dashboard#
- Go to Agents → [Your Agent] → Environments
- Click on the environment you want to configure
- Open the Webhook Settings dialog
- Enter your webhook URL (must be HTTPS)
- Click Save - a webhook secret will be automatically generated
- Copy and save the secret - it's only shown once!
Via API#
Set or update your webhook URL using the environment update endpoint:
import requests response = requests.patch( f"https://api.voicerun.com/v1/agents/{agent_id}/environments/{environment_id}", headers={ "Authorization": "Bearer YOUR_API_KEY", "Content-Type": "application/json", }, json={"webhookUrl": "https://your-api.com/webhooks/voicerun"} ) data = response.json() # Save the webhookSecret from the response! print(f"Webhook secret: {data['data']['webhookSecret']}")
Regenerate Webhook Secret#
If your webhook secret is compromised:
curl -X POST https://api.voicerun.com/v1/agents/{agentId}/environments/{environmentId}/regenerate-webhook-secret \ -H "Authorization: Bearer YOUR_API_KEY"
Remove Webhook#
To disable webhooks for an environment:
curl -X PATCH https://api.voicerun.com/v1/agents/{agentId}/environments/{environmentId} \ -H "Authorization: Bearer YOUR_API_KEY" \ -H "Content-Type: application/json" \ -d '{"webhookUrl": null, "webhookSecret": null}'
Requirements#
- HTTPS only: Webhook URLs must use HTTPS for security
- Publicly accessible: Your endpoint must be reachable from the internet
- Return 2xx status: Respond with 200-299 status code to acknowledge receipt
- Quick response: Process webhooks asynchronously and respond within 10 seconds
Security & Verification#
VoiceRun signs all webhook requests with HMAC-SHA256 to ensure authenticity and prevent tampering.
Signature Headers#
| Header | Description |
|---|---|
X-VoiceRun-Signature | HMAC-SHA256 signature in format: sha256=<hex> |
X-VoiceRun-Timestamp | Unix timestamp (seconds) when webhook was sent |
Signature Verification#
The signature is computed as: HMAC-SHA256(timestamp + "." + raw_body, secret)
Python example:
import hmac import hashlib import time from flask import Flask, request, jsonify app = Flask(__name__) WEBHOOK_SECRET = "your-webhook-secret" # From VoiceRun dashboard def verify_webhook_signature(payload_body, signature_header, timestamp_header): # Step 1: Verify timestamp is recent (within 5 minutes) try: timestamp = int(timestamp_header) current_time = int(time.time()) if abs(current_time - timestamp) > 300: return False except ValueError: return False # Step 2: Compute expected signature signed_payload = f"{timestamp_header}.{payload_body}" expected_signature = hmac.new( WEBHOOK_SECRET.encode('utf-8'), signed_payload.encode('utf-8'), hashlib.sha256 ).hexdigest() expected_signature = f"sha256={expected_signature}" # Step 3: Compare signatures (timing-safe comparison) return hmac.compare_digest(expected_signature, signature_header) @app.route('/webhooks/voicerun', methods=['POST']) def handle_webhook(): signature = request.headers.get('X-VoiceRun-Signature') timestamp = request.headers.get('X-VoiceRun-Timestamp') if not signature or not timestamp: return jsonify({"error": "Missing signature headers"}), 401 payload_body = request.get_data(as_text=True) if not verify_webhook_signature(payload_body, signature, timestamp): return jsonify({"error": "Invalid signature"}), 401 data = request.get_json() event_type = data.get('event') if event_type == 'session.ended': session_id = data.get('sessionId') status = data.get('status') # Process your business logic here return jsonify({"received": True}), 200
JavaScript example:
const express = require('express'); const crypto = require('crypto'); const app = express(); const WEBHOOK_SECRET = 'your-webhook-secret'; app.use(express.json({ verify: (req, res, buf) => { req.rawBody = buf.toString('utf8'); } })); function verifyWebhookSignature(req, res, next) { const signature = req.headers['x-voicerun-signature']; const timestamp = req.headers['x-voicerun-timestamp']; if (!signature || !timestamp) { return res.status(401).json({ error: 'Missing signature headers' }); } // Verify timestamp is recent (within 5 minutes) const currentTime = Math.floor(Date.now() / 1000); if (Math.abs(currentTime - parseInt(timestamp)) > 300) { return res.status(401).json({ error: 'Timestamp too old' }); } // Compute expected signature const signedPayload = `${timestamp}.${req.rawBody}`; const expectedSignature = crypto .createHmac('sha256', WEBHOOK_SECRET) .update(signedPayload) .digest('hex'); if (!crypto.timingSafeEqual( Buffer.from(`sha256=${expectedSignature}`), Buffer.from(signature) )) { return res.status(401).json({ error: 'Invalid signature' }); } next(); } app.post('/webhooks/voicerun', verifyWebhookSignature, (req, res) => { const { event, sessionId, status } = req.body; if (event === 'session.ended') { console.log(`Session ${sessionId} ended with status: ${status}`); } res.status(200).json({ received: true }); });
Event Types#
session.ended#
Triggered when a conversation session completes.
When it triggers:
- Call is disconnected by either party
- Session times out
- Agent explicitly ends the session
- Error causes session termination
Payload:
{ "event": "session.ended", "sessionId": "sess_abc123", "agentId": "agent_xyz789", "environmentId": "env_456", "status": "completed", "createdAt": "2025-12-18T02:45:27.946Z", "agentPhoneNumber": "+15551234567", "clientPhoneNumber": "+15559876543", "duration": "120", "direction": "inbound" }
Fields:
| Field | Type | Description |
|---|---|---|
event | string | Event type (always "session.ended") |
sessionId | string | Unique identifier for the session |
agentId | string | ID of the agent that handled the session |
environmentId | string | Environment ID |
status | string | Session status (e.g., "completed", "failed", "timeout") |
createdAt | string | ISO 8601 timestamp when session was created |
agentPhoneNumber | string | Agent's phone number (E.164 format, optional) |
clientPhoneNumber | string | Client's phone number (E.164 format, optional) |
duration | string | Call duration in seconds (optional) |
direction | string | Call direction (inbound, outbound) |
Best Practices#
1. Respond Quickly#
Return a 200 OK response as quickly as possible (within 10 seconds). Process time-consuming tasks asynchronously.
2. Implement Idempotency#
Webhooks may be delivered more than once. Use the sessionId to detect and handle duplicate events:
import redis redis_client = redis.Redis() def process_webhook(session_id, data): key = f"webhook:processed:{session_id}" if redis_client.exists(key): return # Already processed # Process the webhook # ... your business logic ... # Mark as processed (expire after 24 hours) redis_client.setex(key, 86400, "1")
3. Handle Retries Gracefully#
- 2xx/3xx responses: Success, no retry
- 4xx responses: Client error, no retry
- 5xx responses: Server error, automatic retry with backoff
- Timeout (10s): Automatic retry
4. Security Best Practices#
- Always verify the signature before processing webhook data
- Use timing-safe comparison functions to prevent timing attacks
- Validate the timestamp to prevent replay attacks
- Never log or expose your webhook secret
- Regenerate your secret if it's compromised
