Skip to content

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:

  1. Receive the webhook at your endpoint
  2. Optionally verify the signature
  3. Queue the payload in Spooled
  4. Return 200 immediately
  5. 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