> ## 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<String, String>)
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.