agentsclimarketplace

Stripe webhook signature verification

Skill jacob-balslev/skill-graph/examples/projects/saas-stripe-postgres/skills/stripe-webhook-signature-verification

Skills that know your codebase. Repo-grounded, contract-validated, agent-routable.

Install
npx -y skills add jacob-balslev/skill-graph --skill stripe-webhook-signature-verification

Assembled 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 unparsed Buffer from the request body, and how Next.js App Router routes expose it via request.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 the stripe-signature header; when to tighten it
  • Environment-specific secrets — STRIPE_WEBHOOK_SECRET for production vs whsec_... from the Stripe CLI --forward-to session in development; why they must never be swapped
  • Idempotency key pattern — recording the event.id in 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

  1. Confirm raw body access. In Next.js App Router: const rawBody = Buffer.from(await request.arrayBuffer()). Do NOT pass await request.json() or await request.text() — both transform the bytes.

  2. 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 });
    }
    
  3. 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 RETURNING clause returns no rows, the event was already processed — return 200 immediately without re-running side effects.

  4. Route the verified event to payment-provider-router.

Failure Mode Reference

FailureSymptomFix
Body parsed before verification400 on every real Stripe eventUse arrayBuffer(), not json() or text()
Wrong webhook secret400 with "No signatures found matching the expected signature"Verify STRIPE_WEBHOOK_SECRET matches the endpoint in the Stripe dashboard
Replay attack400 with "Timestamp too old"Legitimate if tolerance is tight; check t= value in the stripe-signature header
Secret from wrong environmentEvents verify in dev but fail in productionUse per-environment secrets; never share between environments

Do NOT Use When

Use insteadWhen
payment-provider-routerYou have a verified event and need to decide which handler processes it
nextjs-server-action-validationYou 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

Keep looking

Skills are one crate of 328,083. Ordering is by how many stacks a row turns up in, so the top of any crate is what has actually been picked rather than what has the most stars.