# Webhooks Receive real-time notifications when transcription jobs complete or fail. Instead of polling for job status, you can register webhook URLs to receive HTTP POST notifications when events occur. This is the recommended approach for production applications using async transcription. ## Event Types - **job.completed** — A transcription job finished successfully - **job.failed** — A transcription job failed with an error - **job.progress** — Periodic progress updates while a job is processing - **\*** — Subscribe to all events ## Registering a Webhook Create a webhook by specifying the URL and the events you want to receive: ```curl curl "https://asr.lesan.ai/v1/webhooks" \ -H "Authorization: Bearer YOUR_API_KEY" \ -H "Content-Type: application/json" \ -d '{ "url": "https://your-app.com/webhooks/lesan", "events": ["job.completed", "job.failed"], "secret": "whsec_abc123...", "description": "Production transcription notifications" }' # Response: # { # "id": "wh_abc123", # "object": "webhook", # "url": "https://your-app.com/webhooks/lesan", # "events": ["job.completed", "job.failed"], # "status": "active", # "created_at": "2024-03-15T10:30:00Z" # } ``` ```python import requests response = requests.post( "https://asr.lesan.ai/v1/webhooks", headers={ "Authorization": "Bearer YOUR_API_KEY", "Content-Type": "application/json" }, json={ "url": "https://your-app.com/webhooks/lesan", "events": ["job.completed", "job.failed"], "secret": "whsec_abc123...", "description": "Production transcription notifications" } ) webhook = response.json() print(f"Webhook ID: {webhook['id']}") ``` ```javascript const response = await fetch("https://asr.lesan.ai/v1/webhooks", { method: "POST", headers: { "Authorization": "Bearer YOUR_API_KEY", "Content-Type": "application/json" }, body: JSON.stringify({ url: "https://your-app.com/webhooks/lesan", events: ["job.completed", "job.failed"], secret: "whsec_abc123...", description: "Production transcription notifications" }) }); const webhook = await response.json(); console.log(`Webhook ID: ${webhook.id}`); ``` If you include a `secret` when creating the webhook, store it securely. You can use it to verify webhook deliveries (recommended for production). ## Payload Format ### job.completed ```json { "event": "job.completed", "webhook_id": "wh_abc123", "timestamp": "2024-03-15T10:35:00Z", "data": { "id": "job_xyz789", "status": "completed", "url": "/v1/transcriptions/job_xyz789" } } ``` ### job.failed ```json { "event": "job.failed", "webhook_id": "wh_abc123", "timestamp": "2024-03-15T10:35:00Z", "data": { "id": "job_xyz789", "status": "failed", "error": { "type": "invalid_request_error", "code": "invalid_audio_format", "message": "The audio file could not be decoded." }, "url": "/v1/transcriptions/job_xyz789" } } ``` For the full transcription result (text, segments, timing, etc.), fetch the job using the ID: ```bash curl -X GET "https://asr.lesan.ai/v1/transcriptions/job_xyz789" \ -H "Authorization: Bearer YOUR_API_KEY" ``` ## Verifying Webhook Signatures If you set a webhook `secret`, you can verify webhook deliveries using an HMAC-SHA256 signature of the raw request body. This helps ensure the request was not tampered with. ```python import hmac import hashlib from flask import Flask, request, jsonify app = Flask(__name__) WEBHOOK_SECRET = "whsec_abc123..." # From webhook creation @app.route("/webhooks/lesan", methods=["POST"]) def handle_webhook(): # Verify the signature signature = request.headers.get("X-Webhook-Signature") # Example header name if signature: expected = hmac.new( WEBHOOK_SECRET.encode(), request.data, hashlib.sha256 ).hexdigest() if not hmac.compare_digest(signature, expected): return jsonify({"error": "Invalid signature"}), 401 # Process the event payload = request.json event = payload["event"] data = payload["data"] if event == "job.completed": print(f"Job {data['id']} completed") # Recommended: fetch the final job payload from /v1/transcriptions/{id} and persist it elif event == "job.failed": print(f"Job {data['id']} failed: {data['error']['message']}") # Handle the failure (retry, notify, etc.) return jsonify({"received": True}), 200 ``` ```javascript import crypto from "crypto"; import express from "express"; const app = express(); const WEBHOOK_SECRET = "whsec_abc123..."; // From webhook creation app.post("/webhooks/lesan", express.raw({ type: "application/json" }), (req, res) => { // Verify the signature const signature = req.headers["x-webhook-signature"]; if (signature) { const expected = crypto .createHmac("sha256", WEBHOOK_SECRET) .update(req.body) .digest("hex"); if (!crypto.timingSafeEqual(Buffer.from(signature), Buffer.from(expected))) { return res.status(401).json({ error: "Invalid signature" }); } } // Process the event const payload = JSON.parse(req.body); const { event, data } = payload; if (event === "job.completed") { console.log(`Job ${data.id} completed`); // Recommended: fetch the final job payload from /v1/transcriptions/{id} and persist it } else if (event === "job.failed") { console.log(`Job ${data.id} failed: ${data.error.message}`); // Handle the failure (retry, notify, etc.) } res.json({ received: true }); }); ``` ## Managing Webhooks ### List Webhooks ```bash curl "https://asr.lesan.ai/v1/webhooks" \ -H "Authorization: Bearer YOUR_API_KEY" ``` ### Update a Webhook ```bash curl -X PATCH "https://asr.lesan.ai/v1/webhooks/wh_abc123" \ -H "Authorization: Bearer YOUR_API_KEY" \ -H "Content-Type: application/json" \ -d '{ "url": "https://your-app.com/webhooks/lesan-v2", "events": ["job.completed"] }' ``` ### Test a Webhook Send a test event to verify your endpoint is working: ```bash curl -X POST "https://asr.lesan.ai/v1/webhooks/wh_abc123/test" \ -H "Authorization: Bearer YOUR_API_KEY" ``` ### Delete a Webhook ```bash curl -X DELETE "https://asr.lesan.ai/v1/webhooks/wh_abc123" \ -H "Authorization: Bearer YOUR_API_KEY" ``` ## Delivery Tracking Lesan AI tracks webhook delivery attempts. If your endpoint is unreachable or returns a non-2xx status code, we retry with exponential backoff: - **Retry schedule** — 1 minute, 5 minutes, 30 minutes, 2 hours, 24 hours - **Max attempts** — 5 retries per event - **Timeout** — 10 seconds per delivery attempt - **Auto-disable** — After 100 consecutive failures, the webhook is automatically disabled ## Best Practices - **Always verify signatures** — Never process unverified webhook payloads - **Respond quickly** — Return a 200 status within 10 seconds. Process data asynchronously if needed - **Handle duplicates** — Use the job `id` to deduplicate events in case of retries - **Use HTTPS** — Webhook URLs must use HTTPS for security - **Store the secret securely** — Treat the webhook secret like an API key - **Monitor delivery** — Check the webhook delivery logs for failures See the [ASR guide](/guides/asr) for async transcription setup, or the [Error Codes](/guides/error-codes) reference for understanding error payloads.