# How to accept donations with Stripe

- Tool: Stripe
- Difficulty: Intermediate
- Time required: 20 minutes
- Compatibility: Stripe API v2024-12+, Node.js 18+
- Last updated: March 2026

## TL;DR

Accept donations with Stripe by creating a Checkout Session where submit_type is 'donate' and the customer can choose their amount. Use price_data with a dynamic amount from your form, or use Stripe Payment Links with customer-chosen pricing. Always use test mode and card 4242 4242 4242 4242 during development.

## Accepting Donations with Stripe

Stripe Checkout supports donation workflows out of the box. By setting submit_type to 'donate', the checkout page shows a 'Donate' button instead of 'Pay'. You can let donors choose custom amounts by passing dynamic price_data from your frontend, or offer preset tiers. Stripe also supports recurring donations via subscription mode. This guide covers both one-time and recurring donation setups.

## Before you start

- A Stripe account (sign up free at dashboard.stripe.com)
- Node.js 18+ with stripe and express packages installed
- Your Stripe secret key (sk_test_) stored in an environment variable
- A basic frontend form where donors select or enter an amount

## Step-by-step guide

### 1. Create a donation amount form

Build a simple frontend where donors can select a preset amount or enter a custom one. Send the chosen amount to your server.

```
<form id="donation-form">
  <h2>Support Our Cause</h2>
  <div>
    <button type="button" class="amount-btn" data-amount="500">$5</button>
    <button type="button" class="amount-btn" data-amount="1000">$10</button>
    <button type="button" class="amount-btn" data-amount="2500">$25</button>
    <button type="button" class="amount-btn" data-amount="5000">$50</button>
  </div>
  <label>Custom amount: $<input type="number" id="custom-amount" min="1" /></label>
  <button type="submit">Donate</button>
</form>
```

**Expected result:** A form with preset buttons ($5, $10, $25, $50) and a custom amount field.

### 2. Create the donation Checkout Session endpoint

On your server, accept the donation amount and create a Checkout Session with submit_type: 'donate'. This changes the button text on the Stripe page to 'Donate' and adjusts the copy for donations.

```
const stripe = require('stripe')(process.env.STRIPE_SECRET_KEY);
const express = require('express');
const app = express();
app.use(express.json());

app.post('/create-donation-session', async (req, res) => {
  const { amount, email } = req.body; // amount in cents

  if (!amount || amount < 100) {
    return res.status(400).json({ error: 'Minimum donation is $1.00' });
  }

  try {
    const session = await stripe.checkout.sessions.create({
      submit_type: 'donate',
      mode: 'payment',
      line_items: [
        {
          price_data: {
            currency: 'usd',
            product_data: {
              name: 'Donation',
              description: 'Thank you for your generous support',
            },
            unit_amount: Math.round(amount),
          },
          quantity: 1,
        },
      ],
      customer_email: email || undefined,
      success_url: 'https://yoursite.com/thank-you?session_id={CHECKOUT_SESSION_ID}',
      cancel_url: 'https://yoursite.com/donate',
      metadata: {
        donation_type: 'one_time',
      },
    });

    res.json({ url: session.url });
  } catch (err) {
    res.status(500).json({ error: err.message });
  }
});
```

**Expected result:** The endpoint returns a Checkout URL. The Stripe page shows 'Donate' instead of 'Pay'.

### 3. Add recurring donation support

For monthly donations, change the mode to 'subscription' and add a recurring interval to the price_data.

```
app.post('/create-recurring-donation', async (req, res) => {
  const { amount } = req.body;

  const session = await stripe.checkout.sessions.create({
    submit_type: 'donate',
    mode: 'subscription',
    line_items: [
      {
        price_data: {
          currency: 'usd',
          product_data: { name: 'Monthly Donation' },
          unit_amount: Math.round(amount),
          recurring: { interval: 'month' },
        },
        quantity: 1,
      },
    ],
    success_url: 'https://yoursite.com/thank-you',
    cancel_url: 'https://yoursite.com/donate',
  });

  res.json({ url: session.url });
});
```

**Expected result:** Donors are billed monthly for the chosen amount. They can cancel via the Stripe Customer Portal.

### 4. Wire up the frontend form

Handle form submission by sending the selected amount to your server and redirecting to the Checkout URL.

```
let selectedAmount = 1000; // default $10

document.querySelectorAll('.amount-btn').forEach((btn) => {
  btn.addEventListener('click', () => {
    selectedAmount = parseInt(btn.dataset.amount);
  });
});

document.getElementById('donation-form').addEventListener('submit', async (e) => {
  e.preventDefault();
  const customInput = document.getElementById('custom-amount');
  if (customInput.value) {
    selectedAmount = Math.round(parseFloat(customInput.value) * 100);
  }

  const res = await fetch('/create-donation-session', {
    method: 'POST',
    headers: { 'Content-Type': 'application/json' },
    body: JSON.stringify({ amount: selectedAmount }),
  });
  const { url } = await res.json();
  window.location.href = url;
});
```

**Expected result:** Clicking Donate redirects to a Stripe-hosted page with 'Donate $X.XX' button.

### 5. Test the donation flow

Use Stripe test mode to verify the flow end-to-end without real charges.

```
// Test card: 4242 4242 4242 4242
// Expiry: 12/34, CVC: 123
// Verify in Dashboard → Payments that the donation appears
```

**Expected result:** The donation appears in your Stripe Dashboard under Payments with the correct amount and metadata.

## Complete code example

File: `donation-server.js`

```javascript
const express = require('express');
const stripe = require('stripe')(process.env.STRIPE_SECRET_KEY);

const app = express();
app.use(express.static('public'));
app.use(express.json());

// One-time donation
app.post('/create-donation-session', async (req, res) => {
  const { amount, email, recurring } = req.body;

  if (!amount || amount < 100) {
    return res.status(400).json({ error: 'Minimum donation is $1.00 (100 cents)' });
  }

  try {
    const lineItem = {
      price_data: {
        currency: 'usd',
        product_data: {
          name: recurring ? 'Monthly Donation' : 'One-Time Donation',
          description: 'Thank you for your generous support',
        },
        unit_amount: Math.round(amount),
      },
      quantity: 1,
    };

    if (recurring) {
      lineItem.price_data.recurring = { interval: 'month' };
    }

    const session = await stripe.checkout.sessions.create({
      submit_type: 'donate',
      mode: recurring ? 'subscription' : 'payment',
      line_items: [lineItem],
      customer_email: email || undefined,
      success_url: `${req.headers.origin}/thank-you?session_id={CHECKOUT_SESSION_ID}`,
      cancel_url: `${req.headers.origin}/donate`,
      metadata: {
        donation_type: recurring ? 'recurring_monthly' : 'one_time',
      },
    });

    res.json({ url: session.url });
  } catch (err) {
    console.error('Donation session error:', err.message);
    res.status(500).json({ error: err.message });
  }
});

const PORT = process.env.PORT || 3000;
app.listen(PORT, () => console.log(`Donation server on port ${PORT}`));
```

## Common mistakes

- **Letting users submit $0 or negative donations** — undefined Fix: Validate the amount server-side. Stripe requires a minimum of 50 cents ($0.50 USD). Set a reasonable minimum like $1.00.
- **Using submit_type: 'donate' with subscription mode incorrectly** — undefined Fix: submit_type: 'donate' works with both payment and subscription modes. Just make sure you add recurring to price_data when using subscription mode.
- **Not converting dollars to cents** — undefined Fix: If your frontend sends dollar amounts, multiply by 100 on your server: Math.round(dollarAmount * 100). Use Math.round to avoid floating-point issues.
- **Not providing tax receipts for donations** — undefined Fix: Use the checkout.session.completed webhook to trigger an email with a donation receipt. Include the amount, date, and your organization's tax ID if applicable.

## Best practices

- Use submit_type: 'donate' to show donation-appropriate copy on the Stripe Checkout page
- Offer both preset amounts ($5, $10, $25) and a custom amount field for flexibility
- Validate donation amounts server-side — minimum $1.00, maximum whatever you set
- Support both one-time and recurring monthly donations to maximize donor options
- Add metadata (donation_type, campaign_name) to track donations in Stripe Dashboard
- Set up checkout.session.completed webhook to send thank-you emails and receipts
- Test with card 4242 4242 4242 4242 in test mode before accepting real donations

## Frequently asked questions

### Can donors choose any amount they want?

Yes. Pass the amount dynamically from your frontend form to your server. Use price_data with the donor's chosen amount. The only constraint is Stripe's minimum of $0.50.

### How do I issue tax receipts for donations?

Listen for the checkout.session.completed webhook. When triggered, send the donor an email with the donation amount, date, and your organization's tax information. Stripe does not generate tax receipts automatically.

### Can donors cancel recurring donations?

Yes. Set up the Stripe Customer Portal so donors can manage and cancel their recurring donations. Alternatively, cancel subscriptions via your admin dashboard using the Stripe API.

### Does Stripe charge fees on donations?

Yes. Stripe's standard processing fee (2.9% + $0.30 per transaction in the US) applies. Stripe does not offer reduced fees for nonprofits, though some payment methods have lower fees.

### What if I need a more complex donation platform?

For features like fundraising campaigns, donor management, or tiered giving levels, RapidDev can help you build a custom donation platform integrated with Stripe, including donor dashboards and automated receipt generation.

---

Source: https://www.rapidevelopers.com/stripe-guide/how-to-accept-donations-with-stripe-api
© RapidDev — https://www.rapidevelopers.com/stripe-guide/how-to-accept-donations-with-stripe-api
