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.ended |
Call completes | Store call record, trigger follow-ups |
call.failed |
Call fails to connect | Retry logic, campaign analytics |
call.analyzed |
AI analysis complete | Update CRM with insights, sentiment |
call.extracted |
Post-call data extraction finished | Push extracted fields to your CRM or sheet |
call.recording_ready |
Recording processed | Store recording, compliance archive |
These five events are the ones available for webhook subscriptions. Voicemail and transfer outcomes arrive as the call's disposition on call.ended.
Configuration#
Agent Webhook Setup#
{
"agent": {
"name": "Customer Support",
"webhooks": {
"url": "https://your-server.com/webhooks/voice-agent",
"events": ["call.ended", "call.analyzed"],
"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.ended", "call.analyzed"],
"headers": {"Authorization": "Bearer analytics-key"}
}
]
}
Event Payloads#
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) |
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
}
}
}
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.ended", "call.analyzed", "call.extracted"],
"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#
Webhook subscription deliveries carry X-Webhook-Signature: t=<unix seconds>,v1=<hex>. The v1 value is an HMAC-SHA256, keyed with your whsec_... secret, over "{t}.{raw request body}".
// Header: X-Webhook-Signature: t=<unix seconds>,v1=<hex HMAC-SHA256>
// Signed string: "<t>.<raw request body>"
func verifyWebhook(body []byte, header, secret string) bool {
var ts, sig string
for _, part := range strings.Split(header, ",") {
kv := strings.SplitN(part, "=", 2)
if len(kv) != 2 {
continue
}
switch kv[0] {
case "t":
ts = kv[1]
case "v1":
sig = kv[1]
}
}
t, err := strconv.ParseInt(ts, 10, 64)
if err != nil || time.Now().Unix()-t > 300 { // 5 minute replay window
return false
}
mac := hmac.New(sha256.New, []byte(secret))
mac.Write([]byte(ts + "." + string(body)))
expected := hex.EncodeToString(mac.Sum(nil))
return hmac.Equal([]byte(expected), []byte(sig))
}
// Usage in handler
func webhookHandler(w http.ResponseWriter, r *http.Request) {
body, _ := io.ReadAll(r.Body)
if !verifyWebhook(body, r.Header.Get("X-Webhook-Signature"), os.Getenv("WEBHOOK_SECRET")) {
http.Error(w, "Invalid signature", http.StatusUnauthorized)
return
}
var event WebhookEvent
json.Unmarshal(body, &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),
})
}
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
# Place a test call that reports to your tunnel when it ends
curl -X POST https://voice-agent.edesy.in/api/v1/calls \
-H "Authorization: Bearer $API_KEY" \
-H "Content-Type: application/json" \
-d '{"agentId": 1042, "phoneNumber": "+91XXXXXXXXXX", "callbackUrl": "https://abc123.ngrok.io/webhook"}'
# Then check the delivery in your ngrok inspector (http://127.0.0.1:4040)
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