Call Status Webhooks
Receive real-time notifications for call lifecycle events
Call Status Webhooks
Call Status Webhooks notify your backend when call lifecycle events occur. Use them for tracking, analytics, billing, and audit logging.
Note: This is different from tool API calls. Call Status Webhooks notify you after events happen (call completed, failed, etc.). For making API calls during tool execution, see Tool Actions.
Overview
┌─────────────────┐ ┌─────────────────┐
│ Agent Studio │ │ Your Backend │
│ │ │ │
│ Call ends │ │ │
│ │ │ POST /webhooks │ │
│ ▼ │ ─────────────────► │ Handle event │
│ Dispatch │ {type: "call. │ │ │
│ webhook │ completed"...} │ ▼ │
│ │ │ Update DB, │
│ │ │ analytics, etc │
└─────────────────┘ └─────────────────┘Event Types
| Event | When Fired | Use Case |
|---|---|---|
call.started | Call initiated, waiting for connection | Track call volume |
call.connected | User connected, agent active | Monitor connection success |
call.completed | Call ended normally | Billing, analytics, audit |
call.failed | Call failed to connect | Alerting, retry logic |
call.disconnected | Call dropped unexpectedly | Quality monitoring |
call.timeout | Call timed out | Quality monitoring |
call.agent.changed | Agent handoff occurred | Workflow analytics |
Webhook Payload
Every webhook request includes:
{
"id": "evt_abc123",
"object": "event",
"api_version": "2026-01-19",
"created": 1705680000,
"type": "call.completed",
"tenant_id": "tenant-uuid",
"livemode": true,
"data": {
"call_id": "call-uuid",
"room_name": "room-abc",
"workflow_slug": "daily-checkup",
"user_id": "user-123",
"status": "completed",
"duration_seconds": 180,
"agent_history": ["greeter", "support"],
"transcript": [
{"role": "assistant", "content": "Hello!", "agent": "greeter"},
{"role": "user", "content": "Hi there"}
],
"context": {
"user.name": "John",
"workflow.logged_meals": ["Breakfast"]
},
"metrics": {
"duration_seconds": 180,
"agent_count": 2,
"message_count": 15
}
}
}Configuring Webhooks
Via Dashboard
- Navigate to Settings → Webhooks
- Enter your endpoint URL
- Select which events to receive
- Choose what data to include (transcript, context, metrics)
- Save - copy the webhook secret immediately
Via API
curl -X POST https://api.studio.tap.health/api/v1/webhooks \
-H "Authorization: Bearer $API_KEY" \
-H "Content-Type: application/json" \
-d '{
"url": "https://your-backend.com/webhooks/agent-studio",
"filter": {
"events": ["call.completed", "call.failed"],
"include_transcript": true,
"include_context": false,
"include_metrics": true
},
"headers": {
"X-Custom-Header": "value"
},
"timeout": 30,
"max_retries": 5
}'Response includes the signing secret:
{
"id": "whk_abc123",
"url": "https://your-backend.com/webhooks/agent-studio",
"secret": "whsec_abc123def456...",
"enabled": true,
"filter": {...}
}Important: Save the secret immediately - it's only shown once!
Configuration Options
| Field | Description | Default |
|---|---|---|
url | Your webhook endpoint URL | Required |
filter.events | Array of event types to receive | All events |
filter.include_transcript | Include full conversation transcript | false |
filter.include_context | Include call context (user data, workflow state) | false |
filter.include_metrics | Include call metrics (duration, message count) | true |
headers | Custom headers to include in requests | {} |
timeout | Request timeout in seconds | 30 |
max_retries | Number of retry attempts | 5 |
Security: Signature Verification
Every webhook request includes an HMAC-SHA256 signature in the X-Webhook-Signature header:
X-Webhook-Signature: t=1705680000,v1=abc123def456...Always verify signatures to prevent spoofing attacks.
Verification Example (Python)
import hmac
import hashlib
import time
WEBHOOK_SECRET = "whsec_your_secret_here"
def verify_signature(payload: bytes, signature_header: str) -> bool:
"""Verify webhook signature."""
try:
parts = dict(p.split("=") for p in signature_header.split(","))
timestamp = int(parts["t"])
signature = parts["v1"]
except (KeyError, ValueError):
return False
# Reject old timestamps (prevent replay attacks)
if abs(time.time() - timestamp) > 300: # 5 minute tolerance
return False
# Compute expected signature
signed_payload = f"{timestamp}.{payload.decode()}"
expected = hmac.new(
WEBHOOK_SECRET.encode(),
signed_payload.encode(),
hashlib.sha256,
).hexdigest()
return hmac.compare_digest(signature, expected)Verification Example (Node.js)
const crypto = require('crypto');
const WEBHOOK_SECRET = 'whsec_your_secret_here';
function verifySignature(payload, signatureHeader) {
const parts = Object.fromEntries(
signatureHeader.split(',').map(p => p.split('='))
);
const timestamp = parseInt(parts.t);
const signature = parts.v1;
// Check timestamp (5 minute tolerance)
if (Math.abs(Date.now() / 1000 - timestamp) > 300) {
return false;
}
// Compute expected signature
const signedPayload = `${timestamp}.${payload}`;
const expected = crypto
.createHmac('sha256', WEBHOOK_SECRET)
.update(signedPayload)
.digest('hex');
return crypto.timingSafeEqual(
Buffer.from(signature),
Buffer.from(expected)
);
}Delivery & Retries
Retry Behavior
Failed deliveries are retried with exponential backoff:
| Attempt | Delay |
|---|---|
| 1 | Immediate |
| 2 | 1 second |
| 3 | 2 seconds |
| 4 | 4 seconds |
| 5 | 8 seconds |
Success criteria: HTTP 2xx response within timeout.
What Triggers Retries
- Connection errors
- Timeout (default 30s)
- HTTP 5xx responses
- HTTP 429 (rate limited)
Not retried: HTTP 4xx responses (except 429) indicate client errors.
Handling Webhooks
FastAPI Example
from fastapi import FastAPI, Request, HTTPException
import json
app = FastAPI()
@app.post("/webhooks/agent-studio")
async def handle_webhook(request: Request):
body = await request.body()
signature = request.headers.get("X-Webhook-Signature", "")
if not verify_signature(body, signature):
raise HTTPException(status_code=401, detail="Invalid signature")
event = json.loads(body)
event_type = event["type"]
data = event["data"]
if event_type == "call.completed":
# Log for analytics
print(f"Call {data['call_id']} completed")
print(f" User: {data['user_id']}")
print(f" Duration: {data['duration_seconds']}s")
print(f" Workflow: {data['workflow_slug']}")
# Store for billing/analytics
# await record_call(data)
elif event_type == "call.failed":
# Alert on failures
print(f"Call {data['call_id']} failed: {data.get('status_reason')}")
# await alert_ops_team(data)
return {"status": "received"}Express.js Example
const express = require('express');
const app = express();
app.post('/webhooks/agent-studio', express.raw({type: '*/*'}), (req, res) => {
const signature = req.headers['x-webhook-signature'];
if (!verifySignature(req.body.toString(), signature)) {
return res.status(401).send('Invalid signature');
}
const event = JSON.parse(req.body);
switch (event.type) {
case 'call.completed':
console.log(`Call ${event.data.call_id} completed`);
// Handle completion
break;
case 'call.failed':
console.log(`Call ${event.data.call_id} failed`);
// Handle failure
break;
}
res.json({ status: 'received' });
});Managing Webhooks
API Endpoints
| Method | Endpoint | Description |
|---|---|---|
GET | /api/v1/webhooks | Get current configuration |
POST | /api/v1/webhooks | Create/replace (returns new secret) |
PATCH | /api/v1/webhooks | Update (keeps existing secret) |
DELETE | /api/v1/webhooks | Delete configuration |
POST | /api/v1/webhooks/test | Send test event |
POST | /api/v1/webhooks/enable | Enable webhook |
POST | /api/v1/webhooks/disable | Disable webhook |
POST | /api/v1/webhooks/rotate-secret | Generate new signing secret |
Testing Your Endpoint
Send a test event to verify your endpoint works:
curl -X POST https://api.studio.tap.health/api/v1/webhooks/test \
-H "Authorization: Bearer $API_KEY"This sends a call.test event to your configured URL.
Rotating Secrets
If your secret is compromised:
curl -X POST https://api.studio.tap.health/api/v1/webhooks/rotate-secret \
-H "Authorization: Bearer $API_KEY"Important: Update your verification code immediately after rotation.
Best Practices
- Always verify signatures - Never process unverified webhooks
- Handle idempotency - Webhooks may be delivered multiple times; use
event.idto dedupe - Respond quickly - Return 2xx within timeout; process asynchronously if needed
- Store the secret securely - Use environment variables or secret managers
- Monitor delivery - Check dashboard for failed deliveries
- Use HTTPS - Production endpoints must use HTTPS
Troubleshooting
Webhooks Not Arriving
- Check webhook is enabled in dashboard
- Verify endpoint URL is correct and publicly accessible
- Check firewall allows inbound HTTPS from Agent Studio
- Review delivery logs in dashboard
Signature Verification Failing
- Ensure you're using the correct secret
- Check timestamp tolerance (default 5 minutes)
- Verify payload isn't being modified by middleware
- Use raw body bytes, not parsed JSON
Retries Happening
- Ensure endpoint returns 2xx status
- Check endpoint responds within timeout
- Review application logs for errors