Webhook API Reference

Complete reference for webhook events, payloads, and integration.

2 min read

Base URL
https://voice-agent.edesy.in/api/v1
Auth
Authorization: Bearer vp_live_...Get a key
Spec
OpenAPI JSON

Technical reference for all webhook events sent by the voice agent platform.

Two different mechanisms — make sure you're reading the right one.

This page Call-ended callback
Configured Per workspace / agent subscription callbackUrl on a single POST/api/v1/calls
Signed Yes, HMAC-SHA256 No
Retried Yes No — single delivery
Events call.ended, call.analyzed, call.failed, call.extracted, call.recording_ready call.completed, call.failed, call.voicemail only

If you passed callbackUrl when placing a call, the payload you receive is the one documented on the Calls page, not the events below.

Webhook Delivery#

Request Format#

POST {your_webhook_url}
Content-Type: application/json
User-Agent: VoiceAgent-Webhook/1.0
X-Webhook-Signature: t=1704067200,v1=5257a869e7ecebeda32affa62cdca3fa51cad7e77a0e56ff536d0ce8e108d8bd

Signature Verification#

The signature is an HMAC-SHA256, keyed with your subscription's whsec_... secret, over the string "{t}.{raw request body}". Reject requests whose t is more than 5 minutes old.

// 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))
}
import hashlib
import hmac
import time

def verify_webhook(body: bytes, header: str, secret: str) -> bool:
    """header is the X-Webhook-Signature value: t=<unix seconds>,v1=<hex>"""
    parts = dict(p.split("=", 1) for p in header.split(",") if "=" in p)
    ts, sig = parts.get("t", ""), parts.get("v1", "")
    if not ts.isdigit() or time.time() - int(ts) > 300:
        return False
    expected = hmac.new(secret.encode(), f"{ts}.".encode() + body, hashlib.sha256).hexdigest()
    return hmac.compare_digest(expected, sig)

Event Types#

call.ended#

Triggered when a call completes.

{
  "event": "call.ended",
  "timestamp": "2024-01-01T12:05:00Z",
  "data": {
    "call_id": "call_abc123",
    "agent_id": "agent_xyz",
    "duration_seconds": 300,
    "end_reason": "user_hangup",
    "disposition": "success",
    "transcript": {
      "summary": "Customer inquired about order status...",
      "turns": [
        {
          "role": "assistant",
          "content": "Hello! How can I help you today?",
          "timestamp": "2024-01-01T12:00:01Z"
        },
        {
          "role": "user",
          "content": "I want to check my order status",
          "timestamp": "2024-01-01T12:00:05Z"
        }
      ]
    },
    "metadata": {
      "functions_called": ["get_order_status"],
      "transfer_occurred": false,
      "recording_url": "https://..."
    }
  }
}

End Reasons:

Reason Description
user_hangup User ended the call
agent_hangup Agent completed and ended
transfer Call was transferred
error Call ended due to error
timeout No activity timeout
max_duration Maximum call duration reached

call.failed#

Triggered when an outbound call fails to connect (busy, no-answer, rejected).

{
  "event": "call.failed",
  "timestamp": "2024-01-01T12:00:30Z",
  "data": {
    "call_id": "call_abc123",
    "agent_id": "agent_xyz",
    "to": "+1234567890",
    "from": "+0987654321",
    "provider": "twilio",
    "failure_reason": "busy",
    "failure_code": "3010",
    "failure_description": "Busy Line",
    "hangup_source": "Carrier",
    "workspace_id": "ws_123"
  }
}

Failure Reasons:

Reason Description
busy Recipient's line is busy
no-answer Call was not answered
rejected Call was declined by recipient
canceled Call was canceled before connecting
failed General failure (network issue)
unallocated Phone number does not exist

call.analyzed, call.extracted, call.recording_ready#

Sent when post-call analysis finishes, when extraction fields are ready, and when the recording is available. See Webhook Subscriptions for their payloads.

These five events (call.ended, call.analyzed, call.failed, call.extracted, call.recording_ready) are the only ones available for subscription.

Retry Policy#

Failed webhook deliveries are retried with exponential backoff:

Attempt Delay
1 Immediate
2 1 second
3 5 seconds
4 30 seconds
5 2 minutes

Webhooks are considered failed if:

  • Response status is not 2xx
  • Connection timeout (10 seconds)
  • Response timeout (30 seconds)

Best Practices#

1. Respond Quickly#

func handleWebhook(w http.ResponseWriter, r *http.Request) {
    // Acknowledge immediately
    w.WriteHeader(http.StatusOK)

    // Process asynchronously
    go processWebhookAsync(r.Body)
}

2. Idempotency#

func processWebhook(payload WebhookPayload) error {
    // Check if already processed
    if db.WebhookProcessed(payload.CallID, payload.Event) {
        return nil // Already handled
    }

    // Process and mark as done
    err := handleEvent(payload)
    if err == nil {
        db.MarkWebhookProcessed(payload.CallID, payload.Event)
    }
    return err
}

3. Verify Signatures#

Always verify webhook signatures in production to prevent spoofing.

Testing#

Local Development#

Use ngrok or similar to expose local endpoints:

ngrok http 3000
# Then configure webhook URL: https://abc123.ngrok.io/webhooks

Test Payload#

curl -X POST https://your-server.com/webhooks \
  -H "Content-Type: application/json" \
  -H "X-Webhook-Event: call.ended" \
  -d '{
    "event": "call.ended",
    "timestamp": "2024-01-01T12:00:00Z",
    "data": {
      "call_id": "test_123",
      "duration_seconds": 60,
      "end_reason": "user_hangup"
    }
  }'
Was this page helpful?