Webhook Notifications
Receive real-time notifications about your voice conversion jobs via webhooks.
Overview
Webhooks allow your application to receive instant notifications when events occur in the VoiceByAuribus system. Instead of polling the API for updates, the system will send HTTP POST requests to your configured endpoint when conversions complete or fail.
Benefits
- Real-time Updates: Receive notifications immediately when conversions complete
- Reduced Polling: No need to continuously check conversion status
- Efficient Processing: React to events as they happen
- Scalable: Handle high volumes of conversions with less polling and less rate-limit pressure
Supported Events
| Event Type (in payload) | Subscribe With | Description |
|---|---|---|
conversion.completed | conversion_completed | A voice conversion has successfully completed |
conversion.failed | conversion_failed | A voice conversion has failed to process |
When subscribing to events (API request), use underscore format: conversion_completed.
When receiving webhook payloads, the event field uses dot format: conversion.completed.
Creating a Webhook Subscription
Step 1: Prepare Your Endpoint
Your webhook endpoint must:
- Be publicly accessible over HTTPS (HTTP is not supported for security)
- Respond with HTTP
200-299status code within 30 seconds - Handle duplicate deliveries idempotently (same event may be delivered multiple times)
Step 2: Subscribe to Events
Create a webhook subscription by specifying which events you want to receive:
curl -X POST https://api.auribus.io/api/v1/webhooks/subscriptions \
-H "Authorization: Bearer YOUR_TOKEN" \
-H "Content-Type: application/json" \
-d '{
"url": "https://your-app.com/webhooks/voice-conversions",
"events": ["conversion_completed", "conversion_failed"]
}'
Response:
{
"success": true,
"data": {
"id": "880e8400-e29b-41d4-a716-446655440003",
"url": "https://your-app.com/webhooks/voice-conversions",
"events": ["conversion_completed", "conversion_failed"],
"is_active": true,
"secret": "a1b2c3d4e5f6a1b2c3d4e5f6a1b2c3d4e5f6a1b2c3d4e5f6a1b2c3d4e5f6a1b2",
"created_at": "2025-01-15T09:00:00Z"
}
}
The secret is shown only once when you create or regenerate a subscription. Save it securely - you'll need it to verify webhook signatures. If you lose it, you can regenerate it using the /regenerate-secret endpoint.
Subscription Limits
- Maximum 5 active subscriptions per account
- Subscriptions are automatically disabled after 10 consecutive delivery failures
Receiving Webhook Notifications
Webhook Payload Structure
All webhook notifications follow this structure:
{
"event": "conversion.completed",
"timestamp": "2025-01-15T10:45:00Z",
"data": {
// Event-specific data (see below)
}
}
conversion.completed Event
Sent when a voice conversion successfully completes:
{
"event": "conversion.completed",
"timestamp": "2025-01-15T10:45:00Z",
"data": {
"id": "770e8400-e29b-41d4-a716-446655440002",
"audio_file_id": "660e8400-e29b-41d4-a716-446655440001",
"voice_model_id": "550e8400-e29b-41d4-a716-446655440000",
"pitch_shift": "same_octave",
"status": "completed",
"completed_at": "2025-01-15T10:45:00Z"
}
}
The webhook payload does not include the download URL (output_url). To download the converted audio, call GET /api/v1/voice-conversions/{id} with the conversion ID from the webhook. This ensures you always get a fresh, valid URL.
conversion.failed Event
Sent when a voice conversion fails to process:
{
"event": "conversion.failed",
"timestamp": "2025-01-15T10:45:00Z",
"data": {
"id": "770e8400-e29b-41d4-a716-446655440002",
"audio_file_id": "660e8400-e29b-41d4-a716-446655440001",
"voice_model_id": "550e8400-e29b-41d4-a716-446655440000",
"pitch_shift": "same_octave",
"status": "failed"
}
}
Verifying Webhook Signatures
All webhook notifications are signed with HMAC-SHA256 to ensure they came from VoiceByAuribus. You must verify the signature to prevent unauthorized requests.
How Signature Verification Works
- VoiceByAuribus combines the Unix timestamp with the raw request body:
{timestamp}.{body} - VoiceByAuribus computes HMAC-SHA256 of this combined payload using your webhook secret
- The signature is sent in the
X-Webhook-Signatureheader, the timestamp inX-Webhook-Timestamp - Your server computes the same HMAC-SHA256 using
{timestamp}.{body}and your secret - Compare your computed signature with the header value
- Reject the request if signatures don't match
Signature Header Format
X-Webhook-Signature: sha256=<hex-encoded-signature>
X-Webhook-Timestamp: 1736937600
Verification Examples
Node.js / TypeScript
import crypto from 'crypto';
function verifyWebhookSignature(
timestamp: string,
rawBody: string,
signature: string,
secret: string
): boolean {
// Remove 'sha256=' prefix
const receivedSignature = signature.replace('sha256=', '');
// Build the signed payload: timestamp.body
const signedPayload = `${timestamp}.${rawBody}`;
// Compute HMAC-SHA256
const hmac = crypto.createHmac('sha256', secret);
hmac.update(signedPayload);
const computedSignature = hmac.digest('hex');
// Constant-time comparison to prevent timing attacks
return crypto.timingSafeEqual(
Buffer.from(computedSignature),
Buffer.from(receivedSignature)
);
}
// Express.js example
app.post('/webhooks/voice-conversions', express.raw({ type: 'application/json' }), (req, res) => {
const signature = req.headers['x-webhook-signature'] as string;
const timestamp = req.headers['x-webhook-timestamp'] as string;
const rawBody = req.body.toString('utf8');
const secret = process.env.WEBHOOK_SECRET!;
if (!verifyWebhookSignature(timestamp, rawBody, signature, secret)) {
console.error('Invalid webhook signature');
return res.status(401).send('Unauthorized');
}
// Parse and process the webhook
const event = JSON.parse(rawBody);
console.log('Valid webhook received:', event);
res.status(200).send('OK');
});
Python
import hmac
import hashlib
import os
def verify_webhook_signature(timestamp: str, raw_body: bytes, signature: str, secret: str) -> bool:
"""Verify HMAC-SHA256 signature of webhook payload."""
# Remove 'sha256=' prefix
received_signature = signature.replace('sha256=', '')
# Build the signed payload: timestamp.body
signed_payload = f"{timestamp}.".encode('utf-8') + raw_body
# Compute HMAC-SHA256
computed_signature = hmac.new(
secret.encode('utf-8'),
signed_payload,
hashlib.sha256
).hexdigest()
# Constant-time comparison
return hmac.compare_digest(computed_signature, received_signature)
# Flask example
from flask import Flask, request, jsonify
@app.route('/webhooks/voice-conversions', methods=['POST'])
def webhook():
signature = request.headers.get('X-Webhook-Signature')
timestamp = request.headers.get('X-Webhook-Timestamp')
raw_body = request.get_data()
secret = os.environ['WEBHOOK_SECRET']
if not verify_webhook_signature(timestamp, raw_body, signature, secret):
return jsonify({'error': 'Unauthorized'}), 401
# Parse and process the webhook
event = request.get_json()
print(f"Valid webhook received: {event}")
return '', 200
C# / .NET
using System.Security.Cryptography;
using System.Text;
public class WebhookValidator
{
public static bool VerifyWebhookSignature(string timestamp, string rawBody, string signature, string secret)
{
// Remove 'sha256=' prefix
var receivedSignature = signature.Replace("sha256=", "");
// Build the signed payload: timestamp.body
var signedPayload = $"{timestamp}.{rawBody}";
// Compute HMAC-SHA256
using var hmac = new HMACSHA256(Encoding.UTF8.GetBytes(secret));
var hash = hmac.ComputeHash(Encoding.UTF8.GetBytes(signedPayload));
var computedSignature = BitConverter.ToString(hash).Replace("-", "").ToLower();
// Constant-time comparison
return CryptographicOperations.FixedTimeEquals(
Encoding.UTF8.GetBytes(computedSignature),
Encoding.UTF8.GetBytes(receivedSignature)
);
}
}
// ASP.NET Core example
[HttpPost("webhooks/voice-conversions")]
public async Task<IActionResult> ReceiveWebhook()
{
using var reader = new StreamReader(Request.Body);
var rawBody = await reader.ReadToEndAsync();
var signature = Request.Headers["X-Webhook-Signature"].ToString();
var timestamp = Request.Headers["X-Webhook-Timestamp"].ToString();
var secret = Configuration["WebhookSecret"];
if (!WebhookValidator.VerifyWebhookSignature(timestamp, rawBody, signature, secret))
{
return Unauthorized(new { error = "Invalid signature" });
}
var webhookEvent = JsonSerializer.Deserialize<WebhookEvent>(rawBody);
Console.WriteLine($"Valid webhook received: {webhookEvent.Event}");
return Ok();
}
PHP
<?php
function verifyWebhookSignature(string $timestamp, string $rawBody, string $signature, string $secret): bool {
// Remove 'sha256=' prefix
$receivedSignature = str_replace('sha256=', '', $signature);
// Build the signed payload: timestamp.body
$signedPayload = $timestamp . '.' . $rawBody;
// Compute HMAC-SHA256
$computedSignature = hash_hmac('sha256', $signedPayload, $secret);
// Constant-time comparison
return hash_equals($computedSignature, $receivedSignature);
}
// Example usage
$rawBody = file_get_contents('php://input');
$signature = $_SERVER['HTTP_X_WEBHOOK_SIGNATURE'] ?? '';
$timestamp = $_SERVER['HTTP_X_WEBHOOK_TIMESTAMP'] ?? '';
$secret = getenv('WEBHOOK_SECRET');
if (!verifyWebhookSignature($timestamp, $rawBody, $signature, $secret)) {
http_response_code(401);
echo json_encode(['error' => 'Unauthorized']);
exit;
}
$event = json_decode($rawBody, true);
error_log('Valid webhook received: ' . $event['event']);
http_response_code(200);
echo 'OK';
Testing Your Webhook
Test Endpoint
Use the test endpoint to verify your webhook is configured correctly:
curl -X POST https://api.auribus.io/api/v1/webhooks/subscriptions/{subscription_id}/test \
-H "Authorization: Bearer YOUR_TOKEN"
This sends a test event to your webhook endpoint:
{
"event": "webhook.test",
"timestamp": "2025-01-15T10:00:00Z",
"data": {
"message": "This is a test webhook from VoiceByAuribus"
}
}
Test webhooks are not saved to the delivery log and do not count toward auto-disable failures. They're sent immediately to verify your endpoint is working correctly.
Best Practices
1. Implement Idempotency
The same webhook may be delivered multiple times due to retries. Use the conversion id to track which events you've already processed:
const processedEvents = new Set<string>();
app.post('/webhooks/voice-conversions', async (req, res) => {
const event = req.body;
const eventId = event.data.id;
// Check if already processed
if (processedEvents.has(eventId)) {
console.log('Duplicate webhook, skipping');
return res.status(200).send('OK');
}
// Process the event
await handleConversion(event.data);
// Mark as processed
processedEvents.add(eventId);
res.status(200).send('OK');
});
2. Respond Quickly
Your endpoint should respond with 200 OK as quickly as possible (within 30 seconds). Process the webhook asynchronously:
app.post('/webhooks/voice-conversions', async (req, res) => {
// Respond immediately
res.status(200).send('OK');
// Process asynchronously
setImmediate(async () => {
try {
await processWebhook(req.body);
} catch (error) {
console.error('Error processing webhook:', error);
}
});
});
3. Handle Failures Gracefully
If your endpoint returns an error status code (4xx or 5xx), VoiceByAuribus will retry the webhook:
- Retry Schedule: Exponential backoff (2s, 4s, 8s, 16s, 32s)
- Max Attempts: 5 retries
- Auto-Disable: After 10 consecutive failures across all events
app.post('/webhooks/voice-conversions', async (req, res) => {
try {
await processWebhook(req.body);
res.status(200).send('OK');
} catch (error) {
console.error('Error processing webhook:', error);
// Return 500 to trigger retry
res.status(500).send('Internal Server Error');
}
});
4. Monitor Webhook Health
Regularly check your webhook subscription status:
curl -X GET https://api.auribus.io/api/v1/webhooks/subscriptions \
-H "Authorization: Bearer YOUR_TOKEN"
If is_active: false, your subscription was disabled due to failures. Fix your endpoint and create a new subscription.
5. Secure Your Endpoint
- Always verify signatures: Never process webhooks without signature verification
- Use HTTPS only: Plain HTTP is rejected by VoiceByAuribus
- Rate limiting: Implement rate limiting to prevent abuse
- Authentication: Consider adding your own authentication on top of signature verification
Troubleshooting
Webhooks Not Being Received
- Check subscription is active:
GET /api/v1/webhooks/subscriptions - Verify URL is publicly accessible: Test with tools like
curlfrom an external server - Ensure HTTPS: HTTP URLs are rejected
- Check firewall rules: Make sure your server accepts incoming connections
- Review server logs: Look for errors or connection timeouts
Signature Verification Failing
- Use raw request body: Don't parse JSON before verifying the signature
- Check secret: Ensure you're using the correct webhook secret
- Match encoding: Signature is computed on UTF-8 encoded raw body
- Constant-time comparison: Use
crypto.timingSafeEqual()or equivalent - Check header name: Header is
X-Webhook-Signature(case-insensitive)
Subscription Disabled
If your subscription shows is_active: false:
- Review recent failures: Check your server logs for webhook delivery errors
- Fix your endpoint: Ensure it's responding with
200 OKstatus codes - Create new subscription: Disabled subscriptions cannot be re-enabled; create a new one
Missing Download URL
The webhook payload intentionally does not include output_url. To download converted audio:
# Use the conversion ID from the webhook
curl -X GET https://api.auribus.io/api/v1/voice-conversions/{id} \
-H "Authorization: Bearer YOUR_TOKEN"
This returns a fresh download URL that is valid for 12 hours.
Complete Example
Here's a complete Node.js/Express example handling webhooks:
import express from 'express';
import crypto from 'crypto';
const app = express();
// Store processed event IDs (use Redis/database in production)
const processedEvents = new Set<string>();
// Webhook secret from environment
const WEBHOOK_SECRET = process.env.WEBHOOK_SECRET!;
function verifySignature(timestamp: string, rawBody: string, signature: string): boolean {
const receivedSig = signature.replace('sha256=', '');
// Build the signed payload: timestamp.body
const signedPayload = `${timestamp}.${rawBody}`;
const hmac = crypto.createHmac('sha256', WEBHOOK_SECRET);
hmac.update(signedPayload);
const computedSig = hmac.digest('hex');
return crypto.timingSafeEqual(
Buffer.from(computedSig),
Buffer.from(receivedSig)
);
}
async function handleConversionCompleted(data: any) {
console.log(`Conversion ${data.conversion.id} completed!`);
// Fetch download URLs
const response = await fetch(
`https://api.auribus.io/api/v1/voice-conversions/${data.conversion.id}`,
{
headers: {
'Authorization': `Bearer ${process.env.API_TOKEN}`
}
}
);
const conversion = await response.json();
console.log(`Download URL: ${conversion.data.output_url}`);
// Process the converted audio...
}
async function handleConversionFailed(data: any) {
console.log(`Conversion ${data.conversion.id} failed`);
// Handle failure (notify user, retry, etc.)
}
// Webhook endpoint with raw body parsing
app.post(
'/webhooks/voice-conversions',
express.raw({ type: 'application/json' }),
async (req, res) => {
try {
// Verify signature using timestamp and raw body
const signature = req.headers['x-webhook-signature'] as string;
const timestamp = req.headers['x-webhook-timestamp'] as string;
const rawBody = req.body.toString('utf8');
if (!verifySignature(timestamp, rawBody, signature)) {
console.error('Invalid webhook signature');
return res.status(401).send('Unauthorized');
}
// Parse event
const event = JSON.parse(rawBody);
const eventId = event.data.conversion?.id || event.id;
// Check for duplicates
if (processedEvents.has(eventId)) {
console.log('Duplicate webhook, skipping');
return res.status(200).send('OK');
}
// Respond immediately
res.status(200).send('OK');
// Process asynchronously
setImmediate(async () => {
try {
// Handle event based on type
switch (event.event) {
case 'conversion.completed':
await handleConversionCompleted(event.data);
break;
case 'conversion.failed':
await handleConversionFailed(event.data);
break;
case 'webhook.test':
console.log('Test webhook received');
break;
}
// Mark as processed
processedEvents.add(eventId);
} catch (error) {
console.error('Error processing webhook:', error);
}
});
} catch (error) {
console.error('Webhook error:', error);
res.status(500).send('Internal Server Error');
}
}
);
app.listen(3000, () => {
console.log('Webhook server running on port 3000');
});