Heppu AI

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

EventConversation TypeTrigger
call.completedPhoneCall ends successfully
call.failedPhoneTechnical failure or error
call.busyPhoneRecipient line is busy
call.no-answerPhoneNo answer within timeout
chat.completedChatChat ends (any outcome)
chat.resolvedChatChat marked as resolved
chat.abandonedChatAuto-closed due to inactivity

Creating Webhooks

Via Dashboard

  1. Navigate to Organization → Webhooks
  2. Click Create Webhook
  3. Enter your webhook URL (must be HTTPS)
  4. Select the events you want to receive
  5. Optionally add a description
  6. 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 deduplication
  • X-Webhook-Timestamp: Unix timestamp when the request was sent
  • X-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=5c9f9b9e8d6f5c4b3a2d1e0f9a8b7c6d5e4f3a2b1c0d9e8f7a6b5c4d3e2f1a0b

The 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}), 200

Security 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 3000

Use 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:

  1. Navigate to Organization → Webhooks
  2. Click on your webhook
  3. Click Send Test Event
  4. Select an event type
  5. Click Send

Viewing Delivery Logs

Check webhook delivery status in the dashboard:

  1. Navigate to Organization → Webhooks
  2. Click on your webhook
  3. 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

  1. Verify your endpoint is publicly accessible via HTTPS
  2. Check firewall rules and security groups
  3. Review webhook delivery logs in the dashboard
  4. Test with ngrok for local development

Signature Verification Failed

  1. Ensure you're using the correct webhook secret
  2. Verify you're comparing the raw request body
  3. Check timestamp - webhooks older than 5 minutes are rejected
  4. Use constant-time comparison for security

High Latency

  1. Respond immediately (return 200) before processing
  2. Use asynchronous processing
  3. Implement message queues for background jobs
  4. Optimize database queries and external API calls

Next Steps

Need Help?

  • Documentation: Review event payloads and examples
  • Support: Email support@heppu.ai with webhook delivery IDs

On this page