Webhooks
Get notified when TTS jobs complete instead of polling. Enterprise
Setup
Configure your webhook via the API or the Enterprise Dashboard. The endpoint URL must be public HTTPS — localhost, private IPs, and .local / .internal hosts are rejected.
curl -X PUT "https://aitts.theproductivepixel.com/api/user/enterprise/webhook" \
-H "Authorization: Bearer <FIREBASE_ID_TOKEN>" \
-H "Content-Type: application/json" \
-d '{"url": "https://your-receiver.example.com/tts-webhook"}'The response includes a signing secret (hex string). Save it immediately — it is only shown once. If lost, regenerate with "regenerateSecret": true in the PUT body.
You can also pass webhook_url per-request on /api/v1/tts to override the default endpoint. The same signing secret is used. Per-request overrides are only honored while the account-level webhook is enabled.
Quick Start
After configuring your webhook, verify it end-to-end in three steps:
1. Send a test event
curl -X POST "https://aitts.theproductivepixel.com/api/user/enterprise/webhook/test" \
-H "Authorization: Bearer <FIREBASE_ID_TOKEN>"Returns 200 with a test_id if your receiver accepted the delivery. Your receiver will see X-TTS-Event: test and a signed payload. If the receiver returns non-2xx or times out, this returns 422.
2. Trigger a real job
curl -X POST "https://aitts.theproductivepixel.com/api/v1/tts" \
-H "Authorization: Bearer tts_<API_KEY>" \
-H "Content-Type: application/json" \
-d '{
"text": "Webhook smoke test.",
"voice_id": "kokoro:en-US-Kokoro-Bella",
"model_type": "premium"
}'3. Confirm delivery
Poll /api/v1/tts/<job_id> until completed, then check your receiver for a job.completed event with headers X-TTS-Signature, X-TTS-Event, and X-TTS-Delivery-ID. Clients can follow audio_endpoint with their API key instead of depending on temporary storage URLs.
Webhook Payload
When a job completes or fails, we POST to your endpoint. Streaming requests use the same job.completed and job.failed events once the job finishes.
audio_url field is a temporary signed URL kept for backward compatibility. Use audio_endpoint as the durable retrieval path — it remains valid for the lifetime of the audio (based on storage tier). Short-lived audio may expire after 24 hours.{
"event": "job.completed",
"created_at": "2026-01-07T12:00:00.000Z",
"data": {
"job_id": "550e8400-e29b-41d4-a716-446655440000",
"status": "completed",
"audio_endpoint": "/api/v1/tts/550e8400-e29b-41d4-a716-446655440000/audio",
"audio_url": "https://storage.example.com/audio/...?signature=...",
"chars_charged": 150
}
}{
"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"
}
}
}Signature Verification
Every webhook includes a X-TTS-Signature header. Always verify it to prevent spoofing.
Format: t=timestamp,v1=signature
import crypto from 'crypto';
function verifyWebhook(payload, signature, secret) {
const parts = Object.fromEntries(
signature.split(',').map(p => p.split('='))
);
const timestamp = parts.t;
const expectedSig = parts.v1;
// Reject if timestamp is older than 5 minutes (replay protection)
const now = Math.floor(Date.now() / 1000);
if (Math.abs(now - parseInt(timestamp)) > 300) return false;
const signedPayload = `${timestamp}.${payload}`;
const computed = crypto
.createHmac('sha256', secret)
.update(signedPayload)
.digest('hex');
return crypto.timingSafeEqual(
Buffer.from(computed),
Buffer.from(expectedSig)
);
}import hmac, hashlib, time
def verify_webhook(payload: bytes, signature: str, secret: str) -> bool:
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
# Reject if timestamp is older than 5 minutes
if abs(int(time.time()) - int(timestamp)) > 300:
return False
signed_payload = f"{timestamp}.{payload.decode()}"
computed = hmac.new(
secret.encode(),
signed_payload.encode(),
hashlib.sha256
).hexdigest()
return hmac.compare_digest(computed, expected_sig)Retry 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 delivery is dropped. After 8 consecutive exhausted deliveries, your webhook endpoint is auto-disabled and you will receive an email notification. To re-enable, fix your endpoint and save the webhook URL again in the Enterprise Dashboard — this resets the failure counters and re-activates delivery.
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.
© 2026 AI TTS Microservice. All rights reserved.