Never Miss a Webhook Again in 5 Minutes
Your app is deploying. Stripe sends a webhook. It hits your server while it’s restarting. Returns 503. Stripe retries a few times. Eventually gives up.
You missed a payment confirmation. Customer gets an error. Support ticket arrives.
You need webhook buffering. Always accept webhooks, even when your app is down. Replay them when you’re ready.
The 5-Minute Version
Catch creates a buffer (bucket) with a public URL. Point your webhook provider at it. Webhooks are stored until you replay them.
Step 1: Create a bucket
curl -X POST https://api.solenoid.systems/v1/catch/buckets \
-H "Authorization: Bearer sm_your_api_key" \
-H "Content-Type: application/json" \
-d '{
"target_url": "https://your-app.com/webhooks",
"provider": "stripe",
"signing_secret": "whsec_your_stripe_signing_secret"
}'
Response:
{
"bucket_id": "brave-elephant-42",
"ingestion_url": "https://api.solenoid.systems/v1/catch/brave-elephant-42",
"target_url": "https://your-app.com/webhooks",
"provider": "stripe",
"state": "paused",
"created_at": "2026-02-05T12:00:00Z"
}
The ingestion_url is your public webhook receiver. Give it to Stripe.
Step 2: Configure Stripe to send webhooks
In your Stripe dashboard:
Endpoint URL: https://api.solenoid.systems/v1/catch/brave-elephant-42
Stripe now sends all webhooks to Catch instead of directly to your app.
Step 3: View buffered webhooks
curl https://api.solenoid.systems/v1/catch/brave-elephant-42/events \
-H "Authorization: Bearer sm_your_api_key"
{
"events": [
{
"id": "01HX1234567890ABCDEF",
"provider": "stripe",
"event_type": "payment_intent.succeeded",
"received_at": "2026-02-05T12:05:00Z",
"status": "pending",
"verification": "valid"
}
]
}
Step 4: Go live and auto-forward
curl -X PATCH https://api.solenoid.systems/v1/catch/brave-elephant-42 \
-H "Authorization: Bearer sm_your_api_key" \
-d '{"state": "live"}'
Now Catch automatically forwards new webhooks to your target_url. You can still manually replay missed events.
Handling the Edge Cases
Signature verification: Catch verifies webhook signatures automatically.
Stripe webhooks have a stripe-signature header. Catch extracts it, verifies with your signing_secret, and stores the result.
Invalid signatures are stored but marked verification: "invalid". You can filter them out or investigate.
Manual replay: Deploy broke webhook handling? Replay events after fixing:
curl -X POST https://api.solenoid.systems/v1/catch/brave-elephant-42/events/01HX1234567890ABCDEF/replay \
-H "Authorization: Bearer sm_your_api_key"
Catch POSTs the webhook to your target_url with original payload and headers, plus Catch signature headers.
Bulk replay: Missed webhooks during a 2-hour outage? Replay them all:
curl https://api.solenoid.systems/v1/catch/brave-elephant-42/events?status=pending&limit=100 \
-H "Authorization: Bearer sm_your_api_key"
Get all pending events. Replay each one. Or write a script:
async function replayAll(bucketId: string) {
let cursor = null
while (true) {
const url = cursor
? `https://api.solenoid.systems/v1/catch/${bucketId}/events?status=pending&cursor=${cursor}`
: `https://api.solenoid.systems/v1/catch/${bucketId}/events?status=pending`
const response = await fetch(url, {
headers: { 'Authorization': `Bearer ${process.env.SOLENOID_API_KEY}` }
})
const { events, cursor: nextCursor } = await response.json()
for (const event of events) {
await fetch(
`https://api.solenoid.systems/v1/catch/${bucketId}/events/${event.id}/replay`,
{
method: 'POST',
headers: { 'Authorization': `Bearer ${process.env.SOLENOID_API_KEY}` }
}
)
console.log(`Replayed ${event.id}`)
}
if (!nextCursor) break
cursor = nextCursor
}
}
Pause during deploys: Deploying a breaking change? Pause the bucket first:
# Before deploy
curl -X PATCH https://api.solenoid.systems/v1/catch/brave-elephant-42 \
-H "Authorization: Bearer sm_your_api_key" \
-d '{"state": "paused"}'
# Deploy your changes
# After deploy
curl -X PATCH https://api.solenoid.systems/v1/catch/brave-elephant-42 \
-H "Authorization: Bearer sm_your_api_key" \
-d '{"state": "live"}'
Webhooks buffer while paused. Resume when ready.
Real-World Patterns
Zero-downtime deploys:
#!/bin/bash
# deploy.sh
BUCKET_ID="brave-elephant-42"
# Pause webhook forwarding
curl -X PATCH "https://api.solenoid.systems/v1/catch/${BUCKET_ID}" \
-H "Authorization: Bearer $SOLENOID_API_KEY" \
-d '{"state": "paused"}'
echo "Webhooks paused. Deploying..."
# Deploy your app
docker pull myapp:latest
docker stop myapp
docker rm myapp
docker run -d --name myapp myapp:latest
# Wait for health check
while ! curl -f http://localhost:8080/health; do
sleep 1
done
echo "App is healthy. Resuming webhooks..."
# Resume webhook forwarding
curl -X PATCH "https://api.solenoid.systems/v1/catch/${BUCKET_ID}" \
-H "Authorization: Bearer $SOLENOID_API_KEY" \
-d '{"state": "live"}'
echo "Deploy complete"
Development/staging environments:
// Create separate buckets per environment
async function setupWebhookBuffering(env: 'dev' | 'staging' | 'prod') {
const targetUrl = {
dev: 'http://localhost:3000/webhooks',
staging: 'https://staging.myapp.com/webhooks',
prod: 'https://myapp.com/webhooks'
}[env]
const response = await fetch('https://api.solenoid.systems/v1/catch/buckets', {
method: 'POST',
headers: {
'Authorization': `Bearer ${process.env.SOLENOID_API_KEY}`,
'Content-Type': 'application/json'
},
body: JSON.stringify({
target_url: targetUrl,
provider: 'stripe'
})
})
const bucket = await response.json()
console.log(`${env}: ${bucket.ingestion_url}`)
return bucket.bucket_id
}
Configure each Stripe environment with its corresponding ingestion URL.
Webhook debugging:
app.get('/admin/webhooks/:bucket_id', async (req, res) => {
const { bucket_id } = req.params
const response = await fetch(
`https://api.solenoid.systems/v1/catch/${bucket_id}/events?limit=50`,
{
headers: { 'Authorization': `Bearer ${process.env.SOLENOID_API_KEY}` }
}
)
const { events } = await response.json()
res.render('webhook-inspector', {
events: events.map(e => ({
id: e.id,
type: e.event_type,
received: new Date(e.received_at).toLocaleString(),
status: e.status,
verified: e.verification === 'valid' ? '✅' : '❌'
}))
})
})
View webhook history. Inspect payloads. Replay specific events.
Provider Support
Catch auto-detects signatures from major providers:
| Provider | Signature Header | Auto-Detected |
|---|---|---|
| Stripe | stripe-signature | ✅ |
| GitHub | x-hub-signature-256 | ✅ |
| Shopify | x-shopify-hmac-sha256 | ✅ |
| Svix | svix-signature | ✅ |
For custom webhooks, Catch stores the payload but verification shows none.
Retention and Limits
Events are retained based on your tier:
| Tier | Retention | Max Events per Bucket |
|---|---|---|
| Free | 24 hours | 100 |
| Starter | 3 days | 1,000 |
| Pro | 30 days | 10,000 |
| Scale | 90 days | 100,000 |
Old events are automatically deleted when limits are reached.
Why Not Build This Yourself?
You could build webhook buffering. Store incoming webhooks in a database. Retry failed deliveries. Clean up old records.
But you need signature verification for multiple providers. You need idempotency handling so replays don’t duplicate work. You need rate limiting so webhook floods don’t crash your database.
When your webhook buffer is down, you’re back to missing webhooks.
Catch runs on Cloudflare Workers and Durable Objects. Always-on ingestion. Provider-specific signature verification. Replay on demand.
You build webhook handlers. Catch ensures they receive every event.
Try It Now
Free tier: 10,000 calls to prototype. Pro tier ($49.99/mo) and above: unlimited Catch usage.
# Create a bucket
curl -X POST https://api.solenoid.systems/v1/catch/buckets \
-H "Authorization: Bearer sm_your_api_key" \
-d '{
"target_url": "https://webhook.site/your-unique-id",
"provider": "stripe"
}'
# Get the ingestion URL from response
# Send a test webhook to it (via curl or webhook provider test feature)
# View buffered events
curl https://api.solenoid.systems/v1/catch/{bucket_id}/events \
-H "Authorization: Bearer sm_your_api_key"
Point any webhook provider at Catch. Watch events buffer. Replay them when ready.
Your webhook reliability layer, in two API calls.