Webhook Processing
Handle incoming webhooks reliably with background jobs.
Why Use Background Jobs for Webhooks?
Webhook endpoints should respond quickly. Processing in the background:
- ✅ Returns 200 immediately (prevents retries from the sender)
- ✅ Handles failures gracefully with retries
- ✅ Scales processing independently
Basic Pattern
jobs/process-webhook.ts
import { pidgey } from '@/lib/pidgey';
export const processWebhook = pidgey.defineJob({
name: 'process-webhook',
handler: async (data: { event: string; payload: unknown }) => {
switch (data.event) {
case 'payment.succeeded':
await handlePaymentSuccess(data.payload);
break;
case 'subscription.cancelled':
await handleCancellation(data.payload);
break;
default:
console.log('Unknown event:', data.event);
}
},
config: {
retries: 5, // Retry processing failures
},
});app/api/webhooks/stripe/route.ts
import { processWebhook } from '@/jobs/process-webhook';
export async function POST(request: Request) {
const payload = await request.json();
const event = payload.type;
// Enqueue and respond immediately
await processWebhook.enqueue({ event, payload });
return new Response('OK', { status: 200 });
}Stripe Example
jobs/stripe-webhook.ts
export const handleStripeWebhook = pidgey.defineJob({
name: 'stripe-webhook',
handler: async (data: { event: Stripe.Event }) => {
const { event } = data;
switch (event.type) {
case 'checkout.session.completed': {
const session = event.data.object as Stripe.Checkout.Session;
await db.orders.update(session.metadata.orderId, {
paymentStatus: 'paid',
});
break;
}
case 'customer.subscription.deleted': {
const subscription = event.data.object as Stripe.Subscription;
await db.subscriptions.cancel(subscription.id);
break;
}
}
},
config: {
retries: 3,
timeout: 30000,
},
});Idempotency
⚠️
Webhooks may be delivered multiple times. Make your handlers idempotent.
handler: async (data: { eventId: string; payload: unknown }) => {
// Check if already processed
const existing = await db.processedEvents.findUnique({
where: { id: data.eventId },
});
if (existing) {
console.log('Already processed:', data.eventId);
return;
}
// Process the event
await processEvent(data.payload);
// Mark as processed
await db.processedEvents.create({
data: { id: data.eventId },
});
};Multiple Webhook Sources
Use different queues for different sources:
export const githubWebhook = pidgey.defineJob({
name: 'github-webhook',
handler: async (data) => {
/* ... */
},
config: { queue: 'github' },
});
export const stripeWebhook = pidgey.defineJob({
name: 'stripe-webhook',
handler: async (data) => {
/* ... */
},
config: { queue: 'stripe' },
});