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

# Webhooks Overview

> Receive real-time notifications when events occur in your Bota project

Webhooks allow your application to receive real-time HTTP notifications when events occur in Bota. Instead of polling the API to check for status changes, webhooks push updates to your server as they happen.

***

## Why Use Webhooks?

| Approach     | Pros                 | Cons                              |
| ------------ | -------------------- | --------------------------------- |
| **Polling**  | Simple to implement  | Wastes resources, delayed updates |
| **Webhooks** | Real-time, efficient | Requires endpoint setup           |

Bota's async processing model (transcription, summarization) makes webhooks essential. When a transcription completes — which may take seconds to minutes — you'll know immediately.

***

## Quick Start

### 1. Create a Webhook Endpoint

Build an HTTP endpoint that accepts POST requests:

<CodeGroup>
  ```javascript Express.js theme={null}
  const express = require('express');
  const app = express();

  app.post('/webhooks/bota', express.raw({ type: 'application/json' }), (req, res) => {
    const event = JSON.parse(req.body);

    console.log('Received event:', event.type);

    // Handle the event
    switch (event.type) {
      case 'transcription.completed':
        handleTranscriptionComplete(event.data);
        break;
      case 'recording.uploaded':
        handleRecordingUploaded(event.data);
        break;
    }

    // Return 200 quickly, process async
    res.status(200).send('OK');
  });

  app.listen(3000);
  ```

  ```python Flask theme={null}
  from flask import Flask, request

  app = Flask(__name__)

  @app.route('/webhooks/bota', methods=['POST'])
  def handle_webhook():
      event = request.json

      print(f"Received event: {event['type']}")

      # Handle the event
      if event['type'] == 'transcription.completed':
          handle_transcription_complete(event['data'])
      elif event['type'] == 'recording.uploaded':
          handle_recording_uploaded(event['data'])

      # Return 200 quickly, process async
      return 'OK', 200

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

### 2. Register the Webhook

```bash theme={null}
curl -X POST https://api.bota.dev/v1/webhooks \
  -H "Authorization: Bearer sk_live_..." \
  -H "Content-Type: application/json" \
  -d '{
    "url": "https://your-app.com/webhooks/bota",
    "events": ["transcription.completed", "recording.uploaded"]
  }'
```

Response:

```json theme={null}
{
  "id": "wh_abc123",
  "url": "https://your-app.com/webhooks/bota",
  "events": ["transcription.completed", "recording.uploaded"],
  "secret": "whsec_xyz789...",
  "created_at": "2025-01-15T10:00:00Z"
}
```

<Warning>
  Save the `secret` — you'll need it to verify webhook signatures. It's only shown once.
</Warning>

### 3. Verify Signatures

Always verify that webhooks are from Bota. See [Signature Verification](#signature-verification) below.

***

## Event Types

| Event                     | Trigger                   | Common Use Case               |
| ------------------------- | ------------------------- | ----------------------------- |
| `recording.created`       | Recording entry created   | Track new recordings          |
| `recording.uploaded`      | Audio upload completed    | Trigger processing            |
| `recording.deleted`       | Recording deleted         | Clean up cached data          |
| `transcription.started`   | Transcription begins      | Show processing status        |
| `transcription.completed` | Transcription finished    | Display results to user       |
| `transcription.failed`    | Transcription failed      | Alert, retry, or fallback     |
| `summary.started`         | Summary generation begins | Show processing status        |
| `summary.completed`       | Summary generated         | Update UI, send notifications |
| `summary.failed`          | Summary generation failed | Alert or retry                |

See [Webhook Events](/webhooks/events) for detailed payload schemas.

***

## Payload Structure

All webhook payloads follow this structure:

```json theme={null}
{
  "id": "evt_abc123",
  "type": "transcription.completed",
  "created_at": "2025-01-15T10:30:00Z",
  "data": {
    "id": "txn_abc123",
    "recording_id": "rec_xyz789",
    "status": "completed",
    // ... event-specific fields
  }
}
```

| Field        | Description                                     |
| ------------ | ----------------------------------------------- |
| `id`         | Unique event identifier (use for deduplication) |
| `type`       | Event type string                               |
| `created_at` | When the event occurred (ISO 8601)              |
| `data`       | Event-specific payload                          |

***

## Signature Verification

All webhooks are signed using HMAC-SHA256. **Always verify signatures** to ensure requests are from Bota.

### Signature Header

```
X-Bota-Signature: sha256=a1b2c3d4e5f6...
```

### Verification Code

<CodeGroup>
  ```javascript Node.js theme={null}
  const crypto = require('crypto');

  function verifyWebhookSignature(payload, signature, secret) {
    const expectedSignature = crypto
      .createHmac('sha256', secret)
      .update(payload, 'utf8')
      .digest('hex');

    const actualSignature = signature.replace('sha256=', '');

    return crypto.timingSafeEqual(
      Buffer.from(expectedSignature),
      Buffer.from(actualSignature)
    );
  }

  // Express middleware
  app.post('/webhooks/bota', express.raw({ type: 'application/json' }), (req, res) => {
    const signature = req.headers['x-bota-signature'];

    if (!verifyWebhookSignature(req.body, signature, process.env.WEBHOOK_SECRET)) {
      return res.status(401).send('Invalid signature');
    }

    const event = JSON.parse(req.body);
    // Process event...

    res.status(200).send('OK');
  });
  ```

  ```python Python theme={null}
  import hmac
  import hashlib
  from flask import Flask, request, abort

  def verify_webhook_signature(payload: bytes, signature: str, secret: str) -> bool:
      expected_signature = hmac.new(
          secret.encode('utf-8'),
          payload,
          hashlib.sha256
      ).hexdigest()

      actual_signature = signature.replace('sha256=', '')

      return hmac.compare_digest(expected_signature, actual_signature)

  @app.route('/webhooks/bota', methods=['POST'])
  def handle_webhook():
      signature = request.headers.get('X-Bota-Signature')

      if not verify_webhook_signature(request.data, signature, os.environ['WEBHOOK_SECRET']):
          abort(401)

      event = request.json
      # Process event...

      return 'OK', 200
  ```
</CodeGroup>

<Warning>
  Never skip signature verification, even in development. Attackers can send fake webhook payloads to trigger unwanted actions.
</Warning>

***

## Retry Policy

Bota retries failed deliveries with exponential backoff:

| Attempt | Delay After Failure |
| ------- | ------------------- |
| 1       | Immediate           |
| 2       | 1 minute            |
| 3       | 5 minutes           |
| 4       | 30 minutes          |
| 5       | 2 hours             |
| 6       | 8 hours             |

After 6 failed attempts, the event is marked as failed and no further retries occur.

### What Counts as Failure?

* Non-2xx HTTP response
* Request timeout (30 seconds)
* Connection refused
* TLS/SSL errors

***

## Best Practices

<AccordionGroup>
  <Accordion title="Respond quickly (< 5 seconds)">
    Return a `200` response immediately, then process the event asynchronously. Long-running processing in the request handler will cause timeouts.

    ```javascript theme={null}
    app.post('/webhooks/bota', (req, res) => {
      // Return immediately
      res.status(200).send('OK');

      // Process async
      processEventAsync(req.body);
    });
    ```
  </Accordion>

  <Accordion title="Handle duplicates with idempotency">
    The same event may be delivered multiple times due to retries. Use the `id` field to deduplicate:

    ```javascript theme={null}
    const processedEvents = new Set();

    function handleEvent(event) {
      if (processedEvents.has(event.id)) {
        return; // Already processed
      }

      processedEvents.add(event.id);
      // Process event...
    }
    ```

    For production, store processed event IDs in a database.
  </Accordion>

  <Accordion title="Use HTTPS endpoints only">
    Bota only delivers webhooks to HTTPS URLs. HTTP endpoints are rejected for security.
  </Accordion>

  <Accordion title="Handle out-of-order delivery">
    Events may arrive out of order. Don't assume `recording.uploaded` arrives before `transcription.completed`. Check object state via API if order matters.
  </Accordion>

  <Accordion title="Log webhook payloads">
    Log incoming webhooks for debugging. Include the event ID, type, and any processing errors.
  </Accordion>

  <Accordion title="Monitor webhook health">
    Track your webhook success rate. Frequent failures may indicate endpoint issues.
  </Accordion>
</AccordionGroup>

***

## Testing Webhooks

### Local Development

Use a tunneling service like [ngrok](https://ngrok.com) to receive webhooks locally:

```bash theme={null}
# Start ngrok
ngrok http 3000

# Register the ngrok URL
curl -X POST https://api.bota.dev/v1/webhooks \
  -H "Authorization: Bearer sk_live_..." \
  -H "Content-Type: application/json" \
  -d '{
    "url": "https://abc123.ngrok.io/webhooks/bota",
    "events": ["transcription.completed"]
  }'
```

### Trigger Test Events

Create a recording and transcription to trigger events:

```bash theme={null}
# Create a recording (triggers recording.created)
curl -X POST https://api.bota.dev/v1/recordings \
  -H "Authorization: Bearer sk_live_..." \
  -H "Content-Type: application/json" \
  -d '{"end_user_id": "eu_abc123", "device_id": "dev_xyz789"}'

# Start transcription (triggers transcription.completed when done)
curl -X POST https://api.bota.dev/v1/transcriptions \
  -H "Authorization: Bearer sk_live_..." \
  -H "Content-Type: application/json" \
  -d '{"recording_id": "rec_abc123"}'
```

***

## Managing Webhooks

### List Webhooks

```bash theme={null}
curl https://api.bota.dev/v1/webhooks \
  -H "Authorization: Bearer sk_live_..."
```

### Delete a Webhook

```bash theme={null}
curl -X DELETE https://api.bota.dev/v1/webhooks/wh_abc123 \
  -H "Authorization: Bearer sk_live_..."
```

### Update Events (Delete and Recreate)

To change which events a webhook receives, delete it and create a new one.

***

## Troubleshooting

<AccordionGroup>
  <Accordion title="Not receiving webhooks">
    1. Verify the webhook is registered: `GET /webhooks`
    2. Check that your endpoint is publicly accessible
    3. Ensure you're returning 2xx status codes
    4. Check your server logs for incoming requests
    5. Verify the events you're subscribed to match what you expect
  </Accordion>

  <Accordion title="Signature verification failing">
    1. Ensure you're using the raw request body, not parsed JSON
    2. Check that you're using the correct webhook secret
    3. Verify you haven't modified the payload before verification
  </Accordion>

  <Accordion title="Duplicate events">
    This is expected behavior. Implement idempotency using the event `id` field.
  </Accordion>

  <Accordion title="Events arriving out of order">
    This is expected. Don't rely on event ordering. Check resource state via API if needed.
  </Accordion>
</AccordionGroup>

***

## Next Steps

<CardGroup cols={2}>
  <Card title="Event Reference" icon="list" href="/webhooks/events">
    Detailed payload schemas for all event types
  </Card>

  <Card title="API Reference" icon="code" href="/api-reference/webhooks/create">
    Webhook management endpoints
  </Card>
</CardGroup>
