Stripe Payment Integration Guide

Learn how to integrate Stripe payments with the Unified Commerce Platform for secure, PCI-compliant payment processing. This guide covers setup, implementation, testing, and production deployment.

Prerequisites

Before starting, ensure you have:

  • Stripe account (Sign up here)
  • Unified Commerce API credentials
  • Basic understanding of payment processing concepts
  • Node.js environment for backend implementation

Table of Contents

  1. Stripe Setup
  2. Backend Integration
  3. Frontend Implementation
  4. Payment Methods
  5. 3D Secure Authentication
  6. Webhooks
  7. Refunds & Disputes
  8. Testing
  9. Production Checklist
  10. Troubleshooting

Stripe Setup

Step 1: Get Your Stripe Keys

  1. Log into Stripe Dashboard
  2. Navigate to Developers → API keys
  3. Copy your keys:
    • Test Mode: pk_test_... and sk_test_...
    • Live Mode: pk_live_... and sk_live_...

Step 2: Configure Unified Commerce

Update your merchant settings via API:

mutation UpdateMerchantSettings($input: UpdateMerchantSettingsInput!) {
  updateMerchantSettings(input: $input) {
    settings {
      payments {
        stripeEnabled
        stripePublishableKey
        stripeSupportedCountries
        stripeSupportedCurrencies
      }
    }
    errors {
      field
      message
    }
  }
}

# Variables
{
  "input": {
    "payments": {
      "stripeEnabled": true,
      "stripePublishableKey": "pk_test_...",
      "stripeSecretKey": "sk_test_...",
      "stripeWebhookSecret": "whsec_...",
      "stripeSupportedCountries": ["US", "CA", "GB", "EU"],
      "stripeSupportedCurrencies": ["USD", "CAD", "GBP", "EUR"]
    }
  }
}

Step 3: Install Dependencies

# Backend dependencies
npm install stripe @types/stripe

# Frontend dependencies
npm install @stripe/stripe-js @stripe/react-stripe-js

Backend Integration

Step 1: Initialize Stripe Client

Create lib/stripe.ts:

import Stripe from 'stripe';

// Initialize Stripe with your secret key
export const stripe = new Stripe(process.env.STRIPE_SECRET_KEY!, {
  apiVersion: '2023-10-16',
  typescript: true,
});

// Helper to convert cents to Stripe amount
export const toStripeAmount = (cents: number): number => {
  return Math.round(cents);
};

// Helper to convert Stripe amount to cents
export const fromStripeAmount = (amount: number): number => {
  return amount;
};

// Validate Stripe webhook signature
export const validateWebhookSignature = (
  payload: string | Buffer,
  signature: string,
  secret: string
): Stripe.Event => {
  try {
    return stripe.webhooks.constructEvent(payload, signature, secret);
  } catch (err) {
    throw new Error(`Webhook signature verification failed: ${err.message}`);
  }
};

Step 2: Create Payment Intent

Create api/payments/create-intent.ts:

import { stripe, toStripeAmount } from '@/lib/stripe';
import { sdk } from '@/lib/api-client';

interface CreatePaymentIntentInput {
  orderId: string;
  amount: number; // in cents
  currency: string;
  customerId?: string;
  metadata?: Record<string, string>;
}

export async function createPaymentIntent({
  orderId,
  amount,
  currency,
  customerId,
  metadata = {},
}: CreatePaymentIntentInput) {
  try {
    // Create payment intent in Stripe
    const intent = await stripe.paymentIntents.create({
      amount: toStripeAmount(amount),
      currency: currency.toLowerCase(),
      customer: customerId,
      metadata: {
        orderId,
        merchantId: process.env.UC_MERCHANT_ID!,
        ...metadata,
      },
      automatic_payment_methods: {
        enabled: true,
      },
      capture_method: 'automatic', // or 'manual' for auth-only
    });

    // Record payment intent in Unified Commerce
    const { data } = await sdk.createPaymentIntent({
      input: {
        orderId,
        stripeIntentId: intent.id,
        amount,
        currency,
        status: 'PENDING',
      }
    });

    if (data?.createPaymentIntent?.errors?.length) {
      throw new Error(data.createPaymentIntent.errors[0].message);
    }

    return {
      clientSecret: intent.client_secret,
      intentId: intent.id,
    };
  } catch (error) {
    console.error('Failed to create payment intent:', error);
    throw error;
  }
}

Step 3: Handle Payment Confirmation

Create api/payments/confirm-payment.ts:

import { stripe } from '@/lib/stripe';
import { sdk } from '@/lib/api-client';

export async function confirmPayment(
  intentId: string,
  paymentMethodId: string
) {
  try {
    // Confirm payment with Stripe
    const intent = await stripe.paymentIntents.confirm(intentId, {
      payment_method: paymentMethodId,
      return_url: `${process.env.NEXT_PUBLIC_SITE_URL}/checkout/success`,
    });

    // Update order status based on payment result
    if (intent.status === 'succeeded') {
      const { data } = await sdk.updateOrderPaymentStatus({
        stripeIntentId: intentId,
        status: 'PAID',
        paidAt: new Date().toISOString(),
      });

      return {
        success: true,
        orderId: data?.updateOrderPaymentStatus?.order?.id,
      };
    } else if (intent.status === 'requires_action') {
      // 3D Secure authentication required
      return {
        requiresAction: true,
        clientSecret: intent.client_secret,
      };
    } else {
      throw new Error(`Payment failed: ${intent.status}`);
    }
  } catch (error) {
    console.error('Payment confirmation failed:', error);
    throw error;
  }
}

Step 4: Create Customer Profile

Create api/payments/customer.ts:

import { stripe } from '@/lib/stripe';
import { sdk } from '@/lib/api-client';

export async function createStripeCustomer(
  customerId: string,
  email: string,
  name?: string
) {
  try {
    // Create customer in Stripe
    const customer = await stripe.customers.create({
      email,
      name,
      metadata: {
        customerId,
        merchantId: process.env.UC_MERCHANT_ID!,
      },
    });

    // Store Stripe customer ID in Unified Commerce
    const { data } = await sdk.updateCustomer({
      id: customerId,
      input: {
        stripeCustomerId: customer.id,
      }
    });

    return customer;
  } catch (error) {
    console.error('Failed to create Stripe customer:', error);
    throw error;
  }
}

// Save payment method for future use
export async function savePaymentMethod(
  customerId: string,
  paymentMethodId: string
) {
  try {
    // Attach payment method to customer
    await stripe.paymentMethods.attach(paymentMethodId, {
      customer: customerId,
    });

    // Set as default payment method
    await stripe.customers.update(customerId, {
      invoice_settings: {
        default_payment_method: paymentMethodId,
      },
    });

    return { success: true };
  } catch (error) {
    console.error('Failed to save payment method:', error);
    throw error;
  }
}

Frontend Implementation

Step 1: Setup Stripe Elements

Create components/payment/stripe-provider.tsx:

'use client';

import { loadStripe } from '@stripe/stripe-js';
import { Elements } from '@stripe/react-stripe-js';
import { ReactNode } from 'react';

// Load Stripe outside of component to avoid recreating on every render
const stripePromise = loadStripe(process.env.NEXT_PUBLIC_STRIPE_PUBLIC_KEY!);

interface StripeProviderProps {
  children: ReactNode;
  options?: {
    clientSecret?: string;
    appearance?: any;
  };
}

export function StripeProvider({ children, options }: StripeProviderProps) {
  const defaultOptions = {
    appearance: {
      theme: 'stripe' as const,
      variables: {
        colorPrimary: '#3B82F6',
        colorBackground: '#ffffff',
        colorText: '#1F2937',
        colorDanger: '#EF4444',
        fontFamily: 'Inter, system-ui, sans-serif',
        spacingUnit: '4px',
        borderRadius: '8px',
      },
      rules: {
        '.Label': {
          fontWeight: '500',
          marginBottom: '8px',
        },
        '.Input': {
          padding: '12px',
          fontSize: '16px',
        },
        '.Input:focus': {
          borderColor: '#3B82F6',
          boxShadow: '0 0 0 3px rgba(59, 130, 246, 0.1)',
        },
        '.Error': {
          fontSize: '14px',
          marginTop: '4px',
        },
      },
    },
    ...options,
  };

  return (
    <Elements stripe={stripePromise} options={defaultOptions}>
      {children}
    </Elements>
  );
}

Step 2: Create Payment Form

Create components/payment/payment-form.tsx:

'use client';

import { useState } from 'react';
import {
  PaymentElement,
  useStripe,
  useElements,
  AddressElement,
} from '@stripe/react-stripe-js';

interface PaymentFormProps {
  amount: number; // in cents
  currency: string;
  onSuccess: (paymentIntent: any) => void;
  onError: (error: string) => void;
}

export function PaymentForm({
  amount,
  currency,
  onSuccess,
  onError,
}: PaymentFormProps) {
  const stripe = useStripe();
  const elements = useElements();
  const [isProcessing, setIsProcessing] = useState(false);
  const [saveCard, setSaveCard] = useState(false);

  const handleSubmit = async (e: React.FormEvent) => {
    e.preventDefault();

    if (!stripe || !elements) {
      return;
    }

    setIsProcessing(true);

    try {
      // Confirm payment
      const { error, paymentIntent } = await stripe.confirmPayment({
        elements,
        confirmParams: {
          return_url: `${window.location.origin}/checkout/success`,
          payment_method_data: {
            billing_details: {
              // Will be collected by AddressElement
            },
          },
          save_payment_method: saveCard,
        },
        redirect: 'if_required', // Only redirect if necessary (3D Secure)
      });

      if (error) {
        if (error.type === 'card_error' || error.type === 'validation_error') {
          onError(error.message || 'Payment failed');
        } else {
          onError('An unexpected error occurred');
        }
      } else if (paymentIntent && paymentIntent.status === 'succeeded') {
        onSuccess(paymentIntent);
      }
    } catch (err) {
      console.error('Payment error:', err);
      onError('Payment processing failed');
    } finally {
      setIsProcessing(false);
    }
  };

  return (
    <form onSubmit={handleSubmit} className="space-y-6">
      {/* Billing Address */}
      <div>
        <h3 className="text-lg font-semibold mb-4">Billing Address</h3>
        <AddressElement
          options={{
            mode: 'billing',
            allowedCountries: ['US', 'CA', 'GB'],
            fields: {
              phone: 'always',
            },
            validation: {
              phone: {
                required: 'always',
              },
            },
          }}
        />
      </div>

      {/* Payment Method */}
      <div>
        <h3 className="text-lg font-semibold mb-4">Payment Method</h3>
        <PaymentElement
          options={{
            layout: 'tabs',
            paymentMethodOrder: ['card', 'apple_pay', 'google_pay'],
            fields: {
              billingDetails: {
                address: 'never', // Collected by AddressElement
              },
            },
          }}
        />
      </div>

      {/* Save Card Option */}
      <div className="flex items-center">
        <input
          type="checkbox"
          id="save-card"
          checked={saveCard}
          onChange={(e) => setSaveCard(e.target.checked)}
          className="mr-2"
        />
        <label htmlFor="save-card" className="text-sm">
          Save payment method for future purchases
        </label>
      </div>

      {/* Submit Button */}
      <button
        type="submit"
        disabled={!stripe || isProcessing}
        className="w-full bg-blue-600 text-white py-3 px-4 rounded-lg font-semibold hover:bg-blue-700 disabled:opacity-50 disabled:cursor-not-allowed"
      >
        {isProcessing ? (
          <span className="flex items-center justify-center">
            <svg className="animate-spin h-5 w-5 mr-3" viewBox="0 0 24 24">
              <circle className="opacity-25" cx="12" cy="12" r="10" stroke="currentColor" strokeWidth="4" />
              <path className="opacity-75" fill="currentColor" d="M4 12a8 8 0 018-8V0C5.373 0 0 5.373 0 12h4z" />
            </svg>
            Processing...
          </span>
        ) : (
          `Pay ${new Intl.NumberFormat('en-US', {
            style: 'currency',
            currency: currency,
          }).format(amount / 100)}`
        )}
      </button>
    </form>
  );
}

Step 3: Handle Express Checkout

Create components/payment/express-checkout.tsx:

'use client';

import { PaymentRequestButtonElement } from '@stripe/react-stripe-js';
import { useStripe } from '@stripe/react-stripe-js';
import { useEffect, useState } from 'react';

interface ExpressCheckoutProps {
  amount: number;
  currency: string;
  onSuccess: (paymentMethod: any) => void;
}

export function ExpressCheckout({
  amount,
  currency,
  onSuccess,
}: ExpressCheckoutProps) {
  const stripe = useStripe();
  const [paymentRequest, setPaymentRequest] = useState<any>(null);

  useEffect(() => {
    if (!stripe) return;

    const pr = stripe.paymentRequest({
      country: 'US',
      currency: currency.toLowerCase(),
      total: {
        label: 'Total',
        amount: amount,
      },
      requestPayerName: true,
      requestPayerEmail: true,
      requestShipping: true,
      shippingOptions: [
        {
          id: 'standard',
          label: 'Standard Shipping',
          detail: '5-7 business days',
          amount: 500, // $5.00
        },
        {
          id: 'express',
          label: 'Express Shipping',
          detail: '2-3 business days',
          amount: 1500, // $15.00
        },
      ],
    });

    // Check if Payment Request API is available
    pr.canMakePayment().then((result) => {
      if (result) {
        setPaymentRequest(pr);
      }
    });

    // Handle payment method
    pr.on('paymentmethod', async (event) => {
      // Process payment
      onSuccess(event.paymentMethod);

      // Complete payment
      event.complete('success');
    });

    // Handle shipping address change
    pr.on('shippingaddresschange', async (event) => {
      // Update shipping options based on address
      event.updateWith({
        status: 'success',
        shippingOptions: [
          {
            id: 'standard',
            label: 'Standard Shipping',
            detail: '5-7 business days',
            amount: 500,
          },
        ],
      });
    });
  }, [stripe, amount, currency]);

  if (!paymentRequest) {
    return null;
  }

  return (
    <div className="mb-6">
      <div className="text-center text-sm text-gray-500 mb-2">
        Express checkout
      </div>
      <PaymentRequestButtonElement
        options={{
          paymentRequest,
          style: {
            paymentRequestButton: {
              type: 'default',
              theme: 'dark',
              height: '48px',
            },
          },
        }}
      />
      <div className="relative my-6">
        <div className="absolute inset-0 flex items-center">
          <div className="w-full border-t border-gray-300" />
        </div>
        <div className="relative flex justify-center text-sm">
          <span className="px-2 bg-white text-gray-500">Or pay with card</span>
        </div>
      </div>
    </div>
  );
}

Payment Methods

Credit/Debit Cards

// Basic card payment
const cardElement = elements.getElement(CardElement);
const { error, paymentMethod } = await stripe.createPaymentMethod({
  type: 'card',
  card: cardElement,
  billing_details: {
    name: customerName,
    email: customerEmail,
    address: {
      line1: address.line1,
      city: address.city,
      state: address.state,
      postal_code: address.postalCode,
      country: address.country,
    },
  },
});

Digital Wallets

// Apple Pay / Google Pay setup
const paymentRequest = stripe.paymentRequest({
  country: 'US',
  currency: 'usd',
  total: {
    label: 'Order Total',
    amount: 2999, // $29.99
  },
  requestPayerName: true,
  requestPayerEmail: true,
});

// Check availability
const canMakePayment = await paymentRequest.canMakePayment();
if (canMakePayment) {
  // Show Apple Pay/Google Pay button
}

Bank Transfers (ACH)

// ACH Direct Debit
const { error, paymentMethod } = await stripe.createPaymentMethod({
  type: 'us_bank_account',
  us_bank_account: {
    account_holder_type: 'individual',
    routing_number: '110000000',
    account_number: '000123456789',
  },
  billing_details: {
    name: customerName,
    email: customerEmail,
  },
});

// Verify micro-deposits
const { error } = await stripe.verifyMicrodeposits(
  paymentIntentId,
  { amounts: [32, 45] } // Customer enters amounts
);

Buy Now, Pay Later (BNPL)

// Klarna integration
const { error } = await stripe.confirmKlarnaPayment(
  clientSecret,
  {
    payment_method: {
      billing_details: {
        address: {
          country: 'US',
        },
        email: customerEmail,
      },
    },
    return_url: `${window.location.origin}/checkout/success`,
  }
);

// Afterpay/Clearpay
const { error } = await stripe.confirmAfterpayClearpayPayment(
  clientSecret,
  {
    payment_method: {
      billing_details: {
        email: customerEmail,
        name: customerName,
        address: billingAddress,
      },
    },
    return_url: `${window.location.origin}/checkout/success`,
  }
);

3D Secure Authentication

Implementation

// Handle 3D Secure authentication
export async function handle3DSecure(
  clientSecret: string,
  paymentMethodId: string
) {
  const stripe = await loadStripe(process.env.NEXT_PUBLIC_STRIPE_PUBLIC_KEY!);

  const { error, paymentIntent } = await stripe.confirmCardPayment(
    clientSecret,
    {
      payment_method: paymentMethodId,
      return_url: `${window.location.origin}/checkout/3ds-redirect`,
    }
  );

  if (error) {
    if (error.code === 'payment_intent_authentication_failure') {
      // Authentication failed
      return { error: '3D Secure authentication failed' };
    }
    return { error: error.message };
  }

  if (paymentIntent.status === 'succeeded') {
    return { success: true, paymentIntent };
  } else if (paymentIntent.status === 'requires_action') {
    // Additional authentication required
    const { error: confirmError } = await stripe.confirmCardPayment(
      clientSecret
    );

    if (confirmError) {
      return { error: confirmError.message };
    }

    return { success: true, paymentIntent };
  }
}

3DS Redirect Handler

Create app/checkout/3ds-redirect/page.tsx:

'use client';

import { useEffect, useState } from 'react';
import { useSearchParams, useRouter } from 'next/navigation';
import { loadStripe } from '@stripe/stripe-js';

export default function ThreeDSRedirect() {
  const searchParams = useSearchParams();
  const router = useRouter();
  const [status, setStatus] = useState<'processing' | 'success' | 'error'>('processing');

  useEffect(() => {
    const handleRedirect = async () => {
      const clientSecret = searchParams.get('payment_intent_client_secret');

      if (!clientSecret) {
        setStatus('error');
        return;
      }

      const stripe = await loadStripe(process.env.NEXT_PUBLIC_STRIPE_PUBLIC_KEY!);
      if (!stripe) {
        setStatus('error');
        return;
      }

      const { paymentIntent } = await stripe.retrievePaymentIntent(clientSecret);

      if (paymentIntent?.status === 'succeeded') {
        // Update order status
        await fetch('/api/payments/confirm', {
          method: 'POST',
          headers: { 'Content-Type': 'application/json' },
          body: JSON.stringify({ paymentIntentId: paymentIntent.id }),
        });

        setStatus('success');
        router.push('/checkout/success');
      } else {
        setStatus('error');
      }
    };

    handleRedirect();
  }, [searchParams, router]);

  return (
    <div className="min-h-screen flex items-center justify-center">
      {status === 'processing' && (
        <div className="text-center">
          <div className="animate-spin rounded-full h-12 w-12 border-b-2 border-blue-600 mx-auto" />
          <p className="mt-4">Processing your payment...</p>
        </div>
      )}

      {status === 'error' && (
        <div className="text-center">
          <p className="text-red-600">Payment authentication failed</p>
          <a href="/checkout" className="mt-4 text-blue-600 underline">
            Return to checkout
          </a>
        </div>
      )}
    </div>
  );
}

Webhooks

Step 1: Create Webhook Handler

Create app/api/webhooks/stripe/route.ts:

import { NextRequest, NextResponse } from 'next/server';
import { stripe, validateWebhookSignature } from '@/lib/stripe';
import { sdk } from '@/lib/api-client';

export async function POST(request: NextRequest) {
  const payload = await request.text();
  const signature = request.headers.get('stripe-signature')!;

  try {
    // Validate webhook signature
    const event = validateWebhookSignature(
      payload,
      signature,
      process.env.STRIPE_WEBHOOK_SECRET!
    );

    // Handle different event types
    switch (event.type) {
      case 'payment_intent.succeeded':
        await handlePaymentSuccess(event.data.object);
        break;

      case 'payment_intent.payment_failed':
        await handlePaymentFailure(event.data.object);
        break;

      case 'charge.refunded':
        await handleRefund(event.data.object);
        break;

      case 'charge.dispute.created':
        await handleDispute(event.data.object);
        break;

      case 'customer.subscription.created':
        await handleSubscriptionCreated(event.data.object);
        break;

      case 'customer.subscription.deleted':
        await handleSubscriptionCanceled(event.data.object);
        break;

      case 'invoice.payment_failed':
        await handleInvoiceFailure(event.data.object);
        break;

      default:
        console.log(`Unhandled event type: ${event.type}`);
    }

    return NextResponse.json({ received: true });
  } catch (error) {
    console.error('Webhook error:', error);
    return NextResponse.json(
      { error: 'Webhook processing failed' },
      { status: 400 }
    );
  }
}

async function handlePaymentSuccess(paymentIntent: any) {
  const orderId = paymentIntent.metadata.orderId;

  // Update order status
  await sdk.updateOrder({
    id: orderId,
    input: {
      status: 'PROCESSING',
      paymentStatus: 'PAID',
      paidAt: new Date().toISOString(),
      stripePaymentIntentId: paymentIntent.id,
    }
  });

  // Send confirmation email
  await sdk.sendOrderConfirmation({ orderId });

  // Trigger inventory update
  await sdk.reserveInventoryForOrder({ orderId });
}

async function handlePaymentFailure(paymentIntent: any) {
  const orderId = paymentIntent.metadata.orderId;

  // Update order status
  await sdk.updateOrder({
    id: orderId,
    input: {
      status: 'PAYMENT_FAILED',
      paymentStatus: 'FAILED',
      paymentError: paymentIntent.last_payment_error?.message,
    }
  });

  // Send failure notification
  await sdk.sendPaymentFailureNotification({ orderId });
}

async function handleRefund(charge: any) {
  const orderId = charge.metadata.orderId;
  const refundAmount = charge.amount_refunded;

  // Create refund record
  await sdk.createRefund({
    input: {
      orderId,
      amount: refundAmount,
      reason: charge.refunds.data[0]?.reason || 'requested_by_customer',
      stripeRefundId: charge.refunds.data[0]?.id,
      status: 'COMPLETED',
    }
  });

  // Update order status
  await sdk.updateOrder({
    id: orderId,
    input: {
      status: refundAmount === charge.amount ? 'REFUNDED' : 'PARTIALLY_REFUNDED',
      refundedAmount: refundAmount,
    }
  });
}

async function handleDispute(dispute: any) {
  const orderId = dispute.metadata.orderId;

  // Create dispute record
  await sdk.createDispute({
    input: {
      orderId,
      amount: dispute.amount,
      reason: dispute.reason,
      status: dispute.status,
      stripeDisputeId: dispute.id,
      evidence_due_by: dispute.evidence_details.due_by,
    }
  });

  // Notify merchant
  await sdk.sendDisputeNotification({
    orderId,
    disputeReason: dispute.reason,
  });
}

Step 2: Configure Webhook Endpoint

In Stripe Dashboard:

  1. Go to Developers → Webhooks
  2. Add endpoint: https://yoursite.com/api/webhooks/stripe
  3. Select events to listen for:
    • payment_intent.succeeded
    • payment_intent.payment_failed
    • charge.refunded
    • charge.dispute.created
    • Add others as needed
  4. Copy the webhook signing secret

Refunds & Disputes

Processing Refunds

// Full refund
export async function processFullRefund(paymentIntentId: string) {
  try {
    const refund = await stripe.refunds.create({
      payment_intent: paymentIntentId,
      reason: 'requested_by_customer',
    });

    return { success: true, refund };
  } catch (error) {
    console.error('Refund failed:', error);
    throw error;
  }
}

// Partial refund
export async function processPartialRefund(
  paymentIntentId: string,
  amount: number
) {
  try {
    const refund = await stripe.refunds.create({
      payment_intent: paymentIntentId,
      amount: amount, // Amount in cents
      reason: 'requested_by_customer',
    });

    return { success: true, refund };
  } catch (error) {
    console.error('Partial refund failed:', error);
    throw error;
  }
}

Handling Disputes

// Submit dispute evidence
export async function submitDisputeEvidence(
  disputeId: string,
  evidence: {
    customer_communication?: string;
    receipt?: string;
    shipping_documentation?: string;
    refund_policy?: string;
    [key: string]: any;
  }
) {
  try {
    const dispute = await stripe.disputes.update(disputeId, {
      evidence: evidence,
      submit: true, // Auto-submit when ready
    });

    return { success: true, dispute };
  } catch (error) {
    console.error('Failed to submit dispute evidence:', error);
    throw error;
  }
}

// Close dispute (accept loss)
export async function closeDispute(disputeId: string) {
  try {
    const dispute = await stripe.disputes.close(disputeId);
    return { success: true, dispute };
  } catch (error) {
    console.error('Failed to close dispute:', error);
    throw error;
  }
}

Testing

Test Card Numbers

// Test cards for different scenarios
const TEST_CARDS = {
  // Successful payment
  SUCCESS: '4242424242424242',
  SUCCESS_3DS: '4000002500003155', // Requires 3D Secure

  // Declined cards
  DECLINED_GENERIC: '4000000000000002',
  DECLINED_INSUFFICIENT_FUNDS: '4000000000009995',
  DECLINED_LOST_CARD: '4000000000009987',
  DECLINED_EXPIRED: '4000000000000069',

  // Specific errors
  INCORRECT_CVC: '4000000000000127',
  PROCESSING_ERROR: '4000000000000119',
  INCORRECT_NUMBER: '4242424242424241',

  // International cards
  CANADA: '4000001240000000',
  UK: '4000008260000000',
  JAPAN: '4000003920000003',
  AUSTRALIA: '4000000360000006',
};

// Test bank accounts (ACH)
const TEST_BANK_ACCOUNTS = {
  SUCCESS: {
    routing: '110000000',
    account: '000123456789',
  },
  FAIL: {
    routing: '110000000',
    account: '000111111116', // Will fail verification
  },
};

Test Scenarios

// Test helper functions
export async function runPaymentTests() {
  const tests = [
    {
      name: 'Successful payment',
      card: TEST_CARDS.SUCCESS,
      expectedStatus: 'succeeded',
    },
    {
      name: '3D Secure required',
      card: TEST_CARDS.SUCCESS_3DS,
      expectedStatus: 'requires_action',
    },
    {
      name: 'Insufficient funds',
      card: TEST_CARDS.DECLINED_INSUFFICIENT_FUNDS,
      expectedError: 'insufficient_funds',
    },
    {
      name: 'Expired card',
      card: TEST_CARDS.DECLINED_EXPIRED,
      expectedError: 'expired_card',
    },
  ];

  for (const test of tests) {
    console.log(`Running test: ${test.name}`);

    try {
      const result = await testPayment(test.card);

      if (test.expectedStatus) {
        assert(result.status === test.expectedStatus);
      }

      if (test.expectedError) {
        assert(result.error?.code === test.expectedError);
      }

      console.log(`✓ ${test.name} passed`);
    } catch (error) {
      console.error(`✗ ${test.name} failed:`, error);
    }
  }
}

Webhook Testing

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

# Login to your account
stripe login

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

# Trigger test events
stripe trigger payment_intent.succeeded
stripe trigger payment_intent.payment_failed
stripe trigger charge.refunded

Production Checklist

Security

  • Use environment variables for all keys
  • Enable webhook signature verification
  • Implement rate limiting on payment endpoints
  • Add CSRF protection
  • Use HTTPS everywhere
  • Implement PCI compliance measures
  • Add fraud detection rules

Configuration

  • Set up production Stripe keys
  • Configure webhook endpoints
  • Enable Stripe Radar for fraud prevention
  • Set up email receipts
  • Configure statement descriptors
  • Set up tax collection (if applicable)
  • Configure currency support

Monitoring

  • Set up payment failure alerts
  • Monitor webhook delivery
  • Track conversion rates
  • Monitor for duplicate payments
  • Set up dispute alerts
  • Track refund rates
  • Monitor API errors

Implementation

// Production configuration
const PRODUCTION_CONFIG = {
  // Enable strict security
  requireCVV: true,
  require3DSecure: 'recommended', // or 'required'

  // Fraud prevention
  enableRadar: true,
  blockHighRiskPayments: true,
  requireEmailVerification: true,

  // Payment limits
  minimumAmount: 100, // $1.00
  maximumAmount: 999999, // $9,999.99

  // Retry configuration
  maxRetries: 3,
  retryDelay: 1000, // ms

  // Webhook configuration
  webhookTimeout: 20000, // 20 seconds
  webhookMaxRetries: 5,
};

Troubleshooting

Common Issues

1. Payment Intent Creation Fails

Problem: "Amount must be at least $0.50" Solution:

// Ensure minimum amount
const amount = Math.max(50, calculatedAmount); // Minimum 50 cents

2. 3D Secure Not Triggering

Problem: 3D Secure not showing for EU cards Solution:

// Force 3D Secure for EU cards
const paymentIntent = await stripe.paymentIntents.create({
  amount: 2000,
  currency: 'eur',
  payment_method_options: {
    card: {
      request_three_d_secure: 'any', // or 'automatic'
    },
  },
});

3. Webhook Signature Verification Fails

Problem: "No signatures found matching the expected signature" Solution:

// Use raw body for signature verification
export const config = {
  api: {
    bodyParser: false, // Important!
  },
};

// Get raw body
const buffer = await request.arrayBuffer();
const rawBody = Buffer.from(buffer);

4. Duplicate Payments

Problem: Customer charged multiple times Solution:

// Implement idempotency
const paymentIntent = await stripe.paymentIntents.create({
  amount: 2000,
  currency: 'usd',
}, {
  idempotencyKey: `order_${orderId}_${timestamp}`,
});

5. Currency Conversion Issues

Problem: Incorrect currency amounts Solution:

// Handle zero-decimal currencies
const ZERO_DECIMAL_CURRENCIES = ['JPY', 'KRW', 'VND', 'CLP'];

function toStripeAmount(amount: number, currency: string): number {
  if (ZERO_DECIMAL_CURRENCIES.includes(currency.toUpperCase())) {
    return Math.round(amount); // Already in smallest unit
  }
  return Math.round(amount * 100); // Convert to cents
}

Best Practices

  1. Always validate amounts server-side
  2. Implement proper error handling and recovery
  3. Use idempotency keys to prevent duplicate charges
  4. Store all payment metadata for reconciliation
  5. Implement webhook retry logic
  6. Use Stripe's testing tools extensively
  7. Monitor payment success rates
  8. Implement proper logging for debugging
  9. Keep Stripe SDK updated
  10. Follow PCI compliance guidelines

Next Steps

  • Implement subscription payments
  • Add support for multiple currencies
  • Set up advanced fraud rules
  • Implement payment links
  • Add support for local payment methods
  • Set up Connect for marketplace payments
  • Implement Stripe Terminal for in-person payments

Resources

Was this page helpful?