Skip to main content
RapidDev - Software Development Agency
stripe-guide

How to accept donations with Stripe

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.

What you'll learn

  • How to create a donation-specific Checkout Session with submit_type: 'donate'
  • How to accept custom amounts chosen by the donor
  • How to set up preset donation tiers and one-time vs recurring donations
  • How to track donations with metadata and receipts
Book a free consultation
4.9Clutch rating
600+Happy partners
17+Countries served
190+Team members
Intermediate6 min read20 minutesStripe API v2024-12+, Node.js 18+March 2026RapidDev Engineering Team
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.

Prerequisites

  • 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.

typescript
1<form id="donation-form">
2 <h2>Support Our Cause</h2>
3 <div>
4 <button type="button" class="amount-btn" data-amount="500">$5</button>
5 <button type="button" class="amount-btn" data-amount="1000">$10</button>
6 <button type="button" class="amount-btn" data-amount="2500">$25</button>
7 <button type="button" class="amount-btn" data-amount="5000">$50</button>
8 </div>
9 <label>Custom amount: $<input type="number" id="custom-amount" min="1" /></label>
10 <button type="submit">Donate</button>
11</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.

typescript
1const stripe = require('stripe')(process.env.STRIPE_SECRET_KEY);
2const express = require('express');
3const app = express();
4app.use(express.json());
5
6app.post('/create-donation-session', async (req, res) => {
7 const { amount, email } = req.body; // amount in cents
8
9 if (!amount || amount < 100) {
10 return res.status(400).json({ error: 'Minimum donation is $1.00' });
11 }
12
13 try {
14 const session = await stripe.checkout.sessions.create({
15 submit_type: 'donate',
16 mode: 'payment',
17 line_items: [
18 {
19 price_data: {
20 currency: 'usd',
21 product_data: {
22 name: 'Donation',
23 description: 'Thank you for your generous support',
24 },
25 unit_amount: Math.round(amount),
26 },
27 quantity: 1,
28 },
29 ],
30 customer_email: email || undefined,
31 success_url: 'https://yoursite.com/thank-you?session_id={CHECKOUT_SESSION_ID}',
32 cancel_url: 'https://yoursite.com/donate',
33 metadata: {
34 donation_type: 'one_time',
35 },
36 });
37
38 res.json({ url: session.url });
39 } catch (err) {
40 res.status(500).json({ error: err.message });
41 }
42});

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.

typescript
1app.post('/create-recurring-donation', async (req, res) => {
2 const { amount } = req.body;
3
4 const session = await stripe.checkout.sessions.create({
5 submit_type: 'donate',
6 mode: 'subscription',
7 line_items: [
8 {
9 price_data: {
10 currency: 'usd',
11 product_data: { name: 'Monthly Donation' },
12 unit_amount: Math.round(amount),
13 recurring: { interval: 'month' },
14 },
15 quantity: 1,
16 },
17 ],
18 success_url: 'https://yoursite.com/thank-you',
19 cancel_url: 'https://yoursite.com/donate',
20 });
21
22 res.json({ url: session.url });
23});

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.

typescript
1let selectedAmount = 1000; // default $10
2
3document.querySelectorAll('.amount-btn').forEach((btn) => {
4 btn.addEventListener('click', () => {
5 selectedAmount = parseInt(btn.dataset.amount);
6 });
7});
8
9document.getElementById('donation-form').addEventListener('submit', async (e) => {
10 e.preventDefault();
11 const customInput = document.getElementById('custom-amount');
12 if (customInput.value) {
13 selectedAmount = Math.round(parseFloat(customInput.value) * 100);
14 }
15
16 const res = await fetch('/create-donation-session', {
17 method: 'POST',
18 headers: { 'Content-Type': 'application/json' },
19 body: JSON.stringify({ amount: selectedAmount }),
20 });
21 const { url } = await res.json();
22 window.location.href = url;
23});

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.

typescript
1// Test card: 4242 4242 4242 4242
2// Expiry: 12/34, CVC: 123
3// 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 working example

donation-server.js
1const express = require('express');
2const stripe = require('stripe')(process.env.STRIPE_SECRET_KEY);
3
4const app = express();
5app.use(express.static('public'));
6app.use(express.json());
7
8// One-time donation
9app.post('/create-donation-session', async (req, res) => {
10 const { amount, email, recurring } = req.body;
11
12 if (!amount || amount < 100) {
13 return res.status(400).json({ error: 'Minimum donation is $1.00 (100 cents)' });
14 }
15
16 try {
17 const lineItem = {
18 price_data: {
19 currency: 'usd',
20 product_data: {
21 name: recurring ? 'Monthly Donation' : 'One-Time Donation',
22 description: 'Thank you for your generous support',
23 },
24 unit_amount: Math.round(amount),
25 },
26 quantity: 1,
27 };
28
29 if (recurring) {
30 lineItem.price_data.recurring = { interval: 'month' };
31 }
32
33 const session = await stripe.checkout.sessions.create({
34 submit_type: 'donate',
35 mode: recurring ? 'subscription' : 'payment',
36 line_items: [lineItem],
37 customer_email: email || undefined,
38 success_url: `${req.headers.origin}/thank-you?session_id={CHECKOUT_SESSION_ID}`,
39 cancel_url: `${req.headers.origin}/donate`,
40 metadata: {
41 donation_type: recurring ? 'recurring_monthly' : 'one_time',
42 },
43 });
44
45 res.json({ url: session.url });
46 } catch (err) {
47 console.error('Donation session error:', err.message);
48 res.status(500).json({ error: err.message });
49 }
50});
51
52const PORT = process.env.PORT || 3000;
53app.listen(PORT, () => console.log(`Donation server on port ${PORT}`));

Common mistakes when accepting donations with Stripe

Why it's a problem: Letting users submit $0 or negative donations

How to avoid: Validate the amount server-side. Stripe requires a minimum of 50 cents ($0.50 USD). Set a reasonable minimum like $1.00.

Why it's a problem: Using submit_type: 'donate' with subscription mode incorrectly

How to avoid: submit_type: 'donate' works with both payment and subscription modes. Just make sure you add recurring to price_data when using subscription mode.

Why it's a problem: Not converting dollars to cents

How to avoid: If your frontend sends dollar amounts, multiply by 100 on your server: Math.round(dollarAmount * 100). Use Math.round to avoid floating-point issues.

Why it's a problem: Not providing tax receipts for donations

How to avoid: 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

Still stuck?

Copy one of these prompts to get a personalized, step-by-step explanation.

ChatGPT Prompt

Write a Node.js Express server that accepts donations via Stripe Checkout. Support both one-time and recurring monthly donations with custom amounts. Use submit_type: 'donate' and validate the minimum amount server-side.

Stripe Prompt

Build a donation page for my nonprofit. Add preset buttons ($5, $10, $25, $50) and a custom amount field. Create a server endpoint that makes a Stripe Checkout Session with submit_type: 'donate'. Support both one-time and monthly recurring 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.

RapidDev

Talk to an Expert

Our team has built 600+ apps. Get personalized help with your project.

Book a free consultation

Need help with your project?

Our experts have built 600+ apps and can accelerate your development. Book a free consultation — no strings attached.

Book a free consultation

We put the rapid in RapidDev

Need a dedicated strategic tech and growth partner? Discover what RapidDev can do for your business! Book a call with our team to schedule a free, no-obligation consultation. We'll discuss your project and provide a custom quote at no cost.