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

# Webhook Integration - Developer Guide

> Build robust webhook handlers to process Hamsa voice agent events

# Webhook Integration for Developers

Learn how to build production-ready webhook handlers to receive and process real-time call events from Hamsa voice agents.

<Info>
  **Prerequisites:**

  * Webhook URL configured in dashboard ([Setup Guide](/agents/webhooks/introduction))
  * Basic understanding of HTTP POST requests and JSON
  * Node.js, Python, or your preferred backend framework
</Info>

## Understanding Webhook Data

### Custom Parameters (Echo Pattern)

The most powerful feature of Hamsa webhooks is the **echo pattern**. All custom parameters you send during agent initiation are echoed back in the webhook response.

**What You Send:**

```json theme={null}
{
  "agentId": "agent-123-abc",
  "params": {
    "application_id": "12345",
    "user_id": "user-789",
    "session_id": "sess-abc-def",
    "candidate_name": "John Doe",
    "company_name": "Acme Corp"
  }
}
```

**What You Receive in Webhook:**

```json theme={null}
{
  "eventType": "call.ended",
  "data": {
    "data": {
      "outcomeResult": {
        "application_id": "12345",        // ← Echoed back
        "user_id": "user-789",            // ← Echoed back
        "session_id": "sess-abc-def",     // ← Echoed back
        "candidate_name": "John Doe",     // ← Echoed back
        "company_name": "Acme Corp",      // ← Echoed back
        "expectedSalary": "80000",        // ← New data from call
        "noticePeriod": "2 weeks"         // ← New data from call
      }
    }
  }
}
```

This pattern allows you to match webhook responses to your database records without maintaining state.

### Event Structure

All webhook events follow this structure:

```typescript theme={null}
{
  eventType: "call.started" | "call.answered" | "transcription.update" | "tool.executed" | "call.ended",
  callId: string,              // Unique call identifier
  timestamp: string,           // ISO 8601 timestamp
  projectId: string,           // Your Hamsa project ID
  agentId: string,             // Agent configuration ID
  agentName: string,           // Agent name
  data: object                 // Event-specific data
}
```

### Call Ended Event (Most Important)

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

```json theme={null}
{
  "eventType": "call.ended",
  "callId": "call_uuid_12345",
  "timestamp": "2024-01-15T14:35:00.000Z",
  "data": {
    "timestamp": "2024-01-15T14:35:00.000Z",
    "data": {
      "conversationId": "conv-123-abc",
      "conversationRecording": "https://hamsa-recordings.s3.amazonaws.com/recording.mp3",
      "transcription": [
        { "Agent": "Hello! How can I help you?" },
        { "User": "I need assistance." }
      ],
      "outcomeResult": {
        // Your echoed params + collected data
      }
    }
  }
}
```

## Implementing Webhook Handlers

### Node.js/Express Implementation

<Tabs>
  <Tab title="Basic Handler">
    ```javascript 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;

      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('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;
        }

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

      } catch (error) {
        console.error('Webhook error:', error);
        // Still return 200 to avoid retries
        res.status(200).json({
          received: true,
          processing_error: true
        });
      }
    });

    app.listen(3000, () => {
      console.log('Webhook server running on port 3000');
    });
    ```
  </Tab>

  <Tab title="Call Ended Handler">
    ```javascript theme={null}
    async function handleCallEnded(event) {
      // Extract webhook data
      const { timestamp } = event.data;
      const {
        conversationId,
        conversationRecording,
        transcription,
        outcomeResult
      } = event.data.data;

      // 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 from call)
      const expectedSalary = outcomeResult.expectedSalary;
      const noticePeriod = outcomeResult.noticePeriod;
      const numberOfSpeakers = outcomeResult.number_of_speakers;

      // Build transcript text
      const transcriptText = transcription
        .map(turn => {
          if (turn.Agent) return `Agent: ${turn.Agent}`;
          if (turn.User) return `User: ${turn.User}`;
          return '';
        })
        .filter(Boolean)
        .join('\n');

      // Update your database
      await database.calls.update({
        where: { applicationId },
        data: {
          conversationId,
          recordingUrl: conversationRecording,
          transcript: transcriptText,
          expectedSalary,
          noticePeriod,
          numberOfSpeakers,
          completedAt: timestamp
        }
      });
    }
    ```
  </Tab>
</Tabs>

### Python/Flask Implementation

<Tabs>
  <Tab title="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:
            event = request.get_json()

            print(f"Event: {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)

            return jsonify({'received': True}), 200

        except Exception as e:
            print(f'Webhook error: {str(e)}')
            # Still return 200
            return jsonify({
                'received': True,
                'processing_error': True
            }), 200

    if __name__ == '__main__':
        app.run(port=3000)
    ```
  </Tab>

  <Tab title="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']

        # Extract identifiers (echoed back)
        application_id = outcome_result.get('application_id')
        user_id = outcome_result.get('user_id')
        session_id = outcome_result.get('session_id')

        # Extract collected data
        expected_salary = outcome_result.get('expectedSalary')
        notice_period = outcome_result.get('noticePeriod')
        num_speakers = outcome_result.get('number_of_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)

        # Update database
        update_database({
            'application_id': application_id,
            'conversation_id': conversation_id,
            'recording_url': recording_url,
            'transcript': transcript_text,
            'expected_salary': expected_salary,
            'notice_period': notice_period,
            'completed_at': timestamp
        })
    ```
  </Tab>
</Tabs>

## Advanced Topics

### Async Processing with Queues

Always process webhooks asynchronously to avoid timeouts:

```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
    await eventQueue.add('process-event', event, {
      attempts: 3,
      backoff: {
        type: 'exponential',
        delay: 2000
      }
    });

    // 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;

  if (event.eventType === 'call.ended') {
    await saveToDatabase(event);
    await sendNotifications(event);
    await updateAnalytics(event);
  }
});
```

### Idempotency

Prevent duplicate processing:

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

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

  const exists = await redis.exists(eventKey);
  if (exists) {
    return true; // Already processed
  }

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

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

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

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

### 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
      });
      return res.status(401).json({ error: 'Unauthorized' });
    }

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

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

    await Promise.race([
      processEvent(event),
      timeoutPromise
    ]);

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

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

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

## Common Use Cases

### CRM Integration

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

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

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

  // Update CRM
  await crm.contacts.upsert({
    where: { phone: outcomeResult.phone },
    update: {
      lastCallDate: event.timestamp,
      lastCallNotes: transcriptText,
      intent: outcomeResult.intent,
      sentiment: outcomeResult.sentiment_score
    }
  });

  // Create activity
  await crm.activities.create({
    type: 'phone_call',
    contactPhone: outcomeResult.phone,
    subject: `AI Agent Call - ${outcomeResult.intent}`,
    description: transcriptText,
    recordingUrl: event.data.data.conversationRecording
  });
}
```

### Notification System

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

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

  // Urgent notification
  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*Duration:* ${callDuration}s`
          }
        },
        {
          type: 'actions',
          elements: [
            {
              type: 'button',
              text: { type: 'plain_text', text: 'View Details' },
              url: `https://dashboard.example.com/calls/${event.callId}`
            }
          ]
        }
      ]
    });
  }
}
```

## Troubleshooting

### Cannot Match Webhook to Record

**Problem:** Missing application\_id in outcomeResult

**Solution:**

```javascript theme={null}
// ✅ Always include unique identifiers
const config = {
  agentId: 'agent-123',
  params: {
    application_id: applicationId.toString(),
    user_id: userId.toString(),
    session_id: generateSessionId()
  }
};

// In webhook - verify identifiers exist
const applicationId = outcomeResult.application_id;
if (!applicationId) {
  console.error('Missing identifier');
  console.error('Full payload:', JSON.stringify(req.body, null, 2));
}
```

### Missing or Null Fields

**Problem:** Expected fields are null or undefined

**Solution:**

```javascript theme={null}
// Handle optional fields gracefully
const expectedSalary = outcomeResult.expectedSalary ?? 'Not provided';
const noticePeriod = outcomeResult.noticePeriod || null;

// Distinguish between null and undefined
if (outcomeResult.expectedSalary === undefined) {
  console.log('Field not in agent configuration');
} else if (outcomeResult.expectedSalary === null) {
  console.log('User did not provide information');
} else {
  console.log('Value:', outcomeResult.expectedSalary);
}
```

### Duplicate Events

**Problem:** Receiving same event multiple times

**Solution:** Implement idempotency (see above section)

## Testing

### Local Testing with cURL

```bash theme={null}
# Test with full 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-123",
    "timestamp": "2024-01-15T14:35:00.000Z",
    "data": {
      "data": {
        "conversationId": "conv-test",
        "transcription": [],
        "outcomeResult": {
          "application_id": "12345"
        }
      }
    }
  }'
```

### Monitoring

```javascript theme={null}
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,
    ip: req.ip
  });

  try {
    await processEvent(event);
    logger.info('Processed successfully', {
      callId: event.callId
    });
  } catch (error) {
    logger.error('Processing failed', {
      callId: event.callId,
      error: error.message,
      stack: error.stack
    });
  }

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

## Next Steps

<CardGroup cols={2}>
  <Card title="Configure Webhooks" href="/agents/webhooks/introduction" icon="sliders">
    Set up webhook URLs in the dashboard
  </Card>

  <Card title="Webhooks Overview" href="/overview/features/webhooks" icon="book">
    Learn webhook concepts and patterns
  </Card>

  <Card title="Outcomes Configuration" href="/overview/features/outcomes" icon="database">
    Configure what data to extract from calls
  </Card>

  <Card title="API Reference" href="/api-reference/introduction" icon="code">
    Complete API documentation
  </Card>
</CardGroup>
