[{"data":1,"prerenderedAt":283},["ShallowReactive",2],{"blog-guides/stripe":3},{"id":4,"title":5,"body":6,"category":262,"date":263,"dateModified":264,"description":265,"draft":266,"extension":267,"faq":268,"featured":266,"headerVariant":269,"image":268,"keywords":268,"meta":270,"navigation":271,"ogDescription":272,"ogTitle":268,"path":273,"readTime":274,"schemaOrg":275,"schemaType":276,"seo":277,"sitemap":278,"stem":279,"tags":280,"twitterCard":281,"__hash__":282},"blog/blog/guides/stripe.md","Stripe Security Guide for Vibe Coders",{"type":7,"value":8,"toc":250},"minimark",[9,16,21,24,28,31,42,60,64,67,73,90,94,97,103,107,110,116,120,126,130,136,141,181,185,191,219,238],[10,11,12],"tldr",{},[13,14,15],"p",{},"Stripe handles most payment security, but your integration must be secure too. Always verify webhooks using the signature (never skip this). Keep your secret key server-side only. Use Stripe Checkout or Elements to avoid handling card data directly (PCI compliance). Never trust client-side price data - always set prices server-side. Validate that paying users are authenticated and own the resources they're paying for.",[17,18,20],"h2",{"id":19},"why-stripe-security-matters-for-vibe-coding","Why Stripe Security Matters for Vibe Coding",[13,22,23],{},"Stripe makes payments easy, but payment integrations are high-value targets. When AI tools generate Stripe code, they often create working checkout flows but miss critical security patterns like webhook verification or server-side price validation. A compromised payment integration can lead to financial loss and legal liability.",[17,25,27],{"id":26},"api-key-security","API Key Security",[13,29,30],{},"Stripe uses two types of keys. Handle them differently:",[32,33,38],"pre",{"className":34,"code":36,"language":37},[35],"language-text","# .env.local (never commit)\n# Publishable key - safe for client-side\nNEXT_PUBLIC_STRIPE_PUBLISHABLE_KEY=pk_live_...\n\n# Secret key - server-side ONLY\nSTRIPE_SECRET_KEY=sk_live_...\n\n# Webhook signing secret - server-side ONLY\nSTRIPE_WEBHOOK_SECRET=whsec_...\n","text",[39,40,36],"code",{"__ignoreMap":41},"",[43,44,45],"danger-box",{},[13,46,47,51,52,55,56,59],{},[48,49,50],"strong",{},"Never Expose Your Secret Key:"," The ",[39,53,54],{},"sk_live_"," or ",[39,57,58],{},"sk_test_"," secret key must never appear in client-side code, version control, or logs. If exposed, rotate it immediately in the Stripe Dashboard. Someone with your secret key can create charges, issue refunds, and access customer data.",[17,61,63],{"id":62},"webhook-verification","Webhook Verification",[13,65,66],{},"Webhooks notify your app about payment events. Always verify they're from Stripe:",[32,68,71],{"className":69,"code":70,"language":37},[35],"// app/api/webhooks/stripe/route.ts\nimport Stripe from 'stripe';\nimport { headers } from 'next/headers';\n\nconst stripe = new Stripe(process.env.STRIPE_SECRET_KEY!);\nconst webhookSecret = process.env.STRIPE_WEBHOOK_SECRET!;\n\nexport async function POST(request: Request) {\n  const body = await request.text();\n  const signature = headers().get('stripe-signature');\n\n  if (!signature) {\n    return new Response('Missing signature', { status: 400 });\n  }\n\n  let event: Stripe.Event;\n\n  try {\n    // CRITICAL: Verify the webhook signature\n    event = stripe.webhooks.constructEvent(\n      body,\n      signature,\n      webhookSecret\n    );\n  } catch (err) {\n    console.error('Webhook signature verification failed:', err);\n    return new Response('Invalid signature', { status: 400 });\n  }\n\n  // Now safe to process the event\n  switch (event.type) {\n    case 'checkout.session.completed':\n      const session = event.data.object as Stripe.Checkout.Session;\n      await handleCheckoutComplete(session);\n      break;\n\n    case 'customer.subscription.deleted':\n      const subscription = event.data.object as Stripe.Subscription;\n      await handleSubscriptionCanceled(subscription);\n      break;\n  }\n\n  return new Response('OK', { status: 200 });\n}\n",[39,72,70],{"__ignoreMap":41},[74,75,76],"warning-box",{},[13,77,78,81,82,85,86,89],{},[48,79,80],{},"Common AI-Generated Mistake:"," AI tools sometimes skip webhook verification or use ",[39,83,84],{},"JSON.parse(body)"," directly. This allows attackers to send fake webhook events. Always use ",[39,87,88],{},"stripe.webhooks.constructEvent()"," with the signature.",[17,91,93],{"id":92},"server-side-price-validation","Server-Side Price Validation",[13,95,96],{},"Never trust prices from the client. Always set them server-side:",[32,98,101],{"className":99,"code":100,"language":37},[35],"// DANGEROUS: Using client-provided price\napp.post('/create-checkout', async (req, res) => {\n  const { price } = req.body; // Attacker can send any price!\n\n  const session = await stripe.checkout.sessions.create({\n    line_items: [{\n      price_data: {\n        currency: 'usd',\n        unit_amount: price, // VULNERABLE!\n        product_data: { name: 'Product' },\n      },\n      quantity: 1,\n    }],\n    mode: 'payment',\n    success_url: 'https://example.com/success',\n    cancel_url: 'https://example.com/cancel',\n  });\n});\n\n// SAFE: Use server-side pricing\nconst PRODUCTS = {\n  'pro-monthly': { priceId: 'price_xxx', name: 'Pro Monthly' },\n  'pro-yearly': { priceId: 'price_yyy', name: 'Pro Yearly' },\n};\n\napp.post('/create-checkout', async (req, res) => {\n  const { productId } = req.body;\n\n  // Validate product exists\n  const product = PRODUCTS[productId];\n  if (!product) {\n    return res.status(400).json({ error: 'Invalid product' });\n  }\n\n  // Use predefined price ID\n  const session = await stripe.checkout.sessions.create({\n    line_items: [{ price: product.priceId, quantity: 1 }],\n    mode: 'subscription',\n    success_url: 'https://example.com/success?session_id={CHECKOUT_SESSION_ID}',\n    cancel_url: 'https://example.com/cancel',\n  });\n\n  res.json({ url: session.url });\n});\n",[39,102,100],{"__ignoreMap":41},[17,104,106],{"id":105},"user-authentication-in-payments","User Authentication in Payments",[13,108,109],{},"Always verify the user before creating payments or accessing billing:",[32,111,114],{"className":112,"code":113,"language":37},[35],"// Associate payments with authenticated users\nexport async function POST(request: Request) {\n  const session = await getServerSession(authOptions);\n\n  if (!session?.user) {\n    return new Response('Unauthorized', { status: 401 });\n  }\n\n  // Get or create Stripe customer for this user\n  let customerId = await getStripeCustomerId(session.user.id);\n\n  if (!customerId) {\n    const customer = await stripe.customers.create({\n      email: session.user.email,\n      metadata: { userId: session.user.id },\n    });\n    customerId = customer.id;\n    await saveStripeCustomerId(session.user.id, customerId);\n  }\n\n  // Create checkout with customer attached\n  const checkoutSession = await stripe.checkout.sessions.create({\n    customer: customerId,\n    line_items: [{ price: 'price_xxx', quantity: 1 }],\n    mode: 'subscription',\n    success_url: '...',\n    cancel_url: '...',\n    client_reference_id: session.user.id,\n  });\n\n  return Response.json({ url: checkoutSession.url });\n}\n",[39,115,113],{"__ignoreMap":41},[17,117,119],{"id":118},"protecting-customer-portal","Protecting Customer Portal",[32,121,124],{"className":122,"code":123,"language":37},[35],"// Customer can only access their own billing\nexport async function POST(request: Request) {\n  const session = await getServerSession(authOptions);\n\n  if (!session?.user) {\n    return new Response('Unauthorized', { status: 401 });\n  }\n\n  const customerId = await getStripeCustomerId(session.user.id);\n\n  if (!customerId) {\n    return new Response('No billing account', { status: 404 });\n  }\n\n  const portalSession = await stripe.billingPortal.sessions.create({\n    customer: customerId,\n    return_url: 'https://example.com/account',\n  });\n\n  return Response.json({ url: portalSession.url });\n}\n",[39,125,123],{"__ignoreMap":41},[17,127,129],{"id":128},"handling-webhook-events-safely","Handling Webhook Events Safely",[32,131,134],{"className":132,"code":133,"language":37},[35],"async function handleCheckoutComplete(session: Stripe.Checkout.Session) {\n  const userId = session.client_reference_id;\n\n  if (!userId) {\n    console.error('Checkout without user ID:', session.id);\n    return;\n  }\n\n  if (session.payment_status !== 'paid') {\n    return;\n  }\n\n  await prisma.user.update({\n    where: { id: userId },\n    data: {\n      stripeCustomerId: session.customer as string,\n      subscriptionId: session.subscription as string,\n      subscriptionStatus: 'active',\n    },\n  });\n}\n\nasync function handleInvoicePaid(invoice: Stripe.Invoice) {\n  if (invoice.status !== 'paid') {\n    return;\n  }\n\n  const customerId = invoice.customer as string;\n  const user = await prisma.user.findFirst({\n    where: { stripeCustomerId: customerId },\n  });\n\n  if (!user) {\n    console.error('Invoice for unknown customer:', customerId);\n    return;\n  }\n\n  await prisma.user.update({\n    where: { id: user.id },\n    data: {\n      subscriptionStatus: 'active',\n      currentPeriodEnd: new Date(invoice.lines.data[0].period.end * 1000),\n    },\n  });\n}\n",[39,135,133],{"__ignoreMap":41},[137,138,140],"h4",{"id":139},"stripe-security-checklist","Stripe Security Checklist",[142,143,144,148,151,154,157,160,163,166,169,172,175,178],"ul",{},[145,146,147],"li",{},"Secret key stored in environment variable, never in code",[145,149,150],{},"Publishable key used only for Stripe.js/Elements",[145,152,153],{},"All webhooks verify signature with constructEvent()",[145,155,156],{},"Prices set server-side using Price IDs, not client values",[145,158,159],{},"User authenticated before creating checkout sessions",[145,161,162],{},"Checkout sessions include client_reference_id for user tracking",[145,164,165],{},"Customer portal only accessible to owning user",[145,167,168],{},"Webhook endpoint uses HTTPS",[145,170,171],{},"Payment status verified before granting access",[145,173,174],{},"Idempotency keys used for critical operations",[145,176,177],{},"Test mode keys used only in development",[145,179,180],{},"Restricted API keys used where possible",[17,182,184],{"id":183},"idempotency-for-safe-retries","Idempotency for Safe Retries",[32,186,189],{"className":187,"code":188,"language":37},[35],"import { randomUUID } from 'crypto';\n\nasync function createPaymentIntent(userId: string, amount: number) {\n  const idempotencyKey = `${userId}-${amount}-${Date.now()}`;\n\n  const paymentIntent = await stripe.paymentIntents.create(\n    {\n      amount,\n      currency: 'usd',\n      customer: await getStripeCustomerId(userId),\n    },\n    {\n      idempotencyKey,\n    }\n  );\n\n  return paymentIntent;\n}\n",[39,190,188],{"__ignoreMap":41},[192,193,194,201,207,213],"faq-section",{},[195,196,198],"faq-item",{"question":197},"Do I need to be PCI compliant?",[13,199,200],{},"If you use Stripe Checkout or Stripe Elements, card data never touches your servers, and you qualify for the simplest PCI compliance (SAQ A). Never collect card numbers directly in your own forms. Use Stripe's pre-built components.",[195,202,204],{"question":203},"What if my webhook endpoint is down?",[13,205,206],{},"Stripe retries webhook delivery with exponential backoff for up to 3 days. Your webhook handler should be idempotent (safe to process the same event twice). Always return 200 quickly and process asynchronously if needed.",[195,208,210],{"question":209},"Should I use test keys in development?",[13,211,212],{},"Always. Use pk_test_ and sk_test_ keys in development. Never use live keys locally. Set up separate webhook endpoints for test and live modes. Test mode transactions don't charge real money.",[195,214,216],{"question":215},"How do I handle subscription upgrades/downgrades?",[13,217,218],{},"Use the Stripe Customer Portal for most billing changes, or modify subscriptions server-side. Always verify the user owns the subscription before making changes. Handle proration according to your business rules.",[220,221,222,228,233],"related-articles",{},[223,224],"related-card",{"description":225,"href":226,"title":227},"Row-level security and auth patterns","/blog/guides/supabase","Supabase Security Guide",[223,229],{"description":230,"href":231,"title":232},"Security rules and authentication","/blog/guides/firebase","Firebase Security Guide",[223,234],{"description":235,"href":236,"title":237},"Best practices for key management","/blog/how-to/secure-api-keys","Secure API Keys",[239,240,243,247],"cta-box",{"href":241,"label":242},"/","Start Free Scan",[17,244,246],{"id":245},"scan-your-stripe-integration","Scan Your Stripe Integration",[13,248,249],{},"Find webhook vulnerabilities, exposed keys, and pricing issues before they cost you.",{"title":41,"searchDepth":251,"depth":251,"links":252},2,[253,254,255,256,257,258,259,260,261],{"id":19,"depth":251,"text":20},{"id":26,"depth":251,"text":27},{"id":62,"depth":251,"text":63},{"id":92,"depth":251,"text":93},{"id":105,"depth":251,"text":106},{"id":118,"depth":251,"text":119},{"id":128,"depth":251,"text":129},{"id":183,"depth":251,"text":184},{"id":245,"depth":251,"text":246},"guides","2026-01-28","2026-02-11","Secure your Stripe integration when vibe coding. Learn webhook verification, API key protection, PCI compliance basics, and common payment security mistakes.",false,"md",null,"blue",{},true,"Secure your Stripe integration with proper webhook verification, API key handling, and PCI compliance.","/blog/guides/stripe","13 min read","[object Object]","TechArticle",{"title":5,"description":265},{"loc":273},"blog/guides/stripe",[],"summary_large_image","MdElm6NkRYslrjxjHldr7cmAvT7Cx575iwdupP_iqZQ",1775843929764]