Webhooks
Webhooks allow your systems to receive real-time notifications about call events, enabling integrations with CRMs, analytics, and other systems.
Available Webhooks
| Event | Trigger | Use Case |
|---|---|---|
call.started |
Call begins | Log call start, update CRM |
call.ended |
Call completes | Store call record, trigger follow-ups |
call.failed |
Call fails to connect | Retry logic, campaign analytics |
call.transferred |
Transfer initiated | Update CRM, notify human agent |
call.recording_ready |
Recording processed | Store recording, compliance archive |
call.analyzed |
AI analysis complete | Update CRM with insights, sentiment |
call.voicemail_detected |
Voicemail reached | Schedule callback, update campaign |
transcript.updated |
New transcript segment | Real-time monitoring, live dashboards |
function.called |
Tool executed | Audit trail, integration logging |
error.occurred |
Error detected | Alerting, debugging |
Configuration
Agent Webhook Setup
{
"agent": {
"name": "Customer Support",
"webhooks": {
"url": "https://your-server.com/webhooks/voice-agent",
"events": ["call.started", "call.ended", "transcript.updated"],
"headers": {
"Authorization": "Bearer your-secret-token"
},
"retryPolicy": {
"maxRetries": 3,
"backoffMs": 1000
}
}
}
}
Multiple Endpoints
{
"webhooks": [
{
"url": "https://crm.example.com/api/calls",
"events": ["call.ended"],
"headers": {"X-API-Key": "crm-key"}
},
{
"url": "https://analytics.example.com/events",
"events": ["call.started", "call.ended", "transcript.updated"],
"headers": {"Authorization": "Bearer analytics-key"}
}
]
}
Event Payloads
call.started
{
"event": "call.started",
"timestamp": "2024-12-28T10:30:00Z",
"data": {
"call_id": "call_abc123",
"agent_id": "agent_xyz789",
"direction": "inbound",
"caller_number": "+14155551234",
"called_number": "+14155559876",
"provider": "twilio",
"call_sid": "CA123456789",
"metadata": {
"campaign_id": "summer_promo"
}
}
}
call.ended
{
"event": "call.ended",
"timestamp": "2024-12-28T10:35:00Z",
"data": {
"call_id": "call_abc123",
"agent_id": "agent_xyz789",
"duration_seconds": 300,
"end_reason": "user_hangup",
"was_transferred": false,
"turn_count": 12,
"summary": {
"topics": ["order_status", "return_request"],
"sentiment": "neutral",
"outcome": "resolved"
},
"metrics": {
"avg_response_time_ms": 450,
"interruptions": 2,
"stt_errors": 0
}
}
}
call.failed
{
"event": "call.failed",
"timestamp": "2024-12-28T10:30:15Z",
"data": {
"call_id": "call_abc123",
"agent_id": "agent_xyz789",
"to": "+14155551234",
"from": "+14155559876",
"provider": "twilio",
"failure_reason": "busy",
"failure_code": "3010",
"failure_description": "Busy Line",
"hangup_source": "Carrier",
"campaign_id": "summer_promo"
}
}
Failure Reasons:
| Reason | Description |
|---|---|
busy |
Recipient's line is busy |
no-answer |
Call was not answered within timeout |
rejected |
Call was declined by recipient |
canceled |
Call was canceled before connecting |
failed |
General failure (network issue) |
transcript.updated
{
"event": "transcript.updated",
"timestamp": "2024-12-28T10:32:15Z",
"data": {
"call_id": "call_abc123",
"turn_id": "turn_5",
"role": "user",
"text": "What's the status of my order?",
"is_final": true,
"confidence": 0.95,
"timestamp_ms": 125000
}
}
function.called
{
"event": "function.called",
"timestamp": "2024-12-28T10:32:20Z",
"data": {
"call_id": "call_abc123",
"function_name": "get_order_status",
"arguments": {
"order_id": "ORD-12345"
},
"result": {
"status": "shipped",
"tracking": "1Z999AA10123456784"
},
"duration_ms": 150,
"success": true
}
}
call.transferred
{
"event": "call.transferred",
"timestamp": "2024-12-28T10:33:00Z",
"data": {
"call_id": "call_abc123",
"transfer_type": "cold",
"department": "billing",
"reason": "Billing dispute over $500",
"transfer_number": "+14155551111",
"conversation_summary": "Customer requested refund for order ORD-12345. Amount exceeds agent authorization."
}
}
call.recording_ready
Triggered when call recording has been processed and is available for download.
{
"event": "call.recording_ready",
"timestamp": "2024-12-28T10:36:00Z",
"data": {
"call_id": "call_abc123",
"agent_id": "agent_xyz789",
"recording_url": "https://storage.edesy.in/recordings/call_abc123.mp3",
"recording_duration_seconds": 300,
"recording_format": "mp3",
"file_size_bytes": 2400000,
"expires_at": "2025-01-28T10:36:00Z"
}
}
call.analyzed
Triggered when AI analysis of the call is complete (sentiment, topics, summary).
{
"event": "call.analyzed",
"timestamp": "2024-12-28T10:37:00Z",
"data": {
"call_id": "call_abc123",
"agent_id": "agent_xyz789",
"analysis": {
"sentiment": {
"overall": "positive",
"score": 0.72,
"progression": ["neutral", "negative", "positive"]
},
"topics": ["order_status", "delivery_delay", "resolution"],
"summary": "Customer inquired about delayed order. Agent explained shipping issue and offered expedited delivery. Customer satisfied with resolution.",
"action_items": ["Follow up in 2 days to confirm delivery"],
"customer_intent": "complaint_resolution",
"outcome": "resolved",
"key_phrases": ["where is my order", "thank you for helping"]
},
"extracted_data": {
"order_id": "ORD-12345",
"customer_email": "[email protected]",
"callback_requested": false
}
}
}
call.voicemail_detected
Triggered when the AI detects a voicemail greeting instead of a human.
{
"event": "call.voicemail_detected",
"timestamp": "2024-12-28T10:30:30Z",
"data": {
"call_id": "call_abc123",
"agent_id": "agent_xyz789",
"to": "+14155551234",
"from": "+14155559876",
"voicemail_action": "left_message",
"message_left": "Hi, this is calling from Acme Corp regarding your order. Please call us back at...",
"detection_confidence": 0.95,
"campaign_id": "summer_promo"
}
}
Voicemail Actions:
| Action | Description |
|---|---|
left_message |
AI left a voicemail message |
hung_up |
AI hung up without leaving message |
transferred |
Call transferred to human for voicemail |
Webhook Subscription Management
Manage webhooks programmatically via the API.
List Subscriptions
GET /api/webhooks/subscriptions
Response:
{
"subscriptions": [
{
"id": "sub_abc123",
"name": "CRM Integration",
"url": "https://crm.example.com/webhooks",
"enabled_events": ["call.ended", "call.analyzed"],
"status": "active",
"created_at": "2024-12-01T00:00:00Z"
}
]
}
Create Subscription
POST /api/webhooks/subscriptions
Request:
{
"name": "Analytics Webhook",
"url": "https://analytics.example.com/voice-events",
"enabled_events": ["call.started", "call.ended", "call.analyzed"],
"headers": {
"Authorization": "Bearer your-token"
},
"metadata": {
"environment": "production"
}
}
Rotate Webhook Secret
Rotate the signing secret for a subscription:
POST /api/webhooks/subscriptions/{id}/rotate-secret
Response:
{
"new_secret": "whsec_new_secret_here",
"old_secret_valid_until": "2024-12-29T10:00:00Z"
}
Test Webhook
Send a test event to verify your endpoint:
POST /api/webhooks/subscriptions/{id}/test
View Delivery Logs
See recent webhook delivery attempts:
GET /api/webhooks/subscriptions/{id}/deliveries
Response:
{
"deliveries": [
{
"id": "del_xyz789",
"event_type": "call.ended",
"status": "success",
"response_code": 200,
"response_time_ms": 150,
"timestamp": "2024-12-28T10:35:05Z"
},
{
"id": "del_xyz790",
"event_type": "call.ended",
"status": "failed",
"response_code": 500,
"error": "Connection timeout",
"retry_count": 2,
"timestamp": "2024-12-28T10:40:05Z"
}
]
}
Implementation
Webhook Dispatcher
type WebhookDispatcher struct {
endpoints []WebhookEndpoint
client *http.Client
queue chan WebhookEvent
}
type WebhookEndpoint struct {
URL string
Events []string
Headers map[string]string
MaxRetries int
BackoffMs int
}
func (d *WebhookDispatcher) Dispatch(event WebhookEvent) {
// Queue for async processing
d.queue <- event
}
func (d *WebhookDispatcher) processQueue() {
for event := range d.queue {
for _, endpoint := range d.endpoints {
if d.shouldSend(endpoint, event.Type) {
go d.sendWithRetry(endpoint, event)
}
}
}
}
func (d *WebhookDispatcher) sendWithRetry(endpoint WebhookEndpoint, event WebhookEvent) {
payload, _ := json.Marshal(event)
for attempt := 0; attempt <= endpoint.MaxRetries; attempt++ {
req, _ := http.NewRequest("POST", endpoint.URL, bytes.NewReader(payload))
req.Header.Set("Content-Type", "application/json")
for key, value := range endpoint.Headers {
req.Header.Set(key, value)
}
// Add signature for verification
signature := d.signPayload(payload, endpoint.Secret)
req.Header.Set("X-Webhook-Signature", signature)
resp, err := d.client.Do(req)
if err == nil && resp.StatusCode >= 200 && resp.StatusCode < 300 {
return // Success
}
// Exponential backoff
time.Sleep(time.Duration(endpoint.BackoffMs*(1<<attempt)) * time.Millisecond)
}
log.Printf("Webhook failed after %d retries: %s", endpoint.MaxRetries, endpoint.URL)
}
Signature Verification
Verify webhook authenticity:
// Sender side
func (d *WebhookDispatcher) signPayload(payload []byte, secret string) string {
mac := hmac.New(sha256.New, []byte(secret))
mac.Write(payload)
return hex.EncodeToString(mac.Sum(nil))
}
// Receiver side
func verifyWebhook(r *http.Request, secret string) bool {
signature := r.Header.Get("X-Webhook-Signature")
if signature == "" {
return false
}
body, _ := io.ReadAll(r.Body)
r.Body = io.NopCloser(bytes.NewReader(body)) // Reset body
expected := signPayload(body, secret)
return hmac.Equal([]byte(signature), []byte(expected))
}
// Usage in handler
func webhookHandler(w http.ResponseWriter, r *http.Request) {
if !verifyWebhook(r, os.Getenv("WEBHOOK_SECRET")) {
http.Error(w, "Invalid signature", http.StatusUnauthorized)
return
}
var event WebhookEvent
json.NewDecoder(r.Body).Decode(&event)
// Process event
handleWebhookEvent(event)
w.WriteHeader(http.StatusOK)
}
Integration Examples
CRM Integration
func handleCallEnded(event WebhookEvent) {
data := event.Data.(CallEndedData)
// Update CRM contact
crm.UpdateContact(ContactUpdate{
Phone: data.CallerNumber,
LastCallDate: event.Timestamp,
CallDuration: data.Duration,
Notes: data.Summary.Topics,
Sentiment: data.Summary.Sentiment,
})
// Create activity
crm.CreateActivity(Activity{
Type: "voice_call",
Subject: fmt.Sprintf("AI Call - %s", data.Summary.Outcome),
Description: generateCallSummary(data),
})
}
Analytics Integration
func handleTranscriptUpdated(event WebhookEvent) {
data := event.Data.(TranscriptData)
// Send to analytics
analytics.Track(AnalyticsEvent{
Event: "conversation_turn",
Properties: map[string]any{
"call_id": data.CallID,
"role": data.Role,
"text": data.Text,
"confidence": data.Confidence,
},
})
}
Slack Alerts
func handleError(event WebhookEvent) {
data := event.Data.(ErrorData)
if data.Severity == "critical" {
slack.PostMessage("#alerts", SlackMessage{
Text: fmt.Sprintf("🚨 Voice Agent Error: %s\nCall: %s\nError: %s",
data.AgentName, data.CallID, data.Message),
Attachments: []Attachment{{
Color: "danger",
Fields: []Field{
{Title: "Agent", Value: data.AgentID},
{Title: "Error Type", Value: data.ErrorType},
},
}},
})
}
}
Retry Policy
Configure retry behavior for failed webhooks:
{
"webhooks": {
"url": "https://your-server.com/webhook",
"retryPolicy": {
"maxRetries": 5,
"initialBackoffMs": 1000,
"maxBackoffMs": 60000,
"backoffMultiplier": 2
}
}
}
| Attempt | Delay |
|---|---|
| 1 | 1 second |
| 2 | 2 seconds |
| 3 | 4 seconds |
| 4 | 8 seconds |
| 5 | 16 seconds |
Best Practices
1. Respond Quickly
// Acknowledge immediately, process async
func webhookHandler(w http.ResponseWriter, r *http.Request) {
var event WebhookEvent
json.NewDecoder(r.Body).Decode(&event)
// Queue for async processing
eventQueue <- event
// Respond immediately
w.WriteHeader(http.StatusOK)
}
2. Idempotency
func handleEvent(event WebhookEvent) error {
// Check if already processed
if processed, _ := cache.Get(event.ID); processed {
return nil // Already handled
}
// Process event
err := processEvent(event)
if err != nil {
return err
}
// Mark as processed
cache.Set(event.ID, true, 24*time.Hour)
return nil
}
3. Error Handling
func webhookHandler(w http.ResponseWriter, r *http.Request) {
var event WebhookEvent
if err := json.NewDecoder(r.Body).Decode(&event); err != nil {
http.Error(w, "Invalid payload", http.StatusBadRequest)
return
}
if err := handleEvent(event); err != nil {
// Return 500 to trigger retry
http.Error(w, err.Error(), http.StatusInternalServerError)
return
}
w.WriteHeader(http.StatusOK)
}
Testing Webhooks
# Test webhook locally with ngrok
ngrok http 3000
# Update webhook URL
curl -X PATCH https://api.edesy.in/agents/123 \
-H "Authorization: Bearer $API_KEY" \
-d '{"webhooks": {"url": "https://abc123.ngrok.io/webhook"}}'
# Make a test call and check webhook delivery
Next Steps
- Finding Your Workspace ID - Get your workspace ID for webhook URLs
- Function Calling - Trigger functions
- Recording - Call recordings
- API Reference - Full API docs