> ## Documentation Index > Fetch the complete documentation index at: https://docs.extend.ai/llms.txt > Use this file to discover all available pages before exploring further. > > ## API version > The current API version is `2026-02-09`, served at the site root (no version prefix in URLs). > If this page URL contains `/2025-04-21/` or `/2024-12-23/`, you are reading an older API version. > Prefer the current docs at https://docs.extend.ai/llms.txt unless the user explicitly needs that older version. > Do not treat older-version pages as the source of truth for new integrations. # Webhook Best Practices > Operational best practices for handling Extend webhooks ## Responding to webhooks ### Quick acknowledgment Respond with a 2xx status quickly, preferably within a few seconds. Extend times out after **30 seconds**. > **Note** > > The Python, TypeScript, and Java SDKs ship a `verifyAndParse` / `verify_and_parse` helper. The Go SDK does not, so the Go examples call a `verifyWebhook` function you implement from the [manual verification steps](/webhooks/configuration#manual-verification). **`Python SDK`** ```python Python SDK from extend_ai import Extend from extend_ai.wrapper.errors import WebhookSignatureVerificationError client = Extend(token="YOUR_API_KEY") @app.post('/webhook') async def handle_webhook(request): try: event = client.webhooks.verify_and_parse( body=request.body.decode(), headers=dict(request.headers), signing_secret="wss_your_signing_secret" ) # Queue the event for async processing, then respond immediately await message_queue.send({ "type": "webhook-event", "payload": event }) return {"status": "ok"} except WebhookSignatureVerificationError: return {"error": "Invalid signature"}, 401 ``` **`TypeScript SDK`** ```typescript TypeScript SDK import { ExtendClient } from "extend-ai"; const client = new ExtendClient({ token: "YOUR_API_KEY" }); app.post('/webhook', async (req, res) => { try { const event = client.webhooks.verifyAndParse( req.body.toString(), req.headers, "wss_your_signing_secret" ); // Queue the event for async processing, then respond immediately await messageQueue.send({ type: 'webhook-event', payload: event }); res.status(200).send('OK'); } catch (err) { if (err.name === "WebhookSignatureVerificationError") { return res.status(401).send('Invalid signature'); } res.status(500).send('Internal server error'); } }); ``` **`Java SDK`** ```java Java SDK import ai.extend.ExtendClient; import ai.extend.types.WebhookEvent; import ai.extend.wrapper.errors.WebhookSignatureVerificationError; ExtendClient client = ExtendClient.builder().apiKey("YOUR_API_KEY").build(); // Inside your webhook handler (requestBody: raw body string, requestHeaders: Map) try { WebhookEvent event = client.webhooks().verifyAndParse(requestBody, requestHeaders, "wss_your_signing_secret"); // Queue the event for async processing, then respond immediately messageQueue.send("webhook-event", event); return ResponseEntity.ok("OK"); } catch (WebhookSignatureVerificationError e) { return ResponseEntity.status(401).body("Invalid signature"); } ``` **`Go`** ```go Go // The Go SDK has no webhook helper; verifyWebhook implements the manual // verification steps from the configuration guide. func handleWebhook(w http.ResponseWriter, r *http.Request) { body, err := io.ReadAll(r.Body) if err != nil { http.Error(w, "Bad request", http.StatusBadRequest) return } if err := verifyWebhook(r.Header, body, "wss_your_signing_secret"); err != nil { http.Error(w, "Invalid signature", http.StatusUnauthorized) return } // Queue the raw event for async processing, then respond immediately if err := messageQueue.Send(r.Context(), "webhook-event", body); err != nil { http.Error(w, "Internal server error", http.StatusInternalServerError) return } w.WriteHeader(http.StatusOK) } ``` ### Asynchronous processing Queue tasks that are slow, depend on external services, or may need retries. **`Python SDK`** ```python Python SDK from extend_ai import Extend from extend_ai.wrapper.errors import WebhookSignatureVerificationError client = Extend(token="YOUR_API_KEY") # ❌ Bad: Synchronous work can cause timeouts and duplicates @app.post('/webhook') async def handle_webhook_bad(request): event = client.webhooks.verify_and_parse(body, headers, secret) await send_to_multiple_apis(event["payload"]) await generate_pdf_report(event["payload"]) await enrich_data_from_third_party(event["payload"]) await send_email_notifications(event["payload"]) return {"status": "ok"} # ✅ Good: Enqueue then respond @app.post('/webhook') async def handle_webhook_good(request): try: event = client.webhooks.verify_and_parse( body=request.body.decode(), headers=dict(request.headers), signing_secret="wss_your_signing_secret" ) await job_queue.add("process-webhook", event) return {"status": "ok"} except WebhookSignatureVerificationError: return {"error": "Invalid signature"}, 401 ``` **`TypeScript SDK`** ```typescript TypeScript SDK import { ExtendClient } from "extend-ai"; const client = new ExtendClient({ token: "YOUR_API_KEY" }); // ❌ Bad: Synchronous work can cause timeouts and duplicates app.post('/webhook', async (req, res) => { const event = client.webhooks.verifyAndParse(req.body.toString(), req.headers, secret); await sendToMultipleAPIs(event.payload); await generatePDFReport(event.payload); await enrichDataFromThirdParty(event.payload); await sendEmailNotifications(event.payload); res.status(200).send('OK'); }); // ✅ Good: Enqueue then respond app.post('/webhook', async (req, res) => { try { const event = client.webhooks.verifyAndParse( req.body.toString(), req.headers, "wss_your_signing_secret" ); await jobQueue.add('process-webhook', event); res.status(200).send('OK'); } catch (err) { if (err.name === "WebhookSignatureVerificationError") { return res.status(401).send('Invalid signature'); } res.status(500).send('Internal server error'); } }); ``` **`Java SDK`** ```java Java SDK // ❌ Bad: Synchronous work can cause timeouts and duplicates WebhookEvent event = client.webhooks().verifyAndParse(requestBody, requestHeaders, secret); sendToMultipleApis(event); generatePdfReport(event); enrichDataFromThirdParty(event); sendEmailNotifications(event); return ResponseEntity.ok("OK"); // ✅ Good: Enqueue then respond try { WebhookEvent event = client.webhooks().verifyAndParse(requestBody, requestHeaders, "wss_your_signing_secret"); jobQueue.add("process-webhook", event); return ResponseEntity.ok("OK"); } catch (WebhookSignatureVerificationError e) { return ResponseEntity.status(401).body("Invalid signature"); } ``` **`Go`** ```go Go // ❌ Bad: Synchronous work can cause timeouts and duplicates func handleWebhookBad(w http.ResponseWriter, r *http.Request) { body, _ := io.ReadAll(r.Body) if err := verifyWebhook(r.Header, body, secret); err != nil { http.Error(w, "Invalid signature", http.StatusUnauthorized) return } sendToMultipleAPIs(body) generatePDFReport(body) enrichDataFromThirdParty(body) sendEmailNotifications(body) w.WriteHeader(http.StatusOK) } // ✅ Good: Enqueue then respond func handleWebhookGood(w http.ResponseWriter, r *http.Request) { body, _ := io.ReadAll(r.Body) if err := verifyWebhook(r.Header, body, "wss_your_signing_secret"); err != nil { http.Error(w, "Invalid signature", http.StatusUnauthorized) return } if err := jobQueue.Add(r.Context(), "process-webhook", body); err != nil { http.Error(w, "Internal server error", http.StatusInternalServerError) return } w.WriteHeader(http.StatusOK) } ``` ## Handling duplicates and retries ### Idempotency with event IDs Extend tries to minimize duplicate requests, but occasionally they are unavoidable. If your side effects are not idempotent, you can use the `eventId` (e.g., `event_abc123`) to avoid processing the same event multiple times. **`Python SDK`** ```python Python SDK async def process_webhook(event): event_id = event["eventId"] # Check if already processed if await redis.get(f"processed:{event_id}"): return # Process the event await handle_event(event) # Mark as processed (expire after 7 days) await redis.setex(f"processed:{event_id}", 86400 * 7, "true") ``` **`TypeScript SDK`** ```typescript TypeScript SDK async function processWebhook(event) { const eventId = event.eventId; // Check if already processed if (await redis.get(`processed:${eventId}`)) return; // Process the event await handleEvent(event); // Mark as processed (expire after 7 days) await redis.setex(`processed:${eventId}`, 86400 * 7, 'true'); } ``` **`Java SDK`** ```java Java SDK import com.fasterxml.jackson.databind.ObjectMapper; private static final ObjectMapper MAPPER = new ObjectMapper(); // WebhookEvent is a typed union; read eventId from the raw body you already verified void processWebhook(String requestBody, WebhookEvent event) throws Exception { String eventId = MAPPER.readTree(requestBody).get("eventId").asText(); // Check if already processed if (redis.get("processed:" + eventId) != null) { return; } // Process the event handleEvent(event); // Mark as processed (expire after 7 days) redis.setex("processed:" + eventId, 86400 * 7, "true"); } ``` **`Go`** ```go Go func processWebhook(ctx context.Context, body []byte) error { var envelope struct { EventID string `json:"eventId"` } if err := json.Unmarshal(body, &envelope); err != nil { return err } key := "processed:" + envelope.EventID // Check if already processed if exists, _ := redis.Exists(ctx, key).Result(); exists > 0 { return nil } // Process the event if err := handleEvent(ctx, body); err != nil { return err } // Mark as processed (expire after 7 days) return redis.Set(ctx, key, "true", 7*24*time.Hour).Err() } ``` ## Error handling and reliability ### Retry strategy Extend retries failed or timed-out (30 s) requests with exponential backoff: **`Python SDK`** ```python Python SDK from extend_ai import Extend from extend_ai.wrapper.errors import WebhookSignatureVerificationError client = Extend(token="YOUR_API_KEY") @app.post('/webhook') async def handle_webhook(request): try: event = client.webhooks.verify_and_parse( body=request.body.decode(), headers=dict(request.headers), signing_secret="wss_your_signing_secret" ) queued = await message_queue.send(event) if not queued.success: # Return 503 to trigger a retry from Extend return {"error": "Service temporarily unavailable"}, 503 return {"status": "ok"} except WebhookSignatureVerificationError: return {"error": "Invalid signature"}, 401 ``` **`TypeScript SDK`** ```typescript TypeScript SDK import { ExtendClient } from "extend-ai"; const client = new ExtendClient({ token: "YOUR_API_KEY" }); app.post('/webhook', async (req, res) => { try { const event = client.webhooks.verifyAndParse( req.body.toString(), req.headers, "wss_your_signing_secret" ); const queued = await messageQueue.send(event); if (!queued.success) { // Return 503 to trigger a retry from Extend return res.status(503).send('Service temporarily unavailable'); } res.status(200).send('OK'); } catch (err) { if (err.name === "WebhookSignatureVerificationError") { return res.status(401).send('Invalid signature'); } res.status(500).send('Internal server error'); } }); ``` **`Java SDK`** ```java Java SDK try { WebhookEvent event = client.webhooks().verifyAndParse(requestBody, requestHeaders, "wss_your_signing_secret"); if (!messageQueue.send(event).isSuccess()) { // Return 503 to trigger a retry from Extend return ResponseEntity.status(503).body("Service temporarily unavailable"); } return ResponseEntity.ok("OK"); } catch (WebhookSignatureVerificationError e) { return ResponseEntity.status(401).body("Invalid signature"); } ``` **`Go`** ```go Go func handleWebhook(w http.ResponseWriter, r *http.Request) { body, _ := io.ReadAll(r.Body) if err := verifyWebhook(r.Header, body, "wss_your_signing_secret"); err != nil { http.Error(w, "Invalid signature", http.StatusUnauthorized) return } if err := messageQueue.Send(r.Context(), body); err != nil { // Return 503 to trigger a retry from Extend http.Error(w, "Service temporarily unavailable", http.StatusServiceUnavailable) return } w.WriteHeader(http.StatusOK) } ``` ## Security considerations ### Always verify signatures Always verify the webhook signature using the SDK's `verifyAndParse()` or `verify_and_parse()` method. This ensures: * The request actually came from Extend * The payload hasn't been tampered with * The request is recent (protects against replay attacks) See the [signature verification guide](/webhooks/configuration#verifying-webhook-requests) for more details. > Ship your document processing with Extend. Get started with our APIs, SDKs, and guides.