Guide

Webhooks

Velora POSTs an event to a URL you own the moment it happens, so you stop polling for changes. You subscribe an endpoint to specific events, verify the signature on every delivery, and return a 2xxquickly. Webhooks need a server that is reachable from the internet — if you are writing something that runs on a streamer's desktop, you want the Events API instead.

Building a desktop app or stream tool?

The Events API provides real-time event streaming via WebSocket or SSE—no server required. Perfect for Streamer.bot, OBS plugins, and desktop applications.

Learn about the Events API →

Overview

Webhooks allow your application to receive real-time HTTP notifications when events occur. Instead of polling for changes, you can subscribe to specific events and we'll send them to your endpoint.

  • Real-time event delivery
  • Secure signature verification
  • Automatic retries on failure
  • Delivery history and debugging

Which channels do webhooks fire for?

Webhooks fire for the channel owned by the application's owner. If you own the app, you receive events for your own channel: your stream going live, your new followers, your subs, your raids. No OAuth grant is needed for this and no other channel's events are included.

Events are not site-wide, and a user authorizing your app via OAuth does notcurrently subscribe your webhooks to that user's channel. OAuth grants control API access on behalf of a user; webhook delivery is keyed to the app owner's channel only.

Per-channel scoped subscriptions (receiving events for channels that have authorized your app) are on the roadmap. If your integration needs this, tell us in the developer forum so we can prioritize it against real use cases.

Creating a webhook

You can create webhooks in the Developer Dashboard or via the API:

Via Dashboard

  1. Go to your application in the Developer Dashboard
  2. Navigate to the "Webhooks" tab
  3. Click "Add Webhook"
  4. Enter your endpoint URL and select events
  5. Save and copy your webhook secret

Via API

curl
curl -X POST https://api.velora.tv/api/developer/apps/{clientId}/webhooks \
  -H "Authorization: Bearer YOUR_ACCESS_TOKEN" \
  -H "Content-Type: application/json" \
  -d '{
    "url": "https://yourapp.com/webhooks/velora",
    "events": ["stream.online", "stream.offline", "user.follow"]
  }'

Available events

Stream Events

EventDescription
stream.onlineA stream goes live
stream.offlineA stream ends
stream.updateStream title, category or tags change

Channel Events

EventDescription
channel.subscribeNew or renewed subscription
channel.subscription.giftA subscription was gifted to someone in the channel
channel.subscription.endA subscription ended (canceled or expired) — keep your subscriber count accurate in real time
channel.voltsA viewer sent Volts
channel.cheerLegacy alias of channel.volts, kept so existing integrations keep working. New apps should use channel.volts.
channel.raidA raid arrived at, or left, the channel
channel.banA viewer was banned
channel.unbanA ban was lifted
channel.moderator.addA moderator was added
channel.moderator.removeA moderator was removed
channel.channel_points_redemptionA channel-point reward was redeemed

Polls, Predictions, Giveaways, Goals & Timers

EventDescription
channel.poll.beginA poll starts
channel.poll.endA poll ends or is cancelled (final results included)
channel.giveaway.beginA giveaway opens for entries
channel.giveaway.endA giveaway closes or is cancelled (winners included)
channel.prediction.beginA prediction opens for bets
channel.prediction.lockA prediction stops taking bets
channel.prediction.endA prediction is resolved (payout) or cancelled (refund)
channel.goal.progressA goal's progress changes
channel.goal.completedA goal reaches its target
channel.timer.updateA timer or subathon starts, pauses, resumes, ends, resets or gains time

User Events

EventDescription
user.followSomeone followed the channel
user.unfollowSomeone unfollowed the channel

Chat Events

EventDescription
chat.messageEvery chat message in the channel. High volume — expect one delivery per message. Requires the chat:read scope.

Webhook payload

Each webhook delivery sends a POST request with a JSON payload:

Request body
{
  "id": "evt_abc123",
  "type": "stream.online",
  "timestamp": "2026-01-18T15:30:00Z",
  "data": {
    "streamId": "stream_xyz",
    "userId": "user_123",
    "username": "coolstreamer",
    "title": "Playing Velora Games!",
    "startedAt": "2026-01-18T15:30:00Z"
  }
}

Verifying signatures

Every webhook request includes a signature header for verification. Always verify the signature to ensure the request came from Velora.

Security: Never process webhook payloads without verifying the signature first.

Headers

EventDescription
X-Velora-SignatureHMAC-SHA256 signature
X-Velora-TimestampUnix timestamp of the request, in milliseconds
X-Velora-EventEvent type (e.g., stream.online)

What exactly gets signed

The signature covers the raw request body, which is the full envelope — not just the inner data object:

Signing input
HMAC_SHA256(secret, X-Velora-Timestamp + "." + rawBody)

rawBody = {"event":"stream.online","timestamp":"2026-08-25T05:48:31.046Z","data":{ ... }}

There are two different timestamps, and mixing them up is the most common cause of a signature that fails while every line of your code looks right. The timestamp field inside the body is ISO 8601. The X-Velora-Timestamp header is epoch milliseconds. The HMAC uses the header one.

Hash the bytes exactly as received. Parsing the JSON and re-serializing it will reorder keys and re-escape unicode, which changes the signature — use express.raw(), io.ReadAll, or your framework's raw-body equivalent, and verify before anything parses it.

Verification example — Node.js
const crypto = require('crypto');

function verifyWebhookSignature(payload, timestamp, signature, secret) {
  // The signature is prefixed with "sha256=" - strip it
  const sig = signature.replace('sha256=', '');

  // The signature is computed over: timestamp.payload
  const expected = crypto
    .createHmac('sha256', secret)
    .update(`${timestamp}.${payload}`)
    .digest('hex');

  return crypto.timingSafeEqual(
    Buffer.from(sig),
    Buffer.from(expected)
  );
}

// Express middleware example
app.post('/webhooks/velora', express.raw({ type: 'application/json' }), (req, res) => {
  const signature = req.headers['x-velora-signature'];
  const timestamp = req.headers['x-velora-timestamp'];
  const payload = req.body.toString();

  // Verify the signature using timestamp + payload
  if (!verifyWebhookSignature(payload, timestamp, signature, WEBHOOK_SECRET)) {
    return res.status(401).send('Invalid signature');
  }

  // Optional: Reject old timestamps to prevent replay attacks.
  // X-Velora-Timestamp is epoch MILLISECONDS (same units as Date.now()),
  // so compare in ms — dividing by 1000 here makes every valid webhook look
  // ~56000 years old and get rejected.
  const now = Date.now();
  if (Math.abs(now - parseInt(timestamp)) > 5 * 60 * 1000) { // 5 minute tolerance
    return res.status(401).send('Timestamp too old');
  }

  // Process the webhook
  const event = JSON.parse(payload);
  console.log('Received event:', event.type);

  res.status(200).send('OK');
});

Retry policy

If your endpoint doesn't respond with a 2xx status code within 10 seconds, we'll retry the delivery:

  1. 1.Immediate retry after failure
  2. 2.Retry after 1 minute
  3. 3.Retry after 5 minutes
  4. 4.Retry after 30 minutes
  5. 5.Final retry after 2 hours

After 5 failed attempts, the webhook will be disabled automatically. You can re-enable it from the dashboard.

Best practices

  • Respond quickly

    Return a 200 response immediately, then process asynchronously

  • Handle duplicates

    Use the event ID for idempotency - we may send the same event twice

  • Verify signatures

    Always verify the X-Velora-Signature header

  • Use HTTPS

    Webhook endpoints must use HTTPS

Next steps