Webhook API Reference

Complete reference for webhook events, payloads, and integration.

Webhook API Reference

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.started, call.ended, transcript.updated, function.called, … 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
X-Webhook-Signature: sha256=abc123...
X-Webhook-Event: call.ended
X-Webhook-Timestamp: 1704067200

Signature Verification

func verifyWebhook(payload []byte, signature, secret string) bool {
    mac := hmac.New(sha256.New, []byte(secret))
    mac.Write(payload)
    expected := "sha256=" + hex.EncodeToString(mac.Sum(nil))
    return hmac.Equal([]byte(expected), []byte(signature))
}
import hmac
import hashlib

def verify_webhook(payload: bytes, signature: str, secret: str) -> bool:
    expected = "sha256=" + hmac.new(
        secret.encode(), payload, hashlib.sha256
    ).hexdigest()
    return hmac.compare_digest(expected, signature)

Event Types

call.started

Triggered when a call begins.

{
  "event": "call.started",
  "timestamp": "2024-01-01T12:00:00Z",
  "data": {
    "call_id": "call_abc123",
    "agent_id": "agent_xyz",
    "direction": "inbound",
    "from": "+1234567890",
    "to": "+0987654321",
    "provider": "twilio",
    "stream_sid": "MZxxx",
    "call_sid": "CAxxx",
    "workspace_id": "ws_123"
  }
}

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.transferred

Triggered when a call is transferred.

{
  "event": "call.transferred",
  "timestamp": "2024-01-01T12:03:00Z",
  "data": {
    "call_id": "call_abc123",
    "transfer_to": "+1555123456",
    "transfer_type": "blind",
    "reason": "customer_request",
    "context": "Customer requested to speak with a manager"
  }
}

transcript.updated

Real-time transcript updates during the call.

{
  "event": "transcript.updated",
  "timestamp": "2024-01-01T12:01:30Z",
  "data": {
    "call_id": "call_abc123",
    "turn": {
      "role": "user",
      "content": "I want to check my order ORD12345",
      "timestamp": "2024-01-01T12:01:30Z",
      "is_final": true,
      "confidence": 0.95
    },
    "sequence_number": 5
  }
}

function.called

Triggered when a function/tool is executed.

{
  "event": "function.called",
  "timestamp": "2024-01-01T12:01:35Z",
  "data": {
    "call_id": "call_abc123",
    "function": {
      "name": "get_order_status",
      "parameters": {
        "order_id": "ORD12345"
      },
      "result": {
        "status": "shipped",
        "delivery_date": "2024-01-03"
      },
      "duration_ms": 150
    }
  }
}

error.occurred

Triggered when an error occurs during the call.

{
  "event": "error.occurred",
  "timestamp": "2024-01-01T12:02:00Z",
  "data": {
    "call_id": "call_abc123",
    "error": {
      "code": "stt_timeout",
      "message": "Speech-to-text service timed out",
      "severity": "warning",
      "recovered": true
    }
  }
}

Error Codes:

Code Severity Description
stt_timeout warning STT service timeout
stt_error error STT service failure
tts_error error TTS service failure
llm_timeout warning LLM response timeout
llm_error error LLM service failure
function_error warning Tool execution failed
connection_lost error WebSocket disconnected

dtmf.received

Triggered when DTMF input is received.

{
  "event": "dtmf.received",
  "timestamp": "2024-01-01T12:01:45Z",
  "data": {
    "call_id": "call_abc123",
    "digits": "12345#",
    "aggregated": true
  }
}

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"
    }
  }'