Veedeo API V3 Webhooks Guide
Comprehensive Webhook Implementation Guide Last Updated: January 17, 2025
Table of Contents
- Overview
- Webhook Configuration
- Event Types
- Security Implementation
- Event Filtering
- Implementation Examples
- Error Handling
- Testing and Debugging
- Best Practices
- Troubleshooting
Overview
Webhooks provide real-time notifications about your video rendering tasks. Instead of polling the API repeatedly, your application receives HTTP POST requests when events occur, enabling efficient real-time updates and automated workflows.
Key Benefits
- Real-time Updates: Immediate notifications when task status changes
- Efficient: No need for continuous polling
- Reliable: Built-in retry mechanism with exponential backoff
- Secure: HMAC-SHA256 signature verification
- Flexible: Configurable event filtering
Webhook Flow
1. Create render task with webhook configuration
2. Veedeo processes the task
3. Events trigger HTTP POST to your endpoint
4. Your server processes the event
5. Return 2xx response to acknowledge receipt
Webhook Configuration
⚠️ Important: Webhook URL is Required
The webhook.url
field is mandatory for all render requests. Without it, you will not receive notifications when your render completes, and you won't get the download URL for your rendered video. The webhook is the only way to receive render results.
Basic Configuration
{
"webhook": {
"url": "https://your-app.com/webhooks/veedeo",
"events": ["task.completed", "task.failed"]
}
}
Secure Configuration (Recommended)
{
"webhook": {
"url": "https://your-app.com/webhooks/veedeo",
"events": ["task.completed", "task.failed"],
"secret": "your_webhook_secret_key"
}
}
Advanced Configuration
{
"webhook": {
"url": "https://your-app.com/webhooks/veedeo",
"events": ["task.started", "task.progress", "task.completed", "task.failed"],
"secret": "your_webhook_secret_key",
"headers": {
"X-Custom-Header": "your-value",
"X-Environment": "production"
},
"timeout_seconds": 30,
"max_retries": 3
}
}
Configuration Options
Field | Type | Required | Description |
---|---|---|---|
url | string | Yes | Your webhook endpoint URL (HTTPS required) - Essential for receiving render results |
events | array | No | Event types to receive (default: all) |
secret | string | No | Secret key for HMAC signature verification (recommended for security) |
headers | object | No | Custom headers to include in webhook requests |
timeout_seconds | number | No | Request timeout (default: 30, max: 60) |
max_retries | number | No | Maximum retry attempts (default: 3, max: 5) |
Event Types
task.queued
Sent when a task is successfully queued for processing.
{
"event": "task.queued",
"timestamp": "2025-01-17T10:00:00Z",
"task_id": "tsk_1234567890abcdef",
"data": {
"queue_position": 5,
"estimated_start_time": "2025-01-17T10:02:00Z",
"priority": "standard"
}
}
Use Cases:
- Update UI to show task is queued
- Log task creation for analytics
- Set initial progress indicators
task.started
Sent when rendering begins.
{
"event": "task.started",
"timestamp": "2025-01-17T10:00:15Z",
"task_id": "tsk_1234567890abcdef",
"data": {
"started_at": "2025-01-17T10:00:15Z",
"estimated_completion": "2025-01-17T10:02:15Z",
"worker_id": "worker_abc123"
}
}
Use Cases:
- Show "Processing" status in UI
- Start progress tracking
- Update estimated completion time
task.progress
Sent periodically during rendering (maximum once every 30 seconds).
{
"event": "task.progress",
"timestamp": "2025-01-17T10:01:00Z",
"task_id": "tsk_1234567890abcdef",
"data": {
"progress": {
"percentage": 45,
"current_phase": "render",
"phase_progress": 0.75,
"estimated_completion": "2025-01-17T10:02:15Z"
},
"phases": {
"download": {
"completed": true,
"duration_seconds": 5
},
"render": {
"completed": false,
"progress": 0.75
},
"upload": {
"completed": false,
"progress": 0.0
}
}
}
}
Use Cases:
- Update progress bars in real-time
- Show current processing phase
- Provide detailed progress feedback
task.completed
Sent when rendering completes successfully.
{
"event": "task.completed",
"timestamp": "2025-01-17T10:02:30Z",
"task_id": "tsk_1234567890abcdef",
"data": {
"completed_at": "2025-01-17T10:02:30Z",
"processing_time_seconds": 135,
"output": {
"video_url": "https://storage.veedeo.dev/renders/tsk_1234.mp4",
"thumbnail_url": "https://storage.veedeo.dev/thumbnails/tsk_1234.jpg",
"preview_url": "https://storage.veedeo.dev/previews/tsk_1234.gif",
"duration_ms": 15000,
"file_size_bytes": 12582912,
"resolution": {
"width": 1920,
"height": 1080
},
"format": "mp4",
"expires_at": "2025-01-18T10:02:30Z"
},
"performance": {
"estimated_vs_actual": {
"estimated_seconds": 120,
"actual_seconds": 135,
"accuracy": 0.89
}
}
}
}
Use Cases:
- Download and store output files
- Show completion notification
- Update UI with video player
- Analytics and performance tracking
task.failed
Sent when rendering fails.
{
"event": "task.failed",
"timestamp": "2025-01-17T10:01:30Z",
"task_id": "tsk_1234567890abcdef",
"data": {
"failed_at": "2025-01-17T10:01:30Z",
"processing_time_seconds": 75,
"error": {
"code": "MEDIA_DOWNLOAD_FAILED",
"message": "Unable to download media file from provided URL",
"details": {
"media_url": "https://example.com/unreachable.jpg",
"http_status": 404,
"retry_after_seconds": 300
},
"retry_possible": true,
"suggested_action": "Verify media URL accessibility and retry"
}
}
}
Use Cases:
- Show error messages to users
- Implement automatic retry logic
- Log errors for debugging
- Send failure notifications
task.canceled
Sent when a task is manually canceled.
{
"event": "task.canceled",
"timestamp": "2025-01-17T10:01:15Z",
"task_id": "tsk_1234567890abcdef",
"data": {
"canceled_at": "2025-01-17T10:01:15Z",
"canceled_by": "user_request",
"processing_time_seconds": 45
}
}
Use Cases:
- Update UI to show cancellation
- Clean up related resources
- Log cancellation events
Security Implementation
HMAC-SHA256 Signature Verification
All webhooks include a signature header for verification:
X-Veedeo-Signature: sha256=abcdef1234567890...
X-Veedeo-Timestamp: 1642434600
Implementation Examples
Node.js
const crypto = require('crypto');
function verifyWebhookSignature(payload, signature, secret) {
const expectedSignature = crypto
.createHmac('sha256', secret)
.update(JSON.stringify(payload))
.digest('hex');
const receivedSignature = signature.replace('sha256=', '');
return crypto.timingSafeEqual(
Buffer.from(expectedSignature, 'hex'),
Buffer.from(receivedSignature, 'hex')
);
}
// Express middleware
function verifyWebhook(secret) {
return (req, res, next) => {
const signature = req.headers['x-veedeo-signature'];
const timestamp = req.headers['x-veedeo-timestamp'];
if (!signature || !timestamp) {
return res.status(401).json({ error: 'Missing signature headers' });
}
// Check timestamp to prevent replay attacks (5 minutes tolerance)
const now = Math.floor(1735776000); // Example timestamp
if (Math.abs(now - parseInt(timestamp)) > 300) {
return res.status(401).json({ error: 'Request too old' });
}
if (!verifyWebhookSignature(req.body, signature, secret)) {
return res.status(401).json({ error: 'Invalid signature' });
}
next();
};
}
Python
import hmac
import hashlib
import json
import time
def verify_webhook_signature(payload: dict, signature: str, secret: str) -> bool:
"""Verify HMAC-SHA256 signature for webhook security."""
expected_signature = hmac.new(
secret.encode('utf-8'),
json.dumps(payload, separators=(',', ':')).encode('utf-8'),
hashlib.sha256
).hexdigest()
received_signature = signature.replace('sha256=', '')
return hmac.compare_digest(expected_signature, received_signature)
def verify_timestamp(timestamp: str, tolerance_seconds: int = 300) -> bool:
"""Verify timestamp to prevent replay attacks."""
try:
webhook_time = int(timestamp)
current_time = int(time.time())
return abs(current_time - webhook_time) <= tolerance_seconds
except (ValueError, TypeError):
return False
# Flask decorator
from functools import wraps
from flask import request, jsonify
def verify_webhook(secret: str):
def decorator(f):
@wraps(f)
def decorated_function(*args, **kwargs):
signature = request.headers.get('X-Veedeo-Signature', '')
timestamp = request.headers.get('X-Veedeo-Timestamp', '')
if not signature or not timestamp:
return jsonify({'error': 'Missing signature headers'}), 401
if not verify_timestamp(timestamp):
return jsonify({'error': 'Request too old'}), 401
payload = request.get_json()
if not verify_webhook_signature(payload, signature, secret):
return jsonify({'error': 'Invalid signature'}), 401
return f(*args, **kwargs)
return decorated_function
return decorator
PHP
function verifyWebhookSignature($payload, $signature, $secret) {
$expectedSignature = hash_hmac('sha256', json_encode($payload), $secret);
$receivedSignature = str_replace('sha256=', '', $signature);
return hash_equals($expectedSignature, $receivedSignature);
}
function verifyTimestamp($timestamp, $toleranceSeconds = 300) {
$webhookTime = (int)$timestamp;
$currentTime = time();
return abs($currentTime - $webhookTime) <= $toleranceSeconds;
}
// Usage in webhook handler
$signature = $_SERVER['HTTP_X_VEEDEO_SIGNATURE'] ?? '';
$timestamp = $_SERVER['HTTP_X_VEEDEO_TIMESTAMP'] ?? '';
if (!$signature || !$timestamp) {
http_response_code(401);
echo json_encode(['error' => 'Missing signature headers']);
exit;
}
if (!verifyTimestamp($timestamp)) {
http_response_code(401);
echo json_encode(['error' => 'Request too old']);
exit;
}
$payload = json_decode(file_get_contents('php://input'), true);
if (!verifyWebhookSignature($payload, $signature, $webhookSecret)) {
http_response_code(401);
echo json_encode(['error' => 'Invalid signature']);
exit;
}
Event Filtering
Configure which events you want to receive to reduce noise and processing overhead.
Basic Filtering
{
"webhook": {
"url": "https://your-app.com/webhook",
"events": ["task.completed", "task.failed"]
}
}
Use Case-Specific Filtering
Minimal Notifications (Production)
{
"webhook": {
"url": "https://your-app.com/webhook/production",
"events": ["task.completed", "task.failed"]
}
}
Development/Testing
{
"webhook": {
"url": "https://your-app.com/webhook/dev",
"events": ["task.started", "task.progress", "task.completed", "task.failed"]
}
}
Analytics Only
{
"webhook": {
"url": "https://analytics.your-app.com/veedeo",
"events": ["task.completed"],
"headers": {
"X-Analytics-Source": "veedeo-api"
}
}
}
Implementation Examples
Complete Express.js Handler
const express = require('express');
const crypto = require('crypto');
const app = express();
app.use(express.json());
// Webhook verification middleware
app.use('/webhook/veedeo', verifyWebhook(process.env.WEBHOOK_SECRET));
// Main webhook handler
app.post('/webhook/veedeo', async (req, res) => {
const { event, task_id, data } = req.body;
try {
await handleWebhookEvent(event, task_id, data);
res.status(200).json({ received: true });
} catch (error) {
console.error('Webhook processing error:', error);
res.status(500).json({ error: 'Processing failed' });
}
});
async function handleWebhookEvent(event, taskId, data) {
switch (event) {
case 'task.queued':
await handleTaskQueued(taskId, data);
break;
case 'task.started':
await handleTaskStarted(taskId, data);
break;
case 'task.progress':
await handleTaskProgress(taskId, data);
break;
case 'task.completed':
await handleTaskCompleted(taskId, data);
break;
case 'task.failed':
await handleTaskFailed(taskId, data);
break;
case 'task.canceled':
await handleTaskCanceled(taskId, data);
break;
default:
console.log(`Unknown event type: ${event}`);
}
}
async function handleTaskCompleted(taskId, data) {
const { output } = data;
// 1. Update database
await updateTaskStatus(taskId, 'completed', {
videoUrl: output.video_url,
thumbnailUrl: output.thumbnail_url,
expiresAt: output.expires_at
});
// 2. Download files before expiry
await downloadAndStoreFiles(taskId, output);
// 3. Send user notification
await sendUserNotification(taskId, 'completed');
// 4. Trigger downstream processes
await triggerPostProcessing(taskId, output);
}
async function downloadAndStoreFiles(taskId, output) {
const downloads = [
downloadFile(output.video_url, `renders/${taskId}.mp4`),
downloadFile(output.thumbnail_url, `thumbnails/${taskId}.jpg`),
downloadFile(output.preview_url, `previews/${taskId}.gif`)
];
await Promise.all(downloads);
}
async function handleTaskFailed(taskId, data) {
const { error } = data;
// Log error for debugging
console.error(`Task ${taskId} failed:`, error);
// Update database
await updateTaskStatus(taskId, 'failed', { error });
// Implement retry logic for retryable errors
if (error.retry_possible && await shouldRetry(taskId)) {
await scheduleRetry(taskId, error.retry_after_seconds || 300);
} else {
// Send failure notification
await sendUserNotification(taskId, 'failed', error.message);
}
}
async function shouldRetry(taskId) {
const retryCount = await getRetryCount(taskId);
return retryCount < 3; // Max 3 retries
}
FastAPI Handler (Python)
from fastapi import FastAPI, HTTPException, Depends, Request
from pydantic import BaseModel
from typing import List, Optional, Dict, Any
import asyncio
app = FastAPI()
class WebhookEvent(BaseModel):
event: str
timestamp: str
task_id: str
data: Dict[str, Any]
@app.post("/webhook/veedeo")
async def handle_webhook(
webhook_event: WebhookEvent,
request: Request,
_: None = Depends(verify_webhook_signature)
):
try:
await process_webhook_event(webhook_event)
return {"received": True}
except Exception as e:
raise HTTPException(status_code=500, detail=str(e))
async def process_webhook_event(webhook_event: WebhookEvent):
handlers = {
'task.queued': handle_task_queued,
'task.started': handle_task_started,
'task.progress': handle_task_progress,
'task.completed': handle_task_completed,
'task.failed': handle_task_failed,
'task.canceled': handle_task_canceled,
}
handler = handlers.get(webhook_event.event)
if handler:
await handler(webhook_event.task_id, webhook_event.data)
else:
print(f"Unknown event type: {webhook_event.event}")
async def handle_task_completed(task_id: str, data: Dict[str, Any]):
output = data['output']
# Run tasks concurrently
await asyncio.gather(
update_task_status(task_id, 'completed', output),
download_and_store_files(task_id, output),
send_user_notification(task_id, 'completed'),
trigger_post_processing(task_id, output)
)
async def download_and_store_files(task_id: str, output: Dict[str, Any]):
downloads = [
download_file(output['video_url'], f'renders/{task_id}.mp4'),
download_file(output['thumbnail_url'], f'thumbnails/{task_id}.jpg'),
download_file(output['preview_url'], f'previews/{task_id}.gif')
]
await asyncio.gather(*downloads)
Laravel Handler (PHP)
<?php
namespace App\Http\Controllers;
use Illuminate\Http\Request;
use Illuminate\Http\Response;
use App\Services\WebhookProcessor;
use App\Http\Middleware\VerifyVeedeoWebhook;
class VeedeoWebhookController extends Controller
{
public function __construct()
{
$this->middleware(VerifyVeedeoWebhook::class);
}
public function handle(Request $request, WebhookProcessor $processor)
{
$event = $request->input('event');
$taskId = $request->input('task_id');
$data = $request->input('data', []);
try {
$processor->process($event, $taskId, $data);
return response()->json(['received' => true]);
} catch (\Exception $e) {
\Log::error('Webhook processing failed', [
'event' => $event,
'task_id' => $taskId,
'error' => $e->getMessage()
]);
return response()->json(['error' => 'Processing failed'], 500);
}
}
}
// WebhookProcessor Service
class WebhookProcessor
{
public function process(string $event, string $taskId, array $data): void
{
switch ($event) {
case 'task.completed':
$this->handleTaskCompleted($taskId, $data);
break;
case 'task.failed':
$this->handleTaskFailed($taskId, $data);
break;
case 'task.progress':
$this->handleTaskProgress($taskId, $data);
break;
default:
\Log::info("Unknown webhook event: {$event}");
}
}
private function handleTaskCompleted(string $taskId, array $data): void
{
$output = $data['output'];
// Update database
Task::where('id', $taskId)->update([
'status' => 'completed',
'video_url' => $output['video_url'],
'thumbnail_url' => $output['thumbnail_url'],
'expires_at' => $output['expires_at']
]);
// Queue file download job
DownloadRenderFiles::dispatch($taskId, $output);
// Send notification
$user = Task::find($taskId)->user;
$user->notify(new RenderCompleted($taskId, $output));
}
}
Error Handling
Webhook Delivery Failures
Veedeo implements automatic retry with exponential backoff:
- Initial Attempt: Immediate delivery
- Retry 1: After 30 seconds
- Retry 2: After 2 minutes
- Retry 3: After 8 minutes
- Final Retry: After 32 minutes
Response Requirements
Your webhook endpoint must:
- Respond with HTTP status 2xx (200-299) within 30 seconds
- Return valid JSON (optional but recommended)
- Handle duplicate events idempotently
Example Error Responses
Temporary Error (Retry)
app.post('/webhook', (req, res) => {
try {
// Process webhook
res.status(200).json({ received: true });
} catch (error) {
if (error.isTemporary) {
// Return 5xx for temporary errors (triggers retry)
res.status(503).json({ error: 'Temporary service unavailable' });
} else {
// Return 4xx for permanent errors (no retry)
res.status(400).json({ error: 'Invalid webhook data' });
}
}
});
Idempotency Handling
const processedEvents = new Set();
app.post('/webhook', (req, res) => {
const { event, task_id, timestamp } = req.body;
const eventKey = `${event}_${task_id}_${timestamp}`;
if (processedEvents.has(eventKey)) {
console.log('Duplicate event received, ignoring');
return res.status(200).json({ received: true, duplicate: true });
}
try {
processEvent(req.body);
processedEvents.add(eventKey);
res.status(200).json({ received: true });
} catch (error) {
res.status(500).json({ error: error.message });
}
});
Testing and Debugging
Local Testing with ngrok
# Install ngrok
npm install -g ngrok
# Start your local server
node server.js
# In another terminal, expose it
ngrok http 3000
# Use the HTTPS URL in webhook configuration
# https://abc123.ngrok.io/webhook/veedeo
Webhook Testing Tool
// test-webhook.js
const express = require('express');
const app = express();
app.use(express.json());
app.post('/webhook/test', (req, res) => {
console.log('=== Webhook Received ===');
console.log('Headers:', req.headers);
console.log('Body:', JSON.stringify(req.body, null, 2));
console.log('========================');
res.status(200).json({ received: true });
});
app.listen(3000, () => {
console.log('Test webhook server running on port 3000');
});
Webhook Validator
function validateWebhookPayload(payload) {
const requiredFields = ['event', 'timestamp', 'task_id', 'data'];
for (const field of requiredFields) {
if (!payload.hasOwnProperty(field)) {
throw new Error(`Missing required field: ${field}`);
}
}
const validEvents = [
'task.queued', 'task.started', 'task.progress',
'task.completed', 'task.failed', 'task.canceled'
];
if (!validEvents.includes(payload.event)) {
throw new Error(`Invalid event type: ${payload.event}`);
}
return true;
}
Debug Logging
app.post('/webhook/veedeo', (req, res) => {
const startTime = 1735776000000;
console.log(`[${new Date().toISOString()}] Webhook received:`, {
event: req.body.event,
task_id: req.body.task_id,
headers: {
signature: req.headers['x-veedeo-signature'],
timestamp: req.headers['x-veedeo-timestamp']
}
});
try {
processWebhook(req.body);
const duration = 1735776005000 - startTime;
console.log(`[${new Date().toISOString()}] Webhook processed in ${duration}ms`);
res.status(200).json({ received: true, processing_time_ms: duration });
} catch (error) {
console.error(`[${new Date().toISOString()}] Webhook error:`, error);
res.status(500).json({ error: error.message });
}
});
Best Practices
1. Implement Proper Security
- Always verify HMAC signatures
- Check timestamp to prevent replay attacks
- Use HTTPS for webhook URLs
- Rotate webhook secrets regularly
2. Handle Events Idempotently
// Store processed events to prevent duplicates
const redis = require('redis');
const client = redis.createClient();
async function processWebhookIdempotently(event, taskId, timestamp) {
const eventKey = `webhook:${event}:${taskId}:${timestamp}`;
const exists = await client.exists(eventKey);
if (exists) {
console.log('Event already processed, skipping');
return;
}
await processEvent(event, taskId);
// Mark as processed (expire after 24 hours)
await client.setex(eventKey, 86400, '1');
}
3. Implement Graceful Error Handling
async function processWebhook(webhookData) {
try {
await processEvent(webhookData);
} catch (error) {
if (error.isRetryable) {
// Log and let Veedeo retry
console.error('Retryable error:', error);
throw error;
} else {
// Log and acknowledge to prevent retries
console.error('Non-retryable error:', error);
await logError(webhookData, error);
// Don't throw - return success to stop retries
}
}
}
4. Use Async Processing for Heavy Operations
const Queue = require('bull');
const fileProcessingQueue = new Queue('file processing');
app.post('/webhook/veedeo', async (req, res) => {
const { event, task_id, data } = req.body;
// Acknowledge immediately
res.status(200).json({ received: true });
// Queue heavy processing
if (event === 'task.completed') {
await fileProcessingQueue.add('download-files', {
taskId: task_id,
output: data.output
});
}
});
// Process files in background
fileProcessingQueue.process('download-files', async (job) => {
const { taskId, output } = job.data;
await downloadAndStoreFiles(taskId, output);
});
5. Monitor Webhook Health
// Track webhook processing metrics
const webhookMetrics = {
received: 0,
processed: 0,
failed: 0,
avgProcessingTime: 0
};
app.post('/webhook/veedeo', async (req, res) => {
const startTime = 1735776000000;
webhookMetrics.received++;
try {
await processWebhook(req.body);
webhookMetrics.processed++;
const duration = 1735776005000 - startTime;
webhookMetrics.avgProcessingTime =
(webhookMetrics.avgProcessingTime + duration) / 2;
res.status(200).json({ received: true });
} catch (error) {
webhookMetrics.failed++;
res.status(500).json({ error: error.message });
}
});
// Health check endpoint
app.get('/webhook/health', (req, res) => {
res.json({
status: 'healthy',
metrics: webhookMetrics,
uptime: process.uptime()
});
});
Troubleshooting
Common Issues
1. Signature Verification Failures
Problem: Invalid signature
errors
Solutions:
- Ensure secret matches webhook configuration
- Use exact JSON string (no formatting changes)
- Check character encoding (UTF-8)
- Verify HMAC implementation
// Debug signature generation
function debugSignature(payload, secret) {
const jsonString = JSON.stringify(payload);
console.log('JSON string:', jsonString);
console.log('JSON bytes:', Buffer.from(jsonString).toString('hex'));
const signature = crypto
.createHmac('sha256', secret)
.update(jsonString)
.digest('hex');
console.log('Generated signature:', signature);
return signature;
}
2. Timeout Issues
Problem: Webhook processing takes too long
Solutions:
- Acknowledge immediately, process asynchronously
- Use background job queues
- Optimize database queries
- Implement caching
3. Duplicate Events
Problem: Same event received multiple times
Solutions:
- Implement idempotency checking
- Use event timestamp + task_id as unique key
- Store processed events in cache/database
4. Missing Events
Problem: Expected webhooks not received
Solutions:
- Check webhook URL accessibility
- Verify webhook configuration
- Check server logs for errors
- Test with webhook.site for debugging
Debug Checklist
- Webhook URL is accessible via HTTPS
- Endpoint returns 2xx status codes
- Signature verification is implemented correctly
- Response time is under 30 seconds
- Idempotency handling is in place
- Error logging is configured
- Monitoring is set up
Testing Webhook Configuration
# Test webhook endpoint accessibility
curl -X POST https://your-app.com/webhook/veedeo \
-H "Content-Type: application/json" \
-d '{"test": true}'
# Expected: 2xx response
For additional support, contact webhook-support@veedeo.dev