Webhook best practices
Respond immediately, process asynchronously
Your endpoint must return HTTP 200 within the request timeout. If processing takes longer than the timeout, PAI considers the delivery failed and retries.
Pattern: acknowledge receipt immediately, then hand off to a background job or queue.
app.post('/webhooks', (req, res) => {
res.sendStatus(200); // acknowledge immediately
processEventAsync(req.body); // hand off to queue
});
Never do database writes, external API calls, or heavy computation in the synchronous response path.
Always deduplicate
PAI guarantees at-least-once delivery — the same event may arrive more than once. Your handler must be idempotent.
Pattern: store processed deduplicationId values and skip events you have already handled.
async function handleWebhook(payload) {
const already = await db.webhookEvents.findOne({ id: payload.deduplicationId });
if (already) return;
await db.webhookEvents.insert({ id: payload.deduplicationId });
await processEvent(payload);
}
Do not assume ordering
PAI does not guarantee that events arrive in the order they were generated. A subscription-renewed may arrive before the transaction-processed that triggered it.
Pattern: if your logic depends on current state, fetch it from the API instead of reconstructing it from event arrival order.
// Don't reconstruct state from event deltas
// Do fetch current state when you need it
const subscription = await pai.subscriptions.get(subscriptionId);
Verify the signature
Every delivery includes a signature header. Always verify it before processing and reject requests with a missing or invalid signature with HTTP 401.
See Webhooks overview — Signature verification for code examples in Node.js, Python, PHP, and Ruby.
Return exactly 200
PAI treats any non-200 response as a failure and retries. Do not return 201, 204, or any redirect. If your framework sends 204 for empty bodies by default, explicitly set the status to 200.