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#

  1. Go to Agents → [Your Agent] → Environments
  2. Click on the environment you want to configure
  3. Open the Webhook Settings dialog
  4. Enter your webhook URL (must be HTTPS)
  5. Click Save - a webhook secret will be automatically generated
  6. 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#

HeaderDescription
X-VoiceRun-SignatureHMAC-SHA256 signature in format: sha256=<hex>
X-VoiceRun-TimestampUnix 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:

FieldTypeDescription
eventstringEvent type (always "session.ended")
sessionIdstringUnique identifier for the session
agentIdstringID of the agent that handled the session
environmentIdstringEnvironment ID
statusstringSession status (e.g., "completed", "failed", "timeout")
createdAtstringISO 8601 timestamp when session was created
agentPhoneNumberstringAgent's phone number (E.164 format, optional)
clientPhoneNumberstringClient's phone number (E.164 format, optional)
durationstringCall duration in seconds (optional)
directionstringCall 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
apiwebhookseventsnotifications