How I stopped reconciling Stripe payouts in spreadsheets
March 6, 2026 · 6 min read
The problem
Every two days, Stripe sends a payout to your bank account. The amount is some combination of charges, refunds, fees, and adjustments, bundled together in ways that rarely match what you expected. The number that shows up in your bank account is almost never a number you can find anywhere in Stripe.
If you have a handful of transactions per week, you can probably eyeball it. But once you are processing 50+ charges a day across multiple products, the payout amounts start looking random. $4,271.88 lands in your bank account. Where did that number come from?
What I tried first: CSVs and VLOOKUP
The obvious approach. Export payouts from Stripe, export transactions from your bank, paste both into a spreadsheet, VLOOKUP on the amount. It works for about a week.
Then you hit the edge cases:
- Stripe batches multiple charges into a single payout, so the bank deposit amount does not match any individual charge.
- Stripe deducts fees before sending the payout. A $100 charge becomes a $97.10 deposit (minus the 2.9% + $0.30 fee).
- Refunds issued between payout cycles get netted against future payouts, so a $500 payout might actually represent $600 in charges minus a $100 refund from last week.
- Timing is unpredictable. Stripe says "2 business days," but weekends, holidays, and bank processing delays mean a payout initiated on Friday might not arrive until Wednesday.
VLOOKUP needs an exact match on at least one column. When the amount does not match and the date does not match, you are left manually scanning rows. At 50 payouts a month, this takes a full afternoon. At 200, it is a part-time job.
The matching problem
Reconciliation is fundamentally a matching problem. You have a payout record from Stripe (amount, date, destination bank) and you need to find the corresponding deposit in your bank transactions. The difficulty is that almost nothing lines up exactly.
Consider a real example:
- Stripe payout: $4,271.88, initiated Feb 25, bank account ending in 4829
- Bank deposit: $4,271.88, posted Feb 27, description "STRIPE TRANSFER"
This one is easy. The amounts match exactly and the description contains "STRIPE." Now consider:
- Stripe payout: $2,847.33, initiated Mar 1, bank account ending in 4829
- Bank deposit: $2,847.33, posted Mar 4, description "ACH CREDIT STRIPE PAYOUT"
- Another bank deposit: $2,847.33, posted Mar 4, description "ACH CREDIT SHOPIFY"
Same amount, same date, two candidates. Which one is the Stripe payout? You need more signals: the bank account ID, description keywords, date proximity. This is where spreadsheets fall apart and scoring algorithms start making sense.
What FlowCheck does
FlowCheck connects to both Stripe and your bank (via Plaid), then runs a scoring model against every payout to find its matching bank deposit. The model uses four signals:
- Amount match (40 points): exact match gets full score, with tolerance for rounding within $0.01 plus 1% variance
- Date proximity (30 points): full score if the deposit arrives within the expected window (arrival date minus 2 days to plus 7 days), decaying as the gap grows
- Description match (20 points): looks for "STRIPE," "TRANSFER," payout ID fragments, or bank-specific identifiers in the transaction description
- Bank account match (10 points): confirms the deposit landed in the same bank account the payout was sent to
A score of 80+ with a 10-point gap to the second-best candidate triggers an automatic match. Everything else gets flagged for review.
You can see your overall reconciliation health with a single API call:
curl https://developer.usepopup.com/api/v0/reconcile/summary \
-H "Authorization: Bearer fc_live_your_key"
{
"data": {
"window_days": 30,
"total_payouts": 47,
"matched": 44,
"unmatched": 2,
"pending": 1,
"match_rate": 93.6,
"total_expected": 2847133,
"total_confirmed": 2654200,
"discrepancies": 2
}
}The same call with the TypeScript SDK:
import { FlowCheck } from "@flowcheck/sdk"
const fc = new FlowCheck("fc_live_your_key")
const { data } = await fc.reconciliationSummary()
console.log(`Match rate: ${data.match_rate}%`)
console.log(`Unmatched: ${data.unmatched}`)
console.log(`Discrepancies: ${data.discrepancies}`)Digging into individual payouts
When you need to see why a specific payout did or did not match, query the payout directly:
const payouts = await fc.payouts({ source: "stripe" })
for (const payout of payouts.data) {
if (payout.reconciliation_status === "unmatched") {
console.log(`Missing: ${payout.id} — $${payout.amount / 100}`)
}
}Webhooks for ongoing monitoring
Instead of polling, you can register a webhook to get notified when payouts match or when something looks wrong:
curl -X POST https://developer.usepopup.com/api/v0/webhooks \
-H "Authorization: Bearer fc_live_your_key" \
-H "Content-Type: application/json" \
-d '{
"url": "https://your-app.com/webhooks/flowcheck",
"events": ["payout.matched", "payout.discrepancy", "payout.missing"]
}'FlowCheck signs every webhook delivery with HMAC-SHA256, so you can verify it actually came from FlowCheck and not someone replaying requests.
The result
What used to take a full afternoon now happens automatically. The API checks every Stripe payout against your bank deposits, flags anything it cannot match, and sends a webhook when something needs attention. No more CSV exports. No more VLOOKUP. No more staring at two spreadsheets wondering where $47.33 went.
If you are tired of reconciling Stripe payouts by hand, grab an API key and try it. The sandbox is free and does not touch your production data.