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 |
call.ended |
Call completes | Store call record |
call.transferred |
Transfer initiated | Update CRM |
transcript.updated |
New transcript segment | Real-time monitoring |
function.called |
Tool executed | Audit trail |
error.occurred |
Error detected | Alerting |
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
}
}
}
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."
}
}
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
- Function Calling - Trigger functions
- Recording - Call recordings
- API Reference - Full API docs