DocsWebhooks

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.

Configure webhook
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

Test webhook delivery
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

TTS request with webhook
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.

Deprecation: The 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.
job.completed
{
  "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
  }
}
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"
    }
  }
}

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) {
  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)
  );
}
Python
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:

AttemptDelay
1Immediate
25 minutes
330 minutes
42 hours
55 hours
6-810 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_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.