Webhooks allow your application to receive real-time HTTP notifications when events occur during voice agent calls.
Prerequisites: API key and an agent configured with webhook settings.
Overview
Voice.ai supports two webhook types:
- Event Webhooks for call lifecycle event notifications
- Webhook Tools for function-style tool invocations from the agent runtime
| Event | Description |
|---|
call.started | Call has connected and the agent is ready |
call.completed | Call has ended, includes transcript and usage data |
When configured, your server receives an HTTP POST request for each event with a JSON payload containing event details.
Event Webhooks
Configuration
Add event webhook configuration to your agent’s config.webhooks.events object:
{
"name": "My Agent",
"config": {
"prompt": "You are a helpful assistant.",
"webhooks": {
"events": {
"url": "https://your-server.com/webhooks/voice-events",
"secret": "your-signing-secret",
"events": ["call.started", "call.completed"],
"timeout": 5,
"enabled": true
}
}
}
}
Event configuration parameters
| Parameter | Type | Required | Default | Description |
|---|
url | string | Yes | - | Your webhook endpoint URL (must be HTTPS in production) |
secret | string | No | null | HMAC-SHA256 signing secret for payload verification |
events | array | No | [] | Event types to receive. Empty array = all events |
timeout | number | No | 5 | Request timeout in seconds (1-30) |
enabled | boolean | No | true | Whether webhook notifications are active |
Event Payloads
All webhook events share a common structure with top-level fields and event-specific data:
| Field | Type | Description |
|---|
event | string | Event type (call.started, call.completed) |
timestamp | string | ISO 8601 timestamp of when the event occurred |
call_id | string | Unique identifier for the call |
agent_id | string | Agent handling the call |
data | object | Event-specific additional data |
call.started
Sent when a call connects and the agent is ready to interact.
{
"event": "call.started",
"timestamp": "2025-02-03T14:30:00.000Z",
"call_id": "call_abc123",
"agent_id": "agent_456",
"data": {
"call_type": "sip_inbound",
"started_at": "2025-02-03T14:30:00.000Z",
"from_number": "+14155551234",
"to_number": "+14155559876"
}
}
data fields:
| Field | Type | Description |
|---|
call_type | string | "web", "sip_inbound", or "sip_outbound" |
started_at | string | ISO 8601 timestamp |
from_number | string | Caller’s phone number (SIP calls only) |
to_number | string | Called phone number (SIP calls only) |
from_number and to_number are only included for SIP (phone) calls. Web calls do not have these fields.
call.completed
Sent after the call ends and all data has been processed. Includes transcript and usage information.
{
"event": "call.completed",
"timestamp": "2025-02-03T14:35:00.000Z",
"call_id": "call_abc123",
"agent_id": "agent_456",
"data": {
"call_type": "sip_inbound",
"duration_seconds": 300.5,
"credits_used": 15.25,
"transcript_uri": "https://storage.example.com/transcripts/call_abc123.json",
"transcript_summary": "Customer inquired about pricing for the enterprise plan...",
"from_number": "+14155551234",
"to_number": "+14155559876"
}
}
data fields:
| Field | Type | Description |
|---|
call_type | string | "web", "sip_inbound", or "sip_outbound" |
duration_seconds | number | Call duration in seconds |
credits_used | number | Credits consumed by this call |
transcript_uri | string | URL to full transcript JSON |
transcript_summary | string | AI-generated summary of the call |
from_number | string | Caller’s phone number (SIP calls only) |
to_number | string | Called phone number (SIP calls only) |
Use config.webhooks.tools to declare callable functions that your agent can invoke:
{
"name": "My Agent",
"config": {
"prompt": "You are a helpful assistant.",
"webhooks": {
"events": {
"url": "https://your-server.com/webhooks/voice-events",
"secret": "your-signing-secret",
"events": ["call.started", "call.completed"]
},
"tools": [
{
"name": "get_account_status",
"description": "Fetches the latest account status for a customer.",
"url": "https://your-server.com/webhooks/tools/account-status",
"parameters": {
"customer_id": "string"
},
"response": {
"type": "object",
"properties": {
"status": { "type": "string" },
"tier": { "type": "string" }
}
},
"secret": "tool-signing-secret",
"timeout": 10
},
{
"name": "search_knowledge_base",
"description": "Searches the internal KB and returns ranked snippets.",
"url": "https://your-server.com/webhooks/tools/search-kb",
"parameters": {
"type": "object",
"properties": {
"query": { "type": "string" },
"top_k": { "type": "integer" }
},
"required": ["query"]
},
"timeout": 30
}
]
}
}
}
| Parameter | Type | Required | Default | Description |
|---|
name | string | Yes | - | Tool name used in function_name |
description | string | Yes | - | Human-readable tool behavior description |
url | string | Yes | - | Tool webhook endpoint URL |
parameters | object | No | {} | Supports shorthand maps ({"query":"string"}) or JSON-schema-like objects |
response | object | No | {} | Expected response shape from your tool |
secret | string | No | null | HMAC-SHA256 signing secret for payload verification |
timeout | number | No | 30 | Request timeout in seconds |
Tool invocations use event: "function_call" and include request context in data:
{
"event": "function_call",
"timestamp": "2025-02-03T14:31:00.000Z",
"call_id": "call_abc123",
"agent_id": "agent_456",
"data": {
"request_id": "req-20250203-001",
"function_name": "get_account_status",
"arguments": {
"customer_id": "cust_987"
}
}
}
Recommended tool responses from customer endpoints use this shape:
{
"result": {
"status": "active",
"tier": "enterprise"
}
}
Every webhook request includes these HTTP headers:
| Header | Description |
|---|
Content-Type | Always application/json |
X-Webhook-Timestamp | Unix timestamp of when the request was signed |
X-Webhook-Signature | HMAC-SHA256 signature (only if secret is configured) |
Event and tool details (event, call_id, agent_id, etc.) are in the JSON body, not headers. Headers only contain what’s needed for signature verification.
Signature Verification
If you configure a secret, both event and tool webhook requests are signed using HMAC-SHA256. Always verify signatures in production to ensure requests are authentic.
The signature is computed as:
HMAC-SHA256(secret, "{timestamp}.{payload}")
Where:
timestamp is the value from X-Webhook-Timestamp header
payload is the raw JSON request body
Verification Examples
import hmac
import hashlib
import time
def verify_webhook(request_body: bytes, headers: dict, secret: str) -> bool:
"""Verify webhook signature and timestamp."""
signature = headers.get('X-Webhook-Signature')
timestamp = headers.get('X-Webhook-Timestamp')
if not signature or not timestamp:
return False
# Reject requests older than 5 minutes (replay attack prevention)
if abs(time.time() - int(timestamp)) > 300:
return False
# Compute expected signature
message = f"{timestamp}.{request_body.decode()}"
expected = hmac.new(
secret.encode(),
message.encode(),
hashlib.sha256
).hexdigest()
# Constant-time comparison
return hmac.compare_digest(expected, signature)
# Flask example
from flask import Flask, request, jsonify
app = Flask(__name__)
WEBHOOK_SECRET = "your-signing-secret"
@app.route('/webhooks/voice-events', methods=['POST'])
def handle_webhook():
if not verify_webhook(request.data, request.headers, WEBHOOK_SECRET):
return jsonify({"error": "Invalid signature"}), 401
event = request.json
event_type = event.get('event')
if event_type == 'call.started':
print(f"Call started: {event['call_id']}")
elif event_type == 'call.completed':
print(f"Call completed: {event['call_id']}, duration: {event['data']['duration_seconds']}s")
return jsonify({"status": "ok"}), 200
Retry Logic
Failed webhook deliveries are retried with exponential backoff:
| Attempt | Delay |
|---|
| 1 | Immediate |
| 2 | 1 second |
| 3 | 2 seconds |
| 4 | 4 seconds |
| 5 | 8 seconds |
Retries stop after 5 attempts or on receiving a 4xx response (except 429 rate limit).
Idempotency: Your webhook handler should be idempotent. The same event may be delivered multiple times due to retries. Use call_id to deduplicate events.
Best Practices
- Always verify signatures in production to prevent spoofed requests
- Respond quickly with a 2xx status code within 5 seconds to avoid retries
- Process asynchronously - queue events for processing rather than blocking the response
- Handle duplicates - use
call_id to deduplicate in case of retries
- Check timestamps - reject events older than 5 minutes to prevent replay attacks
- Use HTTPS - ensure your webhook endpoint uses TLS encryption
Filtering Events
You can filter which events you receive by specifying the events array:
{
"webhooks": {
"events": {
"url": "https://your-server.com/webhooks",
"events": ["call.completed"]
}
}
}
This configuration only receives call.completed events. Leave events empty or omit it to receive all event types.
Testing Webhooks
You can test your webhook endpoint using the Test Webhook endpoint:
curl -X POST "https://api.voice.ai/api/v1/agent/{agent_id}/webhook/test" \
-H "Authorization: Bearer YOUR_API_KEY"
This sends a test event to your configured webhook URL and returns the delivery result.
You can test an individual webhook tool using the test tool endpoint:
curl -X POST "https://api.voice.ai/api/v1/agent/{agent_id}/webhook/test-tool/get_account_status" \
-H "Authorization: Bearer YOUR_API_KEY"
This sends a sample function_call payload for the specified tool and returns the delivery result.
For local development, use a tunnel service like ngrok to expose your local server:
ngrok http 3000
# Use the ngrok URL as your webhook URL during development
Next Steps