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
Create a donation amount form
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.
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.
Create the donation Checkout Session endpoint
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.
1const stripe = require('stripe')(process.env.STRIPE_SECRET_KEY);2const express = require('express');3const app = express();4app.use(express.json());56app.post('/create-donation-session', async (req, res) => {7 const { amount, email } = req.body; // amount in cents89 if (!amount || amount < 100) {10 return res.status(400).json({ error: 'Minimum donation is $1.00' });11 }1213 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 });3738 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'.
Add recurring donation support
Add recurring donation support
For monthly donations, change the mode to 'subscription' and add a recurring interval to the price_data.
1app.post('/create-recurring-donation', async (req, res) => {2 const { amount } = req.body;34 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 });2122 res.json({ url: session.url });23});Expected result: Donors are billed monthly for the chosen amount. They can cancel via the Stripe Customer Portal.
Wire up the frontend form
Wire up the frontend form
Handle form submission by sending the selected amount to your server and redirecting to the Checkout URL.
1let selectedAmount = 1000; // default $1023document.querySelectorAll('.amount-btn').forEach((btn) => {4 btn.addEventListener('click', () => {5 selectedAmount = parseInt(btn.dataset.amount);6 });7});89document.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 }1516 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.
Test the donation flow
Test the donation flow
Use Stripe test mode to verify the flow end-to-end without real charges.
1// Test card: 4242 4242 4242 42422// Expiry: 12/34, CVC: 1233// Verify in Dashboard → Payments that the donation appearsExpected result: The donation appears in your Stripe Dashboard under Payments with the correct amount and metadata.
Complete working example
1const express = require('express');2const stripe = require('stripe')(process.env.STRIPE_SECRET_KEY);34const app = express();5app.use(express.static('public'));6app.use(express.json());78// One-time donation9app.post('/create-donation-session', async (req, res) => {10 const { amount, email, recurring } = req.body;1112 if (!amount || amount < 100) {13 return res.status(400).json({ error: 'Minimum donation is $1.00 (100 cents)' });14 }1516 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 };2829 if (recurring) {30 lineItem.price_data.recurring = { interval: 'month' };31 }3233 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 });4445 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});5152const 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.
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.
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.
Talk to an Expert
Our team has built 600+ apps. Get personalized help with your project.
Book a free consultation