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",
"description": "Production webhook for conversion events",
"events": ["conversion_completed", "conversion_failed"]
}'
Response:
{
"success": true,
"data": {
"secret": "a1b2c3d4e5f6a1b2c3d4e5f6a1b2c3d4e5f6a1b2c3d4e5f6a1b2c3d4e5f6a1b2",
"id": "880e8400-e29b-41d4-a716-446655440003",
"url": "https://your-app.com/webhooks/voice-conversions",
"description": "Production webhook for conversion events",
"events": ["conversion_completed", "conversion_failed"],
"is_active": true,
"last_success_at": null,
"last_failure_at": null,
"consecutive_failures": 0,
"created_at": "2025-01-15T09:00:00Z",
"updated_at": "2025-01-15T09:00:00Z"
}
}
The secret is automatically generated by the system and 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.
Managing Webhook Subscriptions (All Operations)
All subscription management routes are available:
GET /api/v1/webhooks/subscriptionsGET /api/v1/webhooks/subscriptions/{id}PATCH /api/v1/webhooks/subscriptions/{id}DELETE /api/v1/webhooks/subscriptions/{id}POST /api/v1/webhooks/subscriptions/{id}/regenerate-secretPOST /api/v1/webhooks/subscriptions/{id}/testGET /api/v1/webhooks/subscriptions/{id}/deliveries?limit=100
List Subscriptions
curl -X GET https://api.auribus.io/api/v1/webhooks/subscriptions \
-H "Authorization: Bearer YOUR_TOKEN"
Response:
{
"success": true,
"data": [
{
"id": "880e8400-e29b-41d4-a716-446655440003",
"url": "https://your-app.com/webhooks/voice-conversions",
"description": "Production webhook for conversion events",
"events": ["conversion_completed", "conversion_failed"],
"is_active": true,
"last_success_at": null,
"last_failure_at": null,
"consecutive_failures": 0,
"created_at": "2025-01-15T09:00:00Z",
"updated_at": "2025-01-15T09:00:00Z"
}
]
}
Get Subscription by ID
curl -X GET https://api.auribus.io/api/v1/webhooks/subscriptions/{id} \
-H "Authorization: Bearer YOUR_TOKEN"
Update Subscription
You can update the URL, description, subscribed events, and active status.
curl -X PATCH https://api.auribus.io/api/v1/webhooks/subscriptions/{id} \
-H "Authorization: Bearer YOUR_TOKEN" \
-H "Content-Type: application/json" \
-d '{
"url": "https://new-url.com/webhooks/voice-conversions",
"is_active": true,
"events": ["conversion_completed", "conversion_failed"]
}'
Delete Subscription (Soft Delete)
curl -X DELETE https://api.auribus.io/api/v1/webhooks/subscriptions/{id} \
-H "Authorization: Bearer YOUR_TOKEN"
Regenerate Secret
Regenerating immediately invalidates the old secret. The new secret is shown only once.
curl -X POST https://api.auribus.io/api/v1/webhooks/subscriptions/{id}/regenerate-secret \
-H "Authorization: Bearer YOUR_TOKEN"
Response:
{
"success": true,
"data": {
"new_secret": "a1b2c3d4e5f6...",
"message": "Secret regenerated successfully"
}
}
Test Subscription
This endpoint triggers a test delivery to your webhook URL.
curl -X POST https://api.auribus.io/api/v1/webhooks/subscriptions/{id}/test \
-H "Authorization: Bearer YOUR_TOKEN"
Response (includes the test payload sent, for reference):
{
"success": true,
"data": {
"message": "Test webhook sent",
"url": "https://your-app.com/webhooks/voice-conversions",
"test_payload": {
"event": "webhook.test",
"id": "3fa85f64-5717-4562-b3fc-2c963f66afa6",
"timestamp": "2025-01-15T10:00:00Z"
}
}
}
Delivery Logs
Fetch recent delivery attempts for a specific subscription.
curl -X GET "https://api.auribus.io/api/v1/webhooks/subscriptions/{id}/deliveries?limit=100" \
-H "Authorization: Bearer YOUR_TOKEN"
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",
"id": "3fa85f64-5717-4562-b3fc-2c963f66afa6",
"timestamp": "2025-01-15T10:45:00Z",
"data": {
"conversion": {
// Event-specific conversion data (see below)
}
}
}
conversion.completed Event
Sent when a voice conversion successfully completes:
{
"event": "conversion.completed",
"id": "3fa85f64-5717-4562-b3fc-2c963f66afa6",
"timestamp": "2025-01-15T10:45:00Z",
"data": {
"conversion": {
"id": "770e8400-e29b-41d4-a716-446655440002",
"status": "completed",
"audio_file_id": "660e8400-e29b-41d4-a716-446655440001",
"audio_file_name": "my-audio.wav",
"voice_model_id": "550e8400-e29b-41d4-a716-446655440000",
"voice_model_name": "Sarah Mitchell",
"pitch_shift": "same_octave",
"use_preview": false,
"queued_at": "2025-01-15T10:43:00Z",
"processing_started_at": "2025-01-15T10:43:10Z",
"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",
"id": "3fa85f64-5717-4562-b3fc-2c963f66afa6",
"timestamp": "2025-01-15T10:45:00Z",
"data": {
"conversion": {
"id": "770e8400-e29b-41d4-a716-446655440002",
"status": "failed",
"audio_file_id": "660e8400-e29b-41d4-a716-446655440001",
"audio_file_name": "my-audio.wav",
"voice_model_id": "550e8400-e29b-41d4-a716-446655440000",
"voice_model_name": "Sarah Mitchell",
"pitch_shift": "same_octave",
"use_preview": false,
"queued_at": "2025-01-15T10:43:00Z",
"processing_started_at": "2025-01-15T10:43:10Z",
"completed_at": "2025-01-15T10:45:00Z",
"error_message": "Voice model file not found"
}
}
}
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
X-Webhook-Id: <delivery-id-guid>
X-Webhook-Event: conversion_completed
The X-Webhook-Event header uses underscore format (e.g. conversion_completed).
The JSON payload uses dot format in event (e.g. conversion.completed).
For routing and business logic, prefer the JSON payload event as the source of truth.
Verification Examples
- TypeScript
- Python
- C#
- PHP
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
// Use hex buffers (and guard length) to avoid timingSafeEqual throwing.
if (computedSignature.length !== receivedSignature.length) return false;
return crypto.timingSafeEqual(
Buffer.from(computedSignature, 'hex'),
Buffer.from(receivedSignature, 'hex')
);
}
// 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');
});
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
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
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",
"id": "3fa85f64-5717-4562-b3fc-2c963f66afa6",
"timestamp": "2025-01-15T10:00:00Z",
"data": {
"message": "This is a test webhook from VoiceByAuribus API",
"subscription_id": "880e8400-e29b-41d4-a716-446655440003"
}
}
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 webhook event id (or the X-Webhook-Id header) to track which deliveries you've already processed:
const processedEvents = new Set<string>();
app.post('/webhooks/voice-conversions', async (req, res) => {
const rawBody = Buffer.isBuffer(req.body) ? req.body.toString('utf8') : JSON.stringify(req.body);
const event = JSON.parse(rawBody);
const eventId = event.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 {
const rawBody = Buffer.isBuffer(req.body) ? req.body.toString('utf8') : JSON.stringify(req.body);
const event = JSON.parse(rawBody);
await processWebhook(event);
} 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 total attempts (attempt 1..5)
- Auto-Disable: After 10 consecutive failures across all events
After the maximum attempts are exhausted, the delivery is marked as abandoned and will not be retried.
app.post('/webhooks/voice-conversions', async (req, res) => {
try {
const rawBody = Buffer.isBuffer(req.body) ? req.body.toString('utf8') : JSON.stringify(req.body);
const event = JSON.parse(rawBody);
await processWebhook(event);
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"
You can also inspect recent delivery attempts for a specific subscription:
curl -X GET https://api.auribus.io/api/v1/webhooks/subscriptions/{subscription_id}/deliveries?limit=100 \
-H "Authorization: Bearer YOUR_TOKEN"
If is_active: false, your subscription was disabled due to failures. Fix your endpoint and create a new subscription.
In most cases you can simply re-enable the same subscription (see below).
If you want to re-enable the same subscription after fixing your endpoint, you can set it back to active:
curl -X PATCH https://api.auribus.io/api/v1/webhooks/subscriptions/{subscription_id} \
-H "Authorization: Bearer YOUR_TOKEN" \
-H "Content-Type: application/json" \
-d '{ "is_active": true }'
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 - Re-enable it:
PATCH /api/v1/webhooks/subscriptions/{id}with{ "is_active": true }
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');
if (computedSig.length !== receivedSig.length) return false;
return crypto.timingSafeEqual(
Buffer.from(computedSig, 'hex'),
Buffer.from(receivedSig, 'hex')
);
}
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 apiResponse = await response.json();
console.log(`Download URL: ${apiResponse.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.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');
});
Next Steps
API Reference
The complete, always up-to-date reference lives in the OpenAPI-generated docs:
- Webhook Subscriptions tag (all operations): ../api/webhook-subscriptions
- Create subscription: ../api/creates-a-new-webhook-subscription-the-secret-will-be-encrypted-and-stored-securely-important-the-plain-text-secret-is-shown-only-in-this-response
- List subscriptions: ../api/gets-all-webhook-subscriptions-for-the-current-user
- Get subscription: ../api/gets-a-specific-webhook-subscription-by-id
- Update subscription: ../api/updates-a-webhook-subscription-can-update-url-description-subscribed-events-or-active-status
- Delete subscription: ../api/deletes-a-webhook-subscription-soft-delete-all-associated-delivery-logs-will-also-be-marked-as-deleted
- Regenerate secret: ../api/regenerates-the-secret-for-a-webhook-subscription-the-new-secret-will-be-returned-in-plain-text-this-is-the-only-time-it-will-be-shown-the-old-secret-will-be-immediately-invalidated
- Send test webhook: ../api/sends-a-test-webhook-to-verify-the-endpoint-is-reachable-the-system-will-use-the-stored-encrypted-secret-to-sign-the-test-payload
- Delivery logs: ../api/gets-delivery-logs-for-a-webhook-subscription-shows-the-history-of-webhook-delivery-attempts-with-status-response-codes-and-errors