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
- Go to your application in the Developer Dashboard
- Navigate to the "Webhooks" tab
- Click "Add Webhook"
- Enter your endpoint URL and select events
- Save and copy your webhook secret
Via API
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
| Event | Description |
|---|---|
stream.online | A stream goes live |
stream.offline | A stream ends |
stream.update | Stream title, category or tags change |
Channel Events
| Event | Description |
|---|---|
channel.subscribe | New or renewed subscription |
channel.subscription.gift | A subscription was gifted to someone in the channel |
channel.subscription.end | A subscription ended (canceled or expired) — keep your subscriber count accurate in real time |
channel.volts | A viewer sent Volts |
channel.cheer | Legacy alias of channel.volts, kept so existing integrations keep working. New apps should use channel.volts. |
channel.raid | A raid arrived at, or left, the channel |
channel.ban | A viewer was banned |
channel.unban | A ban was lifted |
channel.moderator.add | A moderator was added |
channel.moderator.remove | A moderator was removed |
channel.channel_points_redemption | A channel-point reward was redeemed |
Polls, Predictions, Giveaways, Goals & Timers
| Event | Description |
|---|---|
channel.poll.begin | A poll starts |
channel.poll.end | A poll ends or is cancelled (final results included) |
channel.giveaway.begin | A giveaway opens for entries |
channel.giveaway.end | A giveaway closes or is cancelled (winners included) |
channel.prediction.begin | A prediction opens for bets |
channel.prediction.lock | A prediction stops taking bets |
channel.prediction.end | A prediction is resolved (payout) or cancelled (refund) |
channel.goal.progress | A goal's progress changes |
channel.goal.completed | A goal reaches its target |
channel.timer.update | A timer or subathon starts, pauses, resumes, ends, resets or gains time |
User Events
| Event | Description |
|---|---|
user.follow | Someone followed the channel |
user.unfollow | Someone unfollowed the channel |
Chat Events
| Event | Description |
|---|---|
chat.message | Every 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:
{
"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
| Event | Description |
|---|---|
X-Velora-Signature | HMAC-SHA256 signature |
X-Velora-Timestamp | Unix timestamp of the request, in milliseconds |
X-Velora-Event | Event 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:
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.
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.Immediate retry after failure
- 2.Retry after 1 minute
- 3.Retry after 5 minutes
- 4.Retry after 30 minutes
- 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