Agent skill

Bunny Stream Webhooks Skill

Receive and verify Bunny Stream webhooks. Use when setting up Bunny Stream webhook handlers, debugging X-BunnyStream-Signature verification, or handling video encoding events like Status 3 (Finished / encoding done), Status 5 (Failed), or captions and title/description generation.

Install this skill

npx skills add hookdeck/webhook-skills --skill bunny-stream-webhooks


When to Use This Skill

  • Setting up Bunny Stream webhook handlers
  • How do I verify Bunny Stream webhook signatures?
  • Debugging X-BunnyStream-Signature verification failures
  • Handling video state changes (encoding finished, encoding failed)
  • Reacting to Status 3 (Finished), Status 5 (Failed), captions, or title/description events

Verification (core)

Bunny Stream signs the exact raw request body with HMAC-SHA256, keyed on your video library's Read-Only API key, and sends the digest as lowercase hex in the X-BunnyStream-Signature header. Verify against the unparsed raw body (do NOT re-serialize the JSON — whitespace or key-order changes break the digest) and compare timing-safe.

This is a custom scheme, not Standard Webhooks (no webhook-id / webhook-timestamp / webhook-signature). It is also distinct from Bunny's general-platform webhooks (HMAC-SHA1, x-bunny-signature) — Stream uses SHA-256 and X-BunnyStream-Signature. There is no official SDK, so verify manually.

Node:

const crypto = require('crypto');

function verifyBunnyStream(rawBody, signatureHeader, secret) {
  if (!signatureHeader) return false;
  const expected = crypto.createHmac('sha256', secret).update(rawBody).digest('hex');
  try {
    return crypto.timingSafeEqual(
      Buffer.from(signatureHeader, 'hex'),
      Buffer.from(expected, 'hex')
    );
  } catch {
    return false; // malformed hex / length mismatch
  }
}

Python:

import hmac, hashlib

def verify_bunny_stream(raw_body: bytes, signature_header: str, secret: str) -> bool:
    if not signature_header:
        return False
    expected = hmac.new(secret.encode(), raw_body, hashlib.sha256).hexdigest()
    return hmac.compare_digest(signature_header, expected)

For complete handlers with route wiring, event dispatch, and tests, see:

The Payload Is Thin — Fetch Back

The callback body carries only three fields:

{ "VideoLibraryId": 12345, "VideoGuid": "0a1b2c3d-...", "Status": 3 }

There is no title, duration, or resolution in the payload. When you need full metadata, call the Stream API GET /library/{libraryId}/videos/{videoGuid} with your (read-write) AccessKey, using VideoGuid from the webhook. Verify the signature before making any fetch-back call.

Status Codes (the event type lives in Status)

StatusMeaningCommon Use
0QueuedUpload accepted, awaiting processing
1ProcessingIngest started
2EncodingTranscoding in progress
3FinishedEncoding done — video ready to play
4ResolutionFinishedA single resolution finished encoding
5FailedEncoding failed — alert / retry
6PresignedUploadStartedTUS/presigned upload began
7PresignedUploadFinishedPresigned upload completed
8PresignedUploadFailedPresigned upload failed
9CaptionsGeneratedAuto-captions ready
10TitleOrDescriptionGeneratedAI title/description ready

For the full event reference, see Bunny Stream Webhooks.

Important Headers

HeaderDescription
X-BunnyStream-SignatureHMAC-SHA256 of the raw body, lowercase hex — verify this
X-BunnyStream-Signature-VersionSignature scheme version (v1) — unconfirmed (see note)
X-BunnyStream-Signature-AlgorithmAlgorithm identifier (hmac-sha256) — unconfirmed (see note)

The -Version and -Algorithm headers were observed in a single fetch only and are unconfirmed — they may or may not be present. Do not rely on them; verify solely against X-BunnyStream-Signature.

Environment Variables

# The signing secret IS your video library's Read-Only API key
BUNNY_STREAM_WEBHOOK_SECRET=your_library_read_only_api_key

Local Development

# Start tunnel (no account needed)
npx hookdeck-cli listen 3000 bunny-stream --path /webhooks/bunny-stream

Reference Materials


Repository

hookdeck/webhook-skills

v0.1.0 · MIT · Updated Aug 5, 2026

View on GitHub →