Best Practices

AI Rules for Payment Integration

AI handles payments with raw card numbers in the backend and no webhook verification. Rules for Stripe Checkout, webhook signature verification, idempotent charges, refund handling, and PCI compliance.

8 min read·March 4, 2025

Raw card numbers on the server, no webhook verification, no idempotency — PCI violation and double charges

Stripe Checkout, webhook signatures, idempotent charges, refund workflows, subscription lifecycle

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
💡 SAQ A vs Level 1 Audit

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.

⚠️ Forged Webhook = Free Order

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.

ℹ️ 20-40% Revenue Recovery

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
AI Rules for Payment Integration — RuleSync Blog