> ## Documentation Index
> Fetch the complete documentation index at: https://nadsoft.mintlify.site/llms.txt
> Use this file to discover all available pages before exploring further.

# Webhook Integration Guide

> Complete guide to integrating with Hamsa Voice Agent webhooks - from setup to production deployment

## Overview

This comprehensive guide walks you through integrating with Hamsa Voice Agent webhooks, from initial setup to production deployment. Whether you're just getting started or implementing advanced patterns, this guide has you covered.

> [!NOTE]
>
> **What You'll Learn:**
>
> * How to set up and configure webhook endpoints
> * Understanding webhook data structures and events
> * Implementing secure and reliable webhook handlers
> * Best practices for production deployments
> * Troubleshooting common issues

***

## Stage 1: Getting Started

### What Are Webhooks?

Webhooks allow your applications to receive real-time notifications about call events, transcriptions, and conversation outcomes. When a voice call completes or specific events occur, Hamsa sends HTTP POST requests to your configured endpoint with relevant data.

> [card] Real-time Events
>
> Instant notifications as calls progress, from start to completion

> [card] Call Data
>
> Full transcripts, recordings, and extracted information

> [card] Custom Parameters
>
> Pass ANY custom data that gets echoed back for identification

> [card] Secure Delivery
>
> HTTPS-only with Bearer token authentication support

### How It Works - Complete Flow

```mermaid theme={null}
graph TB
    A[Your Application] --> B[Agent Initiation]
    B --> C[Hamsa Voice Agent]
    C --> D[Voice Call]
    D --> E[User Responds]
    E --> D
    D --> F[Process Results]
    F --> G[Your Webhook Endpoint]
    G --> H[Webhook Payload]
    H --> I[Transcript and Recording]
    H --> J[Outcome Data]
    H --> K[Echoed Params]
    K --> L[Your Database]
    I --> L
    J --> L

    style A fill:#1B5E3A,stroke:#B4FD83,color:#fff
    style G fill:#1B5E3A,stroke:#B4FD83,color:#fff
    style L fill:#1B5E3A,stroke:#B4FD83,color:#fff
    style H fill:#2D7A4F,stroke:#B4FD83,color:#fff
```

**The flow works in three stages:**

**Initiation**: Your application sends configuration with `agentId` and custom `params` (identifiers, context data).

**Call**: User interacts with AI agent, which collects information through conversation.

**Webhook**: Hamsa POSTs results to your endpoint with transcript, recording, outcome data, and your original params echoed back.

### Key Concepts

> [accordion] Custom Parameters (params)
>
> **What are they?**
>
> Custom parameters are ANY data you send during agent initiation. They serve two crucial purposes:
>
> 1. **Provide context** to the agent during the conversation
> 2. **Get echoed back** in webhook responses for matching records
>
> **Example:**
>
> ```json theme={null}
> {
>   "application_id": "12345",
>   "user_id": "user-789",
>   "session_id": "sess-abc-def"
> }
> ```
>
> All these fields will be available in the webhook's `outcomeResult` object.

> [accordion] Webhook Events
>
> **Types of Events:**
>
> * `call.started` - Call begins
> * `call.answered` - Call connected
> * `transcription.update` - Real-time speech updates
> * `tool.executed` - Agent uses a tool
> * `call.ended` - Call completes (includes full data)
>
> Each event contains relevant data for that stage of the call lifecycle.

> [accordion] Echo Pattern
>
> **How it works:**
>
> 1. You send: `{ params: { application_id: "12345" } }`
> 2. Agent conducts call
> 3. You receive: `{ outcomeResult: { application_id: "12345", ... } }`
>
> This pattern allows you to match webhook responses to your database records without maintaining state.

> [accordion] Outcome Result
>
> **What is it?**
>
> The `outcomeResult` object in webhook responses contains:
>
> * **Echoed data** - All your custom params (unchanged)
> * **Outcome data** - Information collected by the agent
>
> This combination lets you identify the record AND get the collected information in one payload.

***

## Stage 2: Setup & Configuration

### Prerequisites

Before setting up webhooks, ensure you have:

**1. A publicly accessible server**

Your webhook endpoint must be reachable from the internet. Use ngrok for local development.

**2. HTTPS enabled**

All webhook URLs must use HTTPS. HTTP endpoints will be rejected.

**3. A web framework**

Express.js, Flask, FastAPI, or any framework that can handle POST requests.

**4. Basic authentication setup**

Prepare to implement Bearer token authentication (recommended for production).

### Setting Up Your Webhook URL

#### Webhook URL Requirements

> [!WARNING]
>
> **HTTPS Required:**
>
> * All webhook URLs must use HTTPS protocol
> * HTTP endpoints will be rejected
> * Self-signed certificates are not supported
> * Certificate must be valid and not expired

**Valid Examples:**

```
✅ https://api.yourcompany.com/webhook/hamsa
✅ https://webhook.example.com/hamsa/events
✅ https://your-app.herokuapp.com/webhooks/calls

❌ http://api.yourcompany.com/webhook (HTTP not allowed)
❌ https://192.168.1.100/webhook (local IPs not accessible)
❌ https://localhost:3000/webhook (localhost not accessible)
```

#### Local Development Setup

For local development, use ngrok to expose your localhost:

```bash theme={null}
# Install ngrok
npm install -g ngrok
# or brew install ngrok

# Start your local server
node server.js  # Runs on port 3000

# In another terminal, start ngrok
ngrok http 3000

# Use the ngrok HTTPS URL in your webhook configuration
# Example: https://abc123.ngrok.io/webhook
```

### Configuring Authentication

#### Option 1: No Authentication (Development Only)

Use for development or testing:

```json Configuration theme={null}
{
  "webhookUrl": "https://abc123.ngrok.io/webhook",
  "webhookAuth": {
    "authKey": "noAuth"
  }
}
```

> [!WARNING]
>
> Not recommended for production. Anyone who discovers your webhook URL can send requests to it.

#### Option 2: Bearer Token Authentication (Recommended)

Use for production environments:

```json Configuration theme={null}
{
  "webhookUrl": "https://api.yourcompany.com/webhook",
  "webhookAuth": {
    "authKey": "bearer",
    "authSecret": "Bearer your_secret_token_here"
  }
}
```

**Generating Secure Tokens:**

```bash theme={null}
# Generate a random token (Linux/Mac)
openssl rand -base64 32

# Or use Node.js
node -e "console.log(require('crypto').randomBytes(32).toString('base64'))"

# Or use Python
python -c "import secrets; print(secrets.token_urlsafe(32))"
```

**Token Format Requirements:**

* Must include the word "Bearer" followed by your token
* Example: `Bearer sk_live_abc123xyz789`
* Token should be long and randomly generated
* Never commit tokens to source control

### Adding Webhook to Your Agent

**1. Navigate to Agent Configuration**

Go to your agent's configuration page in the Hamsa dashboard

**2. Find Call Webhook Section**

Scroll to the **Call Webhook** section and click to expand

**3. Enter Webhook URL**

Input your publicly accessible HTTPS webhook URL

**4. Configure Authentication**

Choose authentication method and enter your Bearer token if applicable

**5. Save Configuration**

Save your agent configuration to activate the webhook

> [!NOTE]
>
> Webhooks are configured per agent. Each agent can have its own webhook URL and authentication settings.

***

## Stage 3: Understanding Data Structures

### Agent Initiation - What You Send

When initiating the agent, you send a configuration object with the following structure:

```typescript theme={null}
{
  agentId: string,           // Required: Your Hamsa agent ID
  params?: object,           // Optional: Custom parameters (ANY data you want)
  userId?: string,           // Optional: User identifier for tracking
  voiceEnablement?: boolean, // Optional: Enable voice (default: true)
  preferHeadphonesForIosDevices?: boolean, // Optional: iOS optimization
  connectionDelay?: {        // Optional: Platform-specific delays
    android?: number,
    ios?: number,
    default?: number
  }
}
```

#### Configuration Fields Reference

| Field | Type | Required | Description |
| - | - | - | - |
| `agentId` | string | ✅ Yes | Your Hamsa agent configuration ID |
| `params` | object | ⚠️ Optional | Custom parameters object - can contain ANY fields |
| `userId` | string | ⚠️ Optional | User identifier for tracking purposes |
| `voiceEnablement` | boolean | ⚠️ Optional | Enable voice interaction (default: true) |
| `preferHeadphonesForIosDevices` | boolean | ⚠️ Optional | iOS audio optimization |
| `connectionDelay` | object | ⚠️ Optional | Platform-specific connection delays |

### The `params` Object - Your Custom Data

> [!NOTE]
>
> **Important:** The `params` object can contain ANY fields you want. All fields you pass here will be:
>
> 1. Available to the agent during the conversation
> 2. **Echoed back** to you in the webhook response inside `outcomeResult`

#### Recommended Pattern

```typescript theme={null}
params: {
  // IDENTIFIERS (highly recommended - used to match webhook to your records)
  user_id: string,              // Your user/customer ID
  session_id: string,           // Unique session identifier
  application_id: string,       // Application/form ID

  // CONTEXT DATA (optional - helps agent personalize conversation)
  user_name: string,            // User's name
  user_email: string,           // User's email
  user_phone: string,           // User's phone

  // BUSINESS DATA (optional - agent can reference during conversation)
  job_title: string,            // Job position
  company_name: string,         // Company name
  custom_field_1: string,       // Any other data
  custom_field_2: number,       // Can be strings, numbers, booleans
  custom_field_3: boolean,

  // INSTRUCTIONS (optional - guide agent behavior)
  interview_questions: string,  // JSON stringified array of questions
  context_data: string,         // Any additional context (CV, resume, etc.)
}
```

#### Data Types Supported in `params`

| Type | Supported | Notes |
| - | - | - |
| string | ✅ Yes | Most common, use for text data |
| number | ✅ Yes | Integers and decimals |
| boolean | ✅ Yes | true/false values |
| null | ✅ Yes | Explicit null values |
| Array | ⚠️ Use JSON string | Convert arrays to JSON string: `JSON.stringify([1,2,3])` |
| Object | ⚠️ Use JSON string | Convert objects to JSON string: `JSON.stringify({key: 'value'})` |

> [!NOTE]
>
> **Best Practice:** For complex data (arrays, objects), convert to JSON strings before passing.

#### Complete Initiation Example

```json theme={null}
{
  "agentId": "agent-123-abc",
  "params": {
    "application_id": "12345",
    "user_id": "user-789",
    "session_id": "sess-abc-def",
    "candidate_name": "John Doe",
    "candidate_email": "john@example.com",
    "candidate_phone": "+1234567890",
    "job_title": "Software Engineer",
    "company_name": "Acme Corp",
    "interview_questions": "[\"Tell me about your experience\",\"What are your salary expectations?\"]",
    "custom_note": "VIP candidate",
    "priority_level": 1,
    "require_background_check": true
  },
  "userId": "user-789",
  "voiceEnablement": true
}
```

### Webhook Events - What You Receive

#### Event Types Reference

Your webhook receives various events throughout a call's lifecycle:

| Event | When It Fires | Contains |
| - | - | - |
| **call.started** | Call begins | Caller info, timestamp, custom params |
| **call.answered** | Call connected | Connection details, ring duration |
| **transcription.update** | User/agent speaks | Real-time text, speaker identification |
| **tool.executed** | Agent uses a tool | Tool name, input, output, duration |
| **call.ended** | Call completes | Full transcript, recording, outcome data |

#### Standard Event Structure

All webhook events follow this structure:

```json theme={null}
{
  "eventType": "call.ended",
  "callId": "call_uuid_12345",
  "timestamp": "2024-01-15T14:30:00.000Z",
  "projectId": "proj_abc123",
  "agentId": "agent_xyz789",
  "agentName": "Customer Support Agent",
  "data": {
    // Event-specific data
  }
}
```

### Call Ended Event - The Most Important

The `call.ended` event contains the complete conversation data:

```json theme={null}
{
  "eventType": "call.ended",
  "callId": "call_uuid_12345",
  "timestamp": "2024-01-15T14:35:00.000Z",
  "projectId": "proj_abc123",
  "agentId": "agent_xyz789",
  "agentName": "Customer Support Agent",
  "data": {
    "timestamp": "2025-11-27T09:10:35.000Z",
    "data": {
      "conversationId": "conv-123-abc-456-def",
      "conversationRecording": "https://hamsa-recordings.s3.amazonaws.com/conv-123.mp3",
      "transcription": [
        { "Agent": "Hello! Thank you for joining. What is your name?" },
        { "User": "Hi, my name is John Doe." },
        { "Agent": "Nice to meet you, John. Can you tell me about your experience?" },
        { "User": "I have 5 years of experience in software development." },
        { "Agent": "Excellent. What are your salary expectations?" },
        { "User": "I'm looking for around 80,000 dollars per year." },
        { "Agent": "Thank you. What is your notice period?" },
        { "User": "I have a 2-week notice period." },
        { "Agent": "Great! That concludes our interview. Thank you!" },
        { "User": "Thank you!" }
      ],
      "outcomeResult": {
        "application_id": "12345",
        "user_id": "user-789",
        "session_id": "sess-abc-def",
        "candidate_name": "John Doe",
        "candidate_email": "john@example.com",
        "candidate_phone": "+1234567890",
        "job_title": "Software Engineer",
        "company_name": "Acme Corp",
        "interview_questions": "[\"Tell me about your experience\",\"What are your salary expectations?\"]",
        "custom_note": "VIP candidate",
        "priority_level": 1,
        "require_background_check": true,
        "expectedSalary": "80000 dollars per year",
        "noticePeriod": "2 weeks",
        "number_of_speakers": "1",
        "sentiment_score": 0.8
      }
    }
  }
}
```

#### Field Descriptions

**Top-Level Event Fields:**

| Field | Type | Description |
| - | - | - |
| `eventType` | string | Type of event (e.g., "call.ended") |
| `callId` | string | Unique identifier for this call |
| `timestamp` | string | ISO 8601 timestamp when event occurred |
| `projectId` | string | Your Hamsa project ID |
| `agentId` | string | ID of the agent that handled the call |
| `agentName` | string | Name of the agent configuration |

**Conversation Data Fields (`data.data`):**

| Field | Type | Description |
| - | - | - |
| `conversationId` | string | Hamsa's unique identifier for this conversation |
| `conversationRecording` | string | Full URL to audio recording file Format: `.mp3` or `.wav` Accessible for download |
| `transcription` | Array\<object> | Ordered array of conversation turns |
| `outcomeResult` | object | Contains echoed params + outcome data |

#### Transcription Array Format

Each element represents one turn in the conversation:

```typescript theme={null}
{
  Agent?: string,  // Present when agent spoke
  User?: string    // Present when user spoke
}
```

### Understanding `outcomeResult`

The `outcomeResult` object is where the magic happens - it contains both your echoed parameters AND newly collected outcome data.

#### Echoed Parameters (What You Sent)

These are the exact same parameters you included in the `params` object when initiating the agent. They come from your application and are returned unchanged in the webhook payload, allowing you to match the webhook to your database records.

```json theme={null}
{
  "application_id": "12345",
  "user_id": "user-789",
  "session_id": "sess-abc-def",
  "candidate_name": "John Doe",
  "candidate_email": "john@example.com",
  "candidate_phone": "+1234567890",
  "job_title": "Software Engineer",
  "company_name": "Acme Corp",
  "interview_questions": "[...]",
  "custom_note": "VIP candidate",
  "priority_level": 1,
  "require_background_check": true
}
```

#### Outcome Data (New from Call)

These fields are generated during the voice call based on the conversation between the agent and the user. They come from the agent's analysis of the call and are defined in your agent's outcome configuration.

```json theme={null}
{
  "expectedSalary": "80000 dollars per year",
  "noticePeriod": "2 weeks",
  "number_of_speakers": "1",
  "sentiment_score": 0.8
}
```

### Common Extracted Fields

These are fields commonly extracted by Hamsa agents (depending on your agent's configuration):

| Field | Type | Description | When Present |
| - | - | - | - |
| `expectedSalary` | string \| null | Salary mentioned by user | If agent asked about salary |
| `noticePeriod` | string \| null | Notice period mentioned | If agent asked about availability |
| `number_of_speakers` | string \| number | Count of different speakers detected | If audio analysis enabled |
| `sentiment_score` | number \| null | Conversation sentiment (-1 to 1) | If sentiment analysis enabled |
| `intent` | string \| null | Identified user intent | If intent detection enabled |
| `resolved` | boolean \| null | Whether issue was resolved | If resolution tracking enabled |

> [!NOTE]
>
> **Note:** The exact fields depend on your agent's configuration and outcome schema. Check with your Hamsa agent setup to see what fields are extracted.

***

## Stage 4: Implementation

### Creating Your Webhook Endpoint

#### Node.js/Express Implementation

> [tab] Basic Handler
>
> ```typescript theme={null}
> import express from 'express';
>
> const app = express();
> app.use(express.json());
>
> // Middleware to verify Bearer token
> function verifyToken(req, res, next) {
>   const authHeader = req.headers.authorization;
>   const expectedToken = process.env.WEBHOOK_SECRET; // Bearer your_secret_token
>
>   if (authHeader !== expectedToken) {
>     return res.status(401).json({ error: 'Unauthorized' });
>   }
>
>   next();
> }
>
> // Webhook endpoint
> app.post('/webhook', verifyToken, async (req, res) => {
>   try {
>     const event = req.body;
>
>     console.log('Received event:', event.eventType);
>     console.log('Call ID:', event.callId);
>
>     // Handle different event types
>     switch (event.eventType) {
>       case 'call.started':
>         await handleCallStarted(event);
>         break;
>       case 'call.ended':
>         await handleCallEnded(event);
>         break;
>       case 'transcription.update':
>         await handleTranscription(event);
>         break;
>       default:
>         console.log('Unhandled event type:', event.eventType);
>     }
>
>     // Respond quickly to acknowledge receipt
>     res.status(200).json({ received: true });
>
>   } catch (error) {
>     console.error('Webhook error:', error);
>     res.status(500).json({ error: 'Internal server error' });
>   }
> });
>
> app.listen(3000, () => {
>   console.log('Webhook server listening on port 3000');
> });
> ```

> [tab] Call Ended Handler
>
> ```typescript theme={null}
> async function handleCallEnded(event) {
>   // Extract webhook data
>   const { timestamp } = event.data;
>   const {
>     conversationId,
>     conversationRecording,
>     transcription,
>     outcomeResult
>   } = event.data.data;
>
>   console.log('Call completed:', conversationId);
>   console.log('Completed at:', timestamp);
>
>   // Extract your custom identifiers (echoed back)
>   const applicationId = outcomeResult.application_id;
>   const userId = outcomeResult.user_id;
>   const sessionId = outcomeResult.session_id;
>
>   console.log('Identifiers:', { applicationId, userId, sessionId });
>
>   // Extract collected data (new)
>   const expectedSalary = outcomeResult.expectedSalary;
>   const noticePeriod = outcomeResult.noticePeriod;
>   const numberOfSpeakers = outcomeResult.number_of_speakers;
>
>   console.log('Collected data:', {
>     expectedSalary,
>     noticePeriod,
>     numberOfSpeakers
>   });
>
>   // Build transcript text from array
>   const transcriptText = transcription
>     .map(turn => {
>       if (turn.Agent) return `Agent: ${turn.Agent}`;
>       if (turn.User) return `User: ${turn.User}`;
>       return '';
>     })
>     .filter(Boolean)
>     .join('\n');
>
>   console.log('Transcript length:', transcriptText.length);
>   console.log('Recording URL:', conversationRecording);
>
>   // Update your database
>   await updateDatabase({
>     applicationId,
>     userId,
>     sessionId,
>     conversationId,
>     recordingUrl: conversationRecording,
>     transcript: transcriptText,
>     extractedData: {
>       expectedSalary,
>       noticePeriod,
>       numberOfSpeakers,
>       sentimentScore: outcomeResult.sentiment_score
>     },
>     completedAt: timestamp
>   });
> }
>
> async function updateDatabase(data) {
>   // TODO: Implement your database update logic
>   // Example using Prisma:
>   // await prisma.call.update({
>   //   where: { applicationId: data.applicationId },
>   //   data: {
>   //     conversationId: data.conversationId,
>   //     recordingUrl: data.recordingUrl,
>   //     transcript: data.transcript,
>   //     expectedSalary: data.extractedData.expectedSalary,
>   //     noticePeriod: data.extractedData.noticePeriod,
>   //     completedAt: data.completedAt
>   //   }
>   // });
> }
> ```

#### Python/Flask Implementation

> [tab] Basic Handler
>
> ```python theme={null}
> from flask import Flask, request, jsonify
> import os
>
> app = Flask(__name__)
>
> def verify_token():
>     auth_header = request.headers.get('Authorization')
>     expected_token = os.environ.get('WEBHOOK_SECRET')
>
>     if auth_header != expected_token:
>         return False
>     return True
>
> @app.route('/webhook', methods=['POST'])
> def webhook():
>     # Verify authentication
>     if not verify_token():
>         return jsonify({'error': 'Unauthorized'}), 401
>
>     try:
>         # Parse event data
>         event = request.get_json()
>
>         print(f"Event Type: {event.get('eventType')}")
>         print(f"Call ID: {event.get('callId')}")
>
>         # Handle different event types
>         event_type = event.get('eventType')
>         if event_type == 'call.started':
>             handle_call_started(event)
>         elif event_type == 'call.ended':
>             handle_call_ended(event)
>         elif event_type == 'transcription.update':
>             handle_transcription(event)
>
>         # Acknowledge receipt
>         return jsonify({'received': True}), 200
>
>     except Exception as e:
>         print(f'Webhook error: {str(e)}')
>         return jsonify({'error': 'Internal server error'}), 500
>
> if __name__ == '__main__':
>     app.run(port=3000)
> ```

> [tab] Call Ended Handler
>
> ```python theme={null}
> def handle_call_ended(event):
>     # Extract webhook data
>     data = event['data']
>     timestamp = data['timestamp']
>     conversation_data = data['data']
>
>     conversation_id = conversation_data['conversationId']
>     recording_url = conversation_data['conversationRecording']
>     transcription = conversation_data['transcription']
>     outcome_result = conversation_data['outcomeResult']
>
>     print(f'Call completed: {conversation_id}')
>     print(f'Completed at: {timestamp}')
>
>     # Extract your custom identifiers (echoed back)
>     application_id = outcome_result.get('application_id')
>     user_id = outcome_result.get('user_id')
>     session_id = outcome_result.get('session_id')
>
>     print(f'Identifiers: {application_id}, {user_id}, {session_id}')
>
>     # Extract collected data (new)
>     expected_salary = outcome_result.get('expectedSalary')
>     notice_period = outcome_result.get('noticePeriod')
>     num_speakers = outcome_result.get('number_of_speakers')
>
>     print(f'Collected data: {expected_salary}, {notice_period}, {num_speakers}')
>
>     # Build transcript text
>     transcript_parts = []
>     for turn in transcription:
>         if 'Agent' in turn:
>             transcript_parts.append(f"Agent: {turn['Agent']}")
>         elif 'User' in turn:
>             transcript_parts.append(f"User: {turn['User']}")
>
>     transcript_text = '\n'.join(transcript_parts)
>
>     print(f'Transcript length: {len(transcript_text)}')
>     print(f'Recording URL: {recording_url}')
>
>     # Update your database
>     update_database({
>         'application_id': application_id,
>         'user_id': user_id,
>         'session_id': session_id,
>         'conversation_id': conversation_id,
>         'recording_url': recording_url,
>         'transcript': transcript_text,
>         'extracted_data': {
>             'expected_salary': expected_salary,
>             'notice_period': notice_period,
>             'num_speakers': num_speakers,
>             'sentiment_score': outcome_result.get('sentiment_score')
>         },
>         'completed_at': timestamp
>     })
>
> def update_database(data):
>     # TODO: Implement your database update logic
>     pass
> ```

### Processing Events

#### Handling Different Event Types

```javascript theme={null}
async function handleCallStarted(event) {
  const { callId, data } = event;
  const { callDirection, fromNumber, toNumber, params } = data.data;

  console.log(`Call started: ${callId}`);
  console.log(`Direction: ${callDirection}`);
  console.log(`From: ${fromNumber} To: ${toNumber}`);

  // Store initial call data
  await database.calls.create({
    callId,
    direction: callDirection,
    fromNumber,
    toNumber,
    customParams: params,
    status: 'in_progress',
    startedAt: data.callStartedAt
  });
}

async function handleTranscription(event) {
  const { callId, data } = event;
  const { speaker, text, confidence } = data.data;

  // Store real-time transcription
  await database.transcriptions.create({
    callId,
    speaker,
    text,
    confidence,
    timestamp: event.timestamp
  });

  // Optionally trigger real-time actions
  if (speaker === 'user' && text.toLowerCase().includes('urgent')) {
    await notifySupport({
      callId,
      message: 'User mentioned urgent issue',
      transcript: text
    });
  }
}

async function handleToolExecution(event) {
  const { callId, data } = event;
  const { toolName, input, output, duration } = data.data;

  console.log(`Tool executed: ${toolName}`);
  console.log(`Duration: ${duration}ms`);

  // Log tool usage
  await database.toolExecutions.create({
    callId,
    toolName,
    input,
    output,
    duration,
    executedAt: event.timestamp
  });
}
```

### Error Handling

#### Comprehensive Error Handling

```javascript theme={null}
app.post('/webhook', async (req, res) => {
  try {
    // 1. Verify authentication
    if (!verifyToken(req)) {
      console.warn('Authentication failed:', {
        ip: req.ip,
        headers: req.headers
      });
      return res.status(401).json({ error: 'Unauthorized' });
    }

    // 2. Validate payload structure
    const event = req.body;
    if (!event.callId || !event.eventType) {
      console.error('Invalid payload:', event);
      return res.status(400).json({ error: 'Invalid payload' });
    }

    // 3. Validate event type
    const validEventTypes = [
      'call.started',
      'call.answered',
      'transcription.update',
      'tool.executed',
      'call.ended'
    ];

    if (!validEventTypes.includes(event.eventType)) {
      console.warn('Unknown event type:', event.eventType);
      // Still return 200 to acknowledge receipt
      return res.status(200).json({
        received: true,
        warning: 'Unknown event type'
      });
    }

    // 4. Process event (with timeout)
    const timeoutPromise = new Promise((_, reject) => {
      setTimeout(() => reject(new Error('Processing timeout')), 4000);
    });

    const processingPromise = processEvent(event);

    await Promise.race([processingPromise, timeoutPromise]);

    // 5. Respond successfully
    res.status(200).json({ received: true });

  } catch (error) {
    console.error('Webhook error:', {
      error: error.message,
      stack: error.stack,
      body: req.body
    });

    // Still return 200 to prevent retries for processing errors
    // Log the error for investigation
    res.status(200).json({
      received: true,
      processing_error: true,
      error_id: generateErrorId()
    });
  }
});

function generateErrorId() {
  return `err_${Date.now()}_${Math.random().toString(36).substr(2, 9)}`;
}
```

#### Validation Helper

```javascript theme={null}
const Joi = require('joi');

const eventSchema = Joi.object({
  eventType: Joi.string().required(),
  callId: Joi.string().uuid().required(),
  timestamp: Joi.string().isoDate().required(),
  projectId: Joi.string().required(),
  agentId: Joi.string().required(),
  agentName: Joi.string().required(),
  data: Joi.object().required(),
});

function validateEvent(event) {
  const { error, value } = eventSchema.validate(event);

  if (error) {
    console.error('Validation error:', error.details);
    return { valid: false, error: error.details };
  }

  return { valid: true, value };
}

// Usage
app.post('/webhook', async (req, res) => {
  const validation = validateEvent(req.body);

  if (!validation.valid) {
    return res.status(400).json({
      error: 'Invalid payload',
      details: validation.error
    });
  }

  // Process validated event
  await processEvent(validation.value);
  res.status(200).json({ received: true });
});
```

***

## Stage 5: Advanced Topics

### Async Processing

For production systems, always process webhooks asynchronously to avoid timeouts and ensure reliability.

#### Queue-Based Processing

> [tab] Bull Queue (Node.js)
>
> ```javascript theme={null}
> const Queue = require('bull');
> const eventQueue = new Queue('call-events', {
>   redis: {
>     host: '127.0.0.1',
>     port: 6379
>   }
> });
>
> // Webhook endpoint - respond immediately
> app.post('/webhook', verifyToken, async (req, res) => {
>   const event = req.body;
>
>   try {
>     // Add to queue for processing
>     await eventQueue.add('process-event', event, {
>       attempts: 3,
>       backoff: {
>         type: 'exponential',
>         delay: 2000
>       },
>       removeOnComplete: true,
>       removeOnFail: false
>     });
>
>     // Respond immediately
>     res.status(200).json({ received: true });
>
>   } catch (error) {
>     console.error('Queue error:', error);
>     res.status(500).json({ error: 'Failed to queue event' });
>   }
> });
>
> // Process events in background
> eventQueue.process('process-event', async (job) => {
>   const event = job.data;
>
>   console.log(`Processing event: ${event.eventType} for call ${event.callId}`);
>
>   try {
>     if (event.eventType === 'call.ended') {
>       await saveToDatabase(event);
>       await sendNotifications(event);
>       await updateAnalytics(event);
>     } else if (event.eventType === 'transcription.update') {
>       await processTranscription(event);
>     }
>
>     console.log(`Successfully processed: ${event.callId}`);
>
>   } catch (error) {
>     console.error(`Processing failed for ${event.callId}:`, error);
>     throw error; // Will trigger retry
>   }
> });
>
> // Monitor queue
> eventQueue.on('completed', (job) => {
>   console.log(`Job ${job.id} completed`);
> });
>
> eventQueue.on('failed', (job, err) => {
>   console.error(`Job ${job.id} failed:`, err.message);
> });
> ```

> [tab] Celery (Python)
>
> ```python theme={null}
> from celery import Celery
> from flask import Flask, request, jsonify
>
> app = Flask(__name__)
>
> # Configure Celery
> celery = Celery('tasks', broker='redis://localhost:6379/0')
>
> @celery.task(bind=True, max_retries=3)
> def process_webhook_event(self, event):
>     """Process webhook event asynchronously"""
>     try:
>         event_type = event.get('eventType')
>         call_id = event.get('callId')
>
>         print(f'Processing event: {event_type} for call {call_id}')
>
>         if event_type == 'call.ended':
>             save_to_database(event)
>             send_notifications(event)
>             update_analytics(event)
>         elif event_type == 'transcription.update':
>             process_transcription(event)
>
>         print(f'Successfully processed: {call_id}')
>
>     except Exception as e:
>         print(f'Processing failed for {call_id}: {str(e)}')
>         # Retry with exponential backoff
>         self.retry(exc=e, countdown=2 ** self.request.retries)
>
> @app.route('/webhook', methods=['POST'])
> def webhook():
>     if not verify_token():
>         return jsonify({'error': 'Unauthorized'}), 401
>
>     event = request.get_json()
>
>     # Queue event for async processing
>     process_webhook_event.delay(event)
>
>     # Respond immediately
>     return jsonify({'received': True}), 200
> ```

### Idempotency

Prevent duplicate processing of webhook events:

#### Redis-Based Idempotency

```javascript theme={null}
const redis = require('redis');
const client = redis.createClient();

async function isEventProcessed(event) {
  const eventKey = `webhook:${event.callId}:${event.eventType}:${event.timestamp}`;

  // Check if already processed
  const exists = await client.exists(eventKey);
  if (exists) {
    return true;
  }

  // Mark as processed (expire after 24 hours)
  await client.setex(eventKey, 86400, 'processed');
  return false;
}

app.post('/webhook', async (req, res) => {
  const event = req.body;

  // Check for duplicates
  const alreadyProcessed = await isEventProcessed(event);
  if (alreadyProcessed) {
    console.log('Duplicate event, skipping:', event.callId);
    return res.status(200).json({
      received: true,
      duplicate: true
    });
  }

  // Process event
  await processEvent(event);

  res.status(200).json({ received: true });
});
```

#### Database-Based Idempotency

```javascript theme={null}
async function processEventIdempotent(event) {
  const eventId = `${event.callId}-${event.eventType}-${event.timestamp}`;

  try {
    // Attempt to create event record (unique constraint on eventId)
    await database.webhookEvents.create({
      eventId,
      callId: event.callId,
      eventType: event.eventType,
      eventData: event,
      processedAt: new Date(),
      status: 'processing'
    });

    // Process event
    await processEvent(event);

    // Update status
    await database.webhookEvents.update({
      where: { eventId },
      data: { status: 'completed' }
    });

  } catch (error) {
    if (error.code === 'P2002') { // Unique constraint violation
      console.log('Duplicate event detected:', eventId);
      return { duplicate: true };
    }
    throw error;
  }

  return { duplicate: false };
}
```

### Security Best Practices

> [!WARNING]
>
> **Security Checklist:**
>
> * ✅ Always use HTTPS endpoints
> * ✅ Implement Bearer token authentication
> * ✅ Validate all incoming data
> * ✅ Use strong, randomly generated tokens
> * ✅ Rotate tokens periodically
> * ✅ Log authentication failures
> * ✅ Rate limit webhook endpoint
> * ✅ Monitor for suspicious patterns

#### Token Management

```javascript theme={null}
// ✅ GOOD - Store tokens securely
require('dotenv').config();
const WEBHOOK_SECRET = process.env.WEBHOOK_SECRET;

// ❌ BAD - Never hardcode tokens
const WEBHOOK_SECRET = 'Bearer abc123';  // Don't do this!

// ✅ GOOD - Validate token format
function isValidBearerToken(token) {
  return token && token.startsWith('Bearer ') && token.length > 20;
}

// ✅ GOOD - Rotate tokens periodically
async function rotateWebhookToken() {
  const newToken = `Bearer ${crypto.randomBytes(32).toString('base64')}`;

  // Update in secure storage
  await secretManager.updateSecret('WEBHOOK_SECRET', newToken);

  // Update in Hamsa dashboard
  await hamsaApi.updateAgentWebhook({
    agentId: 'agent-123',
    webhookAuth: {
      authKey: 'bearer',
      authSecret: newToken
    }
  });

  console.log('Webhook token rotated successfully');
}
```

#### Rate Limiting

```javascript theme={null}
const rateLimit = require('express-rate-limit');

const webhookLimiter = rateLimit({
  windowMs: 1 * 60 * 1000, // 1 minute
  max: 100, // Limit each IP to 100 requests per windowMs
  message: 'Too many webhook requests from this IP',
  standardHeaders: true,
  legacyHeaders: false,
  skip: (req) => {
    // Skip rate limiting for valid tokens
    return verifyToken(req);
  }
});

app.post('/webhook', webhookLimiter, verifyToken, async (req, res) => {
  // Process webhook
});
```

#### IP Whitelisting (Optional)

```javascript theme={null}
const ALLOWED_IPS = [
  '34.123.45.67',  // Hamsa webhook server IP
  '52.89.12.34'    // Backup server IP
];

function checkIpWhitelist(req, res, next) {
  const clientIp = req.ip || req.connection.remoteAddress;

  if (!ALLOWED_IPS.includes(clientIp)) {
    console.warn('Unauthorized IP:', clientIp);
    return res.status(403).json({ error: 'Forbidden' });
  }

  next();
}

app.post('/webhook', checkIpWhitelist, verifyToken, async (req, res) => {
  // Process webhook
});
```

### Use Cases & Patterns

#### CRM Integration

```javascript theme={null}
async function syncToCRM(event) {
  if (event.eventType !== 'call.ended') return;

  const { outcomeResult, transcription } = event.data.data;
  const { fromNumber } = event.data.data;

  // Build transcript text
  const transcriptText = transcription
    .map(turn => Object.values(turn)[0])
    .join('\n');

  // Update customer record
  await crm.contacts.upsert({
    where: { phone: fromNumber },
    update: {
      lastCallDate: event.timestamp,
      lastCallDuration: event.data.data.callDuration,
      lastCallNotes: transcriptText,
      customFields: {
        intent: outcomeResult.intent,
        sentiment: outcomeResult.sentiment_score,
        resolved: outcomeResult.resolved
      }
    },
    create: {
      phone: fromNumber,
      firstCallDate: event.timestamp,
      source: 'ai_agent'
    }
  });

  // Create activity record
  await crm.activities.create({
    type: 'phone_call',
    contactPhone: fromNumber,
    subject: `AI Agent Call - ${outcomeResult.intent || 'General'}`,
    description: transcriptText,
    outcome: outcomeResult.resolved ? 'resolved' : 'needs_followup',
    recordingUrl: event.data.data.conversationRecording,
    date: event.timestamp
  });
}
```

#### Analytics Dashboard

```javascript theme={null}
async function updateAnalytics(event) {
  if (event.eventType !== 'call.ended') return;

  const { callDuration, outcomeResult } = event.data.data;

  await analytics.track({
    event: 'Call Completed',
    properties: {
      callId: event.callId,
      agentId: event.agentId,
      agentName: event.agentName,
      duration: callDuration,
      intent: outcomeResult.intent,
      sentiment: outcomeResult.sentiment_score,
      resolved: outcomeResult.resolved,
      speakers: outcomeResult.number_of_speakers,
      timestamp: event.timestamp
    }
  });

  // Update real-time dashboard metrics
  await dashboard.increment('total_calls');
  await dashboard.increment(`calls_by_intent.${outcomeResult.intent}`);
  await dashboard.addToAverage('average_duration', callDuration);

  if (outcomeResult.resolved) {
    await dashboard.increment('resolved_calls');
  }
}
```

#### Notification System

```javascript theme={null}
async function sendNotifications(event) {
  if (event.eventType !== 'call.ended') return;

  const { outcomeResult, callDuration } = event.data.data;

  // High-priority notifications
  if (outcomeResult.priority === 'urgent' || !outcomeResult.resolved) {
    await slack.sendMessage({
      channel: '#support-team',
      text: '🚨 Urgent call requires attention',
      blocks: [
        {
          type: 'section',
          text: {
            type: 'mrkdwn',
            text: `*Call ID:* ${event.callId}\n*Agent:* ${event.agentName}\n*Duration:* ${callDuration}s`
          }
        },
        {
          type: 'section',
          fields: [
            { type: 'mrkdwn', text: `*Intent:*\n${outcomeResult.intent || 'Unknown'}` },
            { type: 'mrkdwn', text: `*Resolved:*\n${outcomeResult.resolved ? '✅ Yes' : '❌ No'}` },
            { type: 'mrkdwn', text: `*Sentiment:*\n${outcomeResult.sentiment_score || 'N/A'}` }
          ]
        },
        {
          type: 'actions',
          elements: [
            {
              type: 'button',
              text: { type: 'plain_text', text: 'View Details' },
              url: `https://dashboard.example.com/calls/${event.callId}`
            }
          ]
        }
      ]
    });
  }

  // Email summary
  await emailService.send({
    to: 'team@company.com',
    subject: `Call Summary - ${event.callId}`,
    template: 'call-summary',
    data: {
      callId: event.callId,
      agentName: event.agentName,
      duration: callDuration,
      transcript: event.data.data.transcription,
      outcome: outcomeResult,
      recordingUrl: event.data.data.conversationRecording
    }
  });
}
```

***

## Stage 6: Troubleshooting & Support

### Common Issues

> [accordion] Webhook not receiving data
>
> **Symptoms:**
>
> * No webhook events arriving at your endpoint
> * Call completes but no POST request received
>
> **Possible Causes:**
>
> 1. Webhook URL is not publicly accessible
> 2. Webhook URL not configured in Hamsa dashboard
> 3. Firewall/security rules blocking POST requests
> 4. Server not running or crashed
> 5. HTTPS certificate invalid
>
> **Solutions:**
>
> ```bash theme={null}
> # Test your webhook endpoint
> curl -X POST https://your-endpoint.com/webhook \
>   -H "Content-Type: application/json" \
>   -H "Authorization: Bearer your_token" \
>   -d '{"test": true, "eventType": "test", "callId": "test-123", "timestamp": "2024-01-01T00:00:00.000Z"}'
>
> # Should return 200 OK
> # If not, check:
> # 1. Is your server running?
> # 2. Is the URL correct?
> # 3. Is HTTPS configured properly?
> # 4. Are firewall rules allowing traffic?
> ```
>
> **For Local Development:**
>
> ```bash theme={null}
> # Use ngrok to expose localhost
> ngrok http 3000
>
> # Use the ngrok HTTPS URL in your webhook configuration
> # Example: https://abc123.ngrok.io/webhook
> ```

> [accordion] Cannot match webhook to your record
>
> **Symptoms:**
>
> * Receiving webhook data but can't identify which record it belongs to
> * Missing application\_id or user\_id in outcomeResult
>
> **Causes:**
>
> * Not passing unique identifier in `params` during initiation
> * Identifier not being echoed correctly
> * Wrong field name being checked
>
> **Solutions:**
>
> ```javascript theme={null}
> // ✅ GOOD - Always include unique identifiers
> const config = {
>   agentId: 'agent-123',
>   params: {
>     application_id: applicationId.toString(),  // Unique identifier
>     user_id: userId.toString(),                // Secondary identifier
>     session_id: generateSessionId()            // Session tracking
>   }
> };
>
> // In webhook handler - verify identifiers exist
> app.post('/webhook', async (req, res) => {
>   const { outcomeResult } = req.body.data.data;
>
>   console.log('Received outcomeResult:', outcomeResult);
>
>   const applicationId = outcomeResult.application_id;
>
>   if (!applicationId) {
>     console.error('Missing application_id in webhook');
>     // Log full payload for debugging
>     console.error('Full payload:', JSON.stringify(req.body, null, 2));
>     return res.status(400).json({ error: 'Missing identifier' });
>   }
>
>   // Proceed with matching
>   const record = await database.findByApplicationId(applicationId);
>   // ...
> });
> ```
>
> **Debugging Tips:**
>
> ```javascript theme={null}
> // Log what you're sending during initiation
> console.log('Initiating agent with params:', config.params);
>
> // Log what you receive in webhook
> console.log('Received in webhook:', outcomeResult);
>
> // Compare field names (case-sensitive!)
> // application_id !== applicationId
> ```

> [accordion] Authentication Failures (401 Unauthorized)
>
> **Symptoms:**
>
> * Receiving 401 Unauthorized responses
> * Webhook attempts shown in logs but rejected
>
> **Causes:**
>
> * Bearer token mismatch
> * Token format incorrect
> * Missing "Bearer" prefix
> * Token changed in dashboard but not in code
>
> **Solutions:**
>
> ```javascript theme={null}
> // ✅ CORRECT format
> {
>   "webhookAuth": {
>     "authKey": "bearer",
>     "authSecret": "Bearer sk_live_abc123xyz789"
>   }
> }
>
> // ❌ Common mistakes:
> // "authSecret": "sk_live_abc123xyz789"  // Missing "Bearer"
> // "authSecret": "bearer sk_live_abc123xyz789"  // Lowercase "bearer"
> // "authKey": "noAuth" with authSecret set  // Conflicting settings
>
> // Debug token mismatch:
> app.post('/webhook', (req, res) => {
>   const receivedAuth = req.headers.authorization;
>   const expectedAuth = process.env.WEBHOOK_SECRET;
>
>   console.log('Received auth:', receivedAuth);
>   console.log('Expected auth:', expectedAuth);
>   console.log('Match:', receivedAuth === expectedAuth);
>
>   if (receivedAuth !== expectedAuth) {
>     console.error('Auth mismatch!');
>     console.error('Length - Received:', receivedAuth?.length, 'Expected:', expectedAuth?.length);
>     return res.status(401).json({ error: 'Unauthorized' });
>   }
>
>   // Continue...
> });
> ```

> [accordion] Missing Events
>
> **Symptoms:**
>
> * Receiving some events but not all
> * `call.started` received but not `call.ended`
>
> **Causes:**
>
> 1. Webhook endpoint returning errors for some events
> 2. Timeout (not responding within 5 seconds)
> 3. Network connectivity issues during specific events
> 4. Processing logic throwing errors for certain event types
>
> **Solutions:**
>
> ```javascript theme={null}
> // Always respond quickly (under 5 seconds)
> app.post('/webhook', async (req, res) => {
>   const event = req.body;
>
>   // Acknowledge immediately
>   res.status(200).json({ received: true });
>
>   // Process asynchronously
>   setImmediate(() => {
>     processEvent(event).catch((err) => {
>       console.error('Processing error:', err);
>       // Event already acknowledged, error won't cause retry
>     });
>   });
> });
>
> // Add monitoring to track all event types
> const eventCounts = {};
>
> app.post('/webhook', (req, res) => {
>   const eventType = req.body.eventType;
>   eventCounts[eventType] = (eventCounts[eventType] || 0) + 1;
>
>   console.log('Event counts:', eventCounts);
>   // Should see: { 'call.started': 1, 'transcription.update': 15, 'call.ended': 1 }
>
>   res.status(200).json({ received: true });
> });
> ```

> [accordion] Duplicate Events
>
> **Symptoms:**
>
> * Receiving the same event multiple times
> * Database records being created twice
>
> **Cause:**
>
> * Webhook retries due to slow responses
> * Network issues causing retransmission
> * Not implementing idempotency
>
> **Solution:**
>
> ```javascript theme={null}
> // Implement idempotency using Redis
> const redis = require('redis').createClient();
>
> app.post('/webhook', async (req, res) => {
>   const event = req.body;
>   const eventKey = `webhook:${event.callId}:${event.eventType}:${event.timestamp}`;
>
>   // Check if already processed
>   const exists = await redis.exists(eventKey);
>   if (exists) {
>     console.log('Duplicate event detected:', eventKey);
>     return res.status(200).json({
>       received: true,
>       duplicate: true
>     });
>   }
>
>   // Mark as processed (expire after 24 hours)
>   await redis.setex(eventKey, 86400, 'processed');
>
>   // Process event
>   await processEvent(event);
>
>   res.status(200).json({ received: true });
> });
> ```

> [accordion] Missing or Null Fields in outcomeResult
>
> **Symptoms:**
>
> * Expected fields are `null` or `undefined`
> * Outcome data not present
>
> **Causes:**
>
> * Agent didn't collect that information during call
> * Field name mismatch with agent configuration
> * User didn't provide the information
> * Agent configuration doesn't include that extraction
>
> **Solutions:**
>
> ```javascript theme={null}
> // Always handle optional fields gracefully
> const expectedSalary = outcomeResult.expectedSalary ?? 'Not provided';
> const noticePeriod = outcomeResult.noticePeriod || null;
>
> // Distinguish between null (not collected) and undefined (field missing)
> if (outcomeResult.expectedSalary === undefined) {
>   console.log('Field not in agent configuration');
> } else if (outcomeResult.expectedSalary === null) {
>   console.log('User did not provide salary information');
> } else {
>   console.log('Salary collected:', outcomeResult.expectedSalary);
> }
>
> // Log all available fields for debugging
> console.log('Available outcomeResult fields:',
>   Object.keys(outcomeResult)
> );
> ```

> [accordion] Complex Data Not Preserved (Arrays/Objects)
>
> **Symptoms:**
>
> * Arrays or objects sent in `params` are not preserved correctly
> * Receiving stringified data or corrupted structures
>
> **Cause:**
>
> * Not converting complex data to JSON strings before sending
>
> **Solution:**
>
> ```javascript theme={null}
> // ✅ GOOD - Convert to JSON string before sending
> const config = {
>   agentId: 'agent-123',
>   params: {
>     application_id: '12345',
>     interview_questions: JSON.stringify([
>       "Tell me about your experience",
>       "What are your salary expectations?"
>     ]),
>     candidate_data: JSON.stringify({
>       name: "John Doe",
>       skills: ["JavaScript", "Python"]
>     })
>   }
> };
>
> // In webhook handler - parse JSON strings
> const questions = JSON.parse(outcomeResult.interview_questions);
> const candidateData = JSON.parse(outcomeResult.candidate_data);
>
> console.log('Questions:', questions);
> console.log('Candidate:', candidateData);
> ```

### Testing Your Webhook

#### Local Testing with cURL

```bash theme={null}
# Test basic connectivity
curl -X POST https://your-endpoint.com/webhook \
  -H "Content-Type: application/json" \
  -d '{"test": true}'

# Test with authentication
curl -X POST https://your-endpoint.com/webhook \
  -H "Content-Type: application/json" \
  -H "Authorization: Bearer your_secret_token" \
  -d '{"test": true}'

# Test with full call.ended payload
curl -X POST https://your-endpoint.com/webhook \
  -H "Content-Type: application/json" \
  -H "Authorization: Bearer your_secret_token" \
  -d '{
    "eventType": "call.ended",
    "callId": "test-call-123",
    "timestamp": "2024-01-15T14:35:00.000Z",
    "projectId": "proj_test",
    "agentId": "agent_test",
    "agentName": "Test Agent",
    "data": {
      "timestamp": "2024-01-15T14:35:00.000Z",
      "data": {
        "conversationId": "conv-test-123",
        "conversationRecording": "https://example.com/recording.mp3",
        "transcription": [
          {"Agent": "Hello, how can I help you?"},
          {"User": "I need assistance."}
        ],
        "outcomeResult": {
          "application_id": "12345",
          "user_id": "user-789",
          "expectedSalary": "80000",
          "noticePeriod": "2 weeks"
        }
      }
    }
  }'
```

#### Monitoring & Logging

```javascript theme={null}
// Add comprehensive logging
const winston = require('winston');

const logger = winston.createLogger({
  level: 'info',
  format: winston.format.json(),
  transports: [
    new winston.transports.File({ filename: 'webhook-errors.log', level: 'error' }),
    new winston.transports.File({ filename: 'webhook-all.log' })
  ]
});

app.post('/webhook', async (req, res) => {
  const event = req.body;

  logger.info('Webhook received', {
    eventType: event.eventType,
    callId: event.callId,
    timestamp: event.timestamp,
    ip: req.ip
  });

  try {
    await processEvent(event);

    logger.info('Webhook processed successfully', {
      callId: event.callId,
      eventType: event.eventType
    });

    res.status(200).json({ received: true });

  } catch (error) {
    logger.error('Webhook processing failed', {
      callId: event.callId,
      eventType: event.eventType,
      error: error.message,
      stack: error.stack
    });

    // Still return 200
    res.status(200).json({
      received: true,
      processing_error: true
    });
  }
});
```

***

## Integration Checklist

### Before Going Live

**1. Data Structure Planning**

Define what custom params you'll send in `params` (identifiers, context). Document your `params` structure for your team. Plan how you'll use echoed data to match records. Decide what outcome data you expect from agent. Ensure identifiers are unique and persistent.

**2. Webhook Setup**

Create webhook endpoint (POST route). Make endpoint publicly accessible (use ngrok for testing). Configure webhook URL in Hamsa dashboard. Test webhook receives POST requests. Verify webhook returns 200 OK within 5 seconds. Implement Bearer token authentication.

**3. Implementation**

Implement event type handling (call.started, call.ended, etc.). Add data extraction from outcomeResult. Implement database updates. Add transcript parsing. Handle recording URL storage. Test with missing/null fields.

**4. Error Handling**

Validate incoming payload structure. Handle authentication failures. Implement proper error logging. Add timeout handling. Test error scenarios.

**5. Advanced Features**

Implement async processing (queue-based). Add idempotency checks. Set up rate limiting. Add monitoring and alerting. Implement retry logic.

**6. Testing**

Test with sample initiation data. Verify all custom params are echoed correctly. Test transcript parsing. Test recording URL access. Test with missing/null fields. Test error handling (invalid data, missing identifiers). Load test with multiple concurrent webhooks.

**7. Production Deployment**

Use HTTPS for webhook endpoint. Use strong Bearer token authentication. Add webhook authentication verification. Add comprehensive logging and monitoring. Set up error alerts. Document your integration. Train team on monitoring.

***

## Summary

### Key Takeaways

> [card] Initiation Data
>
> Send configuration with `agentId` and `params` containing ANY custom data

> [card] Echo Pattern
>
> All fields in `params` are echoed back in webhook's `outcomeResult`

> [card] Identifiers
>
> Always include unique identifiers to match webhooks to your records

> [card] Event Types
>
> Handle different event types: started, answered, transcription, ended

> [card] Quick Response
>
> Always respond with 200 OK within 5 seconds to avoid timeouts

> [card] Async Processing
>
> Use queues for time-consuming operations after acknowledging receipt

> [card] Idempotency
>
> Implement duplicate detection using callId + eventType + timestamp

> [card] Validation
>
> Always validate echoed identifiers and handle missing fields

> [card] Security
>
> Use HTTPS, Bearer tokens, and validate all incoming data

### Quick Reference

**Data Flow:**

```
YOU SEND (Initiation):
params: { application_id: "12345", user_name: "John", ... }

         ↓ [ Call Happens ] ↓

YOU RECEIVE (Webhook):
outcomeResult: {
  application_id: "12345",     // ← Your data (echoed)
  user_name: "John",           // ← Your data (echoed)
  expectedSalary: "80k",       // ← New data (collected)
  noticePeriod: "2 weeks"      // ← New data (collected)
}
```

**Integration Phases:**

| Phase | What You Send/Receive | Purpose |
| - | - | - |
| **Initiation** | Configuration with `params` | Send identifiers + context to agent |
| **Call** | N/A (agent conducts call) | Agent collects information from user |
| **Webhook** | POST with echoed params + new data | Receive results and match to your records |

***

## Next Steps

> [card] [Webhooks Feature](https://nadsoft.mintlify.site/features/webhooks)
>
> Detailed webhook events reference

> [card] [API Integration](https://nadsoft.mintlify.site/guides/api-integration)
>
> Integrate with external APIs using tools

> [card] [Voice Agents SDK](https://nadsoft.mintlify.site/api-reference/voice-agents-sdk.md)
>
> Complete SDK documentation

> [card] [Data Collection](https://nadsoft.mintlify.site/guides/data-collection)
>
> Advanced data extraction patterns

***

## Need Help?

> [card] [Contact Support](mailto:support@tryhamsa.com)
>
> Get technical assistance from our team

> [card] [Documentation](https://docs.tryhamsa.com)
>
> Browse complete documentation

> [!NOTE]
>
> **Document Version:** 2.0\\
>
> **Last Updated:** 2025-12-08\\
>
> **Hamsa SDK Version:** Compatible with @hamsa-ai/voice-agents-sdk v2.x+
>
> For support, contact [Hamsa Support](https://tryhamsa.com/contact/) or refer to official SDK documentation.


This documentation is built and hosted on [Mintlify](https://mintlify.com), a developer documentation platform.