Webhooks

Receive real-time notifications for call events, conversation updates, and agent actions.

2 min read

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#

Was this page helpful?