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.
{
"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.
How the signature is built
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, 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
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.
Check the version header
Ensure X-BunnyStream-Signature-Version is v1.
Check the algorithm header
Ensure X-BunnyStream-Signature-Algorithm is hmac-sha256.
Get your signing secret
Use your library’s Read-Only API key as the signing secret.
Compute the expected signature
expectedSignature = lowercase_hex(HMAC-SHA256(rawBody, signatureSecret))
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
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
});
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
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
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.