/stripe-guides

How to accept donations with Stripe API?

Learn how to accept one-time and recurring donations on your website using the Stripe API, with step-by-step setup, code examples, security tips, and dashboard integration.

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 accept donations with Stripe API?

How to Accept Donations with Stripe API

 

Step 1: Set Up Your Stripe Account

 

Before integrating Stripe into your website, you need to create and set up a Stripe account:

  • Go to stripe.com and sign up for an account
  • Complete the verification process
  • Navigate to the Stripe Dashboard
  • Find your API keys under Developers → API keys
  • Note down both your Publishable Key and Secret Key

 

Step 2: Install Stripe Libraries

 

You'll need to install the Stripe libraries for your project. Here's how to do it in different environments:

For Node.js projects:

npm install stripe

For PHP projects:

composer require stripe/stripe-php

 

Step 3: Include Stripe.js in Your Frontend

 

Add the Stripe.js library to your HTML file:

<!DOCTYPE html>
<html>
<head>
    <title>Donation Form</title>
    <script src="https://js.stripe.com/v3/"></script>
    <style>
        .donation-form {
            max-width: 500px;
            margin: 0 auto;
            padding: 20px;
            border: 1px solid #ddd;
            border-radius: 5px;
        }
        .form-row {
            margin-bottom: 15px;
        }
        button {
            background: #635BFF;
            color: white;
            border: none;
            padding: 10px 20px;
            border-radius: 4px;
            cursor: pointer;
        }
        #card-element {
            border: 1px solid #ddd;
            padding: 10px;
            border-radius: 4px;
        }
        #card-errors {
            color: #fa755a;
            margin-top: 10px;
        }
    </style>
</head>
<body>
    <!-- We'll add the form here in the next step -->
</body>
</html>

 

Step 4: Create Your Donation Form

 

Add the donation form to your HTML body:

<div class="donation-form">
    <h2>Support Our Cause</h2>
    <form id="payment-form">
        <div class="form-row">
            <label for="name">Name</label>
            <input type="text" id="name" placeholder="John Doe" required />
        </div>
        
        <div class="form-row">
            <label for="email">Email</label>
            <input type="email" id="email" placeholder="[email protected]" required />
        </div>
        
        <div class="form-row">
            <label for="amount">Donation Amount ($)</label>
            <input type="number" id="amount" min="1" step="1" value="10" required />
        </div>
        
        <div class="form-row">
            <label for="card-element">Credit or Debit Card</label>
            <div id="card-element">
                <!-- Stripe Card Element will be inserted here -->
            </div>
            <div id="card-errors" role="alert"></div>
        </div>
        
        <button type="submit">Donate Now</button>
    </form>
</div>

 

Step 5: Initialize Stripe Elements

 

Add JavaScript to initialize Stripe Elements:

<script>
    // Initialize Stripe with your publishable key
    const stripe = Stripe('pk_test_your_publishable_key');
    
    // Create an instance of Elements
    const elements = stripe.elements();
    
    // Create and mount the Card Element
    const cardElement = elements.create('card');
    cardElement.mount('#card-element');
    
    // Handle real-time validation errors from the card Element
    cardElement.on('change', function(event) {
        const displayError = document.getElementById('card-errors');
        if (event.error) {
            displayError.textContent = event.error.message;
        } else {
            displayError.textContent = '';
        }
    });
    
    // Handle form submission
    const form = document.getElementById('payment-form');
    form.addEventListener('submit', async (event) => {
        event.preventDefault();
        
        const name = document.getElementById('name').value;
        const email = document.getElementById('email').value;
        const amount = document.getElementById('amount').value;
        
        try {
            // Create a payment method using the card element
            const { paymentMethod, error } = await stripe.createPaymentMethod({
                type: 'card',
                card: cardElement,
                billing\_details: {
                    name: name,
                    email: email
                }
            });
            
            if (error) {
                // Show error to your customer
                const errorElement = document.getElementById('card-errors');
                errorElement.textContent = error.message;
                return;
            }
            
            // Send payment method ID to your server
            const response = await fetch('/process-donation', {
                method: 'POST',
                headers: {
                    'Content-Type': 'application/json'
                },
                body: JSON.stringify({
                    payment_method_id: paymentMethod.id,
                    name: name,
                    email: email,
                    amount: amount
                })
            });
            
            const result = await response.json();
            
            // Handle server response
            if (result.error) {
                // Show error to your customer
                const errorElement = document.getElementById('card-errors');
                errorElement.textContent = result.error.message;
            } else if (result.requires\_action) {
                // Use Stripe.js to handle required card action
                const { error: actionError } = await stripe.handleCardAction(
                    result.payment_intent_client\_secret
                );
                
                if (actionError) {
                    // Show error to your customer
                    const errorElement = document.getElementById('card-errors');
                    errorElement.textContent = actionError.message;
                } else {
                    // The card action has been handled
                    // The PaymentIntent can be confirmed again on the server
                    const serverResponse = await fetch('/process-donation', {
                        method: 'POST',
                        headers: {
                            'Content-Type': 'application/json'
                        },
                        body: JSON.stringify({
                            payment_intent_id: result.payment_intent_id
                        })
                    });
                    
                    const serverResult = await serverResponse.json();
                    
                    if (serverResult.error) {
                        // Show error to your customer
                        const errorElement = document.getElementById('card-errors');
                        errorElement.textContent = serverResult.error.message;
                    } else {
                        // Show a success message to your customer
                        alert('Thank you for your donation!');
                        form.reset();
                    }
                }
            } else {
                // Show a success message to your customer
                alert('Thank you for your donation!');
                form.reset();
            }
        } catch (error) {
            console.error('Error:', error);
            const errorElement = document.getElementById('card-errors');
            errorElement.textContent = 'An unexpected error occurred. Please try again later.';
        }
    });
</script>

 

Step 6: Create Server-Side Endpoint (Node.js Example)

 

Create a server-side endpoint to process the donation using Node.js and Express:

const express = require('express');
const bodyParser = require('body-parser');
const stripe = require('stripe')('sk_test_your_secret_key');

const app = express();
app.use(bodyParser.json());

app.post('/process-donation', async (req, res) => {
    try {
        const { payment_method_id, payment_intent_id, name, email, amount } = req.body;
        
        // If payment_intent_id is passed, confirm the payment
        if (payment_intent_id) {
            const intent = await stripe.paymentIntents.confirm(payment_intent_id);
            
            return res.json({
                success: true,
                payment: intent
            });
        }
        
        // Create a new PaymentIntent
        const paymentIntent = await stripe.paymentIntents.create({
            payment_method: payment_method\_id,
            amount: amount \* 100, // Stripe expects amount in cents
            currency: 'usd',
            confirmation\_method: 'manual',
            confirm: true,
            description: 'Donation',
            receipt\_email: email,
            metadata: {
                donor\_name: name,
                donor\_email: email
            }
        });
        
        // Handle payment status
        if (
            paymentIntent.status === 'requires\_action' &&
            paymentIntent.next_action.type === 'use_stripe\_sdk'
        ) {
            // Tell the client to handle the action
            return res.json({
                requires\_action: true,
                payment_intent_client_secret: paymentIntent.client_secret,
                payment_intent_id: paymentIntent.id
            });
        } else if (paymentIntent.status === 'succeeded') {
            // The payment didn't need any additional actions and completed!
            return res.json({
                success: true,
                payment: paymentIntent
            });
        } else {
            // Invalid status
            return res.json({
                error: {
                    message: 'Invalid PaymentIntent status'
                }
            });
        }
    } catch (error) {
        return res.json({
            error: {
                message: error.message
            }
        });
    }
});

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

 

Step 7: Create Server-Side Endpoint (PHP Example)

 

Here's an alternative server-side implementation using PHP:

confirm();
        echo json\_encode(['success' => true, 'payment' => $intent]);
    } else {
        // Create a new PaymentIntent
        $paymentIntent = \Stripe\PaymentIntent::create([
            'payment_method' => $body['payment_method\_id'],
            'amount' => $body['amount'] \* 100, // Stripe expects amount in cents
            'currency' => 'usd',
            'confirmation\_method' => 'manual',
            'confirm' => true,
            'description' => 'Donation',
            'receipt\_email' => $body['email'],
            'metadata' => [
                'donor\_name' => $body['name'],
                'donor\_email' => $body['email']
            ]
        ]);
        
        if ($paymentIntent->status == 'requires\_action' && 
            $paymentIntent->next_action->type == 'use_stripe\_sdk') {
            // Tell the client to handle the action
            echo json\_encode([
                'requires\_action' => true,
                'payment_intent_client_secret' => $paymentIntent->client_secret,
                'payment_intent_id' => $paymentIntent->id
            ]);
        } else if ($paymentIntent->status == 'succeeded') {
            // The payment didn't need any additional actions and completed!
            echo json\_encode([
                'success' => true,
                'payment' => $paymentIntent
            ]);
        } else {
            // Invalid status
            echo json\_encode([
                'error' => [
                    'message' => 'Invalid PaymentIntent status'
                ]
            ]);
        }
    }
} catch (\Stripe\Exception\ApiErrorException $e) {
    echo json\_encode([
        'error' => [
            'message' => $e->getMessage()
        ]
    ]);
}
?>

 

Step 8: Test Your Donation System

 

Before going live, thoroughly test your donation system:

  • Make sure your Stripe API keys are set to test mode
  • Use Stripe's test card numbers:
    • 4242 4242 4242 4242 (successful payment)
    • 4000 0027 6000 3184 (requires authentication)
    • 4000 0000 0000 9995 (declined payment)
  • Check that donations appear in your Stripe dashboard
  • Verify that success/error messages appear correctly

 

Step 9: Implement Donation Success Page

 

Create a success page to redirect users after successful donations:

<!DOCTYPE html>
<html>
<head>
    <title>Donation Successful</title>
    <style>
        .success-container {
            max-width: 600px;
            margin: 50px auto;
            text-align: center;
            padding: 30px;
            border: 1px solid #e0e0e0;
            border-radius: 5px;
        }
        .success-icon {
            color: #4CAF50;
            font-size: 60px;
            margin-bottom: 20px;
        }
        .donation-details {
            margin-top: 30px;
            text-align: left;
            padding: 20px;
            background-color: #f9f9f9;
            border-radius: 5px;
        }
    </style>
</head>
<body>
    <div class="success-container">
        <div class="success-icon">✓</div>
        <h1>Thank You for Your Donation!</h1>
        <p>Your contribution helps us continue our important work.</p>
        
        <div class="donation-details">
            <h3>Donation Details</h3>
            <p><strong>Amount:</strong> $<span id="donation-amount"></span></p>
            <p><strong>Date:</strong> <span id="donation-date"></span></p>
            <p><strong>Transaction ID:</strong> <span id="transaction-id"></span></p>
        </div>
        
        <p>A receipt has been sent to your email.</p>
        <a href="index.html">Return to Homepage</a>
    </div>
    
    <script>
        // Get URL parameters to display donation details
        const urlParams = new URLSearchParams(window.location.search);
        document.getElementById('donation-amount').textContent = urlParams.get('amount') || 'N/A';
        document.getElementById('transaction-id').textContent = urlParams.get('transaction\_id') || 'N/A';
        document.getElementById('donation-date').textContent = new Date().toLocaleDateString();
    </script>
</body>
</html>

 

Step 10: Add Webhook Support for Asynchronous Events

 

Set up webhooks to handle asynchronous events like successful payments or disputes:

// Node.js webhook implementation
const express = require('express');
const bodyParser = require('body-parser');
const stripe = require('stripe')('sk_test_your_secret_key');

const app = express();

// Use JSON parser for webhook events
app.post('/webhook', bodyParser.raw({type: 'application/json'}), 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 Error: ${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;
            console.log(`PaymentIntent ${paymentIntent.id} succeeded`);
            // Update your database, send thank-you emails, etc.
            
            // Example: Record donation in database
            await recordDonationInDatabase({
                payment\_id: paymentIntent.id,
                amount: paymentIntent.amount / 100, // Convert back from cents
                email: paymentIntent.receipt\_email,
                name: paymentIntent.metadata.donor\_name,
                status: 'succeeded',
                date: new Date()
            });
            
            break;
            
        case 'charge.refunded':
            const charge = event.data.object;
            console.log(`Charge ${charge.id} was refunded`);
            // Handle refund logic
            break;
            
        case 'payment_intent.payment_failed':
            const failedPayment = event.data.object;
            console.log(`Payment failed: ${failedPayment.id}`);
            // Handle failed payment
            break;
            
        default:
            // Unexpected event type
            console.log(`Unhandled event type ${event.type}`);
    }
    
    // Return a 200 response to acknowledge receipt of the event
    res.json({received: true});
});

// Example function to record donation in database
async function recordDonationInDatabase(donationData) {
    // In a real implementation, you would save to your database
    console.log('Donation recorded:', donationData);
}

app.listen(3000, () => {
    console.log('Webhook server running on port 3000');
});

 

Step 11: Implement Recurring Donations

 

To implement recurring donations, use Stripe Subscriptions:

// Frontend JavaScript additions for recurring donations
// Add this to your form HTML first:
/\*
<div class="form-row">
    <label>Donation Frequency</label>
    <select id="donation-frequency">
        <option value="one-time">One-time</option>
        <option value="monthly">Monthly</option>
        <option value="yearly">Yearly</option>
    </select>
</div>
\*/

// In your form submission handler, modify to handle subscriptions:
const handleSubmit = async (event) => {
    event.preventDefault();
    
    const name = document.getElementById('name').value;
    const email = document.getElementById('email').value;
    const amount = document.getElementById('amount').value;
    const frequency = document.getElementById('donation-frequency').value;
    
    try {
        // Create a payment method
        const { paymentMethod, error } = await stripe.createPaymentMethod({
            type: 'card',
            card: cardElement,
            billing\_details: {
                name: name,
                email: email
            }
        });
        
        if (error) {
            const errorElement = document.getElementById('card-errors');
            errorElement.textContent = error.message;
            return;
        }
        
        // Send to server
        const response = await fetch('/process-donation', {
            method: 'POST',
            headers: {
                'Content-Type': 'application/json'
            },
            body: JSON.stringify({
                payment_method_id: paymentMethod.id,
                name: name,
                email: email,
                amount: amount,
                frequency: frequency // Add the frequency parameter
            })
        });
        
        // Handle response as before
        // ...
    } catch (error) {
        console.error('Error:', error);
    }
};

Server-side implementation for subscriptions:

// Node.js endpoint for handling both one-time and recurring donations
app.post('/process-donation', async (req, res) => {
    try {
        const { payment_method_id, name, email, amount, frequency } = req.body;
        
        // Handle based on donation frequency
        if (frequency === 'one-time') {
            // Process one-time donation (as shown in previous steps)
            const paymentIntent = await stripe.paymentIntents.create({
                payment_method: payment_method\_id,
                amount: amount \* 100,
                currency: 'usd',
                confirmation\_method: 'manual',
                confirm: true,
                description: 'One-time Donation',
                receipt\_email: email,
                metadata: {
                    donor\_name: name,
                    donor\_email: email
                }
            });
            
            // Handle payment intent status...
            
        } else {
            // Process recurring donation (subscription)
            // First, create a customer
            const customer = await stripe.customers.create({
                payment_method: payment_method\_id,
                email: email,
                name: name,
                invoice\_settings: {
                    default_payment_method: payment_method_id,
                },
            });
            
            // Determine the interval based on frequency
            const interval = frequency === 'monthly' ? 'month' : 'year';
            
            // Create or retrieve a product for the donation
            const product = await stripe.products.create({
                name: `${frequency.charAt(0).toUpperCase() + frequency.slice(1)} Donation`,
            });
            
            // Create a price for the subscription
            const price = await stripe.prices.create({
                product: product.id,
                unit\_amount: amount \* 100,
                currency: 'usd',
                recurring: {
                    interval: interval,
                },
                metadata: {
                    type: 'donation'
                }
            });
            
            // Create the subscription
            const subscription = await stripe.subscriptions.create({
                customer: customer.id,
                items: [{ price: price.id }],
                expand: ['latest_invoice.payment_intent'],
                metadata: {
                    donor\_name: name,
                    donor\_email: email
                }
            });
            
            // Check if further authentication is needed
            const status = subscription.latest_invoice.payment_intent.status;
            const client_secret = subscription.latest_invoice.payment_intent.client_secret;
            
            if (status === 'requires\_action') {
                // Handle 3D Secure authentication
                return res.json({
                    requires\_action: true,
                    payment_intent_client_secret: client_secret,
                    subscription\_id: subscription.id
                });
            } else {
                // Subscription created successfully
                return res.json({
                    success: true,
                    subscription: subscription
                });
            }
        }
    } catch (error) {
        return res.json({
            error: {
                message: error.message
            }
        });
    }
});

 

Step 12: Add Donation Analytics

 

Implement analytics to track and analyze donation metrics:

// Node.js example for donation analytics
const express = require('express');
const stripe = require('stripe')('sk_test_your_secret_key');
const { Pool } = require('pg'); // Using PostgreSQL for this example

// Database connection
const pool = new Pool({
    user: 'your\_user',
    host: 'localhost',
    database: 'donations\_db',
    password: 'your\_password',
    port: 5432,
});

const app = express();

// API endpoint to get donation analytics
app.get('/donation-analytics', async (req, res) => {
    try {
        // Total amount donated
        const totalResult = await pool.query(
            'SELECT SUM(amount) as total FROM donations WHERE status = $1',
            ['succeeded']
        );
        
        // Donations by month
        const monthlyResult = await pool.query(\`
            SELECT 
                DATE_TRUNC('month', donation_date) as month,
                SUM(amount) as monthly\_total,
                COUNT(\*) as donation\_count
            FROM donations
            WHERE status = $1
            GROUP BY DATE_TRUNC('month', donation_date)
            ORDER BY month DESC
            LIMIT 12
        \`, ['succeeded']);
        
        // New vs returning donors
        const donorTypeResult = await pool.query(\`
            SELECT
                COUNT(DISTINCT donor_email) as unique_donors,
                SUM(CASE WHEN donation_count = 1 THEN 1 ELSE 0 END) as new_donors,
                SUM(CASE WHEN donation_count > 1 THEN 1 ELSE 0 END) as returning_donors
            FROM (
                SELECT 
                    donor\_email,
                    COUNT(\*) as donation\_count
                FROM donations
                WHERE status = $1
                GROUP BY donor\_email
            ) as donor\_counts
        \`, ['succeeded']);
        
        // Average donation amount
        const avgResult = await pool.query(
            'SELECT AVG(amount) as average FROM donations WHERE status = $1',
            ['succeeded']
        );
        
        // Return combined analytics
        res.json({
            total\_donated: totalResult.rows[0].total || 0,
            monthly\_stats: monthlyResult.rows,
            donor\_stats: donorTypeResult.rows[0],
            average\_donation: avgResult.rows[0].average || 0
        });
        
    } catch (error) {
        console.error('Analytics error:', error);
        res.status(500).json({ error: 'Failed to retrieve donation analytics' });
    }
});

app.listen(3000, () => {
    console.log('Analytics server running on port 3000');
});

 

Step 13: Add a Donation Management Dashboard

 

Create a simple dashboard to manage donations:

<!DOCTYPE html>
<html>
<head>
    <title>Donation Management</title>
    <script src="https://cdn.jsdelivr.net/npm/chart.js"></script>
    <style>
        body {
            font-family: Arial, sans-serif;
            margin: 0;
            padding: 0;
        }
        .dashboard {
            display: flex;
            min-height: 100vh;
        }
        .sidebar {
            width: 250px;
            background-color: #2c3e50;
            color: white;
            padding: 20px;
        }
        .sidebar ul {
            list-style-type: none;
            padding: 0;
        }
        .sidebar li {
            margin-bottom: 15px;
        }
        .sidebar a {
            color: white;
            text-decoration: none;
        }
        .main-content {
            flex: 1;
            padding: 20px;
        }
        .stat-cards {
            display: grid;
            grid-template-columns: repeat(auto-fill, minmax(250px, 1fr));
            gap: 20px;
            margin-bottom: 30px;
        }
        .stat-card {
            background-color: white;
            border-radius: 5px;
            box-shadow: 0 2px 5px rgba(0,0,0,0.1);
            padding: 20px;
        }
        .stat-value {
            font-size: 24px;
            font-weight: bold;
            margin: 10px 0;
        }
        .chart-container {
            margin-bottom: 30px;
            height: 300px;
        }
        table {
            width: 100%;
            border-collapse: collapse;
        }
        th, td {
            padding: 12px 15px;
            border-bottom: 1px solid #ddd;
            text-align: left;
        }
        th {
            background-color: #f2f2f2;
        }
    </style>
</head>
<body>
    <div class="dashboard">
        <div class="sidebar">
            <h2>Donation Admin</h2>
            <ul>
                <li><a href="#" class="active">Dashboard</a></li>
                <li><a href="#">Donations</a></li>
                <li><a href="#">Donors</a></li>
                <li><a href="#">Reports</a></li>
                <li><a href="#">Settings</a></li>
            </ul>
        </div>
        
        <div class="main-content">
            <h1>Donation Dashboard</h1>
            
            <div class="stat-cards">
                <div class="stat-card">
                    <div>Total Donations</div>
                    <div class="stat-value" id="total-donations">$0</div>
                    <div>All time</div>
                </div>
                
                <div class="stat-card">
                    <div>Donations This Month</div>
                    <div class="stat-value" id="monthly-donations">$0</div>
                    <div>vs previous month</div>
                </div>
                
                <div class="stat-card">
                    <div>Average Donation</div>
                    <div class="stat-value" id="avg-donation">$0</div>
                    <div>Per transaction</div>
                </div>
                
                <div class="stat-card">
                    <div>Total Donors</div>
                    <div class="stat-value" id="donor-count">0</div>
                    <div>Unique contributors</div>
                </div>
            </div>
            
            <div class="chart-container">
                <canvas id="donations-chart"></canvas>
            </div>
            
            <h2>Recent Donations</h2>
            <table id="recent-donations">
                <thead>
                    <tr>
                        <th>Date</th>
                        <th>Donor</th>
                        <th>Amount</th>
                        <th>Type</th>
                        <th>Status</th>
                    </tr>
                </thead>
                <tbody>
                    <!-- Donation rows will be added here -->
                </tbody>
            </table>
        </div>
    </div>
    
    <script>
        // Fetch donation analytics from server
        async function fetchDonationData() {
            try {
                const response = await fetch('/donation-analytics');
                const data = await response.json();
                
                // Update dashboard stats
                document.getElementById('total-donations').textContent = '$' + 
                    data.total\_donated.toLocaleString('en-US', {minimumFractionDigits: 2, maximumFractionDigits: 2});
                
                document.getElementById('avg-donation').textContent = '$' + 
                    data.average\_donation.toLocaleString('en-US', {minimumFractionDigits: 2, maximumFractionDigits: 2});
                
                document.getElementById('donor-count').textContent = 
                    data.donor_stats.unique_donors.toLocaleString('en-US');
                
                // Get current month's donations
                const currentMonth = data.monthly_stats[0] || {monthly_total: 0};
                document.getElementById('monthly-donations').textContent = '$' + 
                    currentMonth.monthly\_total.toLocaleString('en-US', {minimumFractionDigits: 2, maximumFractionDigits: 2});
                
                // Create donation chart
                createDonationChart(data.monthly\_stats);
                
                // Fetch and populate recent donations
                fetchRecentDonations();
                
            } catch (error) {
                console.error('Error fetching donation data:', error);
            }
        }
        
        // Create chart for donation trends
        function createDonationChart(monthlyData) {
            const ctx = document.getElementById('donations-chart').getContext('2d');
            
            // Prepare data for chart
            const months = monthlyData.map(item => {
                const date = new Date(item.month);
                return date.toLocaleDateString('en-US', {month: 'short', year: 'numeric'});
            }).reverse();
            
            const amounts = monthlyData.map(item => item.monthly\_total).reverse();
            const counts = monthlyData.map(item => item.donation\_count).reverse();
            
            new Chart(ctx, {
                type: 'bar',
                data: {
                    labels: months,
                    datasets: [
                        {
                            label: 'Donation Amount ($)',
                            data: amounts,
                            backgroundColor: 'rgba(54, 162, 235, 0.5)',
                            borderColor: 'rgba(54, 162, 235, 1)',
                            borderWidth: 1,
                            yAxisID: 'y'
                        },
                        {
                            label: 'Number of Donations',
                            data: counts,
                            type: 'line',
                            backgroundColor: 'rgba(255, 99, 132, 0.2)',
                            borderColor: 'rgba(255, 99, 132, 1)',
                            borderWidth: 2,
                            yAxisID: 'y1'
                        }
                    ]
                },
                options: {
                    responsive: true,
                    maintainAspectRatio: false,
                    scales: {
                        y: {
                            type: 'linear',
                            display: true,
                            position: 'left',
                            title: {
                                display: true,
                                text: 'Amount ($)'
                            }
                        },
                        y1: {
                            type: 'linear',
                            display: true,
                            position: 'right',
                            grid: {
                                drawOnChartArea: false
                            },
                            title: {
                                display: true,
                                text: 'Count'
                            }
                        }
                    }
                }
            });
        }
        
        // Fetch recent donations
        async function fetchRecentDonations() {
            try {
                const response = await fetch('/recent-donations');
                const donations = await response.json();
                
                const tableBody = document.querySelector('#recent-donations tbody');
                tableBody.innerHTML = '';
                
                donations.forEach(donation => {
                    const row = document.createElement('tr');
                    
                    // Format date
                    const date = new Date(donation.donation\_date);
                    const formattedDate = date.toLocaleDateString('en-US', {
                        year: 'numeric',
                        month: 'short',
                        day: 'numeric'
                    });
                    
                    row.innerHTML = \`
                        <td>${formattedDate}</td>
                        <td>${donation.donor\_name}</td>
                        <td>$${donation.amount.toFixed(2)}</td>
                        <td>${donation.frequency || 'One-time'}</td>
                        <td>
                            <span class="status-${donation.status.toLowerCase()}">
                                ${donation.status.charAt(0).toUpperCase() + donation.status.slice(1)}
                            </span>
                        </td>
                    \`;
                    
                    tableBody.appendChild(row);
                });
                
            } catch (error) {
                console.error('Error fetching recent donations:', error);
            }
        }
        
        // Initialize dashboard
        fetchDonationData();
    </script>
</body>
</html>

 

Step 14: Implement Security Best Practices

 

Ensure your donation system is secure:

// Node.js example with security enhancements
const express = require('express');
const helmet = require('helmet');
const rateLimit = require('express-rate-limit');
const cors = require('cors');
const csrf = require('csurf');
const cookieParser = require('cookie-parser');
const stripe = require('stripe')('sk_test_your_secret_key');

const app = express();

// Security middleware
app.use(helmet()); // Sets various HTTP headers for security
app.use(cors({
    origin: 'https://yourdomain.com', // Restrict to your domain
    methods: ['GET', 'POST'],
    allowedHeaders: ['Content-Type', 'Authorization']
}));

// Rate limiting to prevent abuse
const apiLimiter = rateLimit({
    windowMs: 15 _ 60 _ 1000, // 15 minutes
    max: 100, // limit each IP to 100 requests per windowMs
    message: 'Too many requests, please try again later.'
});
app.use('/api/', apiLimiter);

// For CSRF protection
app.use(cookieParser());
app.use(csrf({ cookie: true }));

// Input validation middleware example
const validateDonationInput = (req, res, next) => {
    const { name, email, amount } = req.body;
    
    // Check for required fields
    if (!name || !email || !amount) {
        return res.status(400).json({
            error: 'Missing required fields'
        });
    }
    
    // Validate email format
    const emailRegex = /^[^\s@]+@[^\s@]+.[^\s@]+$/;
    if (!emailRegex.test(email)) {
        return res.status(400).json({
            error: 'Invalid email format'
        });
    }
    
    // Validate amount
    const numAmount = parseFloat(amount);
    if (isNaN(numAmount) || numAmount <= 0) {
        return res.status(400).json({
            error: 'Amount must be greater than zero'
        });
    }
    
    // If all validations pass, proceed
    next();
};

// Include CSRF token in forms
app.get('/donation-form', (req, res) => {
    // Pass CSRF token to your form
    res.render('donation-form', { csrfToken: req.csrfToken() });
});

// Secure donation processing endpoint
app.post('/process-donation', validateDonationInput, async (req, res) => {
    try {
        // Donation processing logic
        // ...
    } catch (error) {
        console.error('Donation processing error:', error);
        // Do not expose detailed error info to client
        res.status(500).json({ error: 'An error occurred while processing your donation' });
    }
});

// Error handling middleware
app.use((err, req, res, next) => {
    console.error(err.stack);
    
    if (err.code === 'EBADCSRFTOKEN') {
        return res.status(403).json({
            error: 'Invalid or expired form submission. Please try again.'
        });
    }
    
    res.status(500).json({
        error: 'Something went wrong on our end. Please try again later.'
    });
});

app.listen(3000, () => {
    console.log('Secure server running on port 3000');
});

 

Step 15: Set Up Stripe in Production

 

When ready to accept real donations, switch to production mode:

  • Change API keys from test to live in your Stripe Dashboard
  • Update your code to use the live keys (often using environment variables)
  • Set up a proper SSL certificate for your domain
  • Complete any additional identity verification required by Stripe
  • Configure webhooks for the production environment
  • Test the entire flow with a real payment card

 

Conclusion

 

You've now implemented a complete donation system using Stripe API. This solution includes one-time and recurring donations, a management dashboard, security best practices, and analytics. Remember to:

  • Always use HTTPS for secure connections
  • Regularly update Stripe libraries
  • Monitor your Stripe Dashboard for transaction issues
  • Test thoroughly before going live
  • Comply with relevant regulations like PCI-DSS and local tax laws

Stripe's comprehensive documentation is available at stripe.com/docs for additional details and advanced features.

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