webhooks
Skill26 starsChanged 6 months agoMITView on GitHub
Use when the user is receiving webhooks (Stripe, GitHub, Slack, Clerk, Polar, Resend, Linear, Polar, custom), building a webhook endpoint, or signing webhooks for someone else. Catches missing signature verification, missing idempotency, sync handlers that time out, and replay vulnerabilities.
- Reads credentials
15 other projects use a version of this.
Adopted by
Most recently taken up by
- zedarvates/frugal-skillstheir own version, first seen Nov 8, 2026
- Leostruka/devin-bundletheir own version, first seen Nov 1, 2026
- oidsdev/honeypottheir own version, first seen Oct 15, 2026
- editframe/skillstheir own version, first seen Oct 13, 2026
- G1Joshi/Agent-Skillstheir own version, first seen Oct 13, 2026
- ayrshare/ayrshare-social-media-api-claude-plugintheir own version, first seen Oct 13, 2026
Used with
Projects with it also use data-pipelines and incident-response.
Other files people use alongside this one.
Files found in the same public projects as a version of this one, more often than they turn up anywhere else. Each project counts once, large collections count for less, and this file's own repository is left out.
YepAPISkilldata-pipelines
ETL patterns, cron scheduling, queue-based processing, and batch jobs with Inngest/BullMQ.
Also in 3 projects that use this
AnthropicSkillincident-response
Run an incident response workflow — triage, communicate, and write postmortem. Trigger with "we have an incident", "production is down", an alert that needs severity assessment, a status update mid-incident, or when writing a blameless postmortem after resolution.
Also in 3 projects that use this
Other versions15 projects use a version of this.
Word for word · 0
None found.
Edited · 0
None found.
Wrote their own version · 15
- nirholas/XActions★ 648same name
- kid-sid/claude-spellbook★ 190same name
- joelhooks/joelclaw★ 65same name
- nahid-sparktales/agent-dispatcher★ 59same name
- ayrshare/ayrshare-social-media-api-claude-plugin★ 19same name
- editframe/skills★ 19same name
- G1Joshi/Agent-Skills★ 15same name
- khadinakbarlabs/shopify-app-builder★ 2same name
- zedarvates/frugal-skills★ 1same name
- Abdox-menara/guard-skills★ 0same name
5 more not listed
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 repository19 other files it gives its agents.
- api-route-hardeningSkill
- auth-and-sessionsSkill
- authorizationSkill
- caching-and-invalidationSkill
- database-query-safetySkill
- data-privacySkill
- error-handlingSkill
- file-uploadsSkill
- input-validationSkill
- jobs-and-queuesSkill
- llm-endpoint-safetySkill
- migrations-and-schemaSkill
- observabilitySkill
- production-readinessSkill
- reliability-patternsSkill
- rsc-boundariesSkill
- secrets-and-envSkill
- testing-strategySkill
- transactions-and-consistencySkill
The file
We split this skill into the rules it gives an agent
5 rules. 0 are common, 4 are distinctive, and 1 is very specific to this project.
0
Common
Most of the 12 edited versions we read keep these.
4
Distinctive
This author's own choices. Worth a look.
1
Very specific
Tied to their project's names, files or tools. Probably skip.
- Common
- Distinctive
- Very specific
- Very specific: line 24
Acknowledge fast (
200quickly), process async: Don't do heavy work inside the handler. - Distinctive: line 27
Every receiver, every time.
- Distinctive: line 152
If you're the sender:
- Distinctive: line 157
Stop after N attempts; record in a DLQ.
- Distinctive: line 159
Document the IPs you send from (so customers can allowlist) — or use a fixed proxy
“Common” is measured: kept by at least 6 in 10 of the 12 edited versions of this file we could read. “Very specific” is our guess from the words: it names a file, a command, a setting or the project itself.
Files
Webhooks
A webhook URL is a public endpoint that strangers will POST to. The sender is whoever can guess the URL — until you verify the signature.
When to use
- Building a webhook receiver:
POST /api/webhooks/stripe,/api/webhooks/github, etc. - Adding a new third-party integration that calls back
- Reviewing a webhook handler before launch
- Sending webhooks out (your service notifying customer servers)
When to skip
- Pure outbound API calls (you call them, they don't call you back asynchronously)
The five rules every webhook receiver must follow
- Verify the signature. Reject if absent or wrong.
- Check the timestamp. Reject if too old (replay protection).
- Idempotent by event ID. Same event delivered twice → no double effects.
- Acknowledge fast (
200quickly), process async. Don't do heavy work inside the handler. - Don't echo the payload back. It's not your data; don't log secrets it carries.
Every receiver, every time. No exceptions.
The patterns
1. Signature verification — use the provider's library, not your own.
Each provider has subtle rules about what's signed (raw body? headers? URL?), what hash, what header name. Roll-your-own gets it wrong.
// Stripe (Next.js Route Handler)
import Stripe from 'stripe'
const stripe = new Stripe(process.env.STRIPE_SECRET_KEY!)
export async function POST(req: Request) {
const sig = req.headers.get('stripe-signature')
if (!sig) return new Response('No sig', { status: 400 })
const rawBody = await req.text() // RAW body, not parsed JSON
let event: Stripe.Event
try {
event = stripe.webhooks.constructEvent(rawBody, sig, process.env.STRIPE_WEBHOOK_SECRET!)
} catch {
return new Response('Bad sig', { status: 400 })
}
// ...
}The critical detail: raw bytes. If you await req.json() first and re-serialize, the signature won't match — JSON serialization isn't byte-stable. Always work from req.text() for the verification.
2. Reject too-old events — replay protection.
Even with a valid signature, an attacker who captured a webhook delivery (e.g. via misconfigured logging) can replay it. Most providers include a timestamp; reject if outside a window.
const now = Math.floor(Date.now() / 1000)
if (Math.abs(now - event.created) > 5 * 60) { // 5-minute tolerance
return new Response('Stale event', { status: 400 })
}3. Idempotent by event ID. Persist the dedup record.
Webhooks are at-least-once. The same event will arrive twice. Sometimes hours apart.
const seen = await db.processedWebhook.findUnique({ where: { id: event.id } })
if (seen) return new Response(null, { status: 200 })
await db.$transaction(async (tx) => {
await processEvent(tx, event)
await tx.processedWebhook.create({ data: { id: event.id, source: 'stripe', at: new Date() } })
})
return new Response(null, { status: 200 })Dedup record + business write in the same transaction, otherwise a crash mid-transaction causes either double-processing or "we said we processed it but didn't."
4. Acknowledge fast. Process async.
Many providers retry if you don't respond in <5–10 seconds. If your handler does heavy work synchronously, retries pile up, you process the same event multiple times concurrently, and inboxes fill with "did this ship?" emails.
export async function POST(req: Request) {
const event = verify(req) // signature + timestamp + parse
// Enqueue a background job; return immediately
await queue.send({ kind: 'stripe.event', payload: event })
return new Response(null, { status: 200 })
}The worker does the heavy lifting, with full retry semantics (see jobs-and-queues). The handler is small and fast.
5. Don't trust the payload's customer_id. Look it up.
Even with a valid signature, never trust IDs that should map to your data without checking. Look them up; reject events for unknown / wrong-tenant IDs.
const sub = await db.subscription.findUnique({ where: { stripeCustomerId: event.data.object.customer } })
if (!sub) return new Response('Unknown customer', { status: 200 }) // 200 so Stripe doesn't retry6. 2xx tells the sender "stop retrying." Use it deliberately.
| Situation | Status |
| Processed (or already-processed dedup hit) | 200 |
| Bad signature | 400 |
| Body malformed | 400 |
| Unknown event type you ignore on purpose | 200 (or 202), with a log |
| Transient internal failure (DB down) | 5xx — let the sender retry |
| Permanent internal failure (data inconsistent, terminal) | 200 + alert; do not loop forever |
Returning 5xx for "we don't care about this event type" causes the sender to retry forever, eventually filling their internal DLQ and getting your endpoint disabled.
7. Order is not guaranteed.
Webhooks can arrive out of order: subscription.deleted before subscription.updated. Don't assume the latest delivered = latest reality. Either:
- Re-fetch from the provider's API to get current state
- Store a
version/seqon the event and ignore older versions
For Stripe specifically, the docs explicitly say to fetch state from their API rather than rely on event order.
8. Webhook secret rotation. Plan for it.
Most providers let you rotate the signing secret with overlap (old + new both valid for a window). Use it: deploy new secret env var, both verify, rotate via provider, remove old. Test the rotation in staging first.
9. Per-source endpoints. Don't multiplex.
POST /api/webhooks/stripe
POST /api/webhooks/github
POST /api/webhooks/clerkNot /api/webhooks that branches on a body field. Per-source URLs let each have its own secret, signature scheme, and handler — without scary if-else logic.
10. Edge runtime needs a body-stable read.
Some edge runtimes parse request.json() lazily; reading body twice fails. Always: const raw = await req.text(), verify on raw, parse with JSON.parse(raw) if you need the object after.
Sending webhooks (you → customer)
If you're the sender:
- Sign with HMAC-SHA256 of the body + a timestamp; expose the secret in their dashboard
- Include a unique
event_idandcreated_atin every payload - Retry on
5xxand timeouts with exponential backoff - Stop after N attempts; record in a DLQ; surface in the customer's dashboard
- Set a tight timeout (5–10s); customer endpoints that hang shouldn't pin your workers
- Document the IPs you send from (so customers can allowlist) — or use a fixed proxy
War story
A vibe-coded SaaS took Stripe webhooks and credited account balances in the handler. They forgot to verify the signature ("Stripe wouldn't fake their own webhooks, right?"). A bad actor noticed the URL in their HAR file, crafted a fake payment_intent.succeeded payload, and credited their own account $50,000. The fix was 6 lines (Stripe's constructEvent). The cost was 2 days of account-by-account reconciliation and a public-facing apology.
Quick checklist
- Signature verified using the provider's library on the raw body
- Timestamp / event-age check rejects replays
- Idempotency dedup by event ID, persisted alongside the business write
- Heavy work moved to a queue; handler is small and fast
- Resource IDs in the payload are looked up + scoped (not trusted)
- Status codes used deliberately (200 for "don't retry", 5xx only for transient)
- Out-of-order delivery handled (re-fetch or version check)
- Webhook secrets are server-only env vars; rotation tested
- One endpoint per source; no multiplexing
- Body read once via
req.text(); verified, then parsed
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.

