Webhooks
Spooled helps you process webhooks reliably. Queue incoming webhooks, process them with workers, and let Spooled handle retries when things fail.
How It Works
Instead of processing webhooks synchronously in your endpoint, you queue them in Spooled. This provides reliability, retries, and observability for all your webhook processing.
%%{init: {'theme': 'base', 'themeVariables': { 'primaryColor': '#ecfdf5', 'primaryTextColor': '#065f46', 'primaryBorderColor': '#10b981', 'lineColor': '#6b7280'}}}%%
flowchart LR
STRIPE[Stripe] -->|webhook| SP[Spooled]
GITHUB[GitHub] -->|webhook| SP
CUSTOM[Your Service] -->|webhook| SP
SP -->|queue| Q[(Queue)]
Q -->|process| W[Your Workers] Benefits
- Fast webhook responses — Return 200 immediately, process async
- Automatic retries — Failed processing retries with backoff
- Deduplication — Idempotency prevents duplicate processing
- Observability — Monitor all webhook processing in real-time
Note: Code examples use SDK syntax (under development). For production today, use the equivalent REST API endpoints.
Stripe Webhooks
Stripe sends webhooks for payment events like successful charges, failed payments, and subscription changes. Queue them in Spooled for reliable processing.
Webhook Endpoint
// Express.js webhook handler
app.post('/webhooks/stripe', async (req, res) => {
const sig = req.headers['stripe-signature'];
try {
// Verify and parse the webhook
const event = stripe.webhooks.constructEvent(
req.body,
sig,
process.env.STRIPE_WEBHOOK_SECRET
);
// Queue in Spooled for reliable processing
await spooled.jobs.enqueue({
queue: 'stripe-events',
payload: event,
idempotencyKey: event.id,
});
// Return 200 immediately - Spooled handles retries
res.status(200).send('Queued');
} catch (error) {
res.status(400).send(`Webhook Error: ${error.message}`);
}
}); Worker to Process Events
// Worker to process Stripe events
const worker = new SpooledWorker({
apiKey: process.env.SPOOLED_API_KEY,
queue: 'stripe-events',
});
worker.on('job', async (job) => {
const event = job.payload;
switch (event.type) {
case 'invoice.paid':
await handleInvoicePaid(event.data.object);
break;
case 'customer.subscription.updated':
await handleSubscriptionUpdate(event.data.object);
break;
case 'payment_intent.succeeded':
await handlePaymentSuccess(event.data.object);
break;
default:
console.log(`Unhandled event type: ${event.type}`);
}
await job.complete();
});
worker.start(); Best Practice: Use Event IDs
Always use the webhook event ID (e.g., evt_1234) as the idempotency key.
This prevents duplicate processing if Stripe retries the webhook.
GitHub Webhooks
GitHub webhooks notify you about repository events like pushes, pull requests, and issues.
// GitHub webhook handler
app.post('/webhooks/github', async (req, res) => {
const event = req.headers['x-github-event'];
const delivery = req.headers['x-github-delivery'];
// Queue for processing
await spooled.jobs.enqueue({
queue: 'github-events',
payload: {
event,
payload: req.body,
},
idempotencyKey: delivery, // Use delivery ID for deduplication
});
res.status(200).send('Queued');
}); Custom HTTP Webhooks
For any HTTP webhook source, the pattern is the same:
- Receive the webhook at your endpoint
- Optionally verify the signature
- Queue the payload in Spooled
- Return 200 immediately
- Process asynchronously with a worker
Signature Verification
Always verify webhook signatures to ensure requests come from the expected source. Each service has its own signature format:
| Service | Header | Algorithm |
|---|---|---|
| Stripe | Stripe-Signature | HMAC-SHA256 |
| GitHub | X-Hub-Signature-256 | HMAC-SHA256 |
| Shopify | X-Shopify-Hmac-Sha256 | HMAC-SHA256 |
| Twilio | X-Twilio-Signature | HMAC-SHA1 |
Next Steps
- Jobs & queues — Deep dive into job processing
- Building workers — Production-ready worker patterns
- Retry strategies — Handle failures gracefully