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.
Related guides:
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)
# 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
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.
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:
- 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 |
Dashboard Tip
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
- Jobs & queues — Deep dive into job processing
- Building workers — Production-ready worker patterns
- Retry strategies — Handle failures gracefully