# Webhooks

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

Source: https://edesy.in/docs/voice-agent/features/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.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

```json
{
  "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

```json
{
  "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

```json
{
  "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

```json
{
  "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.

```json
{
  "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).

```json
{
  "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": "customer@example.com",
      "callback_requested": false
    }
  }
}
```

## Webhook Subscription Management

Manage webhooks programmatically via the API.

### List Subscriptions

```bash
GET /api/webhooks/subscriptions
```

**Response:**
```json
{
  "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

```bash
POST /api/webhooks/subscriptions
```

**Request:**
```json
{
  "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:

```bash
POST /api/webhooks/subscriptions/{id}/rotate-secret
```

**Response:**
```json
{
  "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:

```bash
POST /api/webhooks/subscriptions/{id}/test
```

### View Delivery Logs

See recent webhook delivery attempts:

```bash
GET /api/webhooks/subscriptions/{id}/deliveries
```

**Response:**
```json
{
  "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

```go
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}"`.

```go
// 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))
}
```

```go
// 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

```go
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:

```json
{
  "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

```go
// 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

```go
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

```go
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

```bash
# 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](https://edesy.in/docs/voice-agent/workspace/finding-workspace-id) - Get your workspace ID for webhook URLs
- [Function Calling](https://edesy.in/docs/voice-agent/features/function-calling) - Trigger functions
- [Recording](https://edesy.in/docs/voice-agent/features/recording) - Call recordings
- [API Reference](https://edesy.in/docs/voice-agent/api) - Full API docs
