> ## 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**.

```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 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');
  }
});
```

### Asynchronous processing

Queue tasks that are slow, depend on external services, or may need retries.

```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 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');
  }
});
```

## 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 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 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');
}
```

## Error handling and reliability

### Retry strategy

Extend retries failed or timed-out (30 s) requests with exponential backoff:

```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 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');
  }
});
```

## 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.