Webhooks
Receive real-time notifications for call completions, messages, and agent events
Webhooks
Webhooks allow you to receive real-time HTTP notifications when events occur in your Heppu AI account. Instead of polling for changes, Heppu will send POST requests to your specified endpoint whenever subscribed events happen.
Overview
When an event occurs (like a call completing or a message being received), Heppu sends an HTTP POST request to all registered webhook URLs. Your server receives the event data and can process it immediately.
Benefits:
- Real-time notifications
- No need for constant polling
- Reduced API calls and improved efficiency
- Immediate reaction to important events
Webhooks are sent from Heppu's servers to your endpoint, so your endpoint must be publicly accessible via HTTPS.
Available Events
Heppu supports webhook events for both voice calls and chat conversations.
Call Webhook Events
call.completed
Triggered when a voice call finishes successfully.
{
"event": "call.completed",
"timestamp": "2024-12-01T12:00:00Z",
"webhookId": "whd_abc123",
"agentId": "550e8400-e29b-41d4-a716-446655440000",
"agentName": "Sales Agent",
"conversationId": "conv_xyz789",
"conversationType": "phone",
"status": "completed",
"outcome": "resolved",
"summary": "Customer called to inquire about their order status. Agent confirmed the order is being processed and will ship within 2 business days.",
"duration": 125,
"direction": "inbound",
"agentNumber": "+14155551234",
"twilioCallSid": "CA1234567890abcdef",
"callMetadata": {
"customer": {
"name": "John Doe",
"company": "Acme Corp"
},
"context": {
"campaignType": "customer_support",
"priority": "normal"
},
"custom": {
"orderId": "ORD-12345",
"accountManager": "Jane Smith"
}
},
"metadata": {
"contactName": "John Doe",
"contactPhone": "+1234567890",
"contactEmail": "john@example.com"
},
"organizationId": "org_abc123"
}Use Cases:
- Log completed calls to your CRM
- Send follow-up emails or SMS
- Update customer records with call summary
- Generate analytics reports
- Trigger post-call workflows
call.failed
Triggered when a voice call encounters a technical error.
{
"event": "call.failed",
"timestamp": "2024-12-01T12:00:00Z",
"webhookId": "whd_def456",
"agentId": "550e8400-e29b-41d4-a716-446655440000",
"agentName": "Sales Agent",
"conversationId": "conv_abc789",
"conversationType": "phone",
"status": "failed",
"outcome": null,
"duration": 0,
"direction": "outbound",
"agentNumber": "+14155551234",
"twilioCallSid": "CA0987654321fedcba",
"callMetadata": {
"context": {
"campaignType": "outbound_sales",
"priority": "high",
"previousAttempts": 0
}
},
"metadata": {
"contactName": "Jane Smith",
"contactPhone": "+1987654321"
},
"organizationId": "org_abc123"
}Use Cases:
- Queue calls for retry
- Alert team about connection issues
- Log failures for investigation
- Update call campaign status
call.busy
Triggered when the recipient's phone line is busy.
{
"event": "call.busy",
"timestamp": "2024-12-01T12:00:00Z",
"webhookId": "whd_ghi789",
"agentId": "550e8400-e29b-41d4-a716-446655440000",
"agentName": "Appointment Reminder",
"conversationId": "conv_busy123",
"conversationType": "phone",
"status": "busy",
"outcome": null,
"duration": 0,
"direction": "outbound",
"agentNumber": "+14155551234",
"callMetadata": {
"context": {
"campaignType": "appointment_reminder",
"previousAttempts": 1
}
},
"metadata": {
"contactName": "Bob Wilson",
"contactPhone": "+1555123456"
},
"organizationId": "org_abc123"
}Use Cases:
- Schedule automatic retry after delay
- Send SMS as fallback
- Update contact status in CRM
- Track busy signal patterns
call.no-answer
Triggered when the call rings but is not answered.
{
"event": "call.no-answer",
"timestamp": "2024-12-01T12:00:00Z",
"webhookId": "whd_jkl012",
"agentId": "550e8400-e29b-41d4-a716-446655440000",
"agentName": "Follow-up Agent",
"conversationId": "conv_noanswer456",
"conversationType": "phone",
"status": "no_answer",
"outcome": null,
"duration": 30,
"direction": "outbound",
"agentNumber": "+14155551234",
"callMetadata": {
"context": {
"campaignType": "follow_up",
"previousAttempts": 2,
"maxAttempts": 3
}
},
"metadata": {
"contactName": "Alice Johnson",
"contactPhone": "+1555987654"
},
"organizationId": "org_abc123"
}Use Cases:
- Retry at a different time
- Send voicemail or SMS
- Update lead status
- Trigger alternative contact method
Chat Webhook Events
chat.completed
Triggered when a chat conversation ends (any outcome).
{
"event": "chat.completed",
"timestamp": "2024-12-01T12:00:00Z",
"webhookId": "whd_mno345",
"agentId": "550e8400-e29b-41d4-a716-446655440000",
"agentName": "Support Bot",
"conversationId": "conv_chat789",
"conversationType": "chat",
"status": "completed",
"outcome": "ongoing",
"summary": "Customer asked about pricing tiers. Agent provided overview of available plans.",
"duration": 180,
"messageCount": 8,
"chatMetadata": {
"widgetId": "widget_abc123",
"resolved": false,
"totalCost": 0.0023,
"toolsUsed": ["knowledge_base", "pricing_lookup"],
"firstMessageAt": "2024-12-01T11:57:00Z",
"lastMessageAt": "2024-12-01T12:00:00Z"
},
"metadata": {
"visitorId": "visitor_xyz789",
"ipAddress": "192.168.1.1",
"userAgent": "Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7)",
"referrer": "https://example.com/pricing"
},
"organizationId": "org_abc123"
}Use Cases:
- Archive chat transcripts
- Update customer records
- Trigger follow-up workflows
- Aggregate conversation analytics
chat.resolved
Triggered when a chat is explicitly marked as resolved by the agent or user.
{
"event": "chat.resolved",
"timestamp": "2024-12-01T12:00:00Z",
"webhookId": "whd_pqr678",
"agentId": "550e8400-e29b-41d4-a716-446655440000",
"agentName": "Support Bot",
"conversationId": "conv_resolved123",
"conversationType": "chat",
"status": "completed",
"outcome": "resolved",
"summary": "Customer had trouble logging in. Agent helped reset password and verified account access.",
"duration": 240,
"messageCount": 12,
"chatMetadata": {
"widgetId": "widget_abc123",
"resolved": true,
"feedback": {
"rating": 5,
"comment": "Very helpful, thank you!"
},
"totalCost": 0.0045,
"toolsUsed": ["password_reset", "account_verification"],
"firstMessageAt": "2024-12-01T11:56:00Z",
"lastMessageAt": "2024-12-01T12:00:00Z",
"goal": {
"achieved": true,
"type": "password_reset"
}
},
"metadata": {
"visitorId": "visitor_def456",
"contactEmail": "user@example.com"
},
"organizationId": "org_abc123"
}Use Cases:
- Close support tickets
- Update CSAT scores
- Trigger satisfaction surveys
- Calculate resolution metrics
chat.abandoned
Triggered when a chat is auto-closed due to user inactivity.
{
"event": "chat.abandoned",
"timestamp": "2024-12-01T12:30:00Z",
"webhookId": "whd_stu901",
"agentId": "550e8400-e29b-41d4-a716-446655440000",
"agentName": "Support Bot",
"conversationId": "conv_abandoned456",
"conversationType": "chat",
"status": "completed",
"outcome": "auto_completed_inactive",
"summary": "Customer started asking about product features but stopped responding mid-conversation.",
"duration": 1800,
"messageCount": 4,
"chatMetadata": {
"widgetId": "widget_abc123",
"resolved": false,
"totalCost": 0.0012,
"toolsUsed": ["product_catalog"],
"firstMessageAt": "2024-12-01T12:00:00Z",
"lastMessageAt": "2024-12-01T12:05:00Z"
},
"metadata": {
"visitorId": "visitor_ghi789",
"referrer": "https://example.com/products"
},
"organizationId": "org_abc123"
}Use Cases:
- Send follow-up email
- Analyze abandonment patterns
- Trigger re-engagement campaigns
- Update lead scoring
Event Summary
| Event | Conversation Type | Trigger |
|---|---|---|
call.completed | Phone | Call ends successfully |
call.failed | Phone | Technical failure or error |
call.busy | Phone | Recipient line is busy |
call.no-answer | Phone | No answer within timeout |
chat.completed | Chat | Chat ends (any outcome) |
chat.resolved | Chat | Chat marked as resolved |
chat.abandoned | Chat | Auto-closed due to inactivity |
Creating Webhooks
Via Dashboard
- Navigate to Organization → Webhooks
- Click Create Webhook
- Enter your webhook URL (must be HTTPS)
- Select the events you want to receive
- Optionally add a description
- Click Create
Via API
curl -X POST https://v2.heppu.ai/api/v1/webhooks \
-H "x-api-key: $HEPPU_API_KEY" \
-H "Content-Type: application/json" \
-d '{
"url": "https://api.yourcompany.com/webhooks/heppu",
"events": ["call.completed", "call.failed", "chat.message.received"],
"description": "Production webhook endpoint",
"secret": "whsec_your_secret_key_here"
}'const response = await fetch('https://v2.heppu.ai/api/v1/webhooks', {
method: 'POST',
headers: {
'x-api-key': process.env.HEPPU_API_KEY,
'Content-Type': 'application/json'
},
body: JSON.stringify({
url: 'https://api.yourcompany.com/webhooks/heppu',
events: ['call.completed', 'call.failed', 'chat.message.received'],
description: 'Production webhook endpoint',
secret: 'whsec_your_secret_key_here'
})
});
const webhook = await response.json();
console.log('Webhook created:', webhook.data.id);import requests
import os
response = requests.post(
'https://v2.heppu.ai/api/v1/webhooks',
headers={'x-api-key': os.environ.get('HEPPU_API_KEY')},
json={
'url': 'https://api.yourcompany.com/webhooks/heppu',
'events': ['call.completed', 'call.failed', 'chat.message.received'],
'description': 'Production webhook endpoint',
'secret': 'whsec_your_secret_key_here'
}
)
webhook = response.json()
print('Webhook created:', webhook['data']['id'])Response:
{
"data": {
"id": "webhook_pqr678",
"url": "https://api.yourcompany.com/webhooks/heppu",
"events": ["call.completed", "call.failed", "chat.message.received"],
"description": "Production webhook endpoint",
"status": "active",
"createdAt": "2024-12-01T12:00:00Z",
"lastDeliveredAt": null,
"secretPrefix": "whsec_abc"
},
"meta": {
"timestamp": "2024-12-01T12:00:00Z",
"message": "Webhook created successfully"
}
}The webhook secret is only shown during creation. Store it securely - you'll need it to verify webhook signatures.
Receiving Webhooks
Webhook Request Format
Heppu sends webhooks as HTTP POST requests with these headers:
POST /webhooks/heppu HTTP/1.1
Host: api.yourcompany.com
Content-Type: application/json
X-Webhook-Event: call.completed
X-Webhook-ID: whd_abc123
X-Webhook-Timestamp: 1701432000
X-Webhook-Signature-256: sha256=5c9f9b9e8d6f5c4b3a2d1e0f9a8b7c6d...
User-Agent: Heppu-Webhooks/1.0
{
"event": "call.completed",
"timestamp": "2024-12-01T12:00:00Z",
"webhookId": "whd_abc123",
...
}Headers:
X-Webhook-Event: The event type (e.g.,call.completed)X-Webhook-ID: Unique webhook delivery ID for deduplicationX-Webhook-Timestamp: Unix timestamp when the request was sentX-Webhook-Signature-256: HMAC-SHA256 signature (if secret is configured)
Basic Webhook Handler
const express = require('express');
const app = express();
app.post('/webhooks/heppu', express.json(), (req, res) => {
const event = req.body;
const webhookId = req.headers['x-webhook-id'];
console.log('Received webhook:', event.event, 'ID:', webhookId);
// Process based on event type
switch (event.event) {
// Call events
case 'call.completed':
handleCallCompleted(event);
break;
case 'call.failed':
case 'call.busy':
case 'call.no-answer':
handleCallFailed(event);
break;
// Chat events
case 'chat.completed':
case 'chat.resolved':
handleChatCompleted(event);
break;
case 'chat.abandoned':
handleChatAbandoned(event);
break;
default:
console.log('Unknown event type:', event.event);
}
// Acknowledge receipt immediately
res.status(200).json({ received: true });
});
function handleCallCompleted(event) {
console.log(`Call ${event.conversationId} completed`);
console.log(`Duration: ${event.duration}s`);
console.log(`Summary: ${event.summary}`);
// Update CRM, send follow-up, etc.
}
function handleCallFailed(event) {
console.log(`Call ${event.conversationId} ${event.event}`);
const attempts = event.callMetadata?.context?.previousAttempts || 0;
console.log(`Previous attempts: ${attempts}`);
// Queue for retry, alert team, etc.
}
function handleChatCompleted(event) {
console.log(`Chat ${event.conversationId} completed`);
console.log(`Messages: ${event.messageCount}`);
console.log(`Resolved: ${event.chatMetadata?.resolved}`);
// Archive transcript, close ticket, etc.
}
function handleChatAbandoned(event) {
console.log(`Chat ${event.conversationId} abandoned`);
// Send follow-up email, update lead score, etc.
}
app.listen(3000, () => {
console.log('Webhook server listening on port 3000');
});from flask import Flask, request, jsonify
app = Flask(__name__)
@app.route('/webhooks/heppu', methods=['POST'])
def handle_webhook():
event = request.json
webhook_id = request.headers.get('X-Webhook-ID')
print(f"Received webhook: {event['event']} ID: {webhook_id}")
# Process based on event type
event_type = event['event']
# Call events
if event_type == 'call.completed':
handle_call_completed(event)
elif event_type in ['call.failed', 'call.busy', 'call.no-answer']:
handle_call_failed(event)
# Chat events
elif event_type in ['chat.completed', 'chat.resolved']:
handle_chat_completed(event)
elif event_type == 'chat.abandoned':
handle_chat_abandoned(event)
else:
print(f"Unknown event type: {event_type}")
# Acknowledge receipt
return jsonify({'received': True}), 200
def handle_call_completed(event):
print(f"Call {event['conversationId']} completed")
print(f"Duration: {event.get('duration')}s")
print(f"Summary: {event.get('summary')}")
# Update CRM, send follow-up, etc.
def handle_call_failed(event):
print(f"Call {event['conversationId']} {event['event']}")
call_meta = event.get('callMetadata', {})
context = call_meta.get('context', {})
print(f"Previous attempts: {context.get('previousAttempts', 0)}")
# Queue for retry, alert team, etc.
def handle_chat_completed(event):
print(f"Chat {event['conversationId']} completed")
print(f"Messages: {event.get('messageCount')}")
chat_meta = event.get('chatMetadata', {})
print(f"Resolved: {chat_meta.get('resolved')}")
# Archive transcript, close ticket, etc.
def handle_chat_abandoned(event):
print(f"Chat {event['conversationId']} abandoned")
# Send follow-up email, update lead score, etc.
if __name__ == '__main__':
app.run(port=3000)// app/api/webhooks/heppu/route.ts
import { NextRequest, NextResponse } from 'next/server';
interface WebhookEvent {
event: string;
timestamp: string;
webhookId: string;
conversationId: string;
conversationType: 'phone' | 'chat';
agentId: string;
agentName: string;
status: string;
outcome: string | null;
summary: string | null;
duration: number | null;
messageCount?: number;
direction?: 'inbound' | 'outbound';
callMetadata?: Record<string, any>;
chatMetadata?: Record<string, any>;
metadata: Record<string, any>;
organizationId: string;
}
export async function POST(request: NextRequest) {
try {
const event: WebhookEvent = await request.json();
const webhookId = request.headers.get('x-webhook-id');
console.log('Received webhook:', event.event, 'ID:', webhookId);
// Process based on event type
switch (event.event) {
// Call events
case 'call.completed':
await handleCallCompleted(event);
break;
case 'call.failed':
case 'call.busy':
case 'call.no-answer':
await handleCallFailed(event);
break;
// Chat events
case 'chat.completed':
case 'chat.resolved':
await handleChatCompleted(event);
break;
case 'chat.abandoned':
await handleChatAbandoned(event);
break;
default:
console.log('Unknown event type:', event.event);
}
return NextResponse.json({ received: true });
} catch (error) {
console.error('Webhook error:', error);
return NextResponse.json(
{ error: 'Webhook processing failed' },
{ status: 500 }
);
}
}
async function handleCallCompleted(event: WebhookEvent) {
console.log(`Call ${event.conversationId} completed`);
console.log(`Duration: ${event.duration}s`);
console.log(`Summary: ${event.summary}`);
// Update CRM, send follow-up, etc.
}
async function handleCallFailed(event: WebhookEvent) {
console.log(`Call ${event.conversationId} ${event.event}`);
const attempts = event.callMetadata?.context?.previousAttempts || 0;
console.log(`Previous attempts: ${attempts}`);
// Queue for retry, alert team, etc.
}
async function handleChatCompleted(event: WebhookEvent) {
console.log(`Chat ${event.conversationId} completed`);
console.log(`Messages: ${event.messageCount}`);
console.log(`Resolved: ${event.chatMetadata?.resolved}`);
// Archive transcript, close ticket, etc.
}
async function handleChatAbandoned(event: WebhookEvent) {
console.log(`Chat ${event.conversationId} abandoned`);
// Send follow-up email, update lead score, etc.
}Verifying Webhook Signatures
Always verify webhook signatures to ensure requests are from Heppu and haven't been tampered with.
Signature Format
The X-Webhook-Signature-256 header contains an HMAC-SHA256 signature:
sha256=5c9f9b9e8d6f5c4b3a2d1e0f9a8b7c6d5e4f3a2b1c0d9e8f7a6b5c4d3e2f1a0bThe signature is computed as: sha256=hex(HMAC-SHA256(payload, secret))
Use the X-Webhook-Timestamp header to prevent replay attacks.
Verification Implementation
const crypto = require('crypto');
function verifyWebhookSignature(payload, signature, timestamp, secret) {
// Check timestamp (prevent replay attacks)
const currentTime = Math.floor(Date.now() / 1000);
const timeDiff = currentTime - parseInt(timestamp);
if (timeDiff > 300) { // 5 minutes
throw new Error('Webhook timestamp too old');
}
// Parse signature (format: sha256=<hash>)
if (!signature.startsWith('sha256=')) {
throw new Error('Invalid signature format');
}
const receivedHash = signature.replace('sha256=', '');
// Compute expected signature
const expectedHash = crypto
.createHmac('sha256', secret)
.update(payload)
.digest('hex');
// Compare signatures (constant-time comparison)
if (!crypto.timingSafeEqual(
Buffer.from(receivedHash),
Buffer.from(expectedHash)
)) {
throw new Error('Signature verification failed');
}
return true;
}
// Express middleware
function webhookVerification(req, res, next) {
const signature = req.headers['x-webhook-signature-256'];
const timestamp = req.headers['x-webhook-timestamp'];
const secret = process.env.WEBHOOK_SECRET;
// Skip verification if no secret configured
if (!secret) {
return next();
}
try {
verifyWebhookSignature(req.rawBody, signature, timestamp, secret);
next();
} catch (error) {
console.error('Webhook verification failed:', error.message);
return res.status(401).json({ error: 'Invalid signature' });
}
}
// Usage - must capture raw body before JSON parsing
app.post('/webhooks/heppu',
express.json({
verify: (req, res, buf) => { req.rawBody = buf.toString(); }
}),
webhookVerification,
(req, res) => {
// Process verified webhook
console.log('Verified webhook:', req.body.event);
res.status(200).json({ received: true });
}
);import hmac
import hashlib
import time
import os
from flask import Flask, request, jsonify, abort
def verify_webhook_signature(payload, signature, timestamp, secret):
# Check timestamp (prevent replay attacks)
current_time = int(time.time())
time_diff = current_time - int(timestamp)
if time_diff > 300: # 5 minutes
raise ValueError('Webhook timestamp too old')
# Parse signature (format: sha256=<hash>)
if not signature.startswith('sha256='):
raise ValueError('Invalid signature format')
received_hash = signature.replace('sha256=', '')
# Compute expected signature
expected_hash = hmac.new(
secret.encode(),
payload.encode(),
hashlib.sha256
).hexdigest()
# Compare signatures (constant-time comparison)
if not hmac.compare_digest(received_hash, expected_hash):
raise ValueError('Signature verification failed')
return True
# Flask route
@app.route('/webhooks/heppu', methods=['POST'])
def handle_webhook():
signature = request.headers.get('X-Webhook-Signature-256')
timestamp = request.headers.get('X-Webhook-Timestamp')
secret = os.environ.get('WEBHOOK_SECRET')
# Skip verification if no secret configured
if secret:
try:
payload = request.get_data(as_text=True)
verify_webhook_signature(payload, signature, timestamp, secret)
except ValueError as error:
print(f"Webhook verification failed: {error}")
abort(401)
# Process verified webhook
event = request.json
print(f"Verified webhook: {event['event']}")
return jsonify({'received': True}), 200Security Best Practices:
- Always verify webhook signatures
- Reject webhooks with timestamps older than 5 minutes
- Use constant-time comparison to prevent timing attacks
- Store webhook secrets securely (environment variables, secret managers)
Webhook Delivery
Retry Logic
If your endpoint returns a non-2xx status code or times out, Heppu will retry the webhook:
- Attempt 1: Immediate
- Attempt 2: After 1 minute
- Attempt 3: After 5 minutes
- Attempt 4: After 15 minutes
- Attempt 5: After 1 hour
After 5 failed attempts, the webhook will be marked as failed and no more retries will be attempted.
Timeout
Webhook requests timeout after 30 seconds. Ensure your endpoint responds quickly:
app.post('/webhooks/heppu', async (req, res) => {
// Acknowledge receipt immediately
res.status(200).json({ received: true });
// Process webhook asynchronously
processWebhookAsync(req.body).catch(console.error);
});
async function processWebhookAsync(event) {
// Long-running processing here
await saveToDatabase(event);
await sendNotifications(event);
await updateAnalytics(event);
}Idempotency
Webhooks may be delivered multiple times. Use the X-Webhook-ID header for deduplication:
const processedDeliveries = new Set();
app.post('/webhooks/heppu', (req, res) => {
const webhookId = req.headers['x-webhook-id'];
// Check if already processed
if (processedDeliveries.has(webhookId)) {
console.log('Duplicate webhook, skipping');
return res.status(200).json({ received: true });
}
// Process webhook
processWebhook(req.body);
// Mark as processed
processedDeliveries.add(webhookId);
res.status(200).json({ received: true });
});For production, use a database or cache instead of in-memory Set:
// Using Redis
const redis = require('redis');
const client = redis.createClient();
app.post('/webhooks/heppu', async (req, res) => {
const webhookId = req.headers['x-webhook-id'];
// Check if already processed (TTL: 24 hours)
const exists = await client.exists(`webhook:${webhookId}`);
if (exists) {
console.log('Duplicate webhook, skipping');
return res.status(200).json({ received: true });
}
// Process webhook
await processWebhook(req.body);
// Mark as processed (expire after 24 hours)
await client.setex(`webhook:${webhookId}`, 86400, '1');
res.status(200).json({ received: true });
});Managing Webhooks
List Webhooks
curl https://v2.heppu.ai/api/v1/webhooks \
-H "x-api-key: $HEPPU_API_KEY"const response = await fetch('https://v2.heppu.ai/api/v1/webhooks', {
headers: { 'x-api-key': process.env.HEPPU_API_KEY }
});
const data = await response.json();
console.log('Webhooks:', data.data);response = requests.get(
'https://v2.heppu.ai/api/v1/webhooks',
headers={'x-api-key': os.environ.get('HEPPU_API_KEY')}
)
print('Webhooks:', response.json()['data'])Update Webhook
curl -X PATCH https://v2.heppu.ai/api/v1/webhooks/webhook_pqr678 \
-H "x-api-key: $HEPPU_API_KEY" \
-H "Content-Type: application/json" \
-d '{
"events": ["call.completed"],
"status": "active"
}'const response = await fetch('https://v2.heppu.ai/api/v1/webhooks/webhook_pqr678', {
method: 'PATCH',
headers: {
'x-api-key': process.env.HEPPU_API_KEY,
'Content-Type': 'application/json'
},
body: JSON.stringify({
events: ['call.completed'],
status: 'active'
})
});response = requests.patch(
'https://v2.heppu.ai/api/v1/webhooks/webhook_pqr678',
headers={'x-api-key': os.environ.get('HEPPU_API_KEY')},
json={
'events': ['call.completed'],
'status': 'active'
}
)Delete Webhook
curl -X DELETE https://v2.heppu.ai/api/v1/webhooks/webhook_pqr678 \
-H "x-api-key: $HEPPU_API_KEY"const response = await fetch('https://v2.heppu.ai/api/v1/webhooks/webhook_pqr678', {
method: 'DELETE',
headers: { 'x-api-key': process.env.HEPPU_API_KEY }
});
console.log('Webhook deleted:', response.status === 204);response = requests.delete(
'https://v2.heppu.ai/api/v1/webhooks/webhook_pqr678',
headers={'x-api-key': os.environ.get('HEPPU_API_KEY')}
)
print('Webhook deleted:', response.status_code == 204)Testing Webhooks
Local Testing with ngrok
For local development, use ngrok to expose your local server:
# Start your local server
node webhook-server.js
# In another terminal, start ngrok
ngrok http 3000Use the ngrok URL when creating webhooks:
curl -X POST https://v2.heppu.ai/api/v1/webhooks \
-H "x-api-key: $HEPPU_API_KEY" \
-H "Content-Type: application/json" \
-d '{
"url": "https://abc123.ngrok.io/webhooks/heppu",
"events": ["call.completed"]
}'Manual Testing
Trigger a test webhook from the dashboard:
- Navigate to Organization → Webhooks
- Click on your webhook
- Click Send Test Event
- Select an event type
- Click Send
Viewing Delivery Logs
Check webhook delivery status in the dashboard:
- Navigate to Organization → Webhooks
- Click on your webhook
- View the Delivery Log tab
Each delivery shows:
- Timestamp
- Event type
- HTTP status code
- Response time
- Response body
- Retry attempts
Production Best Practices
1. Respond Quickly
Acknowledge webhooks immediately and process asynchronously:
app.post('/webhooks/heppu', async (req, res) => {
// Respond immediately
res.status(200).json({ received: true });
// Queue for background processing
await queue.add('process-webhook', {
event: req.body,
deliveryId: req.headers['x-heppu-delivery-id']
});
});2. Implement Proper Error Handling
app.post('/webhooks/heppu', async (req, res) => {
try {
// Verify signature
verifyWebhookSignature(req);
// Respond immediately
res.status(200).json({ received: true });
// Process asynchronously
await processWebhook(req.body);
} catch (error) {
console.error('Webhook error:', error);
// Log error details
logger.error('Webhook processing failed', {
error: error.message,
deliveryId: req.headers['x-heppu-delivery-id'],
event: req.body.event
});
// Still respond with 200 if already acknowledged
if (!res.headersSent) {
res.status(500).json({ error: 'Processing failed' });
}
}
});3. Monitor Webhook Health
const webhookMetrics = {
received: 0,
processed: 0,
failed: 0,
avgProcessingTime: 0
};
app.post('/webhooks/heppu', async (req, res) => {
const startTime = Date.now();
webhookMetrics.received++;
try {
await processWebhook(req.body);
webhookMetrics.processed++;
} catch (error) {
webhookMetrics.failed++;
console.error('Webhook failed:', error);
} finally {
const duration = Date.now() - startTime;
webhookMetrics.avgProcessingTime =
(webhookMetrics.avgProcessingTime * (webhookMetrics.processed - 1) + duration) /
webhookMetrics.processed;
}
res.status(200).json({ received: true });
});
// Expose metrics
app.get('/metrics/webhooks', (req, res) => {
res.json(webhookMetrics);
});4. Use a Message Queue
For high-volume webhooks, use a message queue:
const Bull = require('bull');
const webhookQueue = new Bull('webhooks', {
redis: { host: 'localhost', port: 6379 }
});
// Webhook endpoint
app.post('/webhooks/heppu', async (req, res) => {
// Add to queue
await webhookQueue.add({
event: req.body,
deliveryId: req.headers['x-heppu-delivery-id']
});
res.status(200).json({ received: true });
});
// Process queue
webhookQueue.process(async (job) => {
const { event, deliveryId } = job.data;
// Check for duplicates
const exists = await redis.exists(`webhook:${deliveryId}`);
if (exists) return;
// Process webhook
await processWebhook(event);
// Mark as processed
await redis.setex(`webhook:${deliveryId}`, 86400, '1');
});Troubleshooting
Webhook Not Received
- Verify your endpoint is publicly accessible via HTTPS
- Check firewall rules and security groups
- Review webhook delivery logs in the dashboard
- Test with ngrok for local development
Signature Verification Failed
- Ensure you're using the correct webhook secret
- Verify you're comparing the raw request body
- Check timestamp - webhooks older than 5 minutes are rejected
- Use constant-time comparison for security
High Latency
- Respond immediately (return 200) before processing
- Use asynchronous processing
- Implement message queues for background jobs
- Optimize database queries and external API calls
Next Steps
Schedule Calls
Schedule outbound calls that trigger webhooks
Batch Calling
Schedule large campaigns with webhook notifications
Error Handling
Learn how to handle webhook errors properly
Authentication
Secure your API requests and webhook endpoints
Need Help?
- Documentation: Review event payloads and examples
- Support: Email support@heppu.ai with webhook delivery IDs