AI Handles Payments Like a Toy Project
AI generates payment code with: raw card numbers sent to the backend (PCI DSS violation — your servers must never see card numbers), no webhook verification (accepting unverified payment events — attackers can forge success notifications), no idempotency (retrying a failed request charges the customer twice), synchronous payment in the checkout flow (user waits 5 seconds while the charge processes), and no refund handling (no workflow for disputes, cancellations, or partial refunds). Each of these is either a compliance violation or a business-critical bug.
Modern payment integration is: tokenized (Stripe Checkout or Elements handle card collection — your server never sees card numbers), webhook-verified (cryptographic signature verification on every webhook), idempotent (Idempotency-Key header prevents duplicate charges), async-confirmed (payment intent created synchronously, confirmation via webhook), and refund-ready (automated refund workflows for cancellations, disputes, and partial returns). AI generates none of these.
These rules cover: Stripe Checkout for PCI compliance, webhook signature verification, idempotent charge creation, payment intent lifecycle, refund handling, and subscription management.
Rule 1: Stripe Checkout for PCI Compliance
The rule: 'Use Stripe Checkout (hosted payment page) or Stripe Elements (embedded card form) — never collect card numbers on your server. Stripe Checkout: redirect the user to a Stripe-hosted page, Stripe collects the card, processes the payment, and redirects back with a session ID. Your server creates the Checkout Session with line items and receives the result via webhook. Card numbers never touch your infrastructure — PCI SAQ A (simplest compliance level).'
For the Checkout Session flow: '(1) Client clicks "Pay": your server creates a Checkout Session with line items, success_url, and cancel_url. (2) Stripe returns a session URL. (3) Client redirects to the Stripe-hosted page. (4) User enters card details on Stripe page. (5) Stripe processes payment and redirects to success_url. (6) Your server receives checkout.session.completed webhook. (7) Fulfill the order based on the webhook (not the redirect — the user can close the browser before redirecting).'
AI generates: a custom card form that sends { cardNumber, expiry, cvv } to the backend API. Your server now handles raw card data — PCI DSS Level 1 compliance required (annual audit, $50K-$500K cost, quarterly scans). Stripe Checkout: card data goes to Stripe directly. Your PCI scope: SAQ A (self-assessment questionnaire, minimal requirements). Same payment capability, zero PCI audit cost.
- Stripe Checkout or Elements — card numbers never touch your servers
- PCI SAQ A (simplest) vs PCI Level 1 (most expensive) — architecture determines scope
- Checkout Session: create server-side, redirect client, receive webhook on completion
- Fulfill on webhook, not on redirect — user may close browser before success_url
- Never log, store, or transmit raw card numbers — even in error messages
Custom card form: raw card data hits your server, PCI Level 1 required ($50K-$500K annual audit). Stripe Checkout: card data goes to Stripe directly, PCI SAQ A (self-assessment questionnaire). Same payment capability, zero audit cost. Architecture determines compliance scope.
Rule 2: Webhook Signature Verification
The rule: 'Verify every Stripe webhook with the signature header. Stripe signs webhooks with your webhook secret: const event = stripe.webhooks.constructEvent(req.body, req.headers["stripe-signature"], webhookSecret). If verification fails, reject the webhook (return 400). Without verification: an attacker can send a forged checkout.session.completed event to your webhook endpoint, and your server fulfills an order that was never paid for.'
For raw body requirement: 'Webhook verification requires the raw request body (not parsed JSON). Express: app.post("/webhooks/stripe", express.raw({ type: "application/json" }), handler). Next.js App Router: the request body is already raw in route handlers. If your framework parses JSON before your handler sees it, the signature verification fails because the parsed-then-serialized body differs from the original. This is the most common webhook integration bug.'
AI generates: app.post('/webhooks/stripe', express.json(), (req, res) => { const event = req.body; if (event.type === 'checkout.session.completed') fulfillOrder(event.data); }) — no signature verification. Anyone can POST { type: 'checkout.session.completed', data: { orderId: '...' } } to your endpoint and get a free order. Signature verification: one function call that prevents payment fraud entirely.
Without signature verification: anyone can POST a fake checkout.session.completed to your endpoint and get a free order. stripe.webhooks.constructEvent with the raw body and webhook secret: one function call that prevents payment fraud entirely.
Rule 3: Idempotent Charge Creation
The rule: 'Use Stripe Idempotency-Key on every charge creation: const paymentIntent = await stripe.paymentIntents.create({ amount: 2999, currency: "usd" }, { idempotencyKey: orderId }). If the request is retried (network timeout, client retry, webhook redelivery), Stripe returns the original result instead of creating a duplicate charge. The idempotency key should be: the order ID (unique per order, same key on retry = same charge).'
For the retry scenario: 'Client submits payment, network drops, client retries. Without idempotency: two PaymentIntents created, customer charged twice. With idempotency key (order ID): Stripe sees the same key, returns the first PaymentIntent, customer charged once. The key is valid for 24 hours — retries within 24 hours are safe. After 24 hours: a new charge would be created, which is correct (it is a new attempt, not a retry).'
AI generates: await stripe.paymentIntents.create({ amount, currency }) with no idempotency key. The frontend has a retry mechanism. User clicks pay, network drops, frontend retries: two charges. Or: the webhook handler creates the charge, Stripe redelivers the webhook: two charges. Idempotency key on every mutating Stripe API call: retries are safe, webhooks are safe, the customer is never double-charged.
- Idempotency-Key on every charge: stripe.paymentIntents.create({}, { idempotencyKey: orderId })
- Order ID as idempotency key — unique per order, same key on retry = same charge
- 24-hour key validity — retries within window return original result
- Prevents: network retry duplicates, webhook redelivery duplicates, UI double-click
- Apply to all mutating Stripe calls: create, capture, refund — not just charges
Rule 4: Refund and Dispute Handling
The rule: 'Build refund workflows from day one: full refund (cancel order, refund total), partial refund (return one item, refund item price), and dispute handling (customer disputes with bank, Stripe notifies via charge.dispute.created webhook). Refund via API: await stripe.refunds.create({ payment_intent: paymentIntentId, amount: partialAmount }). On refund webhook: update order status, adjust inventory, send refund confirmation email.'
For dispute handling: 'On charge.dispute.created webhook: (1) immediately gather evidence (order details, delivery confirmation, customer communication), (2) submit evidence via the Disputes API within the deadline (usually 7-21 days), (3) update the order status to "disputed", (4) notify the customer service team. Win rate for disputes with evidence: 40-60%. Win rate without evidence: near 0%. Automated evidence collection on dispute webhook: the difference between recovering revenue and losing it.'
AI generates: no refund endpoint, no dispute handling. Customer requests a refund: manual Stripe Dashboard operation. Customer disputes a charge: the dispute deadline passes with no evidence submitted, the merchant loses automatically. Automated refund API + dispute webhook handler: refunds are self-service (customer dashboard), disputes are automatically populated with evidence. Less manual work, higher win rate, better customer experience.
Rule 5: Subscription Lifecycle Management
The rule: 'For recurring payments, use Stripe Billing with webhook-driven lifecycle management. Key webhooks: customer.subscription.created (provision access), customer.subscription.updated (plan change), invoice.payment_succeeded (renewal), invoice.payment_failed (payment method issue — send dunning email), customer.subscription.deleted (cancel — revoke access). Each webhook updates the user subscription status in your database. The database status and Stripe status must stay in sync.'
For failed payment handling: 'Stripe retries failed invoice payments automatically (Smart Retries). Your dunning workflow: on invoice.payment_failed: (1) send email: "Your payment failed, please update your card." (2) After 3 days: send reminder. (3) After 7 days: downgrade to free plan or restrict features. (4) After 14 days: cancel subscription. (5) On invoice.payment_succeeded (recovery): restore full access, send confirmation. 20-40% of failed payments are recovered with proper dunning — without it, they are all lost revenue.'
AI generates: a subscribe button that creates a subscription and assumes it works forever. First failed renewal: the subscription is technically cancelled in Stripe, but the user still has access in your app (database not updated). Or: the subscription renews successfully, but the database is not updated (user sees expired status). Webhook-driven sync: every Stripe event updates the database. Status is always accurate. Failed payments trigger dunning. Revenue is recovered.
Without dunning: every failed payment renewal is lost revenue. With email reminders on invoice.payment_failed (day 1, day 3, day 7): 20-40% of failed payments are recovered. Automated dunning is free money — the emails write themselves from templates.
Complete Payment Integration Rules Template
Consolidated rules for payment integration.
- Stripe Checkout or Elements — card numbers never on your servers, PCI SAQ A
- Webhook signature verification: stripe.webhooks.constructEvent with raw body
- Idempotency-Key on every mutating Stripe call — order ID as key, safe retries
- Fulfill on webhook, not redirect — user may close browser before success_url
- Refund workflows from day one: full, partial, and dispute handling via API
- Dispute evidence auto-collection: 40-60% win rate with evidence vs 0% without
- Subscription lifecycle via webhooks: created, renewed, failed, cancelled
- Dunning for failed payments: email reminders recover 20-40% of failed charges