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:

  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!

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

MethodEndpointDescription
PATCH/v1/agents/{agentId}/environments/{envId}Update environment (set webhookUrl)
POST/v1/agents/{agentId}/environments/{envId}/regenerate-webhook-secretGenerate 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

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

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