Skip to main content

Studio sends HTTP POST requests to your endpoint when stream or asset events occur. Your server responds with 200 to acknowledge. Any other response is treated as a failure and Studio retries.

Available events

Step 1 — Create a webhook

import { Livepeer } from 'livepeer';

const client = new Livepeer({ apiKey: process.env.LIVEPEER_API_KEY });

const webhook = await client.webhook.create({
  name: 'asset-ready-handler',
  url: 'https://your-server.com/webhooks/livepeer',
  events: ['asset.ready', 'asset.failed'],
  // sharedSecret: 'your-secret' -- used to verify signatures
});

console.log('Webhook ID:', webhook.webhook.id);

Step 2 — Handle incoming events

Set up an HTTP endpoint that accepts POST requests. Express.js example:
const express = require('express');
const crypto = require('crypto');
const app = express();

// Use raw body for signature verification
app.use('/webhooks/livepeer', express.raw({ type: 'application/json' }));

app.post('/webhooks/livepeer', (req, res) => {
  // Verify signature before processing (see Step 3)
  const isValid = verifySignature(req.headers['livepeer-signature'], req.body, process.env.WEBHOOK_SECRET);
  if (!isValid) return res.sendStatus(401);

  const event = JSON.parse(req.body);

  switch (event.event) {
    case 'asset.ready':
      console.log('Asset ready:', event.payload.id);
      // Notify user, update database, etc.
      break;
    case 'asset.failed':
      console.error('Asset failed:', event.payload.id, event.payload.error);
      break;
    case 'stream.started':
      console.log('Stream live:', event.payload.id);
      break;
  }

  res.sendStatus(200);
});

Step 3 — Verify webhook signatures

Studio signs every request with HMAC-SHA256. Verifying the signature rejects spoofed or tampered requests. The Livepeer-Signature header format:
Livepeer-Signature: t=1710000000,v1=abc123def456...
Verification logic:
function verifySignature(signatureHeader, rawBody, secret) {
  if (!signatureHeader) return false;

  // Parse t= and v1= from the header
  const parts = Object.fromEntries(
    signatureHeader.split(',').map(part => part.split('='))
  );
  const timestamp = parts['t'];
  const signature = parts['v1'];

  if (!timestamp || !signature) return false;

  // Replay attack protection: reject events older than 5 minutes
  const age = Date.now() / 1000 - parseInt(timestamp);
  if (age > 300) return false;

  // Compute expected signature
  const payload = `${timestamp}.${rawBody}`;
  const expected = crypto
    .createHmac('sha256', secret)
    .update(payload)
    .digest('hex');

  // Constant-time comparison to prevent timing attacks
  return crypto.timingSafeEqual(
    Buffer.from(signature, 'hex'),
    Buffer.from(expected, 'hex')
  );
}

Testing locally

Use ngrok to expose your local server during development:
# Start your server
node app.js  # listening on port 3000

# In a second terminal
ngrok http 3000
# ngrok provides a public URL, e.g. https://abc123.ngrok.io
Register the ngrok URL as your webhook endpoint in Studio, or via API with the URL from ngrok. Studio’s webhook dashboard at https://livepeer.studio/dashboard/developers/webhooks shows delivery history and lets you resend failed events.

Webhook payload structure

{
  "webhookId": "web_abc123",
  "createdAt": "2026-04-05T10:00:00Z",
  "timestamp": "2026-04-05T10:01:23Z",
  "event": "asset.ready",
  "payload": {
    "id": "asset_xyz789",
    "name": "my-video.mp4",
    "status": { "phase": "ready" },
    "playbackId": "pla_abc123"
  }
}
The payload object shape varies by event type. Refer to the Studio API Reference for per-event schemas.

Access Control

Use playback.accessControl webhooks to gate content dynamically.

Create a Livestream

Create streams and attach webhook event subscriptions.
Last modified on April 7, 2026