Webhook API Reference

Complete reference for webhook events, payloads, and integration.

3 min read

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"
    }
  }'
Was this page helpful? Suggest an edit