agentleFS
AlteriomAlteriom/ai-dev-skills/skills/webhook/SKILL.md

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

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
  1. Distinctive:

    Use this skill when:

    line 25
  2. Distinctive:

    Don't use: when you need:

    line 34
  3. Distinctive:

    Karpathy Principle: Simplicity First: - Start by receiving webhooks correctly (verify signature, respond fast, process async).

    line 73
  4. Distinctive:

    Karpathy Principle: Goal-Driven Execution: - After implementing signature verification, test it: send a webhook with wrong signature.

    line 195
  5. Distinctive:

    Karpathy Principle: Security By Design: - Always verify signatures.

    line 197
  6. Distinctive:

    Karpathy Principle: When NOT to Use: - Don't use webhooks for real-time chat (use WebSockets).

    line 199
  7. Distinctive:

    Before deploying webhook systems:

    line 542

≈ “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.

Keep what fits and drop the rest. Your version, ready for your agents.
Files
SKILL.md

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 internet

Security 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 response

Karpathy 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

Download now (ZIP)

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
What did you use it for? e.g. Reviewing pull requests in a Rails app

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.