Best Practices

Responding to webhooks

Quick acknowledgment

Respond with a 2xx status quickly, preferably within a few seconds. Extend times out after 30 seconds.

1import { ExtendClient } from "extend-ai";
2
3const client = new ExtendClient({ token: "YOUR_API_KEY" });
4
5app.post('/webhook', async (req, res) => {
6 try {
7 const event = client.webhooks.verifyAndParse(
8 req.body.toString(),
9 req.headers,
10 "wss_your_signing_secret"
11 );
12
13 // Queue the event for async processing, then respond immediately
14 await messageQueue.send({
15 type: 'webhook-event',
16 payload: event
17 });
18
19 res.status(200).send('OK');
20 } catch (err) {
21 if (err.name === "WebhookSignatureVerificationError") {
22 return res.status(401).send('Invalid signature');
23 }
24 res.status(500).send('Internal server error');
25 }
26});

Asynchronous processing

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

1import { ExtendClient } from "extend-ai";
2
3const client = new ExtendClient({ token: "YOUR_API_KEY" });
4
5// ❌ Bad: Synchronous work can cause timeouts and duplicates
6app.post('/webhook', async (req, res) => {
7 const event = client.webhooks.verifyAndParse(req.body.toString(), req.headers, secret);
8
9 await sendToMultipleAPIs(event.payload);
10 await generatePDFReport(event.payload);
11 await enrichDataFromThirdParty(event.payload);
12 await sendEmailNotifications(event.payload);
13
14 res.status(200).send('OK');
15});
16
17// ✅ Good: Enqueue then respond
18app.post('/webhook', async (req, res) => {
19 try {
20 const event = client.webhooks.verifyAndParse(
21 req.body.toString(),
22 req.headers,
23 "wss_your_signing_secret"
24 );
25
26 await jobQueue.add('process-webhook', event);
27 res.status(200).send('OK');
28 } catch (err) {
29 if (err.name === "WebhookSignatureVerificationError") {
30 return res.status(401).send('Invalid signature');
31 }
32 res.status(500).send('Internal server error');
33 }
34});

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.

1async function processWebhook(event) {
2 const eventId = event.eventId;
3
4 // Check if already processed
5 if (await redis.get(`processed:${eventId}`)) return;
6
7 // Process the event
8 await handleEvent(event);
9
10 // Mark as processed (expire after 7 days)
11 await redis.setex(`processed:${eventId}`, 86400 * 7, 'true');
12}

Error handling and reliability

Retry strategy

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

1import { ExtendClient } from "extend-ai";
2
3const client = new ExtendClient({ token: "YOUR_API_KEY" });
4
5app.post('/webhook', async (req, res) => {
6 try {
7 const event = client.webhooks.verifyAndParse(
8 req.body.toString(),
9 req.headers,
10 "wss_your_signing_secret"
11 );
12
13 const queued = await messageQueue.send(event);
14 if (!queued.success) {
15 // Return 503 to trigger a retry from Extend
16 return res.status(503).send('Service temporarily unavailable');
17 }
18
19 res.status(200).send('OK');
20 } catch (err) {
21 if (err.name === "WebhookSignatureVerificationError") {
22 return res.status(401).send('Invalid signature');
23 }
24 res.status(500).send('Internal server error');
25 }
26});

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 for more details.