How to Build and Test a Donation Checkout Flow on an NGO Website
A reliable NGO donation checkout is built in a fixed order: a donation page that collects the amount and donor details, a server that creates the payment order, a checkout the donor completes (hosted by the provider or embedded in your page), a signed webhook that confirms the result to your server, and only then a receipt and a thank-you. The key rule is that the donor's browser never decides whether money arrived; your server decides, based on a verified message from the payment provider. If you build it that way and test it in the provider's test mode before launch, you avoid the two failures NGOs dread most: a donor who paid but got no receipt, and a donor who got a receipt for money that never arrived.
This guide is for NGO managers who have to brief or judge a developer, and for developers who want a checklist. It replaces an earlier version of this page that was about gateway fees and settlement timing. That is an accounts question and it belongs with your finance team; fees differ by provider and change over time, so read them from the provider's current pricing page. Everything below is about how the checkout is built and tested.
The Shape of a Donation Flow
Before choosing any technology, agree the steps on paper. A donation flow has seven moments, and each one needs an owner (the page, your server or the provider).
- Donor chooses an amount and a frequency. One-time or monthly, with preset amounts and a custom field.
- Donor enters details. Name, email, phone and, if you issue 80G acknowledgements, the fields your finance team needs (our guide to designing the donation page covers which fields to ask for and why).
- Your server creates a payment order. The amount is set here, on the server, not trusted from the page.
- Donor pays. On the provider's page or in a window embedded in yours.
- The provider tells your server the outcome. This is the webhook.
- Your server records the donation and triggers the receipt.
- Donor sees a clear result. Success, failure or "still confirming".
Most bugs live in the gaps between steps 4, 5 and 6, so most of this article is about those gaps.
Choosing an Integration Type: Hosted Checkout, Embedded Checkout or Payment Links
Providers generally offer several ways to take a payment. Names differ, but the families are similar. Razorpay's own documentation describes Hosted Checkout as a redirection-based integration where the complete checkout is developed and managed by Razorpay, and contrasts it with Standard Checkout, where the customer enters payment details in a pop-up on your page. It also describes Hosted Checkout as taking the card-data compliance burden off you because payment information is held by the provider. Check your own provider's current docs for the exact options it offers.
Hosted checkout (redirect)
The donor clicks Donate, lands on a page run by the provider, pays and is sent back to your site.
- Good for: small teams, fast launch, the least code to maintain, and the least sensitive data touching your server.
- Trade-off: the donor briefly leaves your branding, and you have less control over the look and the steps.
- Watch for: the donor who never returns to your site after paying. Your confirmation cannot depend on the return trip.
Embedded or pop-up checkout
Your page loads the provider's script, opens a payment window over your page and receives a result in the browser.
- Good for: a donation page that feels like part of your site, and tighter control of the steps before payment.
- Trade-off: more front-end code, more testing across phones and browsers, and a temptation to trust the browser's "success" callback. Do not. That callback is a hint to show a spinner and ask your server, not proof of payment.
Payment links and QR codes
A link or QR generated for a specific amount or campaign, shared on WhatsApp, in print or at an event.
- Good for: field collection, event appeals and one-off campaigns where you do not need a full page.
- Trade-off: donor details are whatever the link collects, so your receipt data may be thinner. Decide upfront how these donations will be matched to donor records.
Which one should an NGO pick?
A practical rule of thumb: start with hosted checkout if you have no developer on retainer and want the shortest path to a working, safe flow. Choose embedded checkout if donor completion on your own page matters more than saving development time, and you have someone to test it properly. Whichever you choose, the server-side parts (order creation, webhook confirmation, receipt trigger) are identical, and those are the parts that matter most.
If your NGO also runs other online payments such as memberships, event tickets or merchandise, ask whether one checkout setup can serve all of them. Our checkout system service is built around that idea.
Create the Order on the Server
When the donor taps Donate, your page should send the amount, frequency and campaign to your server. The server validates them (minimum and maximum, allowed currency, campaign exists), saves a pending donation record with your own unique reference, and then asks the provider to create a payment order. The provider returns an order identifier that the checkout uses.
Three reasons this matters:
- Tamper resistance. If the amount only lives in the browser, a donor, a bot or a bug can change it. Setting it on the server means the amount the donor sees is the amount that is charged.
- A record before the money moves. The pending row lets you match the later webhook to a donor, a campaign and a receipt.
- Duplicate protection. If a donor double-clicks Donate, your reference or the provider's idempotency mechanism (read your provider's docs for how it works) should stop two orders being created for one intention.
Store the provider's order identifier next to your pending record. Every later message from the provider will mention it.
Confirm With a Signed Webhook, Not the Redirect
A webhook is a server-to-server message: the provider's server posts the result to a URL on your server. It is the dependable way to know what happened, because it does not depend on the donor's phone staying connected, the tab staying open or the donor returning to your site.
Verify the signature
Your webhook URL is public, so anyone could post "donation 500 paid" to it. Providers therefore sign each message. The details come from each provider's docs:
- Razorpay documents that it creates an HMAC-SHA256 signature sent in the
X-Razorpay-Signatureheader, computed with your webhook secret over the raw request body, and warns against parsing or casting the body before generating the signature. - Cashfree documents the signature in an
x-webhook-signatureheader and a timestamp inx-webhook-timestamp, generated from the raw payload, and recommends rejecting requests whose timestamp is older than five minutes for payment gateway webhooks.
The common lesson is the same: verify against the raw bytes, keep the secret on the server only, reject anything that fails and log the rejection. Always check the current documentation for your provider rather than copying header names from an article.
Expect duplicates and out-of-order messages
Razorpay's documentation says webhooks can arrive multiple times and recommends using the x-razorpay-event-id header, unique per event, to filter duplicates by tracking processed ids. It also notes that webhooks may not arrive in the expected order, for example payment.authorized before payment.captured, so endpoints should cope. Cashfree recommends validating an x-idempotency-header that is unique for each unique payload.
For your build, this means:
- Save each event id in a table with a unique constraint. If saving fails because it already exists, acknowledge and stop.
- Write handlers that set a state ("mark donation paid if it is pending") rather than add to a number.
- Never create a second receipt for the same donation. Put a unique constraint on the donation reference in the receipts table.
Answer quickly, work later
Cashfree's documentation states that it retries when your endpoint does not return HTTP 200 and counts any response outside the 2xx range, including redirects, as a failed delivery. So your endpoint should verify the signature, store the raw event, return success and let a background job do the rest. A slow or crashing endpoint causes retries, and retries cause duplicates if your handler is careless. Beware of a security plugin, a login redirect or a maintenance page sitting in front of the webhook URL; a redirect is a failure.
Which events matter
Providers send many event types. Razorpay's webhook introduction names payment.authorized, order.paid and payment.failed as examples. For a donation page, your developer needs a short written map of which provider events move a donation to which state, taken from your provider's current event list. Ask for that map in writing.
Trigger the Receipt Only After Confirmed Payment
The receipt is where an NGO's legal and trust obligations meet the checkout. The rule is simple: the receipt trigger fires from the confirmed-payment handler on your server, never from the thank-you page.
A safe sequence:
- The verified webhook marks the donation paid.
- In the same transaction, or via a queue entry that cannot be lost, a receipt job is created for that donation.
- The receipt job generates the document and emails it. If the email fails, the donation stays paid and the job retries.
- The admin screen shows donations that are paid but have no receipt, so nothing quietly falls through.
What the receipt must contain, and how it differs from the statutory certificate, is covered in our article on automated 80G receipts and avoiding donor errors. Have your accountant sign off the receipt template, not the developer.
Design for Failure and Retry
Most donation tests only check the happy path. A real donor experiences failure often enough that the page should treat it as a normal state.
Show three honest outcomes. Success (server confirmed), failed (provider said failed) and pending (no confirmation yet). If the donor returns to your page before the webhook arrives, show "We are confirming your donation" and poll your own server for a short time. Do not show success on the strength of the browser callback.
Make retry one click. On failure, keep the amount and details filled in and offer "Try again" and "Try another method". Do not send the donor back to an empty form.
Say what happens to the money. If the donor sees a debit from their bank but a failed page, they will worry. Add a plain line such as: if money was deducted, it will be confirmed here or returned by the bank and provider through their own process; include your contact email. Do not promise timelines you cannot verify; check your provider's help pages for what to tell donors.
Add a safety net. A scheduled job that asks the provider for the status of donations that have stayed pending too long catches the rare lost webhook. Our article on keeping webhooks, orders and refunds in sync describes this pattern from the online-store side and the same ideas apply to donations.
A Test-Mode Checklist Before Launch
Providers give you a test mode with separate keys where no real money moves. Razorpay's documentation, for instance, describes using test-mode API keys in the checkout code, with simulated transactions where no money is deducted from the customer's account. Its UPI test identifiers include success@razorpay to succeed and failure@razorpay to fail. These values change, so take them from your provider's current test page and not from this article.
Work through this list in test mode, then repeat the essentials once with a small real donation in live mode before announcing the page.
Payment outcomes
- Successful payment on each method you enable (UPI, card, net banking, wallet as applicable).
- Failed payment, then a successful retry on the same page.
- Donor closes the window midway, then starts again.
- Donor pays and closes the tab before returning to your site. The donation must still become paid via the webhook.
Webhook behaviour
- A webhook with a correct signature is accepted and processed.
- A webhook with a wrong signature or edited body is rejected and logged.
- The same event delivered twice produces one donation and one receipt.
- Events arriving out of order leave the donation in the right final state.
- Your endpoint returns a plain success response, not a redirect or an error page, even when your site has a login or security plugin.
- If your server is down for a few minutes, the provider's retries (if your provider offers them) bring the donation through afterwards, or your safety-net job does.
Razorpay's documentation says test webhooks can be tried by triggering test-mode transactions and by using a tunnel or staging site to reach your endpoint, and that some common tunnelling and request-capture domains are blocked. Read the current page before choosing a testing method.
Data and receipts
- The amount charged equals the amount shown, including custom amounts, and a tampered amount from the browser is ignored.
- The donation record carries the right campaign, frequency and donor details.
- A receipt is created once per paid donation, with a unique number, and emailed.
- A failed or abandoned donation never gets a receipt.
Front end and experience
- Works on a mid-range Android phone on a slow connection, because that is where many donors are.
- Works in the in-app browsers of the apps your donors share links in.
Security basics
- Keys and webhook secrets live in server configuration, never in the page source or a public repository.
- Test keys and live keys are kept separate, and the live switch is a recorded, deliberate step.
- Card details are never stored on your server; with hosted or standard checkouts the provider handles them.
Recurring Donations Need Their Own Test Pass
Monthly giving is a second flow, not a checkbox: the donor authorises once and later charges happen without them on the page, so you depend even more on webhooks. In test mode, ask your developer to show a first charge, a later charge, a failed charge and a cancellation, and check the provider's current documentation for what it supports.
FCRA and Foreign Donations: Keep Them Separate
If your NGO is registered under FCRA, foreign contributions have legal handling that ordinary donations do not, including the requirement to receive them through a designated account. This article does not explain those rules. For the build, the point is structural: do not run foreign and domestic donations through one undifferentiated form. Ask your developer for a separate, clearly labelled foreign-contribution page or path, with its own payment setup, its own records and its own receipt wording. Have your compliance adviser decide what that path must collect and show, and confirm with your provider whether and how they support it.
What to Ask a Developer Before You Sign
Use these as questions in a briefing or a tender. A good developer answers them without hesitation, and the answers should end up in the scope.
- Which integration type will you use (hosted, embedded or links), and why for us?
- Where is the amount set, and what happens if the browser sends a different one?
- Which provider events move a donation to paid, failed and pending? Can I see the map?
- How do you verify webhook signatures, and where are secrets stored?
- What happens if the same webhook arrives twice, or out of order?
- How does a lost webhook get repaired?
- What triggers the receipt, and how do you prevent duplicates and number clashes?
- What does the donor see on failure, pending and abandoned payments?
- Can you show me a test-mode run of every item in the checklist above?
- How are foreign contributions kept apart from domestic ones?
- Who owns the provider account, the keys and the code? The answer should be your NGO.
- What admin screen will show paid-without-receipt and long-pending donations?
Where Fees and Settlements Fit
The earlier version of this page discussed charges and settlement timing. In one line: fees and payout timelines differ by provider and plan, so read them from your provider's current pricing and settlement pages, and ask your accountant to reconcile payouts against donation records.
Putting It Together
Build in this order: agree the flow on paper, pick the integration type, create orders on the server, add signed and idempotent webhooks, connect receipts to confirmed payments, design failure states, then run the test-mode checklist and one small live test. Skipping the webhook step to launch sooner is the most expensive shortcut.
If you are planning an NGO website with donations, our NGO website service covers the donation page, payment flow and receipts together, and website creation covers the wider site around it. If you already have a site and the checkout is the weak part, talk to us through the contact page and bring your provider name and your current donation page link; we can review the flow against this checklist and tell you plainly what needs to change.
Frequently Asked Questions
Should an NGO use hosted checkout or an embedded checkout?
Hosted checkout is usually the simpler and safer starting point: the provider runs the payment page, so there is less code to build and less sensitive data on your server. Embedded checkout keeps the donor on your page and gives more design control, but needs more development and testing. In both cases, confirm payments with a server-side webhook, not the browser.
Why can't I just show the thank-you page when the donor returns?
The donor may close the tab, lose signal or never return, and a thank-you page address can be reached without paying. A signed webhook from the provider is the dependable confirmation, so receipts and "paid" status should be triggered by it.
When should the 80G receipt be generated?
Only after your server has confirmed the payment through a verified webhook. Create the receipt job at that moment, keep receipt numbers unique, and make sure a failed or abandoned payment never produces one.
How do I test a donation page without real money?
Use the provider's test mode with test keys and the test card or UPI details from the provider's current documentation. Test success, failure, retry, duplicate and out-of-order webhooks and a closed tab, then make one small live donation before launch.
Do foreign donations go through the same checkout?
Treat them separately. If you receive foreign contributions under FCRA, keep a distinct page or path with its own setup and records, and let your compliance adviser decide what it must collect. This guide only flags the separation.