Chanl

Call Webhooks

Receive real-time notifications when call analysis completes, scorecards are evaluated, or transcripts are ready.

Webhook Events

EventTriggerDescription
call.importedAfter import completesCall data saved, processing may continue
call.analysis_completedAfter LLM analysisSummary, sentiment, topics available
call.analysis_failedAnalysis errorCheck error message for details
call.scorecard_completedAfter scorecard evaluationScores and results available
call.transcript_readyTranscript availableTranscription 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', 200

Webhook Headers

HeaderDescription
x-chanl-signatureHMAC-SHA256 signature
x-chanl-timestampUnix timestamp of the request
x-chanl-event-idUnique event ID for idempotency
x-chanl-delivery-attemptRetry 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 externalReferenceIds to correlate with your systems
  • Implement retry handling for your downstream services

On this page