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. When configured, VoiceRun automatically sends POST requests to your webhook endpoint whenever subscribed events occur.
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
Why Use Webhooks vs. Polling?
- Real-time: Receive notifications immediately when events occur
- Efficient: No need to make repeated API calls
- Scalable: Handles high-volume events without rate limiting concerns
- Simple: No complex polling logic required
Setup & Configuration
1. Configure Your Webhook Endpoint
Navigate to your agent's environment settings and add your webhook URL:
- 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!
2. 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
API Configuration
You can also configure webhooks programmatically using the VoiceRun API. This is useful for automated deployments or integrations.
Set Webhook URL
Use the environment update endpoint to set or update your webhook URL:
# Set webhook URL for an environment
curl -X PATCH https://api.voicerun.ai/v1/agents/{agentId}/environments/{environmentId} \
-H "Authorization: Bearer YOUR_API_KEY" \
-H "Content-Type: application/json" \
-d '{"webhookUrl": "https://your-api.com/webhooks/voicerun"}'Response
When you set a webhook URL for the first time, a webhookSecret is automatically generated and included in the response. Save this secret immediately - it won't be shown in subsequent API responses.
Regenerate Webhook Secret
If your webhook secret is compromised or you need to rotate it:
# Regenerate webhook secret
curl -X POST https://api.voicerun.ai/v1/agents/{agentId}/environments/{environmentId}/regenerate-webhook-secret \
-H "Authorization: Bearer YOUR_API_KEY"
# Response:
# {
# "data": {
# "webhookSecret": "whsec_abc123..."
# }
# }Remove Webhook
To disable webhooks for an environment, set the URL to null:
curl -X PATCH https://api.voicerun.ai/v1/agents/{agentId}/environments/{environmentId} \
-H "Authorization: Bearer YOUR_API_KEY" \
-H "Content-Type: application/json" \
-d '{"webhookUrl": null, "webhookSecret": null}'API Reference
| Method | Endpoint | Description |
|---|---|---|
| PATCH | /v1/agents/{agentId}/environments/{envId} | Update environment (set webhookUrl) |
| POST | /v1/agents/{agentId}/environments/{envId}/regenerate-webhook-secret | Generate new webhook secret |
Security & Verification
VoiceRun signs all webhook requests with HMAC-SHA256 to ensure authenticity and prevent tampering. You should always verify the signature before processing webhook data.
Signature Headers
| Header | Description |
|---|---|
| X-Primvoices-Signature | HMAC-SHA256 signature in format: sha256=<hex> |
| X-Primvoices-Timestamp | Unix timestamp (seconds) when webhook was sent |
Signature Verification
The signature is computed as: HMAC-SHA256(timestamp + "." + raw_body, secret)
Example Implementation
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):
"""
Verify the webhook signature to ensure authenticity.
Returns:
bool: True if signature is valid, False otherwise
"""
# 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: # 5 minutes
print("Timestamp too old")
return False
except ValueError:
print("Invalid timestamp format")
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():
# Get headers
signature = request.headers.get('X-Primvoices-Signature')
timestamp = request.headers.get('X-Primvoices-Timestamp')
if not signature or not timestamp:
return jsonify({"error": "Missing signature headers"}), 401
# Get raw body
payload_body = request.get_data(as_text=True)
# Verify signature
if not verify_webhook_signature(payload_body, signature, timestamp):
return jsonify({"error": "Invalid signature"}), 401
# Parse and process webhook
data = request.get_json()
event_type = data.get('event')
if event_type == 'session.ended':
session_id = data.get('sessionId')
status = data.get('status')
print(f"Session {session_id} ended with status: {status}")
# Process your business logic here
return jsonify({"received": True}), 200
if __name__ == '__main__':
app.run(port=3000)⚠️ 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
Event Types
VoiceRun sends webhook notifications for the following events:
session.ended
Triggered when a conversation session completes. This event is sent after the call ends and includes session metadata and call details.
When It Triggers
- Call is disconnected by either party
- Session times out
- Agent explicitly ends the session
- Error causes session termination
Payload Schema
{
"event": "session.ended",
"sessionId": "sess_abc123",
"agentId": "agent_xyz789",
"environmentId": "env_456",
"status": "completed",
"createdAt": "2025-12-18T02:45:27.946Z",
"fromNumber": "+15551234567",
"toNumber": "+15559876543",
"duration": "120"
}Field Descriptions
| Field | Type | Description |
|---|---|---|
| event | string | Event type identifier (always "session.ended") |
| sessionId | string | Unique identifier for the session |
| agentId | string | ID of the agent that handled the session |
| environmentId | string | Environment ID (development, staging, production) |
| status | string | Session completion status (e.g., "completed", "failed", "timeout") |
| createdAt | string | ISO 8601 timestamp when session was created |
| fromNumber | string | Caller's phone number (E.164 format, optional) |
| toNumber | string | Agent's phone number (E.164 format, optional) |
| duration | string | Call duration in seconds (optional) |
Best Practices
1. Respond Quickly
Your webhook endpoint should return a 200 OK response as quickly as possible (ideally 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:
# Example using Redis for deduplication
import redis
redis_client = redis.Redis()
def process_webhook(session_id, data):
# Check if we've already processed this session
key = f"webhook:processed:{session_id}"
if redis_client.exists(key):
print(f"Already processed session {session_id}, skipping")
return
# Process the webhook
# ... your business logic here ...
# Mark as processed (expire after 24 hours)
redis_client.setex(key, 86400, "1")3. Handle Retries Gracefully
VoiceRun retries failed webhook deliveries with exponential backoff:
- 2xx/3xx responses: Success, no retry
- 4xx responses: Client error, no retry (fix your endpoint)
- 5xx responses: Server error, automatic retry with backoff
- Timeout (10s): Automatic retry
4. Log Everything
Log all webhook requests for debugging and audit purposes:
app.post('/webhooks/voicerun', (req, res) => {
// Log incoming webhook
console.log('Webhook received:', {
event: req.body.event,
sessionId: req.body.sessionId,
timestamp: new Date().toISOString(),
headers: req.headers
});
try {
// Process webhook
processWebhook(req.body);
res.status(200).json({ received: true });
} catch (error) {
// Log error but still return 200 to avoid retries for processing errors
console.error('Error processing webhook:', error);
res.status(500).json({ error: 'Processing failed' });
}
});5. Monitor Your Webhook Endpoint
- Set up alerts for webhook delivery failures
- Monitor response times to stay under the 10-second timeout
- Track error rates and investigate spikes
- Use health checks to ensure your endpoint is always available
6. Secure Your Endpoint
- Always use HTTPS
- Verify webhook signatures on every request
- Validate timestamp to prevent replay attacks
- Rate limit your webhook endpoint to prevent abuse
- Keep your webhook secret secure and rotate it periodically