DEV Community

Andrey Karazhev
Andrey Karazhev

Posted on

Stripe reconciliation: carrying September into October

In the first article, I used SQL to explain a €3,496.99 gap between the ledger and the bank for a fictional EU shop. A reader pointed out that some of the harder cases cross the month boundary. A payout arrives after the close, a dispute is resolved the following month, or a USD payment is refunded after the exchange rate has changed.

So I added October to the demo. It has 620 charges, 21 refunds and a payout every weekday. I left September's data as it was and added those three cases. Apart from the two entries tied to September, October's operations match.

What carries over from September

Lines Amount
Open on 30 September 17 -€149.21
Changes in October 2 €37.44
Open on 31 October 18 -€111.77

The amounts are Stripe minus the books. September's 17 unresolved differences include three sales booked twice, two disputes missing from the ledger and three unrecorded fees. Those entries haven't been corrected, so they still need to be accounted for in October.

October adds €37.44 across two changes. The refund below creates a new line, while the dispute changes one that's already open. That's why the count goes from 17 to 18. Looking through October's transactions alone won't explain the closing difference of -€111.77.

The opening list comes from September's reconciliation output. Each item keeps its key and the month when it was first found. The SQL also recalculates the opening and closing amounts for each item from both months' transactions. If those amounts disagree with the carried result, the run fails.

The three cases

A September payment refunded in October. On 9 September a customer paid $459.05. Stripe converted that to €392.35. The customer got the full $459.05 back on 6 October, but the exchange rate had changed. Stripe took €385.76 from the shop's balance, while the books reversed the original €392.35. The difference is €6.59.

Stripe uses the current exchange rate when it processes the refund. If you only compare the euro amounts, this looks like a partial refund. The demo checks the currency first and labels it as an exchange rate difference. That rule is too broad for a partial USD refund, which would get the same label.

A dispute won the following month. On 26 September Stripe deducted €30.85 for a dispute and charged a €15 fee. Neither entry reached the ledger. The shop won the dispute on 14 October. Stripe returned €30.85 and kept the fee charged when the dispute was opened.

If you only look at October, you see an unrecorded €30.85 credit. Across both months, the difference starts at −€45.85, changes by +€30.85 and closes at −€15.00. What remains is the fee that was never booked.

Money that hasn't reached the bank. September's last payout, €3,347.78, arrived on 1 October. By the end of October, another €8,598.59 was still missing from the bank statement. It reaches the bank in two parts:

  • The €3,841.55 payout created on Friday, 30 October, arrives on Monday, 2 November.
  • Saturday's sales go into a €4,757.04 payout created on Monday, 2 November. That arrives on 3 November.

On 31 October, the first amount is in transit and the second is still with Stripe, waiting for its payout. Both belong to October's activity. The payout schedule and the bank's processing time explain why they reach the bank in November.

October also reads September's outstanding payouts from the previous result and checks them against the payout export and bank statement. If a payout doesn't arrive, it stays on the list for the next month.

October on its own

Step Amount
Ledger €120,807.97
USD refund at the October rate €6.59
Dispute won, not booked €30.85
Stripe €120,845.41
Payouts covering October's activity €120,845.41
September payout received on 1 October €3,347.78
October activity paid into the bank in November -€8,598.59
Bank €115,594.60

Both differences between the ledger and Stripe come from September transactions. The bank adjustments reflect when the payouts arrive.

How to repeat it on your own data

You need the same four exports as before: the ledger lines for the Stripe account, the itemized payout reconciliation report, the payout list and a bank statement covering a few days after month end. You also need the previous month's result, including its unresolved differences and outstanding payouts.

If a September error is fixed in October, that entry appears as an October change against the original item. A full correction brings its closing difference to zero.

You can open the October demo to see the results. The SQL and instructions are in the repository.

Limits

The data, payout schedule, fees and exchange rates are fictional. I added the discrepancies on purpose. The demo covers two months and settles in EUR.

Sales and other operations stop on 31 October, so Monday's payout contains only Saturday's activity. The demo doesn't handle a payout containing activity from both October and November. Reserve releases, the reader's fourth case, aren't included yet. Stripe also charges a fee for countering a dispute and returns it when you win; the demo leaves it out.

If your payouts don't match

If your Stripe payouts and books disagree, email me at [email protected]. The first check is free. I review one month of exports, read-only, and give you the size of the gap with totals and counts by cause. The individual line list is part of the full investigation.

Full investigations have a fixed fee, starting at $400. You get every difference with its key, explanation and any correction needed. If the cause is in the mapping or integration, fixing that is included. You pay after you have the result. A recurring monthly check is priced separately after the first engagement.

Top comments (4)

Collapse
 
shuttlewright profile image
Shuttlewright •

The carried-over table hides a distinction worth making explicit: some of those open items can never close. The 15-euro dispute fee from the won case and the 6.59 exchange-rate difference are permanent - no future payout will clear them. Giving them their own permanent-differences bucket, separate from the timing list, keeps the open count honest: it should only hold items that can still clear, and anything sitting there two months later is almost always a posting bug, not timing. A months-open counter on each carried key does that triage automatically - the count moving 17 to 18 matters less than how the 17 are aging.

Collapse
 
akarazhev profile image
Andrey Karazhev •

Agree on aging, it's a good way to see what's stuck. In this demo nothing clears by itself though. Payouts in transit are tracked separately, and the fee and the FX difference stay open until someone posts them, same as the duplicates. Each line already shows the month it was found, so a months-open count would be easy to add. How did you usually catch these, aging or the month-end review?

Collapse
 
shuttlewright profile image
Shuttlewright •

Aging, and your "nothing clears by itself" is the reason. Where items can self-clear - payouts in transit - aging separates slow from stuck. Where everything stays open until someone posts it, the month-end review alone can't rank anything: an item that appeared yesterday looks identical to one open since March. The months-open count turns the open list into a work queue, oldest first, and since each line already carries its found-month it is a group-by away. The review still earns its place as the net check that nothing slipped between months, but aging is what says which line to touch first.

Collapse
 
akarazhev profile image
Andrey Karazhev •

Good point about the work queue, I added it. The October page now shows how many months each line has been open, oldest first: reconciliation.minihub.app/stripe/...