> ## Documentation Index
> Fetch the complete documentation index at: https://bunnynet-cb9733c2-onclientmiddleware.mintlify.site/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.

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

## 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.](https://docs.bunny.net/api-reference/core/stream-video-library/get-video-library#response-read-only-api-key-one-of-0)
</Note>

### 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**](https://docs.bunny.net/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

<Steps>
  <Step title="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.
  </Step>

  <Step title="Check the version header">
    Ensure `X-BunnyStream-Signature-Version` is `v1`.
  </Step>

  <Step title="Check the algorithm header">
    Ensure `X-BunnyStream-Signature-Algorithm` is `hmac-sha256`.
  </Step>

  <Step title="Get your signing secret">
    Use your library's **Read-Only API key** as the signing secret.
  </Step>

  <Step title="Compute the expected signature">
    `expectedSignature = lowercase_hex(HMAC-SHA256(rawBody, signatureSecret))`
  </Step>

  <Step title="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.
  </Step>
</Steps>

### Code examples

<CodeGroup>
  ```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));
  }
  ```
</CodeGroup>

### 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.
</Warning>

* **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.
