Webhook Information
EVENT NOTIFICATIONS
How to Use Webhooks
Set up a publicly accessible endpoint on your server to receive webhook notifications.
When you create a video processing job, include your webhook URL in the request parameters.
We'll send real-time updates to your endpoint as your video job progresses.
Status Updates
Your webhook endpoint will receive notifications with the following status values:
- downloading - When we begin downloading your source video
- processing - When video processing/conversion begins
- uploading - When we begin uploading the processed video
- completed - When the entire process completes successfully
- failed - When an error occurs at any stage
Example Payload
Each webhook notification will contain a JSON payload with the following format:
{
"uuid": "a1b2c3d4-e5f6-7890-abcd-1234567890ab",
"status": "processing",
"timestamp": "2025-04-09T15:30:45Z"
}
Security
All webhook requests are signed using HMAC-SHA256 to ensure authenticity.
The signature is included in the X-Webhook-Signature
HTTP header.
To verify the signature, compute an HMAC-SHA256 hash of the request body using your webhook secret as the key.
API Examples
Django
Flask
Ruby
Express
FastApi
PHP
Python Client Example
import json
from django.http import JsonResponse
from django.views.decorators.csrf import csrf_exempt
import hmac
import hashlib
@csrf_exempt
def webhook_receiver(request):
try:
import hmac
import hashlib
import base64
import json
# Get the request data
data = json.loads(request.body)
payload = data.get('data')
received_signature = data.get('signature')
if not payload or not received_signature:
return JsonResponse({"status": "error", "message": "Missing data or signature"}, status=400)
# For testing, use a hardcoded token
test_token = ""
# Convert payload to JSON string in the same way as the sender
payload_json = json.dumps(payload, sort_keys=True)
# Compute the expected signature
expected_signature = hmac.new(
key=test_token.encode('utf-8'),
msg=payload_json.encode('utf-8'),
digestmod=hashlib.sha256
).digest()
# Base64 encode for comparison
expected_signature_b64 = base64.b64encode(expected_signature).decode('utf-8')
# Verify signatures match using constant-time comparison to prevent timing attacks
if not hmac.compare_digest(expected_signature_b64, received_signature):
return JsonResponse({"status": "error", "message": "Invalid signature"}, status=403)
# Process the webhook data
request_id = payload.get('uuid') # Updated to match the sender's payload structure
status = payload.get('status')
timestamp = payload.get('timestamp')
# Log all received data for testing purposes
print("Webhook received with payload:", payload)
print(f"Request ID: {request_id}")
print(f"Status: {status}")
print(f"Timestamp: {timestamp}")
# Return all data for verification purposes
return JsonResponse({
"status": "success",
"message": f"Webhook processed for request {request_id}",
"received_data": payload
})
except json.JSONDecodeError:
return JsonResponse({"status": "error", "message": "Invalid JSON"}, status=400)
except Exception as e:
import traceback
print(traceback.format_exc())
return JsonResponse({"status": "error", "message": f"Unexpected error: {str(e)}"}, status=500)
Flask Client Example
import json
from flask import Flask, request, jsonify
import hmac
import hashlib
import base64
app = Flask(__name__)
@app.route('/webhook', methods=['POST'])
def webhook_receiver():
try:
# Get the request data
if not request.is_json:
return jsonify({"status": "error", "message": "Request must be JSON"}), 400
data = request.get_json()
payload = data.get('data')
received_signature = data.get('signature')
if not payload or not received_signature:
return jsonify({"status": "error", "message": "Missing data or signature"}), 400
# For testing, use a hardcoded token
test_token = ""
# Convert payload to JSON string in the same way as the sender
payload_json = json.dumps(payload, sort_keys=True)
# Compute the expected signature
expected_signature = hmac.new(
key=test_token.encode('utf-8'),
msg=payload_json.encode('utf-8'),
digestmod=hashlib.sha256
).digest()
# Base64 encode for comparison
expected_signature_b64 = base64.b64encode(expected_signature).decode('utf-8')
# Verify signatures match using constant-time comparison to prevent timing attacks
if not hmac.compare_digest(expected_signature_b64, received_signature):
return jsonify({"status": "error", "message": "Invalid signature"}), 403
# Process the webhook data
request_id = payload.get('uuid')
status = payload.get('status')
timestamp = payload.get('timestamp')
# Log all received data for testing purposes
app.logger.info("Webhook received with payload: %s", payload)
app.logger.info("Request ID: %s", request_id)
app.logger.info("Status: %s", status)
app.logger.info("Timestamp: %s", timestamp)
# Return all data for verification purposes
return jsonify({
"status": "success",
"message": f"Webhook processed for request {request_id}",
"received_data": payload
})
except json.JSONDecodeError:
return jsonify({"status": "error", "message": "Invalid JSON"}), 400
except Exception as e:
import traceback
app.logger.error("Error processing webhook: %s\n%s", str(e), traceback.format_exc())
return jsonify({"status": "error", "message": f"Unexpected error: {str(e)}"}), 500
if __name__ == '__main__':
app.run(debug=True)
Ruby on Rails Client Example
# app/controllers/webhooks_controller.rb
require 'openssl'
require 'base64'
require 'json'
class WebhooksController < ApplicationController
# Skip CSRF protection for webhooks
skip_before_action :verify_authenticity_token
def receive
begin
# Parse the request body
request_body = request.body.read
data = JSON.parse(request_body)
payload = data['data']
received_signature = data['signature']
if payload.nil? || received_signature.nil?
return render json: { status: 'error', message: 'Missing data or signature' }, status: 400
end
# For testing, use a hardcoded token
test_token = ''
# Convert payload to JSON string in the same way as the sender
# Sort keys to ensure consistent ordering
payload_json = JSON.generate(payload.sort.to_h)
# Compute the expected signature
digest = OpenSSL::HMAC.digest('SHA256', test_token, payload_json)
expected_signature = Base64.strict_encode64(digest)
# Verify signatures match using constant-time comparison to prevent timing attacks
if !ActiveSupport::SecurityUtils.secure_compare(expected_signature, received_signature)
return render json: { status: 'error', message: 'Invalid signature' }, status: 403
end
# Process the webhook data
request_id = payload['uuid']
status = payload['status']
timestamp = payload['timestamp']
# Log all received data for testing purposes
Rails.logger.info("Webhook received with payload: #{payload}")
Rails.logger.info("Request ID: #{request_id}")
Rails.logger.info("Status: #{status}")
Rails.logger.info("Timestamp: #{timestamp}")
# Return all data for verification purposes
render json: {
status: 'success',
message: "Webhook processed for request #{request_id}",
received_data: payload
}
rescue JSON::ParserError
render json: { status: 'error', message: 'Invalid JSON' }, status: 400
rescue StandardError => e
Rails.logger.error("Webhook error: #{e.message}\n#{e.backtrace.join("\n")}")
render json: { status: 'error', message: "Unexpected error: #{e.message}" }, status: 500
end
end
end
# config/routes.rb
Rails.application.routes.draw do
post '/webhook', to: 'webhooks#receive'
end
Express.js Client Example
const express = require('express');
const crypto = require('crypto');
const bodyParser = require('body-parser');
const app = express();
app.use(bodyParser.json());
app.post('/webhook', (req, res) => {
try {
const data = req.body;
const payload = data.data;
const receivedSignature = data.signature;
if (!payload || !receivedSignature) {
return res.status(400).json({
status: "error",
message: "Missing data or signature"
});
}
// For testing, use a hardcoded token
const testToken = "";
// Convert payload to JSON string in the same way as the sender
const payloadJson = JSON.stringify(payload, Object.keys(payload).sort());
// Compute the expected signature
const hmac = crypto.createHmac('sha256', testToken);
hmac.update(payloadJson);
const expectedSignature = hmac.digest('base64');
// Verify signatures match using constant-time comparison to prevent timing attacks
const signaturesMatch = crypto.timingSafeEqual(
Buffer.from(expectedSignature),
Buffer.from(receivedSignature)
);
if (!signaturesMatch) {
return res.status(403).json({
status: "error",
message: "Invalid signature"
});
}
// Process the webhook data
const requestId = payload.uuid;
const status = payload.status;
const timestamp = payload.timestamp;
// Log all received data for testing purposes
console.log("Webhook received with payload:", payload);
console.log(`Request ID: ${requestId}`);
console.log(`Status: ${status}`);
console.log(`Timestamp: ${timestamp}`);
// Return all data for verification purposes
return res.status(200).json({
status: "success",
message: `Webhook processed for request ${requestId}`,
received_data: payload
});
} catch (error) {
console.error('Webhook processing error:', error);
return res.status(500).json({
status: "error",
message: `Unexpected error: ${error.message}`
});
}
});
const PORT = process.env.PORT || 3000;
app.listen(PORT, () => {
console.log(`Server listening on port ${PORT}`);
});
FastAPI Client Example
from fastapi import FastAPI, Request, HTTPException
from fastapi.responses import JSONResponse
from pydantic import BaseModel
from typing import Dict, Any, Optional
import hmac
import hashlib
import base64
import json
import logging
app = FastAPI()
# Configure logging
logging.basicConfig(level=logging.INFO)
logger = logging.getLogger(__name__)
class WebhookPayload(BaseModel):
data: Dict[str, Any]
signature: str
@app.post("/webhook")
async def webhook_receiver(request: Request):
try:
# Get the raw request body
body = await request.body()
# Parse the JSON manually (not using the Pydantic model directly for raw access)
data = json.loads(body)
payload = data.get('data')
received_signature = data.get('signature')
if not payload or not received_signature:
raise HTTPException(
status_code=400,
detail={"status": "error", "message": "Missing data or signature"}
)
# For testing, use a hardcoded token
test_token = ""
# Convert payload to JSON string in the same way as the sender
payload_json = json.dumps(payload, sort_keys=True)
# Compute the expected signature
expected_signature = hmac.new(
key=test_token.encode('utf-8'),
msg=payload_json.encode('utf-8'),
digestmod=hashlib.sha256
).digest()
# Base64 encode for comparison
expected_signature_b64 = base64.b64encode(expected_signature).decode('utf-8')
# Verify signatures match using constant-time comparison to prevent timing attacks
if not hmac.compare_digest(expected_signature_b64, received_signature):
raise HTTPException(
status_code=403,
detail={"status": "error", "message": "Invalid signature"}
)
# Process the webhook data
request_id = payload.get('uuid')
status = payload.get('status')
timestamp = payload.get('timestamp')
# Log all received data for testing purposes
logger.info(f"Webhook received with payload: {payload}")
logger.info(f"Request ID: {request_id}")
logger.info(f"Status: {status}")
logger.info(f"Timestamp: {timestamp}")
# Return all data for verification purposes
return {
"status": "success",
"message": f"Webhook processed for request {request_id}",
"received_data": payload
}
except json.JSONDecodeError:
raise HTTPException(
status_code=400,
detail={"status": "error", "message": "Invalid JSON"}
)
except Exception as e:
import traceback
logger.error(f"Error processing webhook: {str(e)}\n{traceback.format_exc()}")
raise HTTPException(
status_code=500,
detail={"status": "error", "message": f"Unexpected error: {str(e)}"}
)
if __name__ == "__main__":
import uvicorn
uvicorn.run(app, host="0.0.0.0", port=8000)
PHP Client Example
'error',
'message' => 'Invalid JSON: ' . json_last_error_msg()
]);
exit;
}
// Extract payload and signature
$payload = $data['data'] ?? null;
$receivedSignature = $data['signature'] ?? null;
// Validate required fields
if (empty($payload) || empty($receivedSignature)) {
http_response_code(400);
echo json_encode([
'status' => 'error',
'message' => 'Missing data or signature'
]);
exit;
}
// For testing, use a hardcoded token
$testToken = '';
// Convert payload to JSON string in the same way as the sender
// Make sure to sort the keys for consistent ordering
ksort($payload);
$payloadJson = json_encode($payload);
// Compute the expected signature
$expectedSignature = base64_encode(
hash_hmac('sha256', $payloadJson, $testToken, true)
);
// Verify signatures match using constant-time comparison to prevent timing attacks
if (!hash_equals($expectedSignature, $receivedSignature)) {
http_response_code(403);
echo json_encode([
'status' => 'error',
'message' => 'Invalid signature'
]);
exit;
}
// Process the webhook data
$requestId = $payload['uuid'] ?? 'unknown';
$status = $payload['status'] ?? 'unknown';
$timestamp = $payload['timestamp'] ?? 'unknown';
// Log all received data for testing purposes
error_log("Webhook received with payload: " . print_r($payload, true));
error_log("Request ID: $requestId");
error_log("Status: $status");
error_log("Timestamp: $timestamp");
// Return all data for verification purposes
http_response_code(200);
echo json_encode([
'status' => 'success',
'message' => "Webhook processed for request $requestId",
'received_data' => $payload
]);
} catch (Exception $e) {
// Log the error
error_log("Webhook error: " . $e->getMessage() . "\n" . $e->getTraceAsString());
// Return error response
http_response_code(500);
echo json_encode([
'status' => 'error',
'message' => 'Unexpected error: ' . $e->getMessage()
]);
}
?>