← Back to Blog

Implementing Stripe Payments: A Complete Guide

Stripe has become the de facto standard for online payments, offering a comprehensive platform that handles everything from simple one-time payments to complex subscription billing. This guide walks you through implementing Stripe payments in your web application, covering best practices, security considerations, and real-world code examples.

Why Choose Stripe?

Getting Started: Initial Setup

1. Create a Stripe Account

Sign up at stripe.com and get your API keys. You'll have test keys for development and live keys for production.

2. Install Stripe SDK

# For Node.js backend
npm install stripe

# For frontend (Stripe.js)
npm install @stripe/stripe-js

# For React (optional)
npm install @stripe/react-stripe-js

3. Initialize Stripe

// Backend (Node.js)
import Stripe from 'stripe';

const stripe = new Stripe(process.env.STRIPE_SECRET_KEY, {
  apiVersion: '2023-10-16',
});

// Frontend
import { loadStripe } from '@stripe/stripe-js';

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

Security Note

Never expose your secret key! Use environment variables and only use the publishable key on the frontend. Your secret key should only exist on your backend server.

Implementing One-Time Payments

Payment Flow Overview

  1. Customer enters payment details on your frontend
  2. Frontend creates a PaymentIntent on your backend
  3. Backend returns client secret to frontend
  4. Frontend confirms payment with Stripe
  5. Webhook notifies your backend of payment success

Backend: Create Payment Intent

// pages/api/create-payment-intent.js
import Stripe from 'stripe';

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

export default async function handler(req, res) {
  if (req.method !== 'POST') {
    return res.status(405).json({ error: 'Method not allowed' });
  }

  try {
    const { amount, currency = 'usd', metadata = {} } = req.body;

    // Create a PaymentIntent
    const paymentIntent = await stripe.paymentIntents.create({
      amount: amount * 100, // Convert to cents
      currency,
      metadata, // Store custom data
      automatic_payment_methods: {
        enabled: true,
      },
    });

    res.status(200).json({
      clientSecret: paymentIntent.client_secret,
    });
  } catch (error) {
    console.error('Payment Intent Error:', error);
    res.status(500).json({ error: error.message });
  }
}

Frontend: Payment Form with Stripe Elements

// components/CheckoutForm.jsx
import React, { useState } from 'react';
import {
  PaymentElement,
  useStripe,
  useElements
} from '@stripe/react-stripe-js';

export default function CheckoutForm() {
  const stripe = useStripe();
  const elements = useElements();
  const [error, setError] = useState(null);
  const [processing, setProcessing] = useState(false);

  const handleSubmit = async (event) => {
    event.preventDefault();

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

    setProcessing(true);

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

    if (error) {
      setError(error.message);
      setProcessing(false);
    }
    // If successful, user will be redirected to return_url
  };

  return (
    
{error &&
{error}
} ); }

Complete Checkout Page

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

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

export default function CheckoutPage() {
  const [clientSecret, setClientSecret] = useState('');

  useEffect(() => {
    // Create PaymentIntent on mount
    fetch('/api/create-payment-intent', {
      method: 'POST',
      headers: { 'Content-Type': 'application/json' },
      body: JSON.stringify({
        amount: 50, // $50.00
        metadata: {
          orderId: '12345',
          userId: 'user_abc',
        },
      }),
    })
      .then((res) => res.json())
      .then((data) => setClientSecret(data.clientSecret));
  }, []);

  return (
    

Checkout

{clientSecret && ( )}
); }

Implementing Subscriptions

Subscription Flow

  1. Create products and prices in Stripe Dashboard
  2. Create a customer in Stripe
  3. Create a subscription with payment method
  4. Listen to subscription events via webhooks

Backend: Create Subscription

// pages/api/create-subscription.js
import Stripe from 'stripe';

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

export default async function handler(req, res) {
  if (req.method !== 'POST') {
    return res.status(405).json({ error: 'Method not allowed' });
  }

  try {
    const { email, priceId, paymentMethodId } = req.body;

    // 1. Create or retrieve customer
    const customer = await stripe.customers.create({
      email,
      payment_method: paymentMethodId,
      invoice_settings: {
        default_payment_method: paymentMethodId,
      },
    });

    // 2. Create subscription
    const subscription = await stripe.subscriptions.create({
      customer: customer.id,
      items: [{ price: priceId }],
      payment_settings: {
        payment_method_types: ['card'],
        save_default_payment_method: 'on_subscription',
      },
      expand: ['latest_invoice.payment_intent'],
    });

    res.status(200).json({
      subscriptionId: subscription.id,
      clientSecret: subscription.latest_invoice.payment_intent.client_secret,
    });
  } catch (error) {
    console.error('Subscription Error:', error);
    res.status(500).json({ error: error.message });
  }
}

Managing Subscription Tiers

// pages/api/change-subscription.js
export default async function handler(req, res) {
  const { subscriptionId, newPriceId } = req.body;

  try {
    // Get current subscription
    const subscription = await stripe.subscriptions.retrieve(subscriptionId);

    // Update to new price
    const updatedSubscription = await stripe.subscriptions.update(
      subscriptionId,
      {
        items: [{
          id: subscription.items.data[0].id,
          price: newPriceId,
        }],
        proration_behavior: 'create_prorations', // Pro-rate the difference
      }
    );

    res.status(200).json(updatedSubscription);
  } catch (error) {
    res.status(500).json({ error: error.message });
  }
}

Cancel Subscription

// pages/api/cancel-subscription.js
export default async function handler(req, res) {
  const { subscriptionId, cancelAtPeriodEnd = true } = req.body;

  try {
    const subscription = await stripe.subscriptions.update(subscriptionId, {
      cancel_at_period_end: cancelAtPeriodEnd,
    });

    // Or cancel immediately:
    // const subscription = await stripe.subscriptions.cancel(subscriptionId);

    res.status(200).json(subscription);
  } catch (error) {
    res.status(500).json({ error: error.message });
  }
}

Webhook Implementation

Webhooks are critical for receiving real-time updates about payments, subscriptions, and disputes. Never rely solely on client-side success notifications.

Setting Up Webhooks

// pages/api/webhooks/stripe.js
import Stripe from 'stripe';
import { buffer } from 'micro';

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

// Disable body parsing for webhooks
export const config = {
  api: {
    bodyParser: false,
  },
};

export default async function handler(req, res) {
  if (req.method !== 'POST') {
    return res.status(405).json({ error: 'Method not allowed' });
  }

  const buf = await buffer(req);
  const sig = req.headers['stripe-signature'];

  let event;

  try {
    // Verify webhook signature
    event = stripe.webhooks.constructEvent(buf, sig, webhookSecret);
  } catch (err) {
    console.error('Webhook signature verification failed:', err.message);
    return res.status(400).send(`Webhook Error: ${err.message}`);
  }

  // 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 'customer.subscription.created':
      await handleSubscriptionCreated(event.data.object);
      break;

    case 'customer.subscription.updated':
      await handleSubscriptionUpdated(event.data.object);
      break;

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

    case 'invoice.payment_succeeded':
      await handleInvoicePayment(event.data.object);
      break;

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

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

  res.status(200).json({ received: true });
}

async function handlePaymentSuccess(paymentIntent) {
  console.log('Payment succeeded:', paymentIntent.id);

  // Update your database
  // Send confirmation email
  // Fulfill the order
}

async function handleSubscriptionCreated(subscription) {
  console.log('Subscription created:', subscription.id);

  // Update user's subscription status in database
  // Grant access to premium features
  // Send welcome email
}

async function handleInvoicePayment(invoice) {
  console.log('Invoice paid:', invoice.id);

  // Extend subscription period
  // Send receipt email
}

Webhook Testing

Use the Stripe CLI to test webhooks locally:

stripe listen --forward-to localhost:3000/api/webhooks/stripe

Security Best Practices

1. Never Trust the Client

Always verify payment status on your backend using webhooks. Clients can manipulate frontend code.

// DON'T DO THIS
// Frontend (insecure)
const handleSuccess = () => {
  // Client could fake this!
  grantPremiumAccess();
};

// DO THIS INSTEAD
// Backend webhook (secure)
async function handlePaymentSuccess(paymentIntent) {
  // Verify payment in webhook
  await grantPremiumAccess(paymentIntent.metadata.userId);
}

2. Use Idempotency Keys

// Prevent duplicate charges
const paymentIntent = await stripe.paymentIntents.create({
  amount: 5000,
  currency: 'usd',
}, {
  idempotencyKey: `order_${orderId}`,
});

3. Validate Webhook Signatures

Always verify that webhook events came from Stripe using the signature verification shown earlier.

4. Store Minimal Card Data

Never store card numbers or CVV codes. Use Stripe's tokens and customer IDs instead.

5. Implement Rate Limiting

// Example with express-rate-limit
import rateLimit from 'express-rate-limit';

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

app.post('/api/create-payment-intent', paymentLimiter, async (req, res) => {
  // Handle payment
});

Handling Edge Cases

Failed Payments

// Retry failed payments with exponential backoff
async function retryFailedPayment(paymentIntentId) {
  try {
    const paymentIntent = await stripe.paymentIntents.retrieve(paymentIntentId);

    if (paymentIntent.status === 'requires_payment_method') {
      // Notify customer to update payment method
      await sendPaymentFailureEmail(paymentIntent.customer);
    }
  } catch (error) {
    console.error('Retry failed:', error);
  }
}

Disputed Charges

// Handle disputes via webhook
case 'charge.dispute.created':
  const dispute = event.data.object;

  // Gather evidence
  await stripe.disputes.update(dispute.id, {
    evidence: {
      customer_name: 'John Doe',
      customer_signature: 'https://example.com/signature.png',
      receipt: 'https://example.com/receipt.pdf',
    },
  });
  break;

Refunds

// pages/api/refund.js
export default async function handler(req, res) {
  const { paymentIntentId, amount, reason } = req.body;

  try {
    const refund = await stripe.refunds.create({
      payment_intent: paymentIntentId,
      amount, // Optional: partial refund
      reason, // 'duplicate', 'fraudulent', or 'requested_by_customer'
    });

    res.status(200).json(refund);
  } catch (error) {
    res.status(500).json({ error: error.message });
  }
}

Testing Your Integration

Test Card Numbers

Use any future expiration date and any 3-digit CVC.

Testing Checklist

Going Live

Pre-Launch Checklist

  1. Complete Stripe account verification
  2. Switch from test keys to live keys
  3. Configure live webhook endpoints
  4. Test with real cards (small amounts)
  5. Set up monitoring and alerts
  6. Review PCI compliance requirements
  7. Configure tax settings if needed
  8. Set up customer support for payment issues

Monitoring and Optimization

Conclusion

Implementing Stripe payments doesn't have to be complicated. By following this guide and security best practices, you can create a robust payment system that handles one-time payments, subscriptions, and edge cases gracefully.

Remember to always verify payments on the backend using webhooks, never trust client-side data, and test thoroughly before going live. Stripe's excellent documentation and developer tools make it easier than ever to build production-ready payment systems.

Need Help Implementing Stripe Payments?

Yonda Solutions specializes in integrating payment systems into web and mobile applications. We can help you implement Stripe payments securely and efficiently, from basic checkout flows to complex subscription billing. Contact us today for a free consultation.