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

# Webhooks

> Bunny Stream allows you to configure a secure webhook URL. Our system will automatically send notifications back to this URL once a video status changes. This allows you to track the updates and progress of the video processing cycles on your side.

Configure a webhook URL on your library to receive a POST notification every time a video changes state — for example, when uploading completes or transcoding finishes. The sections below describe the callback payload, status codes, and signature verification.

## Callback

When the video state changes, a JSON callback will be sent back to your URL as a POST request. Below is an example object that will be sent.

```json JSON theme={null}
{
	"VideoLibraryId": 133,
	"VideoGuid": "657bb740-a71b-4529-a012-528021c31a92",
	"Status": 3
}
```

## Status list

Below are the possible status codes sent with the webhook:

* 0 - **Queued**: The video has been queued for encoding.
* 1 - **Processing**: The video has begun processing the preview and format details.
* 2 - **Encoding**: The video is encoding.
* 3 - **Finished**: The video encoding has finished and the video is fully available.
* 4 - **Resolution finished**: The encoder has finished processing one of the resolutions. The first request also signals that the video is now playable.
* 5 - **Failed**: The video encoding failed. The video has finished processing.
* 6 - **PresignedUploadStarted** : A pre-signed upload has been initiated.
* 7 - **PresignedUploadFinished** : A pre-signed upload has been completed.
* 8 - **PresignedUploadFailed** : A pre-signed upload has failed.
* 9 - **CaptionsGenerated** : Automatic captions were generated.
* 10 - **TitleOrDescriptionGenerated** : Automatic generation of title or description has been completed.

## Signature validation

Verify that an incoming webhook request was sent by Bunny Stream and has not been tampered with.

### Overview

Signed webhook POSTs use signature version **`v1`**. Each signed request includes the following headers:

| Header | Value |
| - | - |
| `X-BunnyStream-Signature-Version` | `v1` |
| `X-BunnyStream-Signature-Algorithm` | `hmac-sha256` |
| `X-BunnyStream-Signature` | Lowercase hex HMAC-SHA256 (64 characters) |

The signature is an **HMAC-SHA256** of the exact raw request body, using the webhook signing secret as the key, encoded as a **lowercase hex string**.

> [!NOTE]
>
> [The signing secret is your library's **Read-Only API key**. Validation must use the **exact raw body** as received — do not parse and re-serialize the JSON body.](/docs/api-reference/core/stream-video-library/get-video-library#response-read-only-api-key-one-of-0)

### How the signature is built

```text theme={null}
version    = "v1"
algorithm  = "hmac-sha256"
signature  = lowercase_hex( HMAC-SHA256( utf8(body), utf8(signatureSecret) ) )
```

* **body**: The exact raw HTTP request body bytes as received.
* **signatureSecret**: Your library's [**Read-Only API key**](/docs/api-reference/core/stream-video-library/get-video-library#response-read-only-api-key-one-of-0), encoded as UTF-8.
* **Output**: 64-character lowercase hexadecimal string.
* The URL, timestamp, HTTP method, and headers are **not** part of the `v1` signature.

### Validation steps

**1. Read the raw request body**

Capture the raw body from the HTTP request stream before any parsing. Do **not** parse to JSON and re-serialize.

**2. Check the version header**

Ensure `X-BunnyStream-Signature-Version` is `v1`.

**3. Check the algorithm header**

Ensure `X-BunnyStream-Signature-Algorithm` is `hmac-sha256`.

**4. Get your signing secret**

Use your library's **Read-Only API key** as the signing secret.

**5. Compute the expected signature**

`expectedSignature = lowercase_hex(HMAC-SHA256(rawBody, signatureSecret))`

**6. Compare signatures**

Read `X-BunnyStream-Signature` and compare it to your computed value using a **constant-time comparison** to avoid timing attacks. If they match, the request is authentic and the body was not modified in transit.

### Code examples

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

function validateWebhookSignature(rawBody, signatureHeader, signatureVersion, signatureAlgorithm, signatureSecret) {
  if (signatureVersion !== 'v1') {
    return false;
  }

  if (signatureAlgorithm !== 'hmac-sha256') {
    return false;
  }

  const expectedHex = crypto
    .createHmac('sha256', signatureSecret)
    .update(rawBody, 'utf8')
    .digest('hex');

  if (
    typeof signatureHeader !== 'string' ||
    signatureHeader.length !== expectedHex.length ||
    !/^[0-9a-f]+$/.test(signatureHeader)
  ) {
    return false;
  }

  return crypto.timingSafeEqual(
    Buffer.from(expectedHex, 'utf8'),
    Buffer.from(signatureHeader, 'utf8')
  );
}

// Express: use body parser that keeps raw body
app.post('/webhook', express.raw({ type: 'application/json' }), (req, res) => {
  const signature = req.headers['x-bunnystream-signature'];
  const version = req.headers['x-bunnystream-signature-version'];
  const algorithm = req.headers['x-bunnystream-signature-algorithm'];
  const rawBody = req.body.toString('utf8');
  if (!validateWebhookSignature(rawBody, signature, version, algorithm, process.env.READ_ONLY_API_KEY)) {
    return res.status(401).send('Invalid signature');
  }
  const data = JSON.parse(rawBody);
  // ... handle webhook
});
```

```python Python theme={null}
import os
import hmac
import hashlib
from flask import request

def validate_webhook_signature(
    raw_body: bytes,
    signature_header: str,
    signature_version: str,
    signature_algorithm: str,
    signature_secret: str,
) -> bool:
    if signature_version != "v1":
        return False

    if signature_algorithm != "hmac-sha256":
        return False

    expected = hmac.new(
        signature_secret.encode('utf-8'),
        raw_body,
        hashlib.sha256
    ).hexdigest()
    return hmac.compare_digest(expected, signature_header)

@app.route('/webhook', methods=['POST'])
def webhook():
    raw_body = request.get_data()
    signature = request.headers.get('X-BunnyStream-Signature', '')
    signature_version = request.headers.get('X-BunnyStream-Signature-Version', '')
    signature_algorithm = request.headers.get('X-BunnyStream-Signature-Algorithm', '')
    if not validate_webhook_signature(raw_body, signature, signature_version, signature_algorithm, os.environ['READ_ONLY_API_KEY']):
        return '', 401
    data = request.get_json()
    # ... handle webhook
```

```csharp C# theme={null}
using System.Security.Cryptography;
using System.Text;

bool ValidateWebhookSignature(
    string rawBody,
    string signatureHeader,
    string signatureVersion,
    string signatureAlgorithm,
    string signatureSecret)
{
    if (!string.Equals(signatureVersion, "v1", StringComparison.Ordinal))
    {
        return false;
    }

    if (!string.Equals(signatureAlgorithm, "hmac-sha256", StringComparison.Ordinal))
    {
        return false;
    }

    byte[] keyBytes = Encoding.UTF8.GetBytes(signatureSecret);
    byte[] bodyBytes = Encoding.UTF8.GetBytes(rawBody);
    using var hmac = new HMACSHA256(keyBytes);
    byte[] hash = hmac.ComputeHash(bodyBytes);
    string expected = Convert.ToHexString(hash).ToLowerInvariant();
    return CryptographicOperations.FixedTimeEquals(
        Encoding.UTF8.GetBytes(expected),
        Encoding.UTF8.GetBytes(signatureHeader));
}
```

### Important notes

> [!WARNING]
>
> Always use the **exact raw body bytes** for validation. If your framework parses JSON and re-serializes it, whitespace or key ordering may change and the signature will not match.

* **Constant-time comparison**: Use `timingSafeEqual`, `hmac.compare_digest`, or `CryptographicOperations.FixedTimeEquals` so comparison time does not leak information about the secret.
* **Signing secret**: Use your library's **Read-Only API key** as the signing secret. Keep it server-side only; never expose it to the client.
* **Version & algorithm headers**: Always validate both `X-BunnyStream-Signature-Version` and `X-BunnyStream-Signature-Algorithm` before computing the signature.


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