Skip to main content
Back to Blog

E-commerce Payment Integration: A Complete Guide

10 min read
By Eric Mitton
E-commercePaymentsStripeWeb Development

Payment processing is the critical component of any e-commerce platform. Get it wrong, and you lose sales, expose yourself to fraud, or face compliance issues. Get it right, and payments become a seamless, trustworthy part of your customer experience. This guide provides comprehensive, practical guidance for implementing payment processing.

Choosing a Payment Processor

Your payment processor handles the complex task of moving money securely from customers to your business.

Major Payment Processors

Stripe: Most developer-friendly option.

  • Excellent documentation and APIs
  • Comprehensive feature set
  • Good for international business
  • Pricing: 2.9% + $0.30 per transaction

PayPal: Ubiquitous and trusted.

  • High customer recognition
  • Simple integration options
  • Good for marketplaces
  • Pricing: 2.9% + $0.30 per transaction

Square: Best for businesses with physical and online presence.

  • Unified point-of-sale and online payments
  • Good hardware options
  • Simple pricing
  • Pricing: 2.9% + $0.30 per online transaction

Braintree: Good for flexibility and PayPal integration.

  • Owned by PayPal
  • Supports multiple payment methods
  • Good for high-volume businesses
  • Pricing: 2.9% + $0.30 per transaction

Selection Criteria

Geographic coverage: Ensure the processor supports your target markets.

Supported payment methods: Credit cards, digital wallets, bank transfers, local payment methods.

Technical capabilities: APIs, webhooks, hosted pages vs. custom integration.

Settlement timing: How quickly funds reach your account (typically 2-7 days).

Fees: Transaction fees, monthly fees, chargeback fees, international fees.

Compliance support: PCI DSS compliance handling, SCA support, tax calculation.

Understanding PCI DSS Compliance

Payment Card Industry Data Security Standard (PCI DSS) is mandatory for handling credit card data.

Compliance Levels

Level 1: 6+ million transactions per year Level 2: 1-6 million transactions per year Level 3: 20,000-1 million e-commerce transactions per year Level 4: <20,000 e-commerce transactions per year

Most small businesses are Level 4, requiring annual self-assessment questionnaire (SAQ).

Reducing PCI Scope

The less card data you handle, the easier compliance becomes.

Best approach: Never touch card data directly. Use:

  • Hosted payment pages (Stripe Checkout, PayPal buttons)
  • Tokenization (card data never reaches your servers)
  • Payment Request API (browser handles card data)

This reduces PCI scope to SAQ-A, the simplest questionnaire.

Security Requirements

Even with reduced scope:

  • Use HTTPS everywhere
  • Maintain secure network
  • Implement access controls
  • Regularly update and patch systems
  • Monitor and test networks

Stripe Integration: A Complete Example

Stripe provides excellent developer experience. Here's a comprehensive implementation.

Setup and Configuration

Install Stripe SDK:

npm install stripe @stripe/stripe-js
# or
pnpm add stripe @stripe/stripe-js

Create environment variables:

# .env.local
STRIPE_SECRET_KEY=sk_test_...
NEXT_PUBLIC_STRIPE_PUBLISHABLE_KEY=pk_test_...
STRIPE_WEBHOOK_SECRET=whsec_...

Backend: Creating Payment Intent

// app/api/create-payment-intent/route.ts
import Stripe from 'stripe';
import { NextRequest, NextResponse } from 'next/server';

const stripe = new Stripe(process.env.STRIPE_SECRET_KEY!, {
  apiVersion: '2024-11-20.acacia',
});

export async function POST(request: NextRequest) {
  try {
    const { amount, currency = 'usd', metadata } = await request.json();

    // Validate amount
    if (!amount || amount < 50) { // Stripe minimum is $0.50
      return NextResponse.json(
        { error: 'Invalid amount' },
        { status: 400 }
      );
    }

    // Create payment intent
    const paymentIntent = await stripe.paymentIntents.create({
      amount: Math.round(amount), // Amount in cents
      currency,
      metadata: {
        orderId: metadata?.orderId,
        customerId: metadata?.customerId,
      },
      automatic_payment_methods: {
        enabled: true,
      },
    });

    return NextResponse.json({
      clientSecret: paymentIntent.client_secret,
    });
  } catch (error) {
    console.error('Payment intent creation error:', error);
    return NextResponse.json(
      { error: 'Payment processing error' },
      { status: 500 }
    );
  }
}

Frontend: Payment Form

// components/CheckoutForm.tsx
'use client';

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

export default function CheckoutForm({ amount }: { amount: number }) {
  const stripe = useStripe();
  const elements = useElements();
  const [loading, setLoading] = useState(false);
  const [error, setError] = useState<string | null>(null);

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

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

    setLoading(true);
    setError(null);

    const { error: submitError } = await stripe.confirmPayment({
      elements,
      confirmParams: {
        return_url: `${window.location.origin}/payment-success`,
      },
    });

    if (submitError) {
      setError(submitError.message || 'An error occurred');
      setLoading(false);
    }
  };

  return (
    <form onSubmit={handleSubmit} className="space-y-6">
      <div>
        <h3 className="text-lg font-semibold mb-4">
          Payment Details
        </h3>
        <PaymentElement />
      </div>

      {error && (
        <div className="bg-red-50 border border-red-200 text-red-600 px-4 py-3 rounded">
          {error}
        </div>
      )}

      <button
        type="submit"
        disabled={!stripe || loading}
        className="w-full bg-blue-600 text-white py-3 px-4 rounded-lg font-semibold hover:bg-blue-700 disabled:bg-gray-400 disabled:cursor-not-allowed"
      >
        {loading ? 'Processing...' : `Pay $${(amount / 100).toFixed(2)}`}
      </button>
    </form>
  );
}

Wrapping with Stripe Provider

// components/CheckoutWrapper.tsx
'use client';

import { Elements } from '@stripe/react-stripe-js';
import { loadStripe } from '@stripe/stripe-js';
import { useState, useEffect } from 'react';
import CheckoutForm from './CheckoutForm';

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

export default function CheckoutWrapper({ amount }: { amount: number }) {
  const [clientSecret, setClientSecret] = useState('');

  useEffect(() => {
    // Create payment intent when component mounts
    fetch('/api/create-payment-intent', {
      method: 'POST',
      headers: { 'Content-Type': 'application/json' },
      body: JSON.stringify({ amount }),
    })
      .then((res) => res.json())
      .then((data) => setClientSecret(data.clientSecret));
  }, [amount]);

  return (
    <>
      {clientSecret && (
        <Elements
          stripe={stripePromise}
          options={{
            clientSecret,
            appearance: {
              theme: 'stripe',
              variables: {
                colorPrimary: '#0066cc',
              },
            },
          }}
        >
          <CheckoutForm amount={amount} />
        </Elements>
      )}
    </>
  );
}

Webhook Integration

Webhooks notify your application of payment events asynchronously.

Setting Up Webhooks

// app/api/webhooks/stripe/route.ts
import { headers } from 'next/headers';
import { NextRequest, NextResponse } from 'next/server';
import Stripe from 'stripe';

const stripe = new Stripe(process.env.STRIPE_SECRET_KEY!);
const webhookSecret = process.env.STRIPE_WEBHOOK_SECRET!;

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

  let event: Stripe.Event;

  try {
    event = stripe.webhooks.constructEvent(
      body,
      signature,
      webhookSecret
    );
  } catch (err) {
    console.error('Webhook signature verification failed:', err);
    return NextResponse.json(
      { error: 'Invalid signature' },
      { status: 400 }
    );
  }

  // Handle the event
  switch (event.type) {
    case 'payment_intent.succeeded':
      const paymentIntent = event.data.object as Stripe.PaymentIntent;
      await handlePaymentSuccess(paymentIntent);
      break;

    case 'payment_intent.payment_failed':
      const failedPayment = event.data.object as Stripe.PaymentIntent;
      await handlePaymentFailure(failedPayment);
      break;

    case 'charge.refunded':
      const refund = event.data.object as Stripe.Charge;
      await handleRefund(refund);
      break;

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

  return NextResponse.json({ received: true });
}

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

  // Update order status in database
  await database.orders.update(orderId, {
    status: 'paid',
    paymentIntentId: paymentIntent.id,
    paidAt: new Date(),
  });

  // Send confirmation email
  await sendOrderConfirmation(orderId);

  // Trigger fulfillment process
  await initiateOrderFulfillment(orderId);
}

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

  await database.orders.update(orderId, {
    status: 'payment_failed',
    failureReason: paymentIntent.last_payment_error?.message,
  });

  // Notify customer
  await sendPaymentFailureNotification(orderId);
}

async function handleRefund(charge: Stripe.Charge) {
  // Find order by charge ID
  const order = await database.orders.findByChargeId(charge.id);

  if (order) {
    await database.orders.update(order.id, {
      status: 'refunded',
      refundedAt: new Date(),
    });

    await sendRefundConfirmation(order.id);
  }
}

Webhook Testing

Test webhooks locally with Stripe CLI:

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

# Login
stripe login

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

# Test specific event
stripe trigger payment_intent.succeeded

Security Best Practices

Payment processing requires rigorous security measures.

Server-Side Validation

Never trust client-side data:

// Always validate on server
export async function POST(request: NextRequest) {
  const { amount, items } = await request.json();

  // Recalculate amount server-side
  const calculatedAmount = items.reduce(
    (total, item) => total + item.price * item.quantity,
    0
  );

  // Verify amounts match
  if (Math.abs(amount - calculatedAmount) > 0.01) {
    return NextResponse.json(
      { error: 'Amount mismatch' },
      { status: 400 }
    );
  }

  // Proceed with payment
  // ...
}

Idempotency

Prevent duplicate charges from retries:

const paymentIntent = await stripe.paymentIntents.create(
  {
    amount,
    currency: 'usd',
  },
  {
    idempotencyKey: `order_${orderId}_${Date.now()}`,
  }
);

Rate Limiting

Prevent abuse:

import rateLimit from 'express-rate-limit';

const limiter = rateLimit({
  windowMs: 15 * 60 * 1000, // 15 minutes
  max: 5, // 5 payment attempts per window
  message: 'Too many payment attempts, please try again later',
});

app.use('/api/create-payment-intent', limiter);

Error Handling

Handle errors gracefully without exposing sensitive information:

try {
  const paymentIntent = await stripe.paymentIntents.create(data);
  return { success: true, clientSecret: paymentIntent.client_secret };
} catch (error) {
  // Log full error server-side
  console.error('Payment intent creation failed:', error);

  // Return safe error to client
  return {
    success: false,
    error: 'Unable to process payment. Please try again.',
  };
}

Handling Common Scenarios

Subscriptions

// Create subscription
const subscription = await stripe.subscriptions.create({
  customer: customerId,
  items: [{ price: 'price_monthly_premium' }],
  payment_behavior: 'default_incomplete',
  payment_settings: { save_default_payment_method: 'on_subscription' },
  expand: ['latest_invoice.payment_intent'],
});

// Get client secret for setup
const clientSecret = subscription.latest_invoice.payment_intent.client_secret;

Refunds

// Full refund
await stripe.refunds.create({
  payment_intent: paymentIntentId,
});

// Partial refund
await stripe.refunds.create({
  payment_intent: paymentIntentId,
  amount: 1000, // $10.00 in cents
  reason: 'requested_by_customer',
});

Multi-Currency Support

const supportedCurrencies = ['usd', 'eur', 'gbp', 'cad'];

function getCurrencyForCountry(countryCode: string): string {
  const currencyMap = {
    US: 'usd',
    GB: 'gbp',
    EU: 'eur',
    CA: 'cad',
  };
  return currencyMap[countryCode] || 'usd';
}

const paymentIntent = await stripe.paymentIntents.create({
  amount: convertToMinorUnits(amount, currency),
  currency: getCurrencyForCountry(userCountry),
});

Tax Calculation

// Using Stripe Tax
const paymentIntent = await stripe.paymentIntents.create({
  amount,
  currency: 'usd',
  automatic_tax: { enabled: true },
  metadata: {
    tax_calculation: 'automatic',
  },
});

Testing Payments

Thorough testing prevents production issues.

Test Card Numbers

Stripe provides test cards for various scenarios:

Success: 4242 4242 4242 4242
Decline: 4000 0000 0000 0002
Insufficient funds: 4000 0000 0000 9995
3D Secure required: 4000 0025 0000 3155

Test Scenarios

  • Successful payment
  • Declined card
  • Insufficient funds
  • Expired card
  • Incorrect CVC
  • 3D Secure authentication
  • Webhook delivery
  • Refund processing
  • Subscription creation and cancellation
  • Currency conversion

Automated Testing

// vitest test example
import { describe, it, expect, vi } from 'vitest';

describe('Payment Processing', () => {
  it('creates payment intent with correct amount', async () => {
    const response = await fetch('/api/create-payment-intent', {
      method: 'POST',
      body: JSON.stringify({ amount: 5000 }),
    });

    const data = await response.json();
    expect(data).toHaveProperty('clientSecret');
    expect(response.status).toBe(200);
  });

  it('rejects invalid amounts', async () => {
    const response = await fetch('/api/create-payment-intent', {
      method: 'POST',
      body: JSON.stringify({ amount: 25 }), // Below $0.50 minimum
    });

    expect(response.status).toBe(400);
  });
});

Optimization Strategies

Reduce Payment Friction

Save payment methods: Allow returning customers to save cards.

Guest checkout: Don't force account creation.

Mobile optimization: Ensure smooth mobile payment experience.

Multiple payment methods: Support credit cards, digital wallets, local methods.

Improve Conversion

Show security badges: Display SSL, PCI compliance badges.

Clear pricing: No surprises at checkout.

Progress indicators: Show checkout steps clearly.

Error recovery: Provide clear, actionable error messages.

Performance Optimization

Lazy load payment form: Load Stripe.js only when needed.

Prefetch payment intent: Create before user reaches payment step.

Cache static assets: Stripe assets, card brand icons.

Monitoring and Analytics

Track payment performance to identify issues and opportunities.

Key Metrics

Conversion funnel:

  • Cart abandonment rate
  • Checkout start rate
  • Payment submission rate
  • Payment success rate

Payment performance:

  • Average payment processing time
  • Decline rate by card type
  • 3D Secure completion rate
  • Refund rate

Financial metrics:

  • Transaction volume
  • Average transaction value
  • Processing fees
  • Net revenue

Stripe Dashboard

Use Stripe's built-in analytics:

  • Payment success/failure trends
  • Dispute monitoring
  • Customer payment methods
  • Radar fraud detection

Conclusion

Payment integration is complex, but modern solutions like Stripe make it accessible to businesses of all sizes. Key principles for success:

  • Prioritize security and compliance
  • Use hosted solutions to reduce PCI scope
  • Implement comprehensive error handling
  • Test thoroughly with various scenarios
  • Monitor payment performance continuously
  • Optimize for conversion and user experience

Take time to implement payments correctly. The investment in robust payment processing pays dividends through customer trust, reduced fraud, and higher conversion rates.


Need help implementing payment processing for your e-commerce platform? Lifestream Dynamics specializes in secure, optimized payment integrations that drive conversions. Contact us to discuss your payment processing needs.