Skip to main content

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.

Node.js (Express)
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.

Node.js
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.