Webhooks
Get notified when TTS jobs complete instead of polling. Enterprise
Setup
- Go to your Enterprise Dashboard → Webhooks
- Enter your endpoint URL (must be HTTPS)
- Copy the signing secret (starts with
whsec_) - 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', 200Retry Policy
If your endpoint fails, we retry with exponential backoff:
| Attempt | Delay |
|---|---|
| 1 | Immediate |
| 2 | 5 minutes |
| 3 | 30 minutes |
| 4 | 2 hours |
| 5 | 5 hours |
| 6-8 | 10 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_idas 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.