webhook
Skill0 starsChanged 3 months agoMITView on GitHub
Build secure, reliable webhook systems for sending and receiving HTTP callbacks with signature verification, retry logic, and idempotency
- Reads credentials
- Installs packages
2 other projects use a version of this.
Adopted by
Most recently taken up by
- Pradnyeshp/Claude-Skillstheir own version, first seen Jul 6, 2026
- nickalie/nclawtheir own version, first seen Feb 25, 2026
Other versions2 projects use a version of this.
Word for word · 0
None found.
Edited · 0
None found.
Wrote their own version · 2
- nickalie/nclaw★ 15same name
- Pradnyeshp/Claude-Skills★ 2same name
How matches are found. Word for word: the exact same text. Edited: shares passages with this file (“99% same” means 99% of its five-word phrases also appear here; 10% or more counts). Their own version: similar meaning, or, weakest, the same name. We can’t tell which project had it first. Projects are counted among the 200 most-starred candidates, and mentions are links to the repository we found, up to 50 per site, so both counts are a floor.
In this repository27 other files it gives its agents.
- architecture-designerSkill
- architecture-patternsSkill
- code-reviewSkill
- dockerSkill
- fastapiSkill
- feature-specificationSkill
- github-issue-resolverSkill
- github-opsSkill
- kubernetes-devopsSkill
- monitoringSkill
- nextjsSkill
- nginxSkill
- prd-to-ddd-designSkill
- prd-writerSkill
- prismaSkill
- react-expertSkill
- redisSkill
- redis-storeSkill
- requirements-analysisSkill
- shadcn-uiSkill
- ssh-essentialsSkill
- task-decomposerSkill
- task-development-workflowSkill
- trpc-best-practicesSkill
- typescript-proSkill
- typescriptSkill
- zodSkill
The file
We split this skill into the rules it gives an agent
7 rules. About 0 are common, 7 are distinctive, and 0 are very specific to this project.
≈0
Common · estimate
Many files like this have a rule of this kind.
≈7
Distinctive · estimate
This author's own choices. Worth a look.
0
Very specific
Tied to their project's names, files or tools. Probably skip.
- Common
- Distinctive
- Very specific
- Distinctive: line 25
Use this skill when:
- Distinctive: line 34
Don't use: when you need:
- Distinctive: line 73
Karpathy Principle: Simplicity First: - Start by receiving webhooks correctly (verify signature, respond fast, process async).
- Distinctive: line 195
Karpathy Principle: Goal-Driven Execution: - After implementing signature verification, test it: send a webhook with wrong signature.
- Distinctive: line 197
Karpathy Principle: Security By Design: - Always verify signatures.
- Distinctive: line 199
Karpathy Principle: When NOT to Use: - Don't use webhooks for real-time chat (use WebSockets).
- Distinctive: line 542
Before deploying webhook systems:
≈ “Common” is an estimate: at least 1 in 5 of the 350,495 skill files we read have a rule of the same kind, not the same words. “Very specific” is our guess from the words: it names a file, a command, a setting or the project itself.
Files
Webhook - Event-Driven HTTP Callbacks
When to Use
Use this skill when:
- Notifying external systems of events (order created, payment received)
- Receiving events from third-party services (GitHub, Stripe, Shopify)
- Building event-driven architectures between microservices
- Real-time integrations without polling
- Triggering workflows based on external events
- Decoupling systems via asynchronous communication
Don't use when you need:
- Bidirectional communication (use WebSockets)
- Guaranteed ordering (webhooks can arrive out of order)
- Synchronous responses (use regular API calls)
- High-frequency events (>100/sec, consider message queues)
Karpathy Principle: Think Before Coding - Webhooks are unreliable by nature (network failures, retries, duplicates). Design for idempotency from day one. A non-idempotent webhook handler will cause duplicate charges, double-sends, and data corruption.
Karpathy Principle: Trade-offs Everywhere - Synchronous webhook handlers (return 200 immediately, process later) are reliable but complex (need queue). Async handlers (process then 200) are simple but fail on timeout. Choose based on processing time (<5s = sync, >5s = async).
Prerequisites
Required Knowledge
- HTTP basics (methods, headers, status codes)
- HMAC signature verification
- Retry strategies and exponential backoff
- Idempotency patterns
- Async processing (queues, background jobs)
Required Tools
# Node.js
npm install express crypto body-parser
# Python
pip install fastapi uvicorn cryptography
# Testing webhooks locally
npm install -g ngrok
ngrok http 3000 # Exposes localhost to internetSecurity Checklist
- HTTPS only (never HTTP)
- Signature verification on all incoming webhooks
- Timestamp validation (reject old requests)
- Idempotency handling (deduplicate events)
- Rate limiting per sender
Karpathy Principle: Simplicity First - Start by receiving webhooks correctly (verify signature, respond fast, process async). Only then add sending webhooks. Mixing both concerns early creates complexity.
Karpathy Principle: Fail Fast, Fail Loud - Webhook retries compound failures. If your endpoint is down for 1 hour, providers queue 100+ events. Log EVERY webhook reception (ID, timestamp, signature status) for debugging.
Core Workflows
1. Receiving Webhooks (Node.js)
Express Webhook Receiver:
import express from 'express'
import crypto from 'crypto'
const app = express()
// IMPORTANT: Use raw body for signature verification
app.use('/webhooks', express.raw({ type: 'application/json' }))
function verifySignature(payload, signature, secret) {
const expectedSignature = crypto
.createHmac('sha256', secret)
.update(payload)
.digest('hex')
// Timing-safe comparison
return crypto.timingSafeEqual(
Buffer.from(signature),
Buffer.from(expectedSignature)
)
}
app.post('/webhooks/github', async (req, res) => {
const signature = req.headers['x-hub-signature-256']?.replace('sha256=', '')
const timestamp = req.headers['x-github-event-timestamp']
const rawBody = req.body.toString('utf8')
// 1. Verify signature
if (!signature || !verifySignature(rawBody, signature, process.env.GITHUB_SECRET)) {
return res.status(401).json({ error: 'Invalid signature' })
}
// 2. Check timestamp (reject if >5 minutes old)
const eventTime = new Date(timestamp).getTime()
if (Date.now() - eventTime > 5 * 60 * 1000) {
return res.status(401).json({ error: 'Event too old' })
}
// 3. Parse payload
const event = JSON.parse(rawBody)
const eventId = req.headers['x-github-delivery']
// 4. Check idempotency (already processed?)
const processed = await redis.get(`webhook:${eventId}`)
if (processed) {
return res.status(200).json({ received: true, duplicate: true })
}
// 5. Respond immediately (before processing)
res.status(202).json({ received: true })
// 6. Process asynchronously
await queue.add('github-webhook', {
eventId,
type: req.headers['x-github-event'],
payload: event,
})
// 7. Mark as processed (24h TTL)
await redis.setex(`webhook:${eventId}`, 86400, 'true')
})
app.listen(3000)FastAPI Webhook Receiver:
from fastapi import FastAPI, Request, HTTPException
from datetime import datetime, timedelta
import hmac
import hashlib
app = FastAPI()
def verify_signature(payload: bytes, signature: str, secret: str) -> bool:
expected = hmac.new(
secret.encode(),
payload,
hashlib.sha256
).hexdigest()
return hmac.compare_digest(signature, expected)
@app.post("/webhooks/stripe")
async def stripe_webhook(request: Request):
# 1. Get raw body
payload = await request.body()
signature = request.headers.get("stripe-signature")
# 2. Verify signature
if not signature or not verify_signature(payload, signature, STRIPE_SECRET):
raise HTTPException(status_code=401, detail="Invalid signature")
# 3. Parse event
import json
event = json.loads(payload)
event_id = event['id']
# 4. Check idempotency
if await redis.exists(f"webhook:{event_id}"):
return {"received": True, "duplicate": True}
# 5. Respond immediately
response = {"received": True}
# 6. Queue for processing
await queue.enqueue("stripe-webhook", event)
# 7. Mark processed
await redis.setex(f"webhook:{event_id}", 86400, "true")
return responseKarpathy Principle: Goal-Driven Execution - After implementing signature verification, test it: send a webhook with wrong signature. It should return 401, not 200 or 500. Verify before building the rest.
Karpathy Principle: Security By Design - Always verify signatures. GitHub/Stripe provide HMAC. Without verification, anyone can POST fake events to your endpoint. One missed check = data corruption.
Karpathy Principle: When NOT to Use - Don't use webhooks for real-time chat (use WebSockets). Don't use them for large payloads (use presigned URLs). Webhooks are for notifications, not data transfer.
2. Sending Webhooks (Node.js)
Webhook Sender with Retry:
import axios from 'axios'
import crypto from 'crypto'
class WebhookSender {
constructor(url, secret) {
this.url = url
this.secret = secret
}
generateSignature(payload, timestamp) {
const data = `${timestamp}.${payload}`
return crypto.createHmac('sha256', this.secret).update(data).digest('hex')
}
async send(event, maxRetries = 5) {
const payload = JSON.stringify(event)
const timestamp = Math.floor(Date.now() / 1000)
const signature = this.generateSignature(payload, timestamp)
let attempt = 0
let delay = 60 // 1 minute
while (attempt < maxRetries) {
try {
const response = await axios.post(this.url, payload, {
headers: {
'Content-Type': 'application/json',
'X-Webhook-Signature': `t=${timestamp},v1=${signature}`,
'X-Webhook-Event': event.type,
},
timeout: 10000, // 10 second timeout
maxRedirects: 0,
})
if (response.status >= 200 && response.status < 300) {
await this.logDelivery(event.id, 'success', attempt + 1)
return { success: true, attempt: attempt + 1 }
}
throw new Error(`Unexpected status: ${response.status}`)
} catch (error) {
attempt++
// Don't retry on 4xx (client errors)
if (error.response?.status >= 400 && error.response?.status < 500) {
await this.logDelivery(event.id, 'failed', attempt, error.message)
return { success: false, attempt, error: 'Client error' }
}
// Retry on 5xx or network errors
if (attempt < maxRetries) {
await this.logDelivery(event.id, 'retry', attempt, error.message)
await this.sleep(delay * 1000)
delay *= 2 // Exponential backoff
} else {
await this.logDelivery(event.id, 'failed', attempt, error.message)
return { success: false, attempt, error: error.message }
}
}
}
}
async logDelivery(eventId, status, attempt, error = null) {
await db.webhookLogs.create({
eventId,
url: this.url,
status,
attempt,
error,
timestamp: new Date(),
})
}
sleep(ms) {
return new Promise(resolve => setTimeout(resolve, ms))
}
}
// Usage
const sender = new WebhookSender('https://example.com/webhooks', 'secret')
await sender.send({
id: 'evt_123',
type: 'order.created',
data: { orderId: '123', amount: 99.99 },
timestamp: new Date().toISOString(),
})3. Signature Verification (Stripe-Style)
Format: t=timestamp,v1=signature
Sender:
function generateStripeSignature(payload, secret) {
const timestamp = Math.floor(Date.now() / 1000)
const signedPayload = `${timestamp}.${payload}`
const signature = crypto.createHmac('sha256', secret).update(signedPayload).digest('hex')
return `t=${timestamp},v1=${signature}`
}Receiver:
function verifyStripeSignature(payload, headerSignature, secret, tolerance = 300) {
const parts = headerSignature.split(',').reduce((acc, part) => {
const [key, value] = part.split('=')
acc[key] = value
return acc
}, {})
const timestamp = parseInt(parts.t)
const signature = parts.v1
// Check timestamp (5 minute tolerance)
if (Math.abs(Date.now() / 1000 - timestamp) > tolerance) {
throw new Error('Timestamp too old or too new')
}
// Verify signature
const signedPayload = `${timestamp}.${payload}`
const expected = crypto.createHmac('sha256', secret).update(signedPayload).digest('hex')
if (!crypto.timingSafeEqual(Buffer.from(signature), Buffer.from(expected))) {
throw new Error('Invalid signature')
}
return true
}Common Patterns
1. Event Schema
interface WebhookEvent {
id: string // Unique event ID (for idempotency)
type: string // Event type (e.g., "order.created")
timestamp: string // ISO 8601 timestamp
api_version: string // API version (for compatibility)
data: {
object: any // Full resource object
}
previous_attributes?: any // For update events
}2. Retry Queue (with BullMQ)
import { Queue, Worker } from 'bullmq'
const webhookQueue = new Queue('webhooks', {
connection: redis,
defaultJobOptions: {
attempts: 5,
backoff: {
type: 'exponential',
delay: 60000, // 1 minute
},
},
})
// Add webhook to queue
await webhookQueue.add('send', {
url: 'https://example.com/webhooks',
event: { type: 'order.created', data: {...} },
secret: 'webhook_secret',
})
// Worker to process webhooks
const worker = new Worker('webhooks', async (job) => {
const { url, event, secret } = job.data
const sender = new WebhookSender(url, secret)
await sender.send(event)
}, { connection: redis })3. Webhook Management API
// Create webhook subscription
app.post('/api/webhooks', async (req, res) => {
const { url, events } = req.body
// Validate URL
if (!url.startsWith('https://')) {
return res.status(400).json({ error: 'HTTPS required' })
}
// Generate secret
const secret = crypto.randomBytes(32).toString('hex')
const webhook = await db.webhooks.create({
userId: req.user.id,
url,
events,
secret,
active: true,
})
res.json({ id: webhook.id, secret })
})
// Test webhook
app.post('/api/webhooks/:id/test', async (req, res) => {
const webhook = await db.webhooks.findById(req.params.id)
const sender = new WebhookSender(webhook.url, webhook.secret)
const result = await sender.send({
id: 'test_' + Date.now(),
type: 'test',
data: { test: true },
})
res.json(result)
})Common Pitfalls
1. Processing Before Responding
❌ Bad (slow response causes retries):
app.post('/webhook', async (req, res) => {
await processEvent(req.body) // Takes 30 seconds!
res.json({ received: true }) // Sender already timed out and retried
})✅ Good (respond immediately):
app.post('/webhook', async (req, res) => {
res.json({ received: true }) // Respond in <1 second
await queue.add('process', req.body) // Process async
})2. No Idempotency
❌ Bad (duplicate processing):
app.post('/webhook', async (req, res) => {
await db.orders.create(req.body.order) // Duplicate if retried!
res.json({ received: true })
})✅ Good (deduplicate):
app.post('/webhook', async (req, res) => {
const eventId = req.body.id
if (await redis.get(`webhook:${eventId}`)) {
return res.json({ received: true, duplicate: true })
}
await queue.add('process', req.body)
await redis.setex(`webhook:${eventId}`, 86400, 'true')
res.json({ received: true })
})3. No Signature Verification
❌ Bad (accepts forged webhooks):
app.post('/webhook', async (req, res) => {
await processPayment(req.body) // Anyone can trigger this!
res.json({ received: true })
})✅ Good (verify signature):
app.post('/webhook', async (req, res) => {
if (!verifySignature(req.body, req.headers['signature'], SECRET)) {
return res.status(401).json({ error: 'Invalid signature' })
}
await queue.add('process', req.body)
res.json({ received: true })
})4. Infinite Retries
❌ Bad (retries forever):
while (true) {
try {
await sendWebhook(url, event)
break
} catch (error) {
await sleep(60000) // Retry forever!
}
}✅ Good (max retries):
for (let attempt = 0; attempt < 5; attempt++) {
try {
await sendWebhook(url, event)
return { success: true }
} catch (error) {
if (attempt === 4) return { success: false }
await sleep(Math.pow(2, attempt) * 60000) // Exponential backoff
}
}5. Trusting Event Data Blindly
❌ Bad (race condition):
app.post('/webhook/stripe', async (req, res) => {
const { customer_id, amount } = req.body
await grantPremium(customer_id) // Don't trust webhook data!
})✅ Good (verify with API):
app.post('/webhook/stripe', async (req, res) => {
const { payment_intent_id } = req.body
// Fetch fresh data from Stripe API
const payment = await stripe.paymentIntents.retrieve(payment_intent_id)
if (payment.status === 'succeeded') {
await grantPremium(payment.metadata.user_id)
}
})Verification Checklist
Before deploying webhook systems:
Receiving
- Signature verification on all webhooks
- Timestamp validation (reject old events)
- Idempotency handling (deduplicate by event ID)
- Fast response (<1 second)
- Async processing (queue or background job)
- Error logging for failed signature checks
Sending
- HTTPS URLs only
- Signature included in headers
- Retry logic (exponential backoff)
- Max retry limit (5-10 attempts)
- Timeout configuration (5-10 seconds)
- Delivery logs (status, attempts, errors)
Security
- Secrets rotated periodically
- HTTPS certificate validation
- Rate limiting per endpoint
- No secrets in payloads
- IP allowlist (optional)
Integration with Other Skills
With Express / FastAPI
- Webhook receiver endpoints
- Signature middleware
- Error handling
With Redis
- Idempotency tracking
- Rate limiting
- Retry queue
With PostgreSQL
- Delivery logs
- Webhook subscriptions
- Event history
With Docker
- Webhook workers in containers
- Queue-based architecture
- Horizontal scaling
References
Guides
Tools
- ngrok - Expose localhost for webhook testing
- webhook.site - Inspect webhook payloads
- svix - Managed webhook service
Meta: Skill Quality
Completeness: ✅ Comprehensive (9/9 sections)
Karpathy Principles: ✅ 5 mentions
Code Examples: ✅ 20+ working examples
Production Tested: ✅ Used in production webhook systems
Last Updated: 2026-04-13
Coverage:
- ✅ Receiving webhooks (signature verification, idempotency)
- ✅ Sending webhooks (retry logic, exponential backoff)
- ✅ Security patterns (HMAC, timestamps, rate limiting)
- ✅ Event schema design
- ✅ Queue-based processing
- ✅ Delivery logging and monitoring
- ✅ Common pitfalls and solutions
Skill Level: Intermediate
Time to Learn: 3-4 hours
Prerequisites Met: HTTP, async programming
Known Gaps: None - production-ready
Does it work?
Reviews from people and from AI agents that ran it: what they used it for, and whether it worked.
No reviews yet
People and AI agents can review whether this works in practice. Say what you used it for, and whether it worked.
- It worked
- Partly
- It didn't
Using an agent? It can post a review for you after it has run the file: the MCP tool registry_write, action report. How to connect one.
Your trail
Files you open will collect here, so you can find them again.
Open two or more, and you can remix the rules you like from each into one list. Kept in this browser only.
Now do this for your own.
Bring your CLAUDE.md, AGENTS.md or skills. agentleFS shows which of your rules are common, which are yours and which only fit one project, and keeps one version your agents read everywhere.

