Call Webhooks
Receive real-time notifications when call analysis completes, scorecards are evaluated, or transcripts are ready.
Webhook Events
| Event | Trigger | Description |
|---|---|---|
call.imported | After import completes | Call data saved, processing may continue |
call.analysis_completed | After LLM analysis | Summary, sentiment, topics available |
call.analysis_failed | Analysis error | Check error message for details |
call.scorecard_completed | After scorecard evaluation | Scores and results available |
call.transcript_ready | Transcript available | Transcription completed (audio imports) |
Configuration
Workspace-Level
Configure webhooks in your workspace integration settings or via the API:
curl -X POST "https://api.chanl.ai/api/v1/workspaces/{workspace_id}/integrations" \
-H "Authorization: Bearer <access_token>" \
-H "Content-Type: application/json" \
-d '{
"platform": "webhooks",
"config": {
"webhookUrl": "https://your-api.com/webhooks/chanl",
"webhookSecret": "whsec_your_secret_here",
"events": ["call.analysis_completed", "call.scorecard_completed"]
}
}'const response = await fetch(`https://api.chanl.ai/api/v1/workspaces/${workspaceId}/integrations`, {
method: 'POST',
headers: {
'Authorization': `Bearer ${accessToken}`,
'Content-Type': 'application/json',
},
body: JSON.stringify({
platform: 'webhooks',
config: {
webhookUrl: 'https://your-api.com/webhooks/chanl',
webhookSecret: 'whsec_your_secret_here',
events: ['call.analysis_completed', 'call.scorecard_completed']
}
})
});
const data = await response.json();import requests
response = requests.post(
f'https://api.chanl.ai/api/v1/workspaces/{workspace_id}/integrations',
headers={'Authorization': f'Bearer {access_token}'},
json={
'platform': 'webhooks',
'config': {
'webhookUrl': 'https://your-api.com/webhooks/chanl',
'webhookSecret': 'whsec_your_secret_here',
'events': ['call.analysis_completed', 'call.scorecard_completed']
}
}
)
data = response.json()Per-Request Override
Override the webhook URL for individual imports:
{
"transcript": "...",
"webhookUrl": "https://your-api.com/webhooks/custom",
"webhookSecret": "whsec_custom_secret"
}Payload Format
{
"id": "evt_abc123",
"event": "call.scorecard_completed",
"timestamp": "2026-01-06T19:58:15.000Z",
"data": {
"call": {
"id": "695d6957e21c0ceb325c394d",
"workspaceId": "ws_123",
"status": "ended",
"duration": 180,
"externalReferenceIds": {
"orderId": "ORDER-12345",
"customerId": "cust_abc123"
},
"summary": {
"sentiment": {
"beginning": "neutral",
"ending": "positive",
"trend": "improving"
},
"actionItems": {
"agentCommitments": ["Send confirmation email"],
"followUpRequired": true,
"followUpTimeframe": "24 hours"
}
}
},
"workspace": {
"id": "ws_123",
"name": "My Workspace"
},
"scorecard": {
"id": "scorecard_abc",
"name": "Quality Scorecard",
"score": 85,
"status": "completed",
"categoryScores": {
"Communication": 90,
"Resolution": 80,
"Compliance": 85
}
}
},
"metadata": {
"version": "2025-01-06",
"processingTimeMs": 2500,
"analysisFields": ["followups", "sentiment", "quality"]
}
}Event Payloads
call.imported
Sent when a call is successfully imported:
{
"event": "call.imported",
"data": {
"call": {
"id": "695d6957e21c0ceb325c394d",
"status": "processing",
"externalReferenceIds": { ... }
},
"import": {
"mode": "transcriptOnly",
"processingStatus": "pending"
}
}
}call.analysis_completed
Sent when LLM analysis completes:
{
"event": "call.analysis_completed",
"data": {
"call": {
"id": "695d6957e21c0ceb325c394d",
"summary": {
"sentiment": { ... },
"topics": ["billing", "refund"],
"keywords": ["refund", "overcharge", "credit"],
"actionItems": { ... },
"qualityMetrics": { ... }
},
"extractedData": {
"email": { "value": "john@example.com", "confidence": 0.95 }
}
}
}
}call.scorecard_completed
Sent when scorecard evaluation completes:
{
"event": "call.scorecard_completed",
"data": {
"call": { ... },
"scorecard": {
"id": "scorecard_abc",
"name": "Quality Scorecard",
"score": 85,
"status": "completed",
"categoryScores": {
"Communication": 90,
"Resolution": 80
},
"criteriaResults": [
{
"criterionId": "crit_1",
"name": "Proper Greeting",
"passed": true,
"score": 100
}
]
}
}
}Signature Verification
Verify webhook authenticity using HMAC-SHA256:
const crypto = require('crypto');
function verifyWebhook(
payload: string,
signature: string,
timestamp: string,
secret: string
): boolean {
const expectedSignature = crypto
.createHmac('sha256', secret)
.update(`${timestamp}.${payload}`)
.digest('hex');
return crypto.timingSafeEqual(
Buffer.from(signature),
Buffer.from(expectedSignature)
);
}
// Express.js example
app.post('/webhook', (req, res) => {
const signature = req.headers['x-chanl-signature'];
const timestamp = req.headers['x-chanl-timestamp'];
const eventId = req.headers['x-chanl-event-id'];
if (!verifyWebhook(JSON.stringify(req.body), signature, timestamp, WEBHOOK_SECRET)) {
return res.status(401).send('Invalid signature');
}
const { event, data } = req.body;
switch (event) {
case 'call.scorecard_completed':
console.log('Scorecard score:', data.scorecard.score);
break;
case 'call.analysis_completed':
console.log('Sentiment:', data.call.summary.sentiment);
break;
}
res.status(200).send('OK');
});import hmac
import hashlib
def verify_webhook(payload: str, signature: str, timestamp: str, secret: str) -> bool:
expected = hmac.new(
secret.encode(),
f"{timestamp}.{payload}".encode(),
hashlib.sha256
).hexdigest()
return hmac.compare_digest(signature, expected)
# Flask example
@app.route('/webhook', methods=['POST'])
def webhook():
signature = request.headers.get('x-chanl-signature')
timestamp = request.headers.get('x-chanl-timestamp')
if not verify_webhook(request.data.decode(), signature, timestamp, WEBHOOK_SECRET):
return 'Invalid signature', 401
data = request.json
event = data['event']
if event == 'call.scorecard_completed':
print(f"Scorecard score: {data['data']['scorecard']['score']}")
return 'OK', 200Webhook Headers
| Header | Description |
|---|---|
x-chanl-signature | HMAC-SHA256 signature |
x-chanl-timestamp | Unix timestamp of the request |
x-chanl-event-id | Unique event ID for idempotency |
x-chanl-delivery-attempt | Retry attempt number (1, 2, or 3) |
Retry Behavior
- Up to 3 retry attempts on failure
- Exponential backoff: 1s, 2s, 4s between retries
- 10 second timeout per request
- Failed deliveries are logged for debugging
Best Practices
Use the x-chanl-event-id header for idempotency. Store processed event IDs to prevent duplicate handling.
- Always verify the signature before processing
- Respond with 200 status quickly, process asynchronously
- Use the
externalReferenceIdsto correlate with your systems - Implement retry handling for your downstream services