/stripe-guides

How to validate Stripe payment success on backend?

Learn how to securely validate Stripe payment success on your backend using webhooks, manual checks, idempotency, metadata, and best practices for fraud prevention.

Matt Graham, CEO of Rapid Developers

Book a call with an Expert

Starting a new venture? Need to upgrade your web app? RapidDev builds application with your growth in mind.

Book a free consultation

How to validate Stripe payment success on backend?

How to Validate Stripe Payment Success on Backend

 

In this comprehensive tutorial, I'll guide you through the process of validating Stripe payment success on your backend. Proper validation is crucial to ensure payments are legitimate, prevent fraud, and maintain the integrity of your payment system.

 

Step 1: Set Up Your Stripe Account and SDK

 

First, you need to set up your Stripe account and install the Stripe SDK in your backend application:

For Node.js:


npm install stripe

Initialize Stripe with your secret key:


const stripe = require('stripe')('sk_test_YOUR_SECRET_KEY');

For Python:


pip install stripe

Initialize Stripe:


import stripe
stripe.api_key = "sk_test_YOUR_SECRET\_KEY"

 

Step 2: Create a Webhook Endpoint

 

Webhooks are the most reliable way to validate payments. Create an endpoint in your backend to receive webhook events from Stripe:

Node.js (Express) example:


const express = require('express');
const app = express();

// This is necessary to parse the webhook payload
app.use('/webhook', express.raw({type: 'application/json'}));

app.post('/webhook', async (req, res) => {
  const sig = req.headers['stripe-signature'];
  
  let event;
  
  try {
    // Verify the event came from Stripe using your webhook secret
    event = stripe.webhooks.constructEvent(
      req.body,
      sig,
      'whsec_YOUR_WEBHOOK\_SECRET'
    );
  } catch (err) {
    console.error('Webhook signature verification failed:', err.message);
    return res.status(400).send(`Webhook Error: ${err.message}`);
  }
  
  // Handle the event
  switch (event.type) {
    case 'payment\_intent.succeeded':
      const paymentIntent = event.data.object;
      await handleSuccessfulPayment(paymentIntent);
      break;
    case 'payment_intent.payment_failed':
      const failedPaymentIntent = event.data.object;
      await handleFailedPayment(failedPaymentIntent);
      break;
    // ... handle other event types
    default:
      console.log(`Unhandled event type ${event.type}`);
  }
  
  // Return a 200 response to acknowledge receipt of the event
  res.send();
});

async function handleSuccessfulPayment(paymentIntent) {
  // Implement your business logic here
  // e.g., fulfill the order, update database, send confirmation email
  console.log('Payment succeeded:', paymentIntent.id);
}

async function handleFailedPayment(paymentIntent) {
  // Handle the failed payment
  console.log('Payment failed:', paymentIntent.id);
}

app.listen(3000, () => console.log('Server running on port 3000'));

Python (Flask) example:


from flask import Flask, request, jsonify
import stripe
import json

app = Flask(**name**)

@app.route('/webhook', methods=['POST'])
def webhook():
    payload = request.data
    sig\_header = request.headers.get('Stripe-Signature')
    
    try:
        event = stripe.Webhook.construct\_event(
            payload, sig_header, 'whsec_YOUR_WEBHOOK_SECRET'
        )
    except ValueError as e:
        # Invalid payload
        return jsonify({'error': str(e)}), 400
    except stripe.error.SignatureVerificationError as e:
        # Invalid signature
        return jsonify({'error': str(e)}), 400
    
    # Handle the event
    if event['type'] == 'payment\_intent.succeeded':
        payment\_intent = event\['data']\['object']
        handle_successful_payment(payment\_intent)
    elif event['type'] == 'payment_intent.payment_failed':
        payment\_intent = event\['data']\['object']
        handle_failed_payment(payment\_intent)
    # ... handle other event types
    else:
        print(f'Unhandled event type {event["type"]}')
    
    return jsonify(success=True)

def handle_successful_payment(payment\_intent):
    # Implement your business logic here
    # e.g., fulfill the order, update database, send confirmation email
    print(f'Payment succeeded: {payment\_intent["id"]}')

def handle_failed_payment(payment\_intent):
    # Handle the failed payment
    print(f'Payment failed: {payment\_intent["id"]}')

if **name** == '**main**':
    app.run(port=3000)

 

Step 3: Set Up Webhook in Stripe Dashboard

 

  1. Go to the Stripe Dashboard > Developers > Webhooks
  2. Click "Add endpoint"
  3. Enter your webhook URL (e.g., https://yourdomain.com/webhook)
  4. Select the events you want to listen for (at minimum select "payment_intent.succeeded")
  5. Click "Add endpoint"
  6. Copy the "Signing secret" provided by Stripe. This is your 'whsec_YOUR_WEBHOOK_SECRET' value

 

Step 4: Verify Payments Manually (Alternative to Webhooks)

 

If you need to verify a payment manually (e.g., after a client-side redirect), you can directly check the payment status:

Node.js example:


const express = require('express');
const app = express();
app.use(express.json());

app.post('/verify-payment', async (req, res) => {
  try {
    const { paymentIntentId } = req.body;
    
    // Retrieve the payment intent from Stripe
    const paymentIntent = await stripe.paymentIntents.retrieve(paymentIntentId);
    
    // Check if the payment was successful
    if (paymentIntent.status === 'succeeded') {
      // Payment successful - implement your business logic here
      // Verify the amount, currency, and metadata
      
      // Example validation
      const expectedAmount = 1999; // $19.99
      const expectedCurrency = 'usd';
      
      if (paymentIntent.amount !== expectedAmount || 
          paymentIntent.currency !== expectedCurrency) {
        return res.status(400).json({ 
          success: false, 
          message: 'Payment amount or currency mismatch' 
        });
      }
      
      // Update your database to mark the order as paid
      // await updateOrderStatus(paymentIntent.metadata.orderId, 'paid');
      
      return res.json({ success: true, paymentIntent });
    } else {
      // Payment not successful
      return res.status(400).json({ 
        success: false, 
        message: `Payment not successful. Status: ${paymentIntent.status}` 
      });
    }
  } catch (error) {
    console.error('Error verifying payment:', error);
    return res.status(500).json({ success: false, error: error.message });
  }
});

app.listen(3000, () => console.log('Server running on port 3000'));

Python (Flask) example:


from flask import Flask, request, jsonify
import stripe

app = Flask(**name**)

@app.route('/verify-payment', methods=['POST'])
def verify\_payment():
    try:
        data = request.json
        payment_intent_id = data.get('paymentIntentId')
        
        # Retrieve the payment intent from Stripe
        payment_intent = stripe.PaymentIntent.retrieve(payment_intent\_id)
        
        # Check if the payment was successful
        if payment\_intent.status == 'succeeded':
            # Payment successful - implement your business logic here
            # Verify the amount, currency, and metadata
            
            # Example validation
            expected\_amount = 1999  # $19.99
            expected\_currency = 'usd'
            
            if (payment_intent.amount != expected_amount or 
                payment_intent.currency != expected_currency):
                return jsonify({
                    'success': False,
                    'message': 'Payment amount or currency mismatch'
                }), 400
            
            # Update your database to mark the order as paid
            # update_order_status(payment_intent.metadata.order_id, 'paid')
            
            return jsonify({'success': True, 'paymentIntent': payment\_intent})
        else:
            # Payment not successful
            return jsonify({
                'success': False,
                'message': f'Payment not successful. Status: {payment\_intent.status}'
            }), 400
    except Exception as e:
        print(f'Error verifying payment: {str(e)}')
        return jsonify({'success': False, 'error': str(e)}), 500

if **name** == '**main**':
    app.run(port=3000)

 

Step 5: Implement Idempotency to Prevent Duplicate Processing

 

Implement idempotency to ensure each payment is processed only once, even if the webhook is triggered multiple times:

Node.js example:


const mongoose = require('mongoose');

// Define a schema for processed payments
const ProcessedPaymentSchema = new mongoose.Schema({
  paymentIntentId: {
    type: String,
    required: true,
    unique: true
  },
  processedAt: {
    type: Date,
    default: Date.now
  }
});

const ProcessedPayment = mongoose.model('ProcessedPayment', ProcessedPaymentSchema);

async function handleSuccessfulPayment(paymentIntent) {
  try {
    // Check if this payment has already been processed
    const existingPayment = await ProcessedPayment.findOne({ 
      paymentIntentId: paymentIntent.id 
    });
    
    if (existingPayment) {
      console.log(`Payment ${paymentIntent.id} already processed. Skipping.`);
      return;
    }
    
    // Process the payment (your business logic)
    // e.g., update order status, send emails, etc.
    console.log(`Processing payment ${paymentIntent.id}`);
    
    // Mark payment as processed
    await new ProcessedPayment({ paymentIntentId: paymentIntent.id }).save();
    
    console.log(`Payment ${paymentIntent.id} processed successfully`);
  } catch (error) {
    console.error(`Error processing payment ${paymentIntent.id}:`, error);
  }
}

Python example:


from datetime import datetime
from pymongo import MongoClient

# Set up MongoDB connection
client = MongoClient('mongodb://localhost:27017/')
db = client['payment\_db']
processed_payments = db['processed_payments']

def handle_successful_payment(payment\_intent):
    try:
        # Check if this payment has already been processed
        existing_payment = processed_payments.find\_one({
            'payment_intent_id': payment\_intent['id']
        })
        
        if existing\_payment:
            print(f"Payment {payment\_intent['id']} already processed. Skipping.")
            return
        
        # Process the payment (your business logic)
        # e.g., update order status, send emails, etc.
        print(f"Processing payment {payment\_intent['id']}")
        
        # Mark payment as processed
        processed_payments.insert_one({
            'payment_intent_id': payment\_intent['id'],
            'processed\_at': datetime.now()
        })
        
        print(f"Payment {payment\_intent['id']} processed successfully")
    except Exception as e:
        print(f"Error processing payment {payment\_intent['id']}: {str(e)}")

 

Step 6: Advanced Validation with Metadata

 

Use metadata to store additional information and perform advanced validation:

  1. When creating a payment intent on your backend, add metadata:

// Node.js
const paymentIntent = await stripe.paymentIntents.create({
  amount: 1999,
  currency: 'usd',
  metadata: {
    orderId: '6735',
    customerId: '12345',
    productIds: 'prod_123,prod_456'
  }
});
  1. When validating the payment, check the metadata:

async function handleSuccessfulPayment(paymentIntent) {
  // Extract metadata
  const { orderId, customerId, productIds } = paymentIntent.metadata;
  
  // Find the order in your database
  const order = await Order.findById(orderId);
  
  // Validate order
  if (!order) {
    console.error(`Order ${orderId} not found for payment ${paymentIntent.id}`);
    return;
  }
  
  // Validate customer
  if (order.customerId !== customerId) {
    console.error(`Customer mismatch for payment ${paymentIntent.id}`);
    return;
  }
  
  // Validate amount
  if (order.totalAmount !== paymentIntent.amount) {
    console.error(`Amount mismatch for payment ${paymentIntent.id}`);
    return;
  }
  
  // Mark order as paid
  order.status = 'paid';
  order.paymentIntentId = paymentIntent.id;
  await order.save();
  
  // Continue with fulfillment
  await sendOrderConfirmationEmail(order);
  console.log(`Order ${orderId} successfully paid`);
}

 

Step 7: Handle Refunds and Disputes

 

Set up handling for refunds and disputes to maintain a complete payment lifecycle:


// In your webhook handler, add additional event types

// Node.js (Express)
app.post('/webhook', async (req, res) => {
  // ... previous webhook validation code
  
  switch (event.type) {
    case 'payment\_intent.succeeded':
      // ... existing code
      break;
    case 'charge.refunded':
      const refund = event.data.object;
      await handleRefund(refund);
      break;
    case 'charge.dispute.created':
      const dispute = event.data.object;
      await handleDisputeCreated(dispute);
      break;
    case 'charge.dispute.closed':
      const closedDispute = event.data.object;
      await handleDisputeClosed(closedDispute);
      break;
    // ... other events
  }
  
  res.send();
});

async function handleRefund(refund) {
  // Find the related order using the charge ID
  const order = await Order.findOne({ chargeId: refund.id });
  if (!order) {
    console.error(`Order not found for refund ${refund.id}`);
    return;
  }
  
  // Update order status based on full or partial refund
  if (refund.amount\_refunded === refund.amount) {
    order.status = 'refunded';
  } else {
    order.status = 'partially\_refunded';
  }
  
  order.refundedAmount = refund.amount\_refunded;
  await order.save();
  
  // Notify customer about the refund
  await sendRefundNotification(order);
}

async function handleDisputeCreated(dispute) {
  // Find the related order
  const order = await Order.findOne({ chargeId: dispute.charge });
  if (!order) {
    console.error(`Order not found for dispute ${dispute.id}`);
    return;
  }
  
  // Update order status
  order.status = 'disputed';
  order.disputeId = dispute.id;
  order.disputeReason = dispute.reason;
  await order.save();
  
  // Alert your team to respond to the dispute
  await sendDisputeAlert(order, dispute);
}

 

Step 8: Implement Error Handling and Logging

 

Robust error handling and logging are essential for tracking payment issues:


// Node.js example with enhanced error handling and logging
const winston = require('winston');

// Set up logger
const logger = winston.createLogger({
  level: 'info',
  format: winston.format.combine(
    winston.format.timestamp(),
    winston.format.json()
  ),
  defaultMeta: { service: 'payment-service' },
  transports: [
    new winston.transports.File({ filename: 'error.log', level: 'error' }),
    new winston.transports.File({ filename: 'payments.log' })
  ]
});

// Add to console in development
if (process.env.NODE\_ENV !== 'production') {
  logger.add(new winston.transports.Console({
    format: winston.format.simple()
  }));
}

app.post('/webhook', async (req, res) => {
  const sig = req.headers['stripe-signature'];
  
  let event;
  
  try {
    event = stripe.webhooks.constructEvent(
      req.body,
      sig,
      'whsec_YOUR_WEBHOOK\_SECRET'
    );
  } catch (err) {
    logger.error('Webhook signature verification failed', {
      error: err.message,
      body: req.body.toString()
    });
    return res.status(400).send(`Webhook Error: ${err.message}`);
  }
  
  // Log the event
  logger.info('Webhook event received', {
    eventType: event.type,
    eventId: event.id
  });
  
  try {
    // Handle the event
    switch (event.type) {
      case 'payment\_intent.succeeded':
        const paymentIntent = event.data.object;
        await handleSuccessfulPayment(paymentIntent);
        break;
      // ... other event types
    }
  } catch (error) {
    logger.error('Error handling webhook event', {
      error: error.message,
      stack: error.stack,
      eventType: event.type,
      eventId: event.id
    });
    
    // We still return a 200 to Stripe so they don't retry the webhook
    // But we'll get notified of the error through our logging system
  }
  
  res.send();
});

async function handleSuccessfulPayment(paymentIntent) {
  try {
    logger.info('Processing successful payment', {
      paymentIntentId: paymentIntent.id,
      amount: paymentIntent.amount,
      currency: paymentIntent.currency
    });
    
    // Implement your business logic here
    
    logger.info('Payment processed successfully', {
      paymentIntentId: paymentIntent.id
    });
  } catch (error) {
    logger.error('Error processing payment', {
      error: error.message,
      stack: error.stack,
      paymentIntentId: paymentIntent.id
    });
    throw error; // Re-throw to be caught by the webhook handler
  }
}

 

Step 9: Implement Security Best Practices

 

Enhance your payment validation with security best practices:


// Node.js example with enhanced security
const crypto = require('crypto');

// Environment variables for sensitive data
const STRIPE_SECRET_KEY = process.env.STRIPE_SECRET_KEY;
const WEBHOOK_SECRET = process.env.STRIPE_WEBHOOK\_SECRET;

// Validate IP addresses
const STRIPE\_IPS = [
  '54.187.174.169',
  '54.187.205.235',
  '54.187.216.72',
  // ... add more Stripe IPs as needed
];

// IP validation middleware
function validateStripeIP(req, res, next) {
  const clientIP = req.ip;
  
  if (!STRIPE\_IPS.includes(clientIP)) {
    logger.warn('Webhook called from unauthorized IP', {
      ip: clientIP,
      path: req.path
    });
    return res.status(403).send('Unauthorized IP');
  }
  
  next();
}

// Rate limiting middleware
const rateLimit = require('express-rate-limit');
const webhookLimiter = rateLimit({
  windowMs: 15 _ 60 _ 1000, // 15 minutes
  max: 100, // limit each IP to 100 requests per windowMs
  message: 'Too many requests from this IP, please try again later'
});

// Apply middlewares
app.use('/webhook', validateStripeIP);
app.use('/webhook', webhookLimiter);
app.use('/webhook', express.raw({type: 'application/json'}));

// Generate a unique reference code for each payment
function generateReferenceCode(orderId) {
  const hmac = crypto.createHmac('sha256', process.env.REFERENCE\_SECRET);
  hmac.update(`${orderId}-${Date.now()}`);
  return hmac.digest('hex').substring(0, 10).toUpperCase();
}

// Use the reference code when creating a payment intent
async function createPaymentIntent(orderId, amount, currency) {
  const referenceCode = generateReferenceCode(orderId);
  
  const paymentIntent = await stripe.paymentIntents.create({
    amount,
    currency,
    metadata: {
      orderId,
      referenceCode
    }
  });
  
  // Store the reference code with the order
  await Order.findByIdAndUpdate(orderId, { 
    referenceCode, 
    paymentIntentId: paymentIntent.id 
  });
  
  return paymentIntent;
}

// Validate the reference code during payment confirmation
async function validatePayment(paymentIntent) {
  const { orderId, referenceCode } = paymentIntent.metadata;
  
  // Find the order
  const order = await Order.findById(orderId);
  
  if (!order) {
    throw new Error(`Order ${orderId} not found`);
  }
  
  // Validate the reference code
  if (order.referenceCode !== referenceCode) {
    throw new Error('Invalid reference code');
  }
  
  // Continue with payment processing
}

 

Step 10: Testing Your Implementation

 

Test your payment validation implementation thoroughly:


// Node.js testing with Jest
const request = require('supertest');
const app = require('../app');
const stripe = require('stripe')(process.env.STRIPE_SECRET_KEY);
const Order = require('../models/Order');

describe('Payment Validation', () => {
  let paymentIntent;
  let order;
  
  beforeAll(async () => {
    // Create a test order
    order = await Order.create({
      customerId: 'test\_customer',
      items: [{ productId: 'prod\_123', quantity: 1, price: 1999 }],
      totalAmount: 1999,
      currency: 'usd',
      status: 'pending'
    });
    
    // Create a test payment intent
    paymentIntent = await stripe.paymentIntents.create({
      amount: 1999,
      currency: 'usd',
      metadata: {
        orderId: order.\_id.toString(),
        customerId: 'test\_customer'
      },
      payment_method_types: ['card'],
      payment_method: 'pm_card\_visa'
    });
    
    // Confirm the payment intent to simulate a successful payment
    await stripe.paymentIntents.confirm(paymentIntent.id);
  });
  
  test('should verify a successful payment', async () => {
    const response = await request(app)
      .post('/verify-payment')
      .send({ paymentIntentId: paymentIntent.id })
      .expect(200);
    
    expect(response.body.success).toBe(true);
    expect(response.body.paymentIntent.id).toBe(paymentIntent.id);
    
    // Verify the order was updated
    const updatedOrder = await Order.findById(order.\_id);
    expect(updatedOrder.status).toBe('paid');
    expect(updatedOrder.paymentIntentId).toBe(paymentIntent.id);
  });
  
  test('should reject invalid payment intent ID', async () => {
    const response = await request(app)
      .post('/verify-payment')
      .send({ paymentIntentId: 'pi_invalid_id' })
      .expect(500);
    
    expect(response.body.success).toBe(false);
  });
  
  // Test for webhook handling
  test('should process webhook events correctly', async () => {
    // Create a mock webhook event
    const payload = {
      id: 'evt\_test',
      type: 'payment\_intent.succeeded',
      data: {
        object: paymentIntent
      }
    };
    
    // Generate a valid signature (you'd need to mock this for testing)
    const signature = 'mock\_signature';
    
    // Mock the stripe.webhooks.constructEvent function
    stripe.webhooks.constructEvent = jest.fn().mockReturnValue(payload);
    
    const response = await request(app)
      .post('/webhook')
      .set('stripe-signature', signature)
      .send(JSON.stringify(payload))
      .expect(200);
    
    // Verify the webhook was processed
    expect(stripe.webhooks.constructEvent).toHaveBeenCalled();
  });
});

 

Conclusion

 

Properly validating Stripe payments on your backend is crucial for security and reliability. By implementing webhooks, performing thorough validation, handling errors gracefully, and following security best practices, you can build a robust payment system that protects both your business and your customers.

Remember these key points:

  • Always use webhooks as your primary source of truth for payment status
  • Implement idempotency to prevent duplicate processing
  • Use metadata for additional validation
  • Implement proper error handling and logging
  • Follow security best practices to protect your payment system
  • Test your implementation thoroughly

With these steps, you'll have a secure and reliable payment validation system for your application.

Want to explore opportunities to work with us?

Connect with our team to unlock the full potential of no-code solutions with a no-commitment consultation!

Book a Free Consultation

Client trust and success are our top priorities

When it comes to serving you, we sweat the little things. That’s why our work makes a big impact.

Rapid Dev was an exceptional project management organization and the best development collaborators I've had the pleasure of working with. They do complex work on extremely fast timelines and effectively manage the testing and pre-launch process to deliver the best possible product. I'm extremely impressed with their execution ability.

CPO, Praction - Arkady Sokolov

May 2, 2023

Working with Matt was comparable to having another co-founder on the team, but without the commitment or cost. He has a strategic mindset and willing to change the scope of the project in real time based on the needs of the client. A true strategic thought partner!

Co-Founder, Arc - Donald Muir

Dec 27, 2022

Rapid Dev are 10/10, excellent communicators - the best I've ever encountered in the tech dev space. They always go the extra mile, they genuinely care, they respond quickly, they're flexible, adaptable and their enthusiasm is amazing.

Co-CEO, Grantify - Mat Westergreen-Thorne

Oct 15, 2022

Rapid Dev is an excellent developer for no-code and low-code solutions.
We’ve had great success since launching the platform in November 2023. In a few months, we’ve gained over 1,000 new active users. We’ve also secured several dozen bookings on the platform and seen about 70% new user month-over-month growth since the launch.

Co-Founder, Church Real Estate Marketplace - Emmanuel Brown

May 1, 2024 

Matt’s dedication to executing our vision and his commitment to the project deadline were impressive. 
This was such a specific project, and Matt really delivered. We worked with a really fast turnaround, and he always delivered. The site was a perfect prop for us!

Production Manager, Media Production Company - Samantha Fekete

Sep 23, 2022