Skip to Content
DocsAPI ReferenceWebhook Events

Webhook Events

Agency plan required

Webhooks let your server receive real-time notifications when call events occur. Configure a webhook URL and BuildVoiceAI will send POST requests to your endpoint whenever a call starts or ends.

Configuration

Setting Up a Webhook URL

  1. Navigate to Settings → API & Webhooks (agency-level) or Client Detail → API & Webhooks (client-level)
  2. Enter your webhook endpoint URL in the Webhook URL field
  3. Click Save

A webhook signing secret is automatically generated when you save. Use this secret to verify incoming webhook signatures.

Your endpoint must:

  • Be publicly accessible (not behind a firewall or VPN)
  • Accept POST requests with a JSON body
  • Return a 2xx status code within 10 seconds
  • Use HTTPS (HTTP endpoints are rejected)

Client-Level Overrides

Client-level webhook URLs override the agency default. When a client has its own webhook URL configured, call events for that client’s agents are sent to the client’s URL instead of the agency’s.

Testing Your Webhook

Use the Send Test Webhook button in Settings to verify your integration before any real calls:

  1. Configure your webhook URL and save
  2. Click Send Test Webhook
  3. A sample call_ended payload is sent with "test": true
  4. Check the result displayed in the dashboard and verify your endpoint received the payload
💡

The test payload mirrors a real call_ended event with all the same fields. Use the "test": true flag to filter test events in your automation.

Event Types

BuildVoiceAI sends the following event types:

EventFired WhenProviders
call_startedA call begins and the connection is establishedRetell, Vapi
call_endedA call finishes (completed, failed, or no-answer)Retell, Vapi, Bland, ElevenLabs
testManual test from the Send Test Webhook buttonAll

Bland AI and ElevenLabs only fire webhooks on call completion. There is no call_started event for Bland or ElevenLabs calls.

Payload Format

All webhook payloads are sent as JSON via POST request. The User-Agent header is set to Prosody-Webhook/1.0.

POSTyour-webhook-url

BuildVoiceAI sends this payload to your configured endpoint.

call_ended

{ "event": "call_ended", "call_id": "f9e8d7c6-b5a4-3210-fedc-ba9876543210", "agent_id": "a1b2c3d4-e5f6-7890-abcd-ef1234567890", "agent_name": "Sales Qualifier", "status": "completed", "direction": "outbound", "duration_seconds": 185, "cost_cents": 28, "from_number": "+15551234567", "to_number": "+15559876543", "transcript": "Agent: Hello, how can I help you today?\nUser: I was wondering about your pricing.", "recording_url": "https://storage.example.com/recordings/abc123.mp3", "summary": "Caller expressed interest in the enterprise plan and requested a follow-up demo.", "sentiment": "positive", "started_at": "2026-03-16T14:30:00.000Z", "ended_at": "2026-03-16T14:33:05.000Z", "metadata": { "source": "hubspot", "deal_id": "deal_12345" }, "provider": "retell" }

call_started

{ "event": "call_started", "call_id": "f9e8d7c6-b5a4-3210-fedc-ba9876543210", "agent_id": "a1b2c3d4-e5f6-7890-abcd-ef1234567890", "agent_name": "Sales Qualifier", "status": "in-progress", "direction": "inbound", "from_number": "+15551234567", "to_number": "+15559876543", "started_at": "2026-03-16T14:30:00.000Z", "metadata": {}, "provider": "vapi" }

Field Reference

FieldTypePresent InDescription
eventstringBothcall_started, call_ended, or test
call_idstringBothUnique call identifier
agent_idstringBothBuildVoiceAI agent UUID
agent_namestringBothHuman-readable agent name
statusstringBothin-progress, completed, failed, no-answer
directionstringBothinbound or outbound
from_numberstringBothCaller phone number (E.164 format)
to_numberstringBothRecipient phone number (E.164 format)
started_atstringBothISO 8601 timestamp
duration_secondsnumbercall_endedCall length in seconds
cost_centsnumbercall_endedCall cost in cents
transcriptstringcall_endedFull conversation transcript
recording_urlstring|nullcall_endedLink to call recording
summarystringcall_endedAI-generated call summary
sentimentstringcall_endedpositive, neutral, or negative
ended_atstringcall_endedISO 8601 timestamp
metadataobjectBothCustom metadata attached to the call
providerstringBothretell, vapi, bland, elevenlabs, or test
testbooleanTest onlytrue on test webhooks

Signature Verification

Every webhook request includes HMAC-SHA256 signing headers when a signing secret is configured:

HeaderDescription
X-Prosody-SignatureHMAC-SHA256 hex digest
X-Prosody-TimestampUnix timestamp in seconds when the request was signed

The signature is computed over the concatenation of the timestamp and the raw request body:

HMAC-SHA256(signing_secret, "${timestamp}.${body}")

Verification Example (Node.js)

const crypto = require('crypto'); function verifyWebhookSignature(rawBody, signature, timestamp, signingSecret) { // Reject old payloads to prevent replay attacks const age = Math.floor(Date.now() / 1000) - parseInt(timestamp); if (age > 300) throw new Error('Webhook payload too old (>5 minutes)'); const expected = crypto .createHmac('sha256', signingSecret) .update(`${timestamp}.${rawBody}`) .digest('hex'); return crypto.timingSafeEqual( Buffer.from(signature), Buffer.from(expected) ); } // Usage in an Express handler: app.post('/webhook', (req, res) => { const rawBody = req.body; // must be raw string, not parsed JSON const signature = req.headers['x-prosody-signature']; const timestamp = req.headers['x-prosody-timestamp']; if (!verifyWebhookSignature(rawBody, signature, timestamp, SIGNING_SECRET)) { return res.status(401).send('Invalid signature'); } // Process the webhook... res.status(200).send('OK'); });

Verification Example (Python)

import hmac import hashlib import time def verify_webhook(raw_body: bytes, signature: str, timestamp: str, secret: str) -> bool: # Reject old payloads age = int(time.time()) - int(timestamp) if age > 300: raise ValueError("Webhook payload too old") expected = hmac.new( secret.encode(), f"{timestamp}.{raw_body.decode()}".encode(), hashlib.sha256 ).hexdigest() return hmac.compare_digest(signature, expected)
⚠️

Always use a timing-safe comparison function (timingSafeEqual in Node.js, hmac.compare_digest in Python) to prevent timing attacks. Do not use === or == for signature comparison.

The signing secret is separate from your API key. Find it in Settings → API & Webhooks — click the eye icon to reveal it.

Retry Policy

If your webhook endpoint returns a non-2xx status code or does not respond within 10 seconds, BuildVoiceAI retries with exponential backoff:

AttemptDelayCondition
1st attemptImmediateAlways
2nd attempt1 second after failure5xx, 429, or network error
3rd attempt2 seconds after 2nd failure5xx, 429, or network error

Not retried: 4xx errors (except 429) indicate a permanent problem (bad URL, authentication failure) and fail immediately.

After 3 failed attempts, the delivery is marked as failed. All attempts are logged in the Delivery Log.

Delivery Log

Every webhook delivery attempt — successful or failed — is logged and visible in Settings → API & Webhooks → Recent Deliveries.

Each log entry includes:

FieldDescription
EventThe event type (call_started, call_ended, test)
TimestampWhen the delivery was attempted
Status CodeHTTP status code returned by your endpoint
SuccessWhether the delivery succeeded
ErrorError message if the delivery failed
AttemptWhich attempt number (1, 2, or 3)

Use the delivery log to:

  • Debug failed webhook integrations
  • Verify test webhooks were received
  • Monitor overall delivery health
  • Identify intermittent endpoint issues
💡

Return a 200 response immediately and process the webhook payload asynchronously. This prevents timeouts if your processing logic takes time.

Security

BuildVoiceAI enforces several security measures on outbound webhooks:

  • HTTPS only — HTTP URLs are rejected
  • SSRF protection — Requests to localhost, private IPs (10.x, 192.168.x, 172.16-31.x), and link-local addresses are blocked
  • HMAC-SHA256 signing — Every webhook is signed when a signing secret is configured
  • Timing-safe verification — Signature comparison uses constant-time algorithms
  • User-Agent identification — All requests include Prosody-Webhook/1.0

Best Practices

  • Respond quickly — Return 200 within 10 seconds to avoid retries
  • Process asynchronously — Queue webhook payloads for background processing
  • Verify signatures — Always validate the X-Prosody-Signature header in production
  • Handle duplicates — Use call_id + event as a dedup key in case of retries
  • Log payloads — Store raw webhook payloads for debugging
  • Use the test button — Verify your integration with a test webhook before relying on real calls
Last updated on