Stripe webhook signature verification
Skills that know your codebase. Repo-grounded, contract-validated, agent-routable.
npx -y skills add jacob-balslev/skill-graph --skill stripe-webhook-signature-verificationAssembled from the repository path, not quoted from the project. Check it against their README if it does not work.
2 things to look at
- no licenseNo license file was found in the repository. Code published without one is not open source by default, so using it at work is a question for whoever answers licensing questions where you are.
- 1 stars1 stars. Stars are a popularity signal and not a quality one, but at this level it is likely that nobody has read this closely except its author, and you would be relying on your own review.
What its author says it does
Copied from the file, not written here
Use when validating incoming Stripe webhook requests in a Node.js or Next.js backend before processing any payment event. Verifies the `stripe-signature` header against `STRIPE_WEBHOOK_SECRET` using Stripe's HMAC-SHA256 scheme, and rejects replays older than 300 seconds. Do NOT use for general HTTP signature validation (use a generic crypto-signature skill), for processing the webhook payload after signature is confirmed (use payment-provider-router), or for Stripe API calls that are not webhook-driven.
The file declares its own license as MIT. That is the author’s claim about this one file, and it is not the same thing as the license GitHub reports for the repository, which is listed with the other numbers below.
SKILL.md
9.8 KB, as published. Nobody here has run it
Stripe Webhook Signature Verification
Concept of the skill
What it is: The security check that proves an incoming Stripe webhook was signed by Stripe before any payment logic runs. Mental model: The raw request body, signature header, and webhook secret form one verification tuple; change any part and the event is untrusted. Why it exists: Webhook routes are public endpoints that can trigger billing and fulfillment, so authenticity has to be established before routing. What it is NOT: It is not payment-event routing, general HTTP signature validation, or Stripe API usage outside webhook delivery. Adjacent concepts: HMAC verification, raw request bodies, replay tolerance, idempotency keys. One-line analogy: It is the seal check before opening the payment envelope. Common misconception: Parsing JSON first is harmless; transforming the raw bytes invalidates the signature comparison.
Coverage
- The raw-body requirement — why
stripe.webhooks.constructEvent()requires the unparsedBufferfrom the request body, and how Next.js App Router routes expose it viarequest.arrayBuffer() - HMAC-SHA256 verification — how
constructEvent(rawBody, signature, secret)reconstructs and compares the Stripe signature internally - Replay protection — the 300-second tolerance window Stripe checks against the
t=timestamp embedded in thestripe-signatureheader; when to tighten it - Environment-specific secrets —
STRIPE_WEBHOOK_SECRETfor production vswhsec_...from the Stripe CLI--forward-tosession in development; why they must never be swapped - Idempotency key pattern — recording the
event.idin Postgres before processing so a retried delivery does not double-charge or double-fulfill
Philosophy of the skill
A webhook that skips signature verification is an unauthenticated public endpoint that can trigger payment processing. The verification step is load-bearing security, not a convenience check. Stripe's SDK makes verification a single call, but two failure modes are common in practice: the request body gets parsed (by a body-parser middleware) before the raw bytes reach the verification call, which silently corrupts the HMAC comparison; and the wrong webhook secret is loaded from environment variables, producing a 400 that is hard to distinguish from a replay rejection. Both failures look the same to the caller — a rejected webhook — and both are invisible until a real event is dropped.
Verification
-
Confirm raw body access. In Next.js App Router:
const rawBody = Buffer.from(await request.arrayBuffer()). Do NOT passawait request.json()orawait request.text()— both transform the bytes. -
Retrieve and verify.
import Stripe from "stripe"; const stripe = new Stripe(process.env.STRIPE_SECRET_KEY!); const sig = request.headers.get("stripe-signature") ?? ""; let event: Stripe.Event; try { event = stripe.webhooks.constructEvent( rawBody, sig, process.env.STRIPE_WEBHOOK_SECRET! ); } catch (err) { return Response.json({ error: "Signature verification failed" }, { status: 400 }); } -
Check idempotency before processing.
INSERT INTO webhook_events (event_id, processed_at) VALUES ($1, now()) ON CONFLICT (event_id) DO NOTHING RETURNING event_id;If the
RETURNINGclause returns no rows, the event was already processed — return 200 immediately without re-running side effects. -
Route the verified event to
payment-provider-router.
Failure Mode Reference
| Failure | Symptom | Fix |
|---|---|---|
| Body parsed before verification | 400 on every real Stripe event | Use arrayBuffer(), not json() or text() |
| Wrong webhook secret | 400 with "No signatures found matching the expected signature" | Verify STRIPE_WEBHOOK_SECRET matches the endpoint in the Stripe dashboard |
| Replay attack | 400 with "Timestamp too old" | Legitimate if tolerance is tight; check t= value in the stripe-signature header |
| Secret from wrong environment | Events verify in dev but fail in production | Use per-environment secrets; never share between environments |
Do NOT Use When
| Use instead | When |
|---|---|
payment-provider-router | You have a verified event and need to decide which handler processes it |
nextjs-server-action-validation | You are validating user-submitted form data, not a Stripe webhook |
| (a generic HTTP signature skill) | You are verifying webhooks from a non-Stripe provider with a different signing scheme |