Automate Donation Receipts and Tracking (For Tax Compliance)
Save 37+ hours monthly with automated donation receipts-ensure IRS compliance and eliminate errors, so your team can focus on mission-critical work.
Your nonprofit lost 37 hours in January to donation receipt errors-37 hours your team could’ve spent on mission-critical work. All because a misplaced date voided three $250+ acknowledgments. The IRS doesn’t forgive mistakes. No valid receipt? No tax deduction. Do this manually, and you’ll cap out at 100 donations a month.
Stop Typing. Start Automating.
Manual receipting burns 8-10 hours per 100 donations: export Stripe data, paste into Sheets, generate PDFs in Canva, email one by one, update YTD totals. One typo in donor ID or date? IRS compliance fails.
The IRS requires written acknowledgment for donations ≥$250. Your receipt must include:
- Org legal name and EIN
- Donor name and address
- Date and amount
- Statement: “No goods or services were provided in exchange, except for intangible religious benefits” (if true)
Miss one item, the deduction fails.
Automate IRS Receipts in 60 Minutes
Use Google Apps Script to connect Stripe or PayPal to Sheets. On new donation, it auto-fills a Docs template, exports PDF, emails receipt within an hour, and updates YTD totals.
The Code: PDF Generator & Emailer
This script takes a row from Google Sheets, fills a Google Doc template, converts it to PDF, and emails it.
1. Setup Your Template
Create a Google Doc. Add placeholders like {{Name}}, {{Amount}}, {{Date}}. Copy the Header ID from the URL (the string between /d/ and /edit).
2. The Script
// CONFIGURATION
const CONFIG = {
TEMPLATE_ID: 'YOUR_DOC_TEMPLATE_ID_HERE',
FOLDER_ID: 'YOUR_DESTINATION_FOLDER_ID_HERE', // Where to save PDFs
SHEET_NAME: 'Donations',
ORG_NAME: 'Acme Nonprofit',
ORG_EIN: '12-3456789'
};
function processDonations() {
const sheet = SpreadsheetApp.getActiveSpreadsheet().getSheetByName(CONFIG.SHEET_NAME);
const dataRange = sheet.getDataRange();
const data = dataRange.getValues();
const headers = data[0];
// Column Mapping (adjust based on your sheet)
const COL = {
EMAIL: headers.indexOf('Email'),
NAME: headers.indexOf('Donor Name'),
AMOUNT: headers.indexOf('Amount'),
DATE: headers.indexOf('Date'),
STATUS: headers.indexOf('Receipt Status')
};
// Iterate rows
for (let i = 1; i < data.length; i++) {
const row = data[i];
const status = row[COL.STATUS];
// Skip if already sent
if (status === 'SENT') continue;
// Check Quota
if (MailApp.getRemainingDailyQuota() < 1) break;
try {
const pdfFile = createReceiptPdf(row, COL);
sendEmail(row[COL.EMAIL], pdfFile, row[COL.NAME]);
// Update Status
sheet.getRange(i + 1, COL.STATUS + 1).setValue('SENT');
sheet.getRange(i + 1, COL.STATUS + 2).setValue(new Date()); // Timestamp column
} catch (e) {
console.error(`Error row ${i + 1}: ${e.message}`);
sheet.getRange(i + 1, COL.STATUS + 1).setValue('ERROR: ' + e.message);
}
}
}
function createReceiptPdf(row, colMap) {
const templateFile = DriveApp.getFileById(CONFIG.TEMPLATE_ID);
const folder = DriveApp.getFolderById(CONFIG.FOLDER_ID);
// Make a temporary copy of the template
const tempFile = templateFile.makeCopy(`Receipt_${row[colMap.NAME]}`, folder);
const tempDoc = DocumentApp.openById(tempFile.getId());
const body = tempDoc.getBody();
// Replace placeholders
body.replaceText('{{Name}}', row[colMap.NAME]);
body.replaceText('{{Amount}}', '$' + parseFloat(row[colMap.AMOUNT]).toFixed(2));
body.replaceText('{{Date}}', formatDate(row[colMap.DATE]));
body.replaceText('{{OrgName}}', CONFIG.ORG_NAME);
body.replaceText('{{EIN}}', CONFIG.ORG_EIN);
tempDoc.saveAndClose();
// Convert to PDF
const pdfContent = tempFile.getAs(MimeType.PDF);
const pdfFile = folder.createFile(pdfContent).setName(`Receipt - ${row[colMap.NAME]}.pdf`);
// Delete temp doc
tempFile.setTrashed(true);
return pdfFile;
}
function sendEmail(email, pdf, name) {
MailApp.sendEmail({
to: email,
subject: `Your Donation Receipt - ${CONFIG.ORG_NAME}`,
body: `Hi ${name},\n\nThank you for your generous donation. Please find your tax receipt attached.\n\nSincerely,\n${CONFIG.ORG_NAME}`,
attachments: [pdf]
});
}
function formatDate(date) {
return Utilities.formatDate(new Date(date), Session.getScriptTimeZone(), "MM/dd/yyyy");
}Implementing Year-End Totals
Instead of scrambling in January, add a function to calculate YTD totals instantly.
function updateYTD(email, amount) {
const sheet = SpreadsheetApp.getActiveSpreadsheet().getSheetByName("Donors");
// Logic to find donor row and add amount to YTD column
// (See full example in our repo or book a call for the complete CRM kit)
}Build vs. Buy: Why DIY Wins Under 5K Donations/Year
| Factor | Build (Apps Script) | Buy (SaaS) |
|---|---|---|
| Cost | $0 (Google Workspace) | $50-300/month |
| Setup Time | 60-90 minutes | 1-2 weeks |
| Control | Full data ownership | Vendor lock-in |
| Compliance | You control IRS language | Generic templates |
Process fewer than 5,000 donations a year? Build it. You keep your data. SaaS only makes sense for donor portals or complex recurring logic.
Reclaim 150+ Hours a Year
- Automate IRS receipts in 60 minutes-no backend
- Enforce exact IRS language-zero guesswork
- Catch errors before they trigger audits
Book a call, running by Friday. 30-day warranty.
Related Industry Guides
Need a Custom Automation Solution?
We specialize in building tailored Google Workspace automations for businesses of all sizes. Let us help you eliminate repetitive tasks and streamline your workflows.