Skip to navigation

Webhook Best Practices

Responding to webhooks

Quick acknowledgment

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

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.

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

Asynchronous processing

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

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

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.

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")

Error handling and reliability

Retry strategy

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

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

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.