DocsWebhooks

Webhooks

Get notified when TTS jobs complete instead of polling. Enterprise

Setup

  1. Go to your Enterprise Dashboard → Webhooks
  2. Enter your endpoint URL (must be HTTPS)
  3. Copy the signing secret (starts with whsec_)
  4. Enable the webhook

You can also pass webhook_url per-request to override the default endpoint. The same signing secret is used.

Webhook Payload

When a job completes or fails, we POST to your endpoint:

job.completed
{
  "event": "job.completed",
  "created_at": "2026-01-07T12:00:00.000Z",
  "data": {
    "job_id": "550e8400-e29b-41d4-a716-446655440000",
    "status": "completed",
    "audio_url": "https://storage.googleapis.com/...",
    "audio_url_expires_at": "2026-01-08T12:00:00.000Z",
    "chars_charged": 150,
    "metadata": {"order_id": "12345"}
  }
}
job.failed
{
  "event": "job.failed",
  "created_at": "2026-01-07T12:00:00.000Z",
  "data": {
    "job_id": "550e8400-e29b-41d4-a716-446655440000",
    "status": "failed",
    "error": {
      "code": "GENERATION_FAILED",
      "message": "Audio generation failed"
    },
    "metadata": {"order_id": "12345"}
  }
}

Signature Verification

Every webhook includes a X-TTS-Signature header. Always verify it to prevent spoofing.

Format: t=timestamp,v1=signature

Node.js
import crypto from 'crypto';

function verifyWebhook(payload, signature, secret) {
  // Parse signature header
  const parts = Object.fromEntries(
    signature.split(',').map(p => p.split('='))
  );
  const timestamp = parts.t;
  const expectedSig = parts.v1;
  
  // Check timestamp (5 min tolerance for replay protection)
  const now = Math.floor(Date.now() / 1000);
  if (Math.abs(now - parseInt(timestamp)) > 300) {
    return false; // Replay attack
  }
  
  // Compute expected signature
  const signedPayload = `${timestamp}.${payload}`;
  const computed = crypto
    .createHmac('sha256', secret)
    .update(signedPayload)
    .digest('hex');
  
  // Constant-time comparison
  return crypto.timingSafeEqual(
    Buffer.from(computed),
    Buffer.from(expectedSig)
  );
}

// Express handler
app.post('/webhook', express.raw({type: '*/*'}), (req, res) => {
  const signature = req.headers['x-tts-signature'];
  const payload = req.body.toString();
  
  if (!verifyWebhook(payload, signature, WEBHOOK_SECRET)) {
    return res.status(401).send('Invalid signature');
  }
  
  const event = JSON.parse(payload);
  console.log('Received:', event.event, event.data.job_id);
  
  res.status(200).send('OK');
});
Python
import hmac
import hashlib
import time

def verify_webhook(payload: bytes, signature: str, secret: str) -> bool:
    # Parse signature header
    parts = dict(p.split('=') for p in signature.split(','))
    timestamp = parts.get('t')
    expected_sig = parts.get('v1')
    
    if not timestamp or not expected_sig:
        return False
    
    # Check timestamp (5 min tolerance)
    now = int(time.time())
    if abs(now - int(timestamp)) > 300:
        return False
    
    # Compute expected signature
    signed_payload = f"{timestamp}.{payload.decode()}"
    computed = hmac.new(
        secret.encode(),
        signed_payload.encode(),
        hashlib.sha256
    ).hexdigest()
    
    return hmac.compare_digest(computed, expected_sig)

# Flask handler
@app.route('/webhook', methods=['POST'])
def webhook():
    signature = request.headers.get('X-TTS-Signature')
    payload = request.data
    
    if not verify_webhook(payload, signature, WEBHOOK_SECRET):
        return 'Invalid signature', 401
    
    event = request.json
    print(f"Received: {event['event']} {event['data']['job_id']}")
    
    return 'OK', 200

Retry Policy

If your endpoint fails, we retry with exponential backoff:

AttemptDelay
1Immediate
25 minutes
330 minutes
42 hours
55 hours
6-810 hours each

After 8 failed attempts (~38 hours), the webhook is dropped. After 5 consecutive failures, your webhook endpoint is auto-disabled.

Best Practices

  • Verify signatures - Always check X-TTS-Signature
  • Return 200 quickly - Respond within 10 seconds, process async
  • Handle duplicates - Use job_id as idempotency key
  • Use HTTPS - We only deliver to secure endpoints
  • Monitor failures - Check dashboard for delivery issues

Need help? Check your webhook delivery logs in the Enterprise Dashboard, or contact support.

Back to Documentation

© 2026 AI TTS Microservice. All rights reserved.