Payment integration that still balances at month end
The charge succeeds on the first afternoon. The trouble starts with the refund that arrives twice, the payout nobody can trace, and the dispute that lands three weeks later.
Hashlogics has shipped Stripe in eight production systems, and every one needed the same thing. A ledger of our own, built to survive the gateway telling us the same story twice, in the wrong order, three days late. Stripe says plainly that it does not guarantee events arrive in the order they were generated. It also says an endpoint might receive the same event more than once. Treat a webhook as a fact rather than a claim and your books will disagree with your bank by month end.
Your ledger, not the gateway's dashboard
Most teams start by mirroring the gateway. A charge succeeds, so a row is marked paid. That works until the day a customer is charged once and shown two receipts, and nobody can prove which of them is real.
The fix is to keep your own record of money, in whole units, appended and never overwritten. The gateway becomes a source you reconcile against rather than the truth you copy. Then a duplicate event is a matched row instead of a second charge, and a support ticket becomes a query rather than an argument.
Every awkward case then has an answer. A refund is a new entry, not a deleted one. A payout is a movement you can trace to the entries it settled. And when finance asks why the numbers differ by a few cents, the answer is a row you can point at.
- 01Store amounts as integers in the smallest unit. A decimal type in the database and a float in the code still gives you a float.
- 02Give every write an idempotency key so a retry cannot double-charge a customer.
- 03Keep the gateway's own identifier on your row. Reconciliation without it is guesswork.
What we have shipped
8
production systems built on Stripe
4X
faster payment collection, reported by Maidily
22
production systems across the firm
- SignedVerify before you read the body.
- QueuedAnswer fast, work later.
- DedupedSame event twice is one entry.
- OrderedArrival order is not event order.
- PostedAppended to the ledger, never edited.
- ReconciledMatched against the payout.
Stripe retries a failed delivery for up to three days with exponential backoff, so a handler that is down for an hour is recoverable. A handler that quietly succeeds on a duplicate is not.
What Stripe actually promises
Four behaviours we design around, taken from Stripe's own documentation rather than from memory. Read them as constraints, because that is how they behave in production.
| Behaviour | What the documentation says | What we build because of it |
|---|---|---|
| Event ordering | Stripe does not guarantee delivery of events in the order they were generated. | State moves forward only. A late event about an earlier state is recorded and ignored, never replayed over a newer one. |
| Duplicate delivery | An endpoint might occasionally receive the same event more than once. | Log processed event identifiers and skip the ones already handled. For split events, match on the object and the event type. |
| Retries | Failed deliveries are retried for up to three days in live mode, with exponential backoff. | Return a 2xx immediately and do the work on a queue. A slow handler looks like a dead one. |
| Idempotency keys | Stripe saves the status and body of the first request for a key, and says keys can be removed once they are at least 24 hours old. | Treat 24 hours as a permission Stripe has reserved, not a guarantee to you. Our own ledger stays the arbiter of what has been charged. |
Where we are useful
Marketplace money
WorkMateAI runs held-and-released payments on Stripe Connect for Australia's on-demand trades. Funds freeze on a dispute and the platform generates chargeback evidence, so the argument has a paper trail.
Subscriptions and invoicing
Golancer bills freelancers through a Stripe invoice engine alongside subscriptions. Recurring billing fails quietly, so the work is in the dunning path rather than the checkout.
Multi-currency checkout
Go Real Travel books European trips for US travellers, so pricing, payment and refund all cross a currency line. Decide where the conversion is recorded before you write the first entry.
Payments beside a payroll
Shift Link pays temporary staff and forecasts margin in the same system that schedules them. When money and eligibility share a database, the reconciliation gets easier and the access model gets harder.
Stay out of card data on purpose
The cheapest compliance decision is the one that keeps card numbers off your servers. A hosted or embedded field means the card never touches your systems, and your audit surface shrinks to what you actually store.
Teams still pull scope back in by accident. A card number typed into a support form, pasted in a chat, or captured in a debug log puts it right back in your systems. Log the identifier the gateway gives you and nothing more.
We are not a QSA and we do not certify anyone. What we do is design the boundary so the question stays small, then write down where it sits.
- Never log a full card number, even at debug level, even temporarily.
- A support tool that displays a payment method is a system holding card data.
- Decide who can issue a refund before launch. It is an access question wearing a finance costume.

Systems where money moved between strangers
“Their attention to detail, quality of employees, and work ethic were outstanding.”
Nicolas de Quesada · CEO, Lexpair
Gateways and money plumbing we have shipped
Gateways
Around the payment
Where we usually build
Questions buyers ask us first
01Why did our payment integration stop reconciling?
Almost always because the gateway is being treated as the ledger. A duplicate webhook posts a second row, a late one overwrites a newer state, and the totals drift a little every week. The fix is an append-only record of your own with the gateway identifier on every entry, reconciled against payouts rather than trusted outright.
02Should we build on Stripe or take card data ourselves?
Use hosted or embedded fields unless you have a reason you can defend to an auditor. Handling raw card numbers moves a large compliance burden onto your team and your servers for a benefit most products never collect. We would rather spend that budget on the ledger, where the actual failures happen.
03How do you stop a customer being charged twice?
Send an idempotency key on every write, so a network retry returns the original result instead of creating a second charge. Stripe saves the status and body of the first request made with a key. Pair that with a deduplicated event log on your side, because the double charge and the double record are two different bugs.
04Can you handle marketplace payouts to third parties?
Yes, and WorkMateAI is the build to look at. Money is held after the customer pays and released against the work, it freezes when someone raises a dispute, and the platform assembles chargeback evidence. The hard part is not the transfer. It is deciding who is owed what, and when, before the first payout runs.
05What breaks first when you add a second currency?
Refunds. The rate moved between the charge and the return, so the customer is made whole in their currency while your books move in yours. Record the rate and the base amount on the original entry. Skip that and the difference has nowhere to live, so someone writes it off by hand every month.
06Do you work with an existing payment integration or rebuild it?
Usually we keep the integration and rebuild the record underneath it. The gateway calls are rarely the problem. Where we have to go into an existing codebase, there is a paid two-week diagnostic. It ends in a fixed price rather than a range, and scoping calls before it are free.
Go deeper
- Why you never use floats for money →The rounding error that shows up as a missing cent a year later.
- Idempotency key →The one header that stops a retry becoming a second charge.
- Settlement vs authorisation →Two different moments, routinely treated as one.
- Reconciliation →Matching what you think happened against what the bank says.

