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

# Webhooks

> Receive real-time notifications when videos are ready

# Webhooks

Webhooks provide real-time notifications when your video generation tasks complete. Instead of polling the API, your server receives an HTTP POST request when the task finishes.

## Setting Up Webhooks

Include a `webhook_url` in your slideshow request:

```bash theme={null}
curl -X POST https://api.vibepeak.ai/v1/real-estate/narrated-slideshow \
  -H "Authorization: Bearer vpk_live_xxxxx" \
  -H "Content-Type: application/json" \
  -d '{
    "images": [
      "https://example.com/living-room.jpg",
      "https://example.com/kitchen.jpg",
      "https://example.com/bedroom.jpg",
      "https://example.com/bathroom.jpg",
      "https://example.com/backyard.jpg"
    ],
    "webhook_url": "https://yourserver.com/webhooks/vibepeak"
  }'
```

## Webhook Payload

When a task completes (successfully or with an error), VibePeak sends a POST request to your webhook URL:

### Successful Completion

```json theme={null}
{
  "event": "task.completed",
  "task_id": "task_abc123xyz",
  "status": "completed",
  "livemode": true,
  "created_at": "2026-01-04T12:00:00Z",
  "completed_at": "2026-01-04T12:15:00Z",
  "result": {
    "video_url": "https://storage.vibepeak.ai/videos/task_abc123xyz.mp4",
    "cover_image_url": "https://storage.googleapis.com/vibepeak-audio/covers/task_abc123xyz.jpg",
    "duration_seconds": 45,
    "resolution": "1080p",
    "expires_at": "2026-01-11T12:15:00Z"
  }
}
```

### Failed Task

```json theme={null}
{
  "event": "task.failed",
  "task_id": "task_abc123xyz",
  "status": "failed",
  "livemode": true,
  "created_at": "2026-01-04T12:00:00Z",
  "completed_at": "2026-01-04T12:06:00Z",
  "error": {
    "code": "IMAGE_PROCESSING_FAILED",
    "message": "Failed to process image at index 2: Invalid image format"
  }
}
```

<Note>
  Every webhook payload includes a boolean **`livemode`** field — `true` for tasks created with a `vpk_live_` key, `false` for tasks created with a `vpk_test_` sandbox key. Branch on it in your handler if you need different behavior for sandbox traffic (e.g. skip downstream billing or storage). See [Test Mode](/concepts/test-mode#the-livemode-field) for the full sandbox lifecycle.
</Note>

## Webhook Headers

Each webhook request includes the following headers:

| Header                 | Description                                    |
| ---------------------- | ---------------------------------------------- |
| `Content-Type`         | `application/json`                             |
| `X-VibePeak-Signature` | HMAC-SHA256 signature for verification         |
| `X-VibePeak-Timestamp` | Unix timestamp when the webhook was sent       |
| `X-VibePeak-Event`     | Event type (`task.completed` or `task.failed`) |

## Webhook Secret

Each API key has an associated webhook secret used for signing webhook payloads. You can find your webhook secret in the [VibePeak Dashboard](https://app.vibepeak.ai/account/api) under your API key settings.

<Info>
  Your webhook secret is displayed only once when you create an API key. Store it securely - if you lose it, you'll need to regenerate your API key.
</Info>

## Verifying Webhooks

<Warning>
  Always verify webhook signatures to ensure requests are from VibePeak.
</Warning>

Webhooks are signed using HMAC-SHA256 with your webhook secret. Verify the signature before processing:

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

  function verifyWebhook(payload, signature, timestamp, secret) {
    const signedPayload = `${timestamp}.${JSON.stringify(payload)}`;
    const expectedSignature = crypto
      .createHmac('sha256', secret)
      .update(signedPayload)
      .digest('hex');

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

  // Express.js example
  app.post('/webhooks/vibepeak', express.json(), (req, res) => {
    const signature = req.headers['x-vibepeak-signature'];
    const timestamp = req.headers['x-vibepeak-timestamp'];

    if (!verifyWebhook(req.body, signature, timestamp, process.env.WEBHOOK_SECRET)) {
      return res.status(401).json({ error: 'Invalid signature' });
    }

    // Process the webhook
    const { event, task_id, result, error } = req.body;

    if (event === 'task.completed') {
      console.log(`Video ready: ${result.video_url}`);
      console.log(`Cover image: ${result.cover_image_url}`);
    } else if (event === 'task.failed') {
      console.log(`Task failed: ${error.message}`);
    }

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

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

  app = Flask(__name__)

  def verify_webhook(payload, signature, timestamp, secret):
      signed_payload = f"{timestamp}.{payload}"
      expected_signature = hmac.new(
          secret.encode(),
          signed_payload.encode(),
          hashlib.sha256
      ).hexdigest()
      return hmac.compare_digest(signature, expected_signature)

  @app.route('/webhooks/vibepeak', methods=['POST'])
  def webhook():
      signature = request.headers.get('X-VibePeak-Signature')
      timestamp = request.headers.get('X-VibePeak-Timestamp')

      if not verify_webhook(
          request.get_data(as_text=True),
          signature,
          timestamp,
          os.environ['WEBHOOK_SECRET']
      ):
          return jsonify({'error': 'Invalid signature'}), 401

      data = request.json

      if data['event'] == 'task.completed':
          print(f"Video ready: {data['result']['video_url']}")
          print(f"Cover image: {data['result']['cover_image_url']}")
      elif data['event'] == 'task.failed':
          print(f"Task failed: {data['error']['message']}")

      return jsonify({'received': True}), 200
  ```
</CodeGroup>

## Webhook Requirements

Your webhook endpoint must:

<CardGroup cols={2}>
  <Card title="HTTPS Only" icon="lock">
    Webhook URLs must use HTTPS for security
  </Card>

  <Card title="Respond Quickly" icon="bolt">
    Return a 2xx response within 30 seconds
  </Card>

  <Card title="Be Idempotent" icon="repeat">
    Handle duplicate deliveries gracefully
  </Card>

  <Card title="Publicly Accessible" icon="globe">
    Be reachable from the internet
  </Card>
</CardGroup>

## Retry Policy

If your webhook endpoint fails to respond with a 2xx status code, VibePeak will retry the delivery with exponential backoff:

| Attempt   | Delay     |
| --------- | --------- |
| 1st retry | 1 second  |
| 2nd retry | 2 seconds |
| 3rd retry | 4 seconds |

After 3 failed attempts, the webhook is marked as failed. You can still retrieve the task status via the API by polling `/v1/tasks/{taskId}`.

## Testing Webhooks

For local development, use a tunneling service like [ngrok](https://ngrok.com):

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

# Use the ngrok URL in your request
curl -X POST https://api.vibepeak.ai/v1/real-estate/narrated-slideshow \
  -H "Authorization: Bearer vpk_test_xxxxx" \
  -H "Content-Type: application/json" \
  -d '{
    "images": ["..."],
    "webhook_url": "https://abc123.ngrok.io/webhooks/vibepeak"
  }'
```

## Best Practices

<Tip>
  Process webhooks asynchronously. Return a 200 response immediately, then process the payload in a background job.
</Tip>

1. **Return quickly** - Acknowledge receipt with a 200 response before processing
2. **Use a queue** - Process webhook payloads asynchronously
3. **Store the task\_id** - Always log the task\_id for debugging
4. **Handle duplicates** - Use the task\_id to deduplicate events
5. **Verify signatures** - Always validate the webhook signature
6. **Download promptly** - Video URLs expire after 7 days
