Nonprofit & PaymentsField engagement

The site, the payment processor and the organisation's records each work correctly on their own, and every gift that goes missing is lost in the gaps between them.

Lead full-stack developer.
The approach, in four words
trust the webhook, not the pageidempotent by event idreconcile against the processorminimal footprint on a live site
donate flowthe processorthank-you page: a courtesy, not the recordwebhook handler, idempotentretried up to three days, may repeat, unorderedorg recordsreconcile
The donor's browser is not a reliable part of the system. The payment succeeds whether or not it comes back, so the webhook is what records the gift and the thank-you page is a courtesy. Reconciliation against the processor is what makes a gap visible.
What was at stake

Donations had to be added to a live website that was staying exactly as it was. Every gift would pass through three systems: the donor's browser, the payment processor, and the organisation's own records. Each of them can report success while the gift is lost somewhere between them. A lost gift is not a missing row in a table. It is a person who gave money and never heard back, a thank-you letter that was never sent, and a donor who quietly decides not to give again. Nobody notices, because every system involved says it worked.

How this works

A card payment has three participants, and the browser is the one nobody can rely on.

When a donor clicks give, three systems take part: the organisation's server, the payment processor, and the donor's browser sitting between them. The server begins by asking the processor to create a payment intent. A payment intent is a record held by the processor that says this amount, in this currency, is expected from someone. It has an identifier and a status, and the status moves as the payment progresses: waiting for card details, perhaps waiting for the donor's bank to confirm, then succeeded or failed. One intent can succeed at most once, which is what stops a double click or a refreshed page from charging the donor twice.

The card details travel from the browser straight to the processor and never pass through the organisation's server. The processor talks to the card network and the donor's bank, which may ask the donor for an extra confirmation. When that is done, the processor sends the browser back to the organisation's thank-you page. Separately, and independently, it sends a message directly to the organisation's server describing what happened to the intent. That server-to-server message is the webhook.

The two messages look redundant, and they are not. The redirect travels through the donor's browser, which can be closed, lose its connection, run out of battery, or sit on a phone that switches networks at the wrong moment. If the redirect is lost, the money has still moved, and nothing on the organisation's side knows. The webhook travels between two servers and is retried until the receiver acknowledges it. It sometimes arrives late, sometimes twice, and sometimes out of order, but it arrives.

Retrying has a consequence that surprises people the first time. If the receiver processes a message and its acknowledgement is lost on the way back, the sender cannot tell the difference between that and a message that never arrived, so it sends it again. Any system that never loses a message must therefore sometimes deliver one twice. This is what at-least-once delivery means, and it is why the code receiving webhooks has to be idempotent: running it twice on the same event must have the same effect as running it once.

  1. Server creates a payment intent
    an amount and currency, held by the processor
  2. Browser sends card to processor
    the card never touches the organisation's server
  3. Bank may ask for confirmation
    the donor often leaves the page here
  4. Processor redirects the browser
    lost if the tab closes or the signal drops
  5. Processor sends the webhook
    server to server, retried until acknowledged
Of the last two steps, trust the one the donor's phone is not part of.
FigureOne donation, and two separate messages back to the organisation. The redirect travels through the donor's browser and is lost whenever the browser is. The webhook travels between servers and is retried, which makes it the only message fit to confirm a gift.
The constraint

The donate button is the visible part of this work and the smallest part. The substance is keeping three systems that never coordinate in agreement about one fact: did this gift happen. The payment processor's own behaviour sets the terms. It retries webhook delivery for up to three days, it can deliver the same event more than once, and it does not promise to deliver events in order. A single payment intent produces at most one successful charge. And the processor's guidance says plainly that the page a donor lands on after paying must not be what confirms the gift.

Where it usually breaks

Payment integrations lose money in the hand-offs, never inside a single system.

Each system in a donation flow is usually tested on its own, and usually works on its own. The site renders the form, the processor charges the card, the records system stores what it is told. The failures live in the hand-offs, where one system assumes another has done something. Because every system's own logs show success, these failures are invisible to anyone who looks at one system at a time.

The second pattern is testing in conditions that hide the hazards. In a test environment the browser always comes back, webhooks arrive once and in order, and nobody closes the tab halfway through a payment. Every one of those conditions is false in production, and code written against a clean test run quietly depends on all of them.

01

The vanished redirect

The payment succeeds and the donor closes the tab before the thank-you page loads. If the page was what recorded the gift, the gift exists nowhere the organisation looks.

02

The double gift

The webhook arrives twice because the first acknowledgement was lost. A handler that creates a record on every delivery thanks the donor twice and reports money that arrived once.

03

The stale overwrite

Events arrive out of order and an older status lands after a newer one. A handler that writes whatever arrived last can mark a successful gift as still pending.

04

The silent handler failure

The handler throws an error on a malformed or unexpected event, and the processor eventually stops retrying. Without reconciliation nobody is told, and the gap becomes permanent.

05

Believing your own records

The organisation's database is treated as the truth. It is only a copy of what it was told, and it cannot know about a message it never received.

Donor's browser
closed before the redirect landed
Thank-you page
never loaded, so it recorded nothing
Payment processor
charged the card and reports success
Organisation's records
show nothing and report no error
The failure point
A gift that exists only at the processor
money moved, no record, no thank-you
Every system involved reports success.
The only system that knows the truth is the one the organisation does not run.
A loss with no error message is a loss nobody is assigned to fix.
FigureHow a gift goes missing when the page is what confirms it. Each part behaves correctly by its own standard, and the loss sits in the hand-off between them. Nothing raises an error, so nothing gets fixed.
The fork

The reflex, and the fix.

Road not taken

Confirm the gift on the thank-you page

Pull

It is immediate, it needs no extra infrastructure, and it is right most of the time.

Why not

It misses every gift where the browser closed, the connection dropped or the redirect failed. In each of those cases the money moved and the organisation has no record of it, which is exactly the case the processor's documentation warns about.

Road taken

Confirm on the webhook, idempotently

Accepted

A webhook handler and a reconciliation job to build and keep running, added to a site that was not going to be rebuilt.

Bought

Gifts recorded whether or not the browser came back, repeat deliveries absorbed without creating a second gift, and a reconciliation pass that turns any gap into a discrepancy someone can see.

Decision

The processor is the only system that knows whether money moved. So the gift is confirmed by the processor's own message to the server, and the organisation's records are checked against the processor on a schedule.

How it was built
01Donate flow
›
02Payment intent
›
03Idempotent webhook
›
04Org records
›
05Reconcile
01

The webhook confirms the gift

The thank-you page is a courtesy to the donor. It is not the record of the transaction, because the donor's browser can close, lose signal or fail to redirect at any point after payment, and a system cannot depend on a participant it does not control.

02

A handler that can safely run twice

Delivery is at-least-once and can repeat for up to three days. The handler is keyed on the processor's event, so a repeat delivery is recognised and does nothing, and one donation never becomes two.

03

Reconciliation against the processor

A standing pass compares the organisation's records with the processor's. A gap shows up as a named discrepancy that someone can act on. That is the difference between a missing gift and a gift nobody knew to look for.

04

A small addition to the existing site

The live site keeps working as it did. What was added is the donate flow and nothing else. Rebuilding was never on the table, and on a site taking real money every extra change is one more thing that can break.

Inside the key decisions

What each choice cost, and what it bought.

AThe webhook as the only thing that confirms a gift

The record of a gift is created from the processor's webhook and from nowhere else. That cost an endpoint and its verification on a site that was otherwise not being touched, and it means the record can appear a moment after the donor sees the thank-you page. It bought a record that does not depend on the donor's browser at all. The tempting middle path was to record from both the page and the webhook. That creates two writers for one fact, and every disagreement between them becomes a merge problem with no obvious winner.

BIdempotency keyed on the processor's event

The handler remembers each event it has processed and treats a repeat as a no-op. That needs a durable memory of seen events and the discipline to hang every side effect, the record, the receipt, the thank-you, off that one check. In return, retries become harmless, which means the handler is free to fail loudly on anything it does not understand and let the processor try again. The alternative, spotting duplicates by matching amount, donor and time, is a guess. Two genuine gifts of the same amount from the same person on the same day are entirely possible, and a guess would merge them.

CReconciliation as a standing job

A scheduled comparison against the processor runs whether or not anything looks wrong. It is one more moving part, and somebody has to own the discrepancy report it produces. What it buys is that the webhook path no longer has to be perfect. Relying on webhooks alone means any gift whose delivery failed beyond the three-day retry window is gone for good, with no signal that it ever existed.

DThe smallest possible change to the live site

Only the donate flow was added. That meant fitting the flow inside the existing site's limits and leaving some improvements for later. It also meant the risk of the work was confined to the new path. Folding a partial rebuild into the donation work would have made every existing page part of the blast radius of a payments change.

Thank-you pagetells the donor, records nothing
Verified webhook from processorthe record of the gift
Event-keyed idempotent handlera repeat delivery does nothing
Reconciliation against the processornames any gap the layers above left
Processor as source of truththe only system that knows money moved
Remove any one layer and a specific, nameable kind of gift goes missing.
FigureEach layer catches what the one above it misses. The page informs the donor, the webhook records the gift, the handler absorbs repeats, and reconciliation finds anything that slipped past all three.
How it was measured

A single headline number hides where a system fails. This work was scored on the dimensions that actually decide whether it holds in production, measured on real, held-out cases rather than the demo path.

Gifts recorded where the browser never returnedBehaviour under duplicate webhook deliveryDiscrepancies surfaced by reconciliationChange footprint on the existing site
Browser never comes backis the gift still recorded from the webhook
Same event delivered twiceis there still exactly one gift and one receipt
Records disagree with processordoes reconciliation name the gap
Footprint on the existing siteis the only change the donate flow
A clean test run proves none of these. Each has to be provoked on purpose.
FigureHow the work is judged, since no figures are published for it. Each check is a deliberately hostile condition, and the pass mark is behaviour: one gift recorded exactly once, and any disagreement made visible.
figures

No measured figures are published for this engagement. Outcomes are described qualitatively.

What it produces
Without this discipline

A donate button that confirms on the thank-you page, quietly loses every gift where the browser closed, and leaves the organisation confident that its records are complete.

This system

Gifts confirmed by the processor's webhook, a handler that absorbs repeat deliveries, reconciliation against the processor as the source of truth, and a minimal addition to a live site that was left standing.

trust the webhook, not the pageidempotent by event idreconcile against the processorminimal footprint on a live site
The operating envelope

What it owns, and what it hands to a person.

Handled with confidence
Gifts where the browser never returns
Duplicate and delayed webhook delivery
Discrepancies between records and the processor
Flagged for review
Events arriving outside the retry window
Records that disagree with the processor
Out of scope by design
Payments the donor never completed
Rebuilding the existing site
The honest limit

Reconciliation makes a missing gift visible and recoverable. It cannot produce a receipt for a donor who never finished paying, because there is no gift to record. It also does not make the organisation's records the authority. The processor stays the source of truth, and the records are only as right as the last reconciliation says they are.

What it generalizes to

Any payment added to an existing site follows the same three rules. Confirm on the webhook, never on the page. Make the handler safe to run twice. Reconcile against the processor, because it is the only party that knows whether money moved.

What to take from this

The page a payer lands on after paying is a courtesy. The record of the payment should come from the processor's server-to-server message, because the browser can disappear at any moment.

Any message that is retried until acknowledged will sometimes arrive twice. Make every handler safe to run twice by keying it on the sender's event identifier.

Spotting duplicates by matching amount, payer and time is a guess, because two genuine payments can look identical. Use the identifier the sender gives you.

Retries have a window, and anything lost beyond it is lost for good. A scheduled reconciliation against the processor is what makes a missed message recoverable.

When several systems each report success and money still goes missing, the fault is in the hand-offs. Test the hand-offs under hostile conditions, not each system alone.

How we engage

You have a system like this one.
Tell us where it stands.

Whether it is failing, not yet built, or about to meet a scale it has never seen, we can tell you what we see.

Start a conversation
mostafa@opulion.dev · Response within 24 hours · By inquiry