Webhooks
Receive real-time notifications when events happen in your Crixin account.
Webhooks allow you to receive HTTP POST requests to your server when events occur, such as when a call starts, ends, or when a transcript is ready.
How it works:
- Configure your webhook URL in the dashboard
- Crixin sends HTTP POST requests to your URL when events occur
- Your server processes the event and returns a 200 response
- If your server fails to respond, we retry with exponential backoff
1. Create a webhook endpoint
Create an HTTP endpoint on your server that accepts POST requests.
// Express.js example
app.post('/webhooks/crixin', (req, res) => {
const event = req.body;
console.log('Received event:', event.type);
// Process the event
// ...
res.status(200).json({ received: true });
});2. Configure in Dashboard
Go to Dashboard → Developers and add your webhook URL.
3. Verify signatures
Always verify webhook signatures to ensure requests are from Crixin.
Sent when an inbound call connects to your assistant.
{
"type": "call.started",
"data": {
"callId": "call_abc123",
"assistantId": "asst_xyz789",
"assistantName": "Support Agent",
"phoneNumber": "+14155551234",
"callerNumber": "+14155559999",
"direction": "inbound",
"startedAt": "2025-01-15T10:30:00Z"
},
"timestamp": "2025-01-15T10:30:00Z"
}Sent when a call ends successfully.
{
"type": "call.completed",
"data": {
"callId": "call_abc123",
"assistantId": "asst_xyz789",
"duration": 127,
"endedAt": "2025-01-15T10:32:07Z",
"summary": "Customer inquired about order status for order #12345"
},
"timestamp": "2025-01-15T10:32:07Z"
}Sent when a call fails due to an error.
{
"type": "call.failed",
"data": {
"callId": "call_abc123",
"assistantId": "asst_xyz789",
"error": {
"code": "ASSISTANT_UNAVAILABLE",
"message": "The assistant is not currently active"
},
"failedAt": "2025-01-15T10:30:05Z"
},
"timestamp": "2025-01-15T10:30:05Z"
}Sent when a call was not answered.
{
"type": "call.missed",
"data": {
"callId": "call_abc123",
"assistantId": "asst_xyz789",
"callerNumber": "+14155559999",
"reason": "no_answer"
},
"timestamp": "2025-01-15T10:30:30Z"
}Sent when the call transcript has been processed and is available.
{
"type": "transcript.ready",
"data": {
"callId": "call_abc123",
"assistantId": "asst_xyz789",
"messageCount": 12,
"duration": 127
},
"timestamp": "2025-01-15T10:32:30Z"
}Sent when usage reaches 80%, 90%, or 100% of included minutes.
{
"type": "usage.threshold",
"data": {
"threshold": 80,
"minutesUsed": 400,
"minutesIncluded": 500,
"tier": "starter"
},
"timestamp": "2025-01-15T10:32:30Z"
}Every webhook request includes a signature header that you should verify to ensure the request came from Crixin and wasn't tampered with.
Headers included:
X-Crixin-Signature- HMAC-SHA256 signatureX-Crixin-Timestamp- Unix timestamp of the request
Node.js Example
const crypto = require('crypto');
function verifyWebhook(body, signature, timestamp, secret) {
// Check timestamp is within 5 minutes
const now = Math.floor(Date.now() / 1000);
if (Math.abs(now - parseInt(timestamp)) > 300) {
return false;
}
// Compute expected signature
const payload = `${timestamp}.${JSON.stringify(body)}`;
const expected = crypto
.createHmac('sha256', secret)
.update(payload)
.digest('hex');
// Compare signatures (timing-safe)
return crypto.timingSafeEqual(
Buffer.from(signature),
Buffer.from(expected)
);
}
// Usage in Express
app.post('/webhooks/crixin', (req, res) => {
const signature = req.headers['x-crixin-signature'];
const timestamp = req.headers['x-crixin-timestamp'];
if (!verifyWebhook(req.body, signature, timestamp, process.env.WEBHOOK_SECRET)) {
return res.status(401).json({ error: 'Invalid signature' });
}
// Process event...
res.json({ received: true });
});Python Example
import hmac
import hashlib
import time
import json
def verify_webhook(body: dict, signature: str, timestamp: str, secret: str) -> bool:
# Check timestamp is within 5 minutes
now = int(time.time())
if abs(now - int(timestamp)) > 300:
return False
# Compute expected signature
payload = f"{timestamp}.{json.dumps(body)}"
expected = hmac.new(
secret.encode('utf-8'),
payload.encode('utf-8'),
hashlib.sha256
).hexdigest()
# Compare signatures (timing-safe)
return hmac.compare_digest(signature, expected)
# Usage in Flask
@app.route('/webhooks/crixin', methods=['POST'])
def handle_webhook():
signature = request.headers.get('X-Crixin-Signature')
timestamp = request.headers.get('X-Crixin-Timestamp')
if not verify_webhook(request.json, signature, timestamp, os.environ['WEBHOOK_SECRET']):
return jsonify({'error': 'Invalid signature'}), 401
# Process event...
return jsonify({'received': True})If your endpoint returns a non-2xx status code or times out, we'll retry the request with exponential backoff.
| Attempt | Delay |
|---|---|
| 1st retry | 1 minute |
| 2nd retry | 5 minutes |
| 3rd retry | 30 minutes |
| 4th retry | 2 hours |
| 5th retry (final) | 24 hours |
Important
Your endpoint should respond within 30 seconds. If processing takes longer, acknowledge the webhook immediately and process asynchronously.
- ✓Respond quickly
Return a 200 response immediately, then process the event asynchronously.
- ✓Handle duplicates
Use the event ID to deduplicate. Retries may cause the same event to be sent multiple times.
- ✓Verify signatures
Always verify the webhook signature before processing any data.
- ✓Use HTTPS
Only use HTTPS endpoints. We won't send webhooks to HTTP URLs.
- ✓Log everything
Log webhook payloads for debugging. Include the request ID in your logs.
Test your webhook endpoint locally before deploying to production.
Using ngrok for local testing
# Install ngrok
brew install ngrok # macOS
# or download from ngrok.com
# Start your local server
npm run dev # or your server start command
# In another terminal, expose your local server
ngrok http 3000
# Use the ngrok URL in your Crixin dashboard
# https://abc123.ngrok.io/webhooks/crixinTest with curl
# Send a test webhook to your endpoint
curl -X POST http://localhost:3000/webhooks/crixin \
-H "Content-Type: application/json" \
-H "X-Crixin-Signature: test_signature" \
-H "X-Crixin-Timestamp: $(date +%s)" \
-d '{
"type": "call.completed",
"data": {
"callId": "call_test123",
"duration": 60
},
"timestamp": "'$(date -u +%Y-%m-%dT%H:%M:%SZ)'"
}'