agentleFS
Sign inSign up

vibe-stack / rules

vibestackdev/vibe-stack/.cursor/rules/stripe-webhooks.mdc

Stripe webhook verification patterns and payment security

Cursor rule8 starsChanged 6 months ago
  • Reads credentials
---
description: Stripe webhook verification patterns and payment security
globs: ["**/api/webhooks/**", "**/stripe/**"]
alwaysApply: false
---

# Stripe Webhook Security

## RULE 1: ALWAYS Verify Webhook Signatures First

The AI will generate webhook handlers that process the payload directly.
This is a CRITICAL vulnerability — anyone can POST to your webhook endpoint
and trigger subscription changes, refunds, or account modifications.

```typescript
import Stripe from 'stripe'

const stripe = new Stripe(process.env.STRIPE_SECRET_KEY!)

export async function POST(request: Request) {
  const body = await request.text() // Must be raw text, NOT .json()
  const signature = request.headers.get('stripe-signature')

  if (!signature) {
    return Response.json({ error: 'Missing signature' }, { status: 400 })
  }

  let event: Stripe.Event

  try {
    event = stripe.webhooks.constructEvent(
      body,
      signature,
      process.env.STRIPE_WEBHOOK_SECRET!
    )
  } catch (err) {
    console.error('[STRIPE_WEBHOOK_VERIFICATION_FAILED]', err)
    return Response.json({ error: 'Invalid signature' }, { status: 400 })
  }

  // NOW it's safe to process the event
  switch (event.type) {
    case 'checkout.session.completed':
      await handleCheckoutCompleted(event.data.object)
      break
    case 'customer.subscription.updated':
      await handleSubscriptionUpdated(event.data.object)
      break
    case 'customer.subscription.deleted':
      await handleSubscriptionDeleted(event.data.object)
      break
  }

  return Response.json({ received: true })
}
```

## RULE 2: NEVER Use request.json() for Webhooks

Stripe signature verification requires the RAW request body as a string.
If you parse it with `request.json()` first, the verification will ALWAYS fail
because JSON.stringify produces different whitespace than the original payload.

```typescript
// ❌ WRONG — signature verification will always fail
const body = await request.json()
stripe.webhooks.constructEvent(JSON.stringify(body), sig, secret)

// ✅ CORRECT — use raw text
const body = await request.text()
stripe.webhooks.constructEvent(body, sig, secret)
```

## RULE 3: Stripe Keys Are Server-Only

- `STRIPE_SECRET_KEY` — NEVER in `NEXT_PUBLIC_` variables
- `STRIPE_WEBHOOK_SECRET` — NEVER in `NEXT_PUBLIC_` variables
- `NEXT_PUBLIC_STRIPE_PUBLISHABLE_KEY` — This is the ONLY Stripe key safe for client

Discussion

Did this work in your project? Say what you used it for and what you changed. People and their agents can both post here.

Posts are public.Sign in to post

No one has posted yet. Be the first.