# Guide to Bunny Stream Webhooks: Features and Best Practices

Bunny Stream webhooks notify your systems about video processing activity: a video finishes encoding, fails, or has captions generated. If you're building on Bunny Stream, webhooks are how you react to video status changes without polling.

This guide covers how Bunny Stream webhooks work, the Status enum, how to verify the `X-BunnyStream-Signature`, and the best practices for production.

## What are Bunny Stream webhooks?

Bunny Stream webhooks are HTTP POSTs delivered to a URL you configure per video library, each carrying a thin payload about a video's status. Bunny signs deliveries with an `X-BunnyStream-Signature` header: an HMAC-SHA256 of the raw body, keyed with the library's Read-Only API key, lowercase hex.

The payload is thin (`VideoLibraryId`, `VideoGuid`, `Status`), so you fetch full metadata via the Stream API using the `VideoGuid`.

## Bunny Stream webhook features

| Feature | Details |
| --- | --- |
| Configuration | Per video library in the Stream dashboard |
| Signature header | `X-BunnyStream-Signature` (lowercase hex) |
| Signature scheme | HMAC-SHA256 over the raw body |
| Key | The video library's Read-Only API key |
| Payload | Thin: `VideoLibraryId`, `VideoGuid`, `Status` |
| Retries | Not documented |
| SDK | None (community only) |

## The Status enum

The payload's `Status` is an integer. The full enum:

| Status | Meaning |
| --- | --- |
| 0 | Queued |
| 1 | Processing |
| 2 | Encoding |
| 3 | Finished (encoding done) |
| 4 | ResolutionFinished |
| 5 | Failed (error) |
| 6 | PresignedUploadStarted |
| 7 | PresignedUploadFinished |
| 8 | PresignedUploadFailed |
| 9 | CaptionsGenerated |
| 10 | TitleOrDescriptionGenerated |

Encoding-done is `3` (Finished) and the error state is `5` (Failed). Branch on the integer `Status`.

## Setting up Bunny Stream webhooks

Configure the webhook URL per video library in the Stream dashboard. The signing secret is that library's Read-Only API key, use it as the HMAC key.

## Securing Bunny Stream webhooks

The `X-BunnyStream-Signature` header is a lowercase-hex HMAC-SHA256 of the raw request body, keyed with the library's Read-Only API key. Compute over the exact raw bytes and compare in constant time. (Bunny signs the raw body, not a field concatenation.)

```javascript
const crypto = require("crypto");

const READ_ONLY_KEY = process.env.BUNNY_STREAM_READONLY_KEY; // library Read-Only API key

function verify(rawBody, signature) {
  const expected = crypto
    .createHmac("sha256", READ_ONLY_KEY)
    .update(rawBody)
    .digest("hex");
  const a = Buffer.from(expected);
  const b = Buffer.from(signature || "");
  return a.length === b.length && crypto.timingSafeEqual(a, b);
}

app.post("/webhook", express.raw({ type: "application/json" }), (req, res) => {
  if (!verify(req.body, req.headers["x-bunnystream-signature"])) {
    return res.sendStatus(401);
  }

  res.sendStatus(200); // acknowledge fast
  const { VideoGuid, Status } = JSON.parse(req.body);
  processQueue.add({ VideoGuid, Status }); // fetch metadata via API, async
});

```

The same check in Python:

```python
import hashlib
import hmac
import os

READ_ONLY_KEY = os.environ["BUNNY_STREAM_READONLY_KEY"].encode()

def verify(raw_body: bytes, signature: str) -> bool:
    expected = hmac.new(READ_ONLY_KEY, raw_body, hashlib.sha256).hexdigest()
    return hmac.compare_digest(expected, signature or "")

```

## Bunny Stream webhook limitations and pain points

### The secret is the library's Read-Only API key

The Problem: There's no dedicated webhook secret, the HMAC key is the library's Read-Only API key. Using the wrong key (or another library's) fails verification.

Why It Happens: Bunny keys webhook signing on the library Read-Only API key.

Workarounds:

* Use the correct library's Read-Only API key as the HMAC key.

How Hookdeck Can Help: Hookdeck verifies the `X-BunnyStream-Signature` at the edge, so the key lives in one place.

### Don't confuse it with Bunny's other webhook scheme

The Problem: A different Bunny product documents an HMAC-SHA1 `x-bunny-signature` scheme for general (non-Stream) webhooks. Applying that to Stream (which is SHA256, `X-BunnyStream-Signature`) fails.

Why It Happens: Bunny has two webhook products with different schemes and headers.

Workarounds:

* For Stream, use HMAC-SHA256 and the `X-BunnyStream-Signature` header.

How Hookdeck Can Help: Hookdeck verifies the Stream scheme at the edge, so your app isn't coupled to which Bunny product it's from.

### Thin payload requires an API fetch

The Problem: The payload carries only `VideoLibraryId`, `VideoGuid`, and `Status`, so you fetch full metadata via the Stream API for anything useful.

Why It Happens: Bunny keeps the payload minimal.

Workarounds:

* Acknowledge fast, then fetch metadata via the API using `VideoGuid` off a queue.

How Hookdeck Can Help: Hookdeck durably queues each event so your worker fetches metadata at a controlled rate.

### Undocumented retries

The Problem: Retry behavior isn't documented, so you can't assume a failed delivery will be retried.

Why It Happens: Bunny doesn't publish the retry policy.

Workarounds:

* Persist events on receipt and reconcile via the API; dedupe on `VideoGuid` + `Status`.

How Hookdeck Can Help: Hookdeck durably stores and retries deliveries on a schedule you control. See our [guide to webhook idempotency](/webhooks/guides/implement-webhook-idempotency).

## Best practices

### Verify with the Read-Only API key over the raw body

Compute lowercase-hex HMAC-SHA256 over the raw body with the library's Read-Only API key and compare in constant time.

### Branch on the integer Status

Use `3` (Finished) for encoding-done and `5` (Failed) for errors; branch on the integer.

### Acknowledge fast, fetch via the API

Return 200 immediately and fetch metadata via the Stream API using `VideoGuid` off a queue. See [why to process webhooks asynchronously](/webhooks/guides/why-implement-asynchronous-processing-webhooks).

### Dedupe and reconcile

Dedupe on `VideoGuid` + `Status`, and reconcile via the API since retries aren't documented.

## Conclusion

Bunny Stream webhooks verify with an `X-BunnyStream-Signature` HMAC-SHA256 over the raw body, keyed with the library's Read-Only API key, and carry a thin `VideoGuid`/`Status` payload (`3` = Finished, `5` = Failed). Verify against the raw body, branch on the integer `Status`, fetch metadata via the API, and don't confuse it with Bunny's SHA1 `x-bunny-signature` product.

[Hookdeck](https://hookdeck.com) verifies the signature, deduplicates, and durably queues every event at the edge, so your app only ever processes verified, unique video events.

[Get started with Hookdeck](https://dashboard.hookdeck.com/signup) for free and handle Bunny Stream webhooks reliably in minutes.