Webhooks
Receive real-time notifications when conversions complete or fail. Webhooks eliminate the need for polling and enable event-driven workflows.
Overview
LogTalk supports two types of webhooks. The same delivery mechanism and the same event names are used for both changelog conversions and document conversions — the source_type field in the payload (changelog vs document) tells you which conversion produced the event.
- Per-request webhooks — Pass a
webhook_urlin your conversion request. The callback is sent only for that specific conversion. Ideal for CI/CD pipelines. - Persistent webhooks — Configure webhooks in your dashboard to receive notifications for all conversions. Useful for Slack notifications, internal dashboards, and monitoring.
Per-Request Webhooks
Include a webhook_url in your conversion request to receive a callback when that specific conversion completes:
curl -X POST https://logtalk.io/api/v1/conversions \-H "Authorization: Bearer lt_live_your_key" \-H "Content-Type: application/json" \-d '{"source_type": "changelog","content": "## v2.0.0\n- New feature","webhook_url": "https://your-app.com/webhooks/logtalk"}'
Note: Webhook URLs must use HTTPS. HTTP URLs will be rejected.
The same webhook_url parameter works for document conversions — pass "source_type": "document" instead of "changelog", and you'll receive the same conversion.completed / conversion.failed events, with the document payload shape shown below.
Event Types
| Event | Description |
|---|---|
conversion.completed | A conversion finished successfully with audio (or video) ready |
conversion.failed | A conversion failed with error details |
Both events are emitted for changelog and document conversions. Branch on data.source_type to tell them apart.
| quota.warning | Approaching quota limit (80%) |
| quota.exceeded | Monthly quota exceeded |
Webhook Payload
Webhooks are sent as POST requests with a JSON body. Every payload shares the same envelope — id, type, created_at, api_version, and data — but the shape of data depends on which conversion produced it.
Changelog conversions (conversion.*)
For conversions created with "source_type": "changelog", the payload carries the changelog shape below.
conversion.completed
{"id": "evt_abc123def456","type": "conversion.completed","created_at": "2026-01-18T14:32:15.000Z","api_version": "2026-01-18","data": {"id": "550e8400-e29b-41d4-a716-446655440000","status": "completed","source_type": "changelog","mode": "changelog","product_name": "Acme Widget","version": "2.0.0","tone": "professional","duration": "short","output_format": "audio","source": "api","audio_url": "https://logtalk.io/api/v1/conversions/550e8400-e29b-41d4-a716-446655440000/audio","video_url": null,"audio_duration_seconds": 180,"script_text": "Here's what's new in Acme Widget 2.0.0...","listen_url": "https://logtalk.io/listen/550e8400-e29b-41d4-a716-446655440000","embed_url": "https://logtalk.io/embed/550e8400-e29b-41d4-a716-446655440000","created_at": "2026-01-18T14:30:00.000Z","completed_at": "2026-01-18T14:32:15.000Z"}}
listen_url and embed_url are null unless the conversion is public. video_url is null for audio-only conversions.
conversion.failed
{"id": "evt_abc123def456","type": "conversion.failed","created_at": "2026-01-18T14:31:00.000Z","api_version": "2026-01-18","data": {"id": "550e8400-e29b-41d4-a716-446655440000","status": "failed","source_type": "changelog","mode": "changelog","product_name": "Acme Widget","version": "2.0.0","tone": "professional","duration": "short","output_format": "audio","source": "api","created_at": "2026-01-18T14:30:00.000Z","failed_at": "2026-01-18T14:31:00.000Z","error": {"code": "GENERATION_FAILED","message": "Audio generation failed"}}}
Document conversions
Conversions created with "source_type": "document" emit the same conversion.completed / conversion.failed events, but the data shape differs — it has no mode or version field, and instead carries source_type, verbosity, and speakers.
conversion.completed (document)
{"id": "evt_def789ghi012","type": "conversion.completed","created_at": "2026-01-18T14:32:15.000Z","api_version": "2026-01-18","data": {"id": "7c9e6679-7425-40de-944b-e07fc1f90ae7","status": "completed","source_type": "document","content_description": "Q3 product roadmap.pdf","tone": "casual","verbosity": "normal","speakers": 2,"source": "api","audio_url": "https://logtalk.io/api/v1/conversions/7c9e6679-7425-40de-944b-e07fc1f90ae7/audio","audio_duration_seconds": 240,"script_text": "Speaker 1: Let's dig into the Q3 roadmap...","listen_url": "https://logtalk.io/listen/7c9e6679-7425-40de-944b-e07fc1f90ae7","embed_url": "https://logtalk.io/embed/7c9e6679-7425-40de-944b-e07fc1f90ae7","created_at": "2026-01-18T14:29:00.000Z","completed_at": "2026-01-18T14:32:15.000Z"}}
speakers is 1 or 2 depending on whether the conversion was generated as a monologue or two-speaker dialogue. source is always "api" — document conversions are API-only.
conversion.failed (document)
{"id": "evt_def789ghi012","type": "conversion.failed","created_at": "2026-01-18T14:31:00.000Z","api_version": "2026-01-18","data": {"id": "7c9e6679-7425-40de-944b-e07fc1f90ae7","status": "failed","source_type": "document","content_description": "Q3 product roadmap.pdf","tone": "casual","verbosity": "normal","speakers": 2,"source": "api","audio_url": null,"audio_duration_seconds": null,"script_text": null,"listen_url": null,"embed_url": null,"created_at": "2026-01-18T14:29:00.000Z","failed_at": "2026-01-18T14:31:00.000Z","error": {"code": "GENERATION_FAILED","message": "Could not extract readable text from the uploaded document"}}}
Webhook Headers
Each webhook request includes these headers:
| Header | Description |
|---|---|
Content-Type | Always application/json |
X-LogTalk-Event | Event type (e.g., conversion.completed) |
X-LogTalk-Delivery | Unique delivery ID for debugging |
X-LogTalk-Signature | HMAC-SHA256 signature (format: t=timestamp,v1=signature) |
X-LogTalk-Timestamp | Unix timestamp when the webhook was sent |
Signature Verification
Always verify webhook signatures to ensure requests are authentic. Signatures use HMAC-SHA256 with the format t=timestamp,v1=signature. The signed payload is ${timestamp}.${JSON.stringify(payload)}, which binds the signature to the timestamp and prevents replay attacks. Reject webhooks with timestamps older than 5 minutes.
const crypto = require("crypto");function verifyWebhookSignature(payload, signatureHeader, secret) {// Parse signature header: t=timestamp,v1=signatureconst parts = signatureHeader.split(",");const timestamp = parseInt(parts.find((p) => p.startsWith("t=")).split("=")[1],);const signature = parts.find((p) => p.startsWith("v1=")).split("=")[1];// Check timestamp (reject if > 5 minutes old)const now = Math.floor(Date.now() / 1000);if (Math.abs(now - timestamp) > 300) {throw new Error("Webhook timestamp too old");}// Compute expected signatureconst signedPayload = `${timestamp}.${JSON.stringify(payload)}`;const expectedSignature = crypto.createHmac("sha256", secret).update(signedPayload).digest("hex");// Use timing-safe comparisonreturn crypto.timingSafeEqual(Buffer.from(signature, "hex"),Buffer.from(expectedSignature, "hex"),);}// Express.js exampleapp.post("/webhooks/logtalk", express.json(), (req, res) => {const signature = req.headers["x-logtalk-signature"];const secret = process.env.LOGTALK_WEBHOOK_SECRET;if (!verifyWebhookSignature(req.body, signature, secret)) {return res.status(401).send("Invalid signature");}// Process the webhookconst { type, data } = req.body;console.log(`Received ${type}: ${data.id}`);res.status(200).send("OK");});
import hmacimport hashlibimport jsonimport timefrom flask import Flask, request, abortapp = Flask(__name__)def verify_webhook_signature(payload, signature_header, secret):"""Verify webhook signature using HMAC-SHA256."""# Parse signature headerparts = dict(p.split('=') for p in signature_header.split(','))timestamp = int(parts['t'])signature = parts['v1']# Check timestamp (reject if > 5 minutes old)now = int(time.time())if abs(now - timestamp) > 300:raise ValueError('Webhook timestamp too old')# Compute expected signaturesigned_payload = f"{timestamp}.{json.dumps(payload, separators=(',', ':'))}"expected_signature = hmac.new(secret.encode('utf-8'),signed_payload.encode('utf-8'),hashlib.sha256).hexdigest()# Use constant-time comparisonreturn hmac.compare_digest(signature, expected_signature)@app.route('/webhooks/logtalk', methods=['POST'])def handle_webhook():signature = request.headers.get('X-LogTalk-Signature')secret = os.environ['LOGTALK_WEBHOOK_SECRET']if not verify_webhook_signature(request.json, signature, secret):abort(401, 'Invalid signature')# Process the webhookevent_type = request.json['type']data = request.json['data']print(f"Received {event_type}: {data['id']}")return 'OK', 200
Retry Policy
If your endpoint returns a non-2xx status code or times out, delivery is retried with exponential backoff:
| Attempt | Delay |
|---|---|
| 1 | Immediate |
| 2 | 1 minute |
| 3 | 5 minutes |
| 4 | 30 minutes |
| 5 | 2 hours |
| 6 | 8 hours |
After 6 failed attempts, the webhook is marked as failing and you'll be notified via email. Webhook timeout is 30 seconds.
Best Practices
- Respond quickly — Return a 2xx response within 30 seconds. Process webhooks asynchronously if needed: acknowledge receipt first, then handle the event.
- Handle duplicates — Webhooks may be delivered more than once. Use the event
idto deduplicate events and ensure idempotent processing. - Verify signatures — Always verify the
X-LogTalk-Signatureheader to ensure webhooks are authentic and haven't been tampered with. - Use queue processing — For high-volume integrations, queue webhook events for processing rather than handling them synchronously in the HTTP handler.