Skip to content

Webhooks

Spooled helps you process external events reliably. Most providers (Stripe/GitHub/Shopify) send fixed webhook formats, so the usual pattern is: receive webhook → verify signature → enqueue a job → return 200.

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.

Incoming webhook authentication

Spooled’s incoming custom webhook endpoint requires X-Webhook-Token. Get it in the dashboard (Organization Settings) or via the API: GET /api/v1/organizations/webhook-token. To rotate it: POST /api/v1/organizations/webhook-token/regenerate.

%%{init: {'theme': 'base', 'themeVariables': { 'primaryColor': '#ecfdf5', 'primaryTextColor': '#065f46', 'primaryBorderColor': '#10b981', 'lineColor': '#6b7280'}}}%%
flowchart LR
  STRIPE[Stripe] -->|webhook| ADAPTER[Your webhook endpoint / adapter]
  GITHUB[GitHub] -->|webhook| ADAPTER
  SHOPIFY[Shopify] -->|webhook| ADAPTER

  ADAPTER -->|enqueue job| SP[Spooled]
  CUSTOM[Your own service] -->|Spooled JSON format| 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

SSRF Protection

When configuring outgoing webhooks (webhooks that Spooled sends to your URLs), Spooled validates URLs in production to prevent Server-Side Request Forgery (SSRF) attacks.

Production URL Requirements

What to look for:

  • Private IP ranges (10.x.x.x, 172.16-31.x.x, 192.168.x.x) are blocked
  • Loopback addresses (127.x.x.x, localhost) are blocked
  • Cloud metadata endpoints (169.254.169.254) are blocked
  • HTTPS is required in production

For local development, use HTTPS URLs or test against a self-hosted instance where SSRF protection is relaxed.

Stripe Webhooks

Stripe sends webhooks for payment events. You should receive them in your own endpoint (so you can verify the signature), then enqueue the event into Spooled as a job.

Important

Stripe webhooks are not in “Spooled job format”, so you generally do not point Stripe directly at Spooled. Use a tiny adapter endpoint that verifies Stripe-Signature and enqueues a job.

Example: Stripe adapter (verify → enqueue)

Stripe webhook handler (verify + enqueue job)
# Queue a Stripe webhook event
curl -X POST https://api.spooled.cloud/api/v1/jobs \
  -H "Authorization: Bearer sp_live_YOUR_API_KEY" \
  -H "Content-Type: application/json" \
  -d '{
    "queue_name": "stripe-events",
    "payload": {
      "id": "evt_1234",
      "type": "invoice.paid",
      "data": {"object": {...}}
    },
    "idempotency_key": "evt_1234"
  }'

Worker to Process Events

Process Stripe events
import { SpooledClient, SpooledWorker } from '@spooled/sdk';

const client = new SpooledClient({
  apiKey: process.env.SPOOLED_API_KEY!,
});

// Worker to process Stripe events
const worker = new SpooledWorker(client, {
  queueName: 'stripe-events',
});

worker.process(async (ctx) => {
  const event = ctx.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}`);
  }
  
  return { processed: true };
});

await 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. The common pattern is the same: receive the webhook, verify it, enqueue a job.

GitHub webhook handler (receive + enqueue job)
import { SpooledClient } from '@spooled/sdk';

const client = new SpooledClient({
  apiKey: process.env.SPOOLED_API_KEY!,
});

// 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 client.jobs.create({
    queueName: 'github-events',
    payload: {
      event,
      data: req.body,
    },
    idempotencyKey: delivery, // Use delivery ID for deduplication
  });
  
  res.status(200).send('Queued');
});

Custom HTTP Webhooks

For any source, the safe 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

Dashboard Tip

📍 Dashboard → Jobs

What to look for:

  • Filter by queue (e.g., stripe-events, github-events)
  • View webhook payload in job details
  • Track processing status and errors

Actions:

  • Set up alerts for DLQ entries
  • Monitor webhook processing latency

Next Steps