DiscordInstagram
DR
David R. Fajardo18 min read

Stripe Integration: Common Production Problems & Testing Setup

A practical guide to integrating Stripe payments. Learn common production pitfalls, webhook handling, testing strategies, and how to avoid costly mistakes.

#Stripe#Payments#API#Testing#Production#E-commerce
Stripe Integration: Common Production Problems & Testing Setup

Stripe is the gold standard for payment processing, but integrating it properly is harder than the documentation makes it seem. After implementing Stripe in multiple production applications, I've learned that most payment bugs happen not during development, but after launch. This guide covers the common problems and how to avoid them.

Payment code is the one place where 'it works on my machine' can cost you real money. Test everything twice.

Setting Up Stripe for Testing

Before writing any payment code, set up your testing environment properly. Stripe provides test mode with fake card numbers - use it extensively.

Step 1: Get Your Test API Keys

# In your .env.local file
STRIPE_SECRET_KEY=sk_test_... # Test secret key
NEXT_PUBLIC_STRIPE_PUBLISHABLE_KEY=pk_test_... # Test publishable key
STRIPE_WEBHOOK_SECRET=whsec_... # Webhook signing secret

# NEVER commit these to git!
# Add .env.local to .gitignore

Step 2: Install and Configure Stripe

// Install Stripe
npm install stripe @stripe/stripe-js

// lib/stripe.ts - Server-side Stripe instance
import Stripe from 'stripe';

export const stripe = new Stripe(process.env.STRIPE_SECRET_KEY!, {
  apiVersion: '2023-10-16', // Always pin the API version!
  typescript: true,
});

// lib/stripe-client.ts - Client-side Stripe
import { loadStripe } from '@stripe/stripe-js';

export const stripePromise = loadStripe(
  process.env.NEXT_PUBLIC_STRIPE_PUBLISHABLE_KEY!
);

Step 3: Test Card Numbers

Stripe provides test cards for every scenario. Memorize these:

// Test Card Numbers (use any future expiry and any CVC)

4242 4242 4242 4242  // Success - Visa
5555 5555 5555 4444  // Success - Mastercard
4000 0000 0000 0002  // Decline - Card declined
4000 0000 0000 9995  // Decline - Insufficient funds
4000 0000 0000 0069  // Decline - Expired card
4000 0000 0000 0127  // Decline - Incorrect CVC
4000 0025 0000 3155  // Requires 3D Secure authentication
4000 0000 0000 3220  // 3D Secure 2 required

Basic Payment Flow

Creating a Checkout Session

// app/api/checkout/route.ts
import { NextResponse } from 'next/server';
import { stripe } from '@/lib/stripe';

export async function POST(req: Request) {
  try {
    const { items, customerEmail } = await req.json();
    
    const session = await stripe.checkout.sessions.create({
      payment_method_types: ['card'],
      mode: 'payment',
      customer_email: customerEmail,
      line_items: items.map((item: any) => ({
        price_data: {
          currency: 'usd',
          product_data: {
            name: item.name,
            images: [item.image],
          },
          unit_amount: Math.round(item.price * 100), // Stripe uses cents!
        },
        quantity: item.quantity,
      })),
      success_url: `${process.env.NEXT_PUBLIC_URL}/success?session_id={CHECKOUT_SESSION_ID}`,
      cancel_url: `${process.env.NEXT_PUBLIC_URL}/cart`,
      metadata: {
        // Store any data you need to reference later
        orderId: 'your-internal-order-id',
      },
    });
    
    return NextResponse.json({ sessionId: session.id, url: session.url });
  } catch (error: any) {
    console.error('Stripe checkout error:', error);
    return NextResponse.json(
      { error: error.message },
      { status: 500 }
    );
  }
}

Common Production Problems

Problem 1: Webhooks Not Working

This is the #1 issue. You set up payments, they work in test mode, but in production orders aren't being fulfilled. The culprit? Webhooks aren't configured or verified properly.

// app/api/webhooks/stripe/route.ts
import { NextResponse } from 'next/server';
import { stripe } from '@/lib/stripe';
import { headers } from 'next/headers';

export async function POST(req: Request) {
  const body = await req.text(); // Must be raw body!
  const signature = headers().get('stripe-signature')!;
  
  let event;
  
  try {
    // CRITICAL: Verify the webhook signature
    event = stripe.webhooks.constructEvent(
      body,
      signature,
      process.env.STRIPE_WEBHOOK_SECRET!
    );
  } catch (err: any) {
    console.error('Webhook signature verification failed:', err.message);
    return NextResponse.json(
      { error: 'Invalid signature' },
      { status: 400 }
    );
  }
  
  // Handle the event
  switch (event.type) {
    case 'checkout.session.completed':
      const session = event.data.object;
      await fulfillOrder(session); // Your fulfillment logic
      break;
    case 'payment_intent.payment_failed':
      const failedPayment = event.data.object;
      await handleFailedPayment(failedPayment);
      break;
    default:
      console.log(`Unhandled event type: ${event.type}`);
  }
  
  return NextResponse.json({ received: true });
}

Problem 2: Currency and Amount Errors

Stripe uses the smallest currency unit (cents for USD). Forgetting to multiply by 100 means charging $0.50 instead of $50.

// WRONG - Charging $0.50 instead of $50
unit_amount: 50

// CORRECT - Charging $50.00
unit_amount: 50 * 100 // or 5000

// SAFE - Always use a helper function
function toCents(dollars: number): number {
  return Math.round(dollars * 100);
}

unit_amount: toCents(product.price)

Problem 3: Missing Idempotency Keys

Network errors can cause duplicate charges. Always use idempotency keys for payment creation.

// Without idempotency - DANGEROUS
// If request fails and retries, customer gets charged twice!
const payment = await stripe.paymentIntents.create({
  amount: 5000,
  currency: 'usd',
});

// With idempotency - SAFE
const payment = await stripe.paymentIntents.create(
  {
    amount: 5000,
    currency: 'usd',
  },
  {
    idempotencyKey: `order_${orderId}_payment`, // Unique per operation
  }
);

Problem 4: Not Handling Failed Payments

Happy path works, but what happens when a card is declined? Users see cryptic errors or worse - nothing.

// Client-side error handling
try {
  const { error } = await stripe.confirmPayment({
    elements,
    confirmParams: {
      return_url: `${window.location.origin}/success`,
    },
  });
  
  if (error) {
    // Show user-friendly error messages
    switch (error.code) {
      case 'card_declined':
        setError('Your card was declined. Please try another card.');
        break;
      case 'insufficient_funds':
        setError('Insufficient funds. Please try another card.');
        break;
      case 'expired_card':
        setError('Your card has expired. Please use a different card.');
        break;
      default:
        setError('Payment failed. Please try again.');
    }
  }
} catch (err) {
  setError('Something went wrong. Please try again.');
}

Problem 5: Test vs Live Key Mixup

Using test keys in production (payments fail) or live keys in development (real charges!). Always verify your environment.

// Add a safety check in your Stripe initialization
const isProduction = process.env.NODE_ENV === 'production';
const stripeKey = process.env.STRIPE_SECRET_KEY!;

// Verify key matches environment
if (isProduction && stripeKey.startsWith('sk_test_')) {
  throw new Error('Using test Stripe key in production!');
}

if (!isProduction && stripeKey.startsWith('sk_live_')) {
  console.warn('WARNING: Using live Stripe key in development!');
}

Testing Webhooks Locally

Webhooks need a public URL, but localhost isn't public. Use Stripe CLI to forward webhooks locally.

# Install Stripe CLI
brew install stripe/stripe-cli/stripe

# Login to your Stripe account
stripe login

# Forward webhooks to your local server
stripe listen --forward-to localhost:3000/api/webhooks/stripe

# You'll get a webhook signing secret (whsec_...)
# Use this in your .env.local for local testing

# In another terminal, trigger test events
stripe trigger checkout.session.completed
stripe trigger payment_intent.payment_failed

Production Checklist

Before going live with Stripe payments, verify everything:

  1. 1Switch to live API keys (sk_live_, pk_live_)
  2. 2Update webhook endpoint URL to production domain
  3. 3Create new webhook signing secret for production
  4. 4Test with real card (charge $1, then refund)
  5. 5Verify webhook events are being received
  6. 6Set up email notifications for failed payments
  7. 7Configure Stripe dashboard alerts
  8. 8Test refund flow works correctly
  9. 9Verify metadata is being stored correctly
  10. 10Check error handling shows user-friendly messages

Monitoring in Production

  • Enable Stripe Radar for fraud detection
  • Set up webhook failure alerts in Stripe dashboard
  • Log all payment events to your database
  • Monitor for unusual patterns (many declines, chargebacks)
  • Review failed payment reports weekly
The best payment integration is one your users never think about. It just works, every time, with clear feedback when something goes wrong.

Conclusion

Stripe is powerful but requires careful implementation. The key is thorough testing, proper webhook handling, and defensive coding. Remember: payment bugs cost real money and real customer trust. Take the time to get it right, test every edge case, and monitor continuously in production.

All posts