Event-driven integrations with HMAC signature verification
Set up your first webhook in 3 steps
Set up HTTPS endpoint to receive events
Register URL and select events via API
Validate HMAC signatures for security
Register your webhook endpoint via API
curl -X POST https://api.wave.inc/v1/webhooks \
-H "Authorization: Bearer wave_live_xxxxx" \
-H "Content-Type: application/json" \
-d '{
"url": "https://your-server.com/webhooks/wave",
"events": [
"stream.started",
"stream.ended",
"recording.completed"
],
"secret": "whsec_your_secret_key_here",
"description": "Production webhook for stream events"
}'
# Response: 201 Created
{
"id": "wh_abc123xyz",
"url": "https://your-server.com/webhooks/wave",
"events": ["stream.started", "stream.ended", "recording.completed"],
"status": "active",
"created_at": "2025-11-14T10:00:00Z",
"secret": "whsec_your_secret_key_here"
}Subscribe to these event types
stream.startedTriggered when a stream goes live
Example Payload
{
"event": "stream.started",
"stream_id": "stream_abc123",
"timestamp": "2025-11-14T10:30:00Z",
"data": {
"title": "Product Launch 2025",
"protocol": "webrtc",
"ingest_endpoint": "https://rtc.wave.inc/whip/stream_abc123"
}
}stream.endedTriggered when a stream stops
Example Payload
{
"event": "stream.ended",
"stream_id": "stream_abc123",
"timestamp": "2025-11-14T12:30:00Z",
"data": {
"duration": 7200,
"total_viewers": 15420,
"peak_viewers": 3245,
"recording_id": "rec_xyz789"
}
}recording.completedTriggered when recording processing is complete
Example Payload
{
"event": "recording.completed",
"stream_id": "stream_abc123",
"recording_id": "rec_xyz789",
"timestamp": "2025-11-14T12:45:00Z",
"data": {
"duration": 7200,
"file_size": 4294967296,
"format": "mp4",
"resolution": "1080p",
"download_url": "https://cdn.wave.inc/recordings/rec_xyz789.mp4?sig=..."
}
}recording.failedTriggered when recording processing fails
Example Payload
{
"event": "recording.failed",
"stream_id": "stream_abc123",
"recording_id": "rec_xyz789",
"timestamp": "2025-11-14T12:45:00Z",
"data": {
"error_code": "PROCESSING_ERROR",
"error_message": "Video codec not supported",
"retry_available": false
}
}viewer.threshold_reachedTriggered when viewer count reaches configured threshold
Example Payload
{
"event": "viewer.threshold_reached",
"stream_id": "stream_abc123",
"timestamp": "2025-11-14T11:00:00Z",
"data": {
"threshold": 1000,
"current_viewers": 1042,
"threshold_type": "concurrent"
}
}billing.usage_warningTriggered when approaching usage quota limits
Example Payload
{
"event": "billing.usage_warning",
"timestamp": "2025-11-14T00:00:00Z",
"data": {
"resource": "bandwidth",
"usage_percentage": 85,
"current_usage_gb": 850,
"quota_gb": 1000,
"period_end": "2025-11-30T23:59:59Z"
}
}Verify webhook authenticity to prevent spoofing
X-Wave-Signature headerimport crypto from 'crypto';
import express from 'express';
import { DesignTokens, getContainer, getSection } from '@/lib/design-tokens';
const app = express();
const WEBHOOK_SECRET = process.env.WAVE_WEBHOOK_SECRET;
// Use raw body for signature verification
app.post('/webhooks/wave',
express.raw({ type: 'application/json' }),
(req, res) => {
const signature = req.headers['x-wave-signature'];
const body = req.body.toString('utf8');
// Compute HMAC-SHA256
const expectedSignature = crypto
.createHmac('sha256', WEBHOOK_SECRET)
.update(body)
.digest('hex');
// Compare signatures (constant-time comparison)
if (!crypto.timingSafeEqual(
Buffer.from(signature),
Buffer.from(expectedSignature)
)) {
console.error('Invalid signature');
return res.status(401).send('Invalid signature');
}
// Signature valid - parse and process event
const event = JSON.parse(body);
console.log('Event received:', event.event);
switch(event.event) {
case 'stream.started':
handleStreamStarted(event);
break;
case 'stream.ended':
handleStreamEnded(event);
break;
case 'recording.completed':
handleRecordingCompleted(event);
break;
}
// Always respond quickly (< 5s)
res.status(200).send('OK');
}
);
function handleStreamStarted(event) {
console.log('Stream started:', event.stream_id);
// Your business logic here
}
function handleStreamEnded(event) {
console.log('Stream ended:', event.stream_id);
console.log('Duration:', event.data.duration, 'seconds');
// Your business logic here
}
function handleRecordingCompleted(event) {
console.log('Recording ready:', event.recording_id);
console.log('Download:', event.data.download_url);
// Your business logic here
}
app.listen(3000);Automatic retries with exponential backoff
| Attempt | Delay | Total Time |
|---|---|---|
| 1st retry | 5 seconds | 5s |
| 2nd retry | 30 seconds | 35s |
| 3rd retry | 5 minutes | ~5m 35s |
| 4th retry | 30 minutes | ~35m 35s |
| 5th retry | 2 hours | ~2h 35m 35s |
After 5 failed attempts, the webhook is marked as failed and no further retries are attempted.
Tools and techniques for local webhook development
# 1. Install ngrok
brew install ngrok # macOS
# or download from https://ngrok.com
# 2. Start your local server
node server.js # Running on port 3000
# 3. Create tunnel
ngrok http 3000
# Output:
# Forwarding https://abc123.ngrok.io -> http://localhost:3000
# 4. Use the HTTPS URL for webhook registration
curl -X POST https://api.wave.inc/v1/webhooks \
-H "Authorization: Bearer wave_live_xxxxx" \
-d '{
"url": "https://abc123.ngrok.io/webhooks/wave",
"events": ["stream.started"],
"secret": "whsec_test_secret"
}'# Test webhook delivery via API
curl -X POST https://api.wave.inc/v1/webhooks/wh_abc123/test \
-H "Authorization: Bearer wave_live_xxxxx"
# Response includes delivery status
{
"webhook_id": "wh_abc123",
"test_event": {
"event": "test.webhook",
"timestamp": "2025-11-14T10:00:00Z"
},
"delivery": {
"status": 200,
"response_time_ms": 45,
"success": true
}
}