Donation Records and Reconciliation Exports on Your NGO Website: What the Admin Panel Should Capture
Your NGO website's admin panel should store one permanent record for every donation attempt, not just the successful ones. Each record needs a unique reference ID of your own, the payment gateway's reference, the campaign, donor details, a status that updates through the gateway's server-to-server webhook, a receipt number once issued, the donor's consent choices, and timestamps. On top of that, the admin should let your accountant filter by date, campaign and status and export the result as a CSV, and it should log who changed what. If a developer builds only a thank-you page and an email notification, you will struggle every month-end.
This article is about what the website records and exports. It is not accounting, tax or payment-cost advice; your auditor and gateway agreement cover those. The aim is narrower: when your accountant opens the admin panel, every donation attempt should be traceable from the donor's click to a receipt, without anyone searching through emails or screenshots.
Why the Website Is Your First Source of Truth
A gateway dashboard and a bank statement both show money movement. Neither knows that a donor chose "Mid-day meals, Pune" on your campaign page, ticked a consent box, or arrived from a WhatsApp link. Only your website sees that context at the moment of donation. If it is not saved then, the accountant asks which campaign a payment belongs to, a donor writes "I paid but got no receipt" and nobody can tell whether the payment or the email failed, and two receipts exist for one donation. To compare donation platforms in the first place, see choosing an NGO donation platform in India.
The Data Your Website Should Store for Every Donation
1. Identity of the record
- Internal donation ID. Generated by your system, never reused, never edited.
- Created-at and updated-at timestamps, stored in one time zone (usually UTC) and shown in IST in the admin.
2. Gateway references
- Gateway order or session ID, created when the donor starts paying.
- Gateway payment ID, which usually arrives only after the donor attempts payment.
- Last gateway event ID, so you can recognise a repeated webhook (more on this below).
Store the raw identifiers exactly as the gateway sends them. Do not reformat them; your accountant will search for them verbatim in the gateway dashboard.
3. Money fields
- Amount and currency. Store amounts as integers in the smallest unit (for example paise) or as a fixed-decimal type, never as floating-point numbers, so totals do not drift by a fraction.
- Amount expected vs amount reported by the gateway. If the two ever differ, the record should be flagged rather than quietly accepted.
4. Donor details
- Name, email, mobile number, and the address or other fields your receipt needs.
- Any identity fields you collect for receipts. Collect only what you will actually use, and confirm with your auditor which fields your receipts must carry. TODO: confirm with the NGO's own advisor which donor identifiers its receipts require; this article does not specify them.
5. Campaign and context
- Campaign or fund the donor selected, stored as an ID that links to a campaign table, not as free text. Free text turns "Education Fund", "education fund" and "Edu Fund" into three different groups in a report.
- Donation type: one-time or recurring, and for recurring the plan or subscription reference.
- Source tags: the page URL, and optionally UTM parameters, so you can see which campaign link brought the donor.
6. Status and receipt
- Status (see the lifecycle in the next section).
- Receipt number, generated by your own sequence after success, plus receipt issued-at time and the file or link of the generated PDF.
- Receipt state: not issued, issued, re-issued, cancelled. A cancelled receipt should stay on file with a reason; it should not be deleted.
7. Consent and communication
- Whether the donor agreed to receive updates, and by which channel (email, WhatsApp, SMS).
- The consent text version and timestamp shown at the time, so you can later prove what the donor agreed to.
India's Digital Personal Data Protection Rules, 2025 were notified in November 2025 with a phased start: some rules took effect on publication and others, including the notice provisions, are reported to come into force 18 months later, and secondary reports disagree on the date for consent-manager provisions. TODO: verify current commencement dates against the Gazette notification and the MeitY site before relying on them. Whatever the dates, storing consent as a versioned, timestamped record is cheap to build now and hard to reconstruct later. Our article on the DPDP consent framework for CRMs goes deeper.
The Status Lifecycle: Initiated, Pending, Success, Failed
Payments are asynchronous. The donor's browser can close, lose signal, or return to your site before the bank has answered. So the page the donor lands on after paying is not reliable evidence that the money arrived.
Razorpay's webhook documentation, for example, notes that a payment can show as failed and later change to authorised, which is why it recommends listening for the authorised event as well as the failed one. Stripe's documentation says webhooks exist to let your integration respond to asynchronous events such as a bank confirming a payment later. The lesson is the same for any gateway: your site should treat the gateway's server-to-server notification as the source of truth.
A practical lifecycle looks like this:
| Status | Meaning on your site | Who or what sets it |
|---|---|---|
| Initiated | Donor submitted the form; order created with the gateway | Your website |
| Pending | Donor attempted payment but no final result has arrived | Webhook or a status check |
| Success | Gateway confirmed the payment as complete | Verified webhook (or a verified status check) |
| Failed | Gateway reported failure | Verified webhook |
| Abandoned | Initiated but no attempt before a time limit you choose | Scheduled job |
| Refunded or reversed | Money returned after success (if you support it) | Admin action plus gateway confirmation |
Some rules make this safe:
- Only forward moves, mostly. Success should not silently change back to initiated because an old event arrived late. Define which transitions are allowed and reject the rest, logging the rejection.
- Failed is not always final. Because a failed payment can later be reported as authorised or captured by the gateway, let a later verified event override Failed, and record that it did.
- Pending needs a way out. A scheduled job should query the gateway for payments stuck in Pending or Initiated beyond a threshold, then update them. Razorpay's best-practice page itself suggests polling the payment API for business-critical, synchronous cases, since webhooks are asynchronous.
- Keep a history table. Each status change is a new row (old status, new status, source, time, event ID), not an overwrite. This is the same idea as an audit log, covered below.
- Receipts follow success only. Generate the receipt number when the status becomes Success, and only once.
Webhook handling basics
Hold your developer to the vendor documentation. Stripe says that without signature verification an attacker could send fake events to modify records, so verify the signature header against the raw request body; other gateways offer equivalent checks. Stripe says to return a 2xx before complex logic, and Razorpay's best-practices page says to respond within 5 seconds and that sustained non-2xx responses over 24 hours can lead to the webhook being disabled; check your gateway's current page for exact numbers. Stripe also recommends processing events in a background queue so campaign spikes do not overwhelm the endpoint. Finally, both vendors say the same event can arrive more than once, which leads to the duplicates section below.
Reference IDs: One Donation, One Traceable Key
A donor, a bank and an accountant each call the same donation something different. Your website can bridge them with a small set of identifiers.
Create your own reference first
Generate an internal reference when the donor submits the form, before sending them to the gateway. Many gateways let you attach your own reference or notes to the order they create; Razorpay's order API, for example, supports a receipt string and notes for this purpose. TODO: confirm the exact field names in the gateway your NGO uses. Passing your reference to the gateway means the gateway's own dashboard and reports will carry it back, so the accountant can match both ways.
Good internal references share these traits:
- Unique and never reused, enforced by a database constraint, not by hope.
- Not guessable. If the reference is a simple counter, anyone could look at other people's confirmation pages by changing a number. Use a non-sequential token on public URLs and keep the readable number for staff.
- Separate from the receipt number. The receipt number is generated only for successful donations and should follow your own numbering rules; the donation ID exists from the first click. Mixing the two leaves gaps in the receipt series whenever an attempt fails.
Avoiding Duplicate Records and Duplicate Receipts
Duplicates arrive from several directions. Each needs its own guard.
Repeated webhooks
Stripe's documentation recommends logging the event IDs you have processed and skipping any you have seen, and warns that in some cases two separate event objects are generated, so the object ID plus event type should also be used to spot duplicates. Razorpay says duplicates are expected under its at-least-once delivery and that each event carries a unique value in the x-razorpay-event-id header you can compare. In plain terms, your database needs a table of processed event IDs with a unique constraint, and the code must check it first.
Also handle events arriving out of order. Stripe states it does not guarantee delivery order and advises not to use the event's timestamp to decide whether you have processed it. Your status rules (above) are what make this safe.
Race conditions between webhook and browser return
The donor returns to your site at about the same moment the webhook arrives. If both paths try to mark success and issue a receipt, you can get two receipts. The fix is a database-level guard: update the status with a condition ("only if not already Success"), and put a unique constraint on receipt number and on the donation ID within the receipts table.
Admin Filters Your Accountant Will Actually Use
A good filter bar saves more hours than a fancy dashboard. Ask for these at minimum:
- Date range, with a choice of "created" date versus "completed" date. They differ whenever a payment is confirmed after midnight or days later.
- Status, with multi-select (for example Success plus Refunded).
- Campaign or fund, selected from a list.
- Donation type: one-time or recurring.
- Gateway payment ID and internal reference, each searchable with exact match.
- Receipt number or receipt state (not issued, issued, cancelled).
- Flagged records: amount mismatch, stuck pending, manually changed.
CSV Exports for the Accountant
What a good export contains
Agree the column list with your accountant before build, then freeze it and version it. A starting set, which you should adjust, might be:
- Internal donation ID
- Created date and time; completed date and time
- Status
- Campaign or fund name and ID
- Amount and currency
- Gateway name, order ID, payment ID
- Receipt number and receipt issue date
- Donor name, email and mobile (role-restricted), donation type, and notes or flags
Include a header row with stable column names, one row per donation, ISO-style dates (2026-10-10 14:32:05) and a stated time zone. Excel opens UTF-8 files with Indian-language names incorrectly unless handled; ask your developer to test Hindi and Marathi donor names in the export and, if necessary, include a byte order mark. TODO: verify this behaviour with the versions of Excel your accountant uses.
Guard against spreadsheet formula injection
Donor-entered fields such as name, dedication or notes can begin with characters that spreadsheet programs treat as formulas. OWASP describes CSV injection as untrusted input placed in a CSV that spreadsheet software then interprets as a formula, and recommends neutralising cells that start with =, +, -, @, tab or carriage return. OWASP also warns that no single sanitisation works in every spreadsheet application and advises testing the export in the applications your users actually use. Ask your developer how the export handles this, and have it tested with a deliberately odd name such as =1+1 (use a test record only).
Control who can export
An export of donor names and mobile numbers is the most sensitive file your site can produce. Limit export rights by role, and let the accountant's default export omit contact columns. Our article on donation site security and hosting covers access control and backups.
Audit Logs: Who Changed What, and When
An audit log is a record of actions, separate from the donation table. OWASP's logging guidance for developers stresses recording security-relevant events with enough context, protecting the logs from tampering, and keeping sensitive data out of them. TODO: re-verify the OWASP logging page content before publication; the page redirected during research and was not read in full.
For a donation admin, a useful log captures:
- Who: the admin user ID and role (never a shared login).
- What: the action (status changed, receipt generated, receipt cancelled, donor edited, export run, settings changed, user added or removed).
- Which record: donation ID or donor ID.
- Before and after values for changes to amount, status, campaign or donor details.
- Why: a required reason field for manual overrides.
Practical rules:
- Logs are append-only. No admin, including the super admin, edits or deletes a log row from the interface.
- Log failures too: rejected webhooks (bad signature), blocked transitions, failed logins.
- Do not store card details, passwords, API secrets or full webhook secrets in the log. Mask sensitive values.
- Set a retention period agreed with your trustees and auditor. TODO: confirm the retention duration with your NGO's advisor; this article does not set one.
Testing Before Launch
Most donation-record bugs show up only when something goes wrong, so test the wrong paths on purpose. Use the gateway's test or sandbox mode and a staging copy of the site. Stripe's docs, for instance, recommend testing in a sandbox and triggering test events with its CLI before going live; other gateways have their own test modes and test credentials.
A pre-launch checklist
- Happy path. One donation per campaign: record created, status moves to Success, receipt number issued once, email sent, admin row correct.
- Failed payment. The record shows Failed with a reason, no receipt is created, and the donor can retry without creating confusion.
- Abandoned payment. The donor closes the tab. The record sits in Initiated, then the scheduled job marks it Abandoned or resolves it by checking with the gateway.
- Delayed confirmation. Simulate a webhook arriving minutes after the donor returned. The status must still update correctly.
- Duplicate webhook. Replay the same event twice. Only one receipt, one status history row for that change.
- Out-of-order events. Send a "failed" after a "success" for the same payment and confirm your transition rules behave as agreed.
- Bad signature. Send a webhook with a wrong signature. It must be rejected and logged.
- Webhook endpoint down. Disable it briefly, then restore, and watch the retries land. Stripe documents retries for up to three days in live mode, and Razorpay documents disabling after 24 hours of failures; confirm your own gateway's behaviour and make sure someone is alerted when the endpoint fails.
- Export check. Export a test month and reconcile row count and total against the on-screen filter. Open the CSV in the accountant's actual software, with Indian-language names and a formula-like test entry.
- Audit log. Make an override and an export; confirm both appear with correct who, what and why.
After launch
Make one small live donation on launch day and trace it end to end with your accountant. For the first week, check daily for payments stuck in Pending and for webhook failures.
What to Ask Your NGO Website Developer
- Which fields are stored for every donation attempt, including failed and abandoned ones? Can we see the data model?
- Is status updated from verified webhooks, and is the signature checked on every request?
- How do you stop duplicate records from double clicks, repeated webhooks and out-of-order events? Where are processed event IDs stored?
- What generates the donation ID and the receipt number, and can either ever repeat or leave gaps unexplained?
- What columns does the CSV export contain, can we change them, and is the column order versioned?
- How is formula injection handled in exports, and have you tested Hindi and Marathi names?
- Is there an append-only audit log, and does it record exports and manual overrides with a reason?
- How do you separate test and live data, and how will launch testing be shown to us?
- What happens to the data if we change gateways or developers? Can we get a complete export in a documented format?
If the answers are vague, treat that as information. A developer who has built donation systems before should answer most of these without hesitation.
Where Govindani Infotech Fits
Govindani Infotech is a Pune web development company that builds NGO websites with donation flows, admin panels and CRM integration. If you want a donation admin designed around these fields, filters and exports, our NGO website page describes what we build, and the website creation service page covers the general process. If donor records need to live in a CRM, see CI CRM. To discuss your records and reporting needs, message us on WhatsApp or use the contact page.
Frequently Asked Questions
What is the minimum a donation record should contain?
At least an internal donation ID, timestamps, the gateway order and payment references, amount, campaign, donor contact details, status, receipt number once issued, and the donor's consent record. Store attempts that fail or are abandoned too, because they are how you answer "I paid but nothing happened" queries.
Why not just trust the page the donor sees after paying?
Because the donor's browser may close or lose connection before it returns to your site, and a bank's final answer can arrive later. Both Razorpay and Stripe document webhooks as the server-to-server way of learning the outcome, so your site should update status from verified webhook events and check stuck payments with the gateway.
How do we stop duplicate receipts?
Use several layers: one-time form tokens, a table of processed webhook event IDs with a unique constraint, a conditional status update that only moves a donation to Success once, and unique constraints on receipt numbers. Stripe and Razorpay both say duplicate event delivery is expected, so the guard must be in your code and database.
Can an NGO admin export be unsafe to open in Excel?
It can be if donor-entered text starting with characters such as =, +, - or @ is written into a CSV unchanged. OWASP calls this CSV injection and recommends neutralising such cells and testing in the spreadsheet software you actually use. Ask your developer to confirm this is handled.
Does this article cover payment fees or tax treatment?
No. It covers only what your website stores, shows and exports. Fee, settlement and tax questions belong to your gateway agreement and your NGO's accountant or auditor.
Sources:
- https://razorpay.com/docs/webhooks/
- https://razorpay.com/docs/webhooks/best-practices/
- https://docs.stripe.com/webhooks
- https://community.owasp.org/attacks/CSV_Injection
- https://owasp.org/www-project-proactive-controls/ (security logging control; TODO re-verify)
- https://www.mondaq.com/india/data-protection/1708164/digital-personal-data-protection-rules-2025-notified (secondary summary; TODO verify against the official Gazette notification)