Reporting & dashboards
Post a daily Stripe payouts and fees summary to Slack
Stripe deposits money to your bank on a rolling schedule, nets out refunds and disputes, skims its fee, and tells you almost none of it unless you log in and dig. This closes that gap. When a payout lands, it pulls the exact transactions that make up the deposit, totals the gross, the fees, the refunds, and the net, works out your effective fee rate, and posts a clean summary to Slack — so the number in your bank account finally has a story attached. Build it yourself with the steps below, or we build it on your own Stripe and Slack accounts, reconcile it to the cent against your dashboard, and hand you the keys so you own it with no monthly fee. Free teardown of what you reconcile by hand first.
- Stripe
- Slack
The manual way today
A deposit shows up in the bank account a couple of days after the sales it came from, for an amount that matches no single number anywhere — because Stripe has quietly netted out refunds, held back disputes, and taken its cut along the way. To find out what actually happened, someone opens the Stripe dashboard, finds the payout, expands it, and reads down the list of charges and fees, then maybe exports the payout report to a CSV and adds up the fee column in a spreadsheet to see what processing actually cost that day. Most months nobody does this at all, so the bank balance just drifts and the true fee rate stays a vibe rather than a figure.
The bleed isn't the ten minutes of reconciling — it's that almost nobody does it, so the one line that matters most for margin runs completely unwatched. Processing fees are usually the second or third biggest cost in a DTC P&L, and a creep from 2.9% to 3.4% effective — from more international cards, more disputes, a Radar or Instant Payout add-on, a plan change — is invisible until you total the fee column, which nobody does daily. Meanwhile bookkeeping treats the lump-sum payout as revenue, so the books are wrong by exactly the fees and refunds Stripe folded in, and the gap only surfaces at tax time when it's expensive to untangle.
What running looks like
Every time Stripe sends a payout, a summary lands in Slack before you've noticed the deposit: the amount that hit the bank and the account it landed in, the gross sales it came from, total Stripe fees with your effective fee rate, refunds and disputes netted out, and the charge count — with a button straight to that payout in the Stripe dashboard. It reconciles to the payout amount to the cent, posts one block per currency for stores that sell internationally, and shouts loudly if a payout fails or comes back negative instead of letting a missing deposit pass unnoticed.
01The build
How to build it
Decide the numbers, then learn how Stripe actually pays you
Needs a humanName the handful of figures that change a decision: amount paid to the bank, the gross it came from, total Stripe fees, refunds and disputes netted out, the resulting net, and the effective fee rate (fees ÷ gross). Then get the mental model straight, because it's the whole reason this is confusing: a charge and a payout are different objects. A charge is a sale; a payout is the deposit Stripe makes to your bank, usually on a rolling delay (US standard is a 2-day rolling schedule), bundling several days of charges minus the refunds, disputes, and fees that settled in that window. So a payout will almost never equal any single day's sales — and that mismatch, not a bug, is what makes this worth automating. Check Settings → Payouts in Stripe for your schedule and bank accounts before you build.
Trigger on the payout.paid webhook — and verify it's really Stripe
Runs itselfRegister a webhook endpoint in Stripe (Developers → Webhooks) subscribed to payout.paid, which fires the moment Stripe sends a payout to your bank — the exact event that means 'money is on the way.' Point it at an n8n Webhook node. Critically, verify the Stripe-Signature header against your endpoint's signing secret (whsec_…) and reject anything that doesn't validate, so a forged POST can't spoof a fake payout into your channel. Test the whole path with the Stripe CLI: `stripe listen --forward-to <your-n8n-url>` and `stripe trigger payout.paid`. If you'd rather have one fixed-time roll-up instead of a post per payout, swap this for a Schedule node that lists payouts with arrival_date = yesterday — same downstream logic, different clock.
Itemize the payout via balance transactions — and paginate
Runs itselfThe webhook gives you a payout id (po_…); the deposit's contents live in the balance transactions tied to it. Call GET /v1/balance_transactions?payout=po_…&limit=100, which returns exactly the charges, refunds, fees, disputes, and adjustments that make up that one deposit. Paginate: a busy day's payout runs past 100 rows, so loop on starting_after=<last txn id> until has_more is false, or you'll silently summarize a fraction of the deposit. Also fetch the payout object itself (GET /v1/payouts/po_…) for amount, arrival_date, currency, status, automatic, and the destination bank's last4. Every amount comes back as an integer in the smallest currency unit — cents for USD — so divide by 100 for display (and remember zero-decimal currencies like JPY don't).
Compute the totals so they reconcile to the payout to the cent
Runs itselfWalk the balance transactions and bucket by type: charge and payment rows are gross sales, refund and payment_refund rows are money returned, dispute rows are chargebacks, adjustment rows are corrections. Sum the net field across every row and it must equal the payout amount exactly — that single equality (Σ net === payout.amount) is your reconciliation guard. If it's off, you missed a page or dropped a transaction type, not found a rounding quirk. Total fees = the sum of the fee field across all rows. Effective fee rate = total fees ÷ gross. This is the figure no Stripe screen puts in front of you daily, and the reason the summary is worth more than the dashboard glance it replaces.
Break the fees down so you can see where Stripe's cut went
Runs itselfA single fee total hides drift; the breakdown explains it. Each charge's balance transaction carries a fee_details[] array — entries typed stripe_fee (card processing), application_fee, or tax — so you can split ordinary processing from the rest. The add-ons that quietly raise your effective rate show up as standalone balance transactions of type stripe_fee with a human-readable description: Radar for Fraud Teams, Instant Payout fees, monthly Billing or account fees. Surface them on their own line. Important so you don't double-count: those standalone stripe_fee rows put the cost in their amount with fee = 0, while a charge puts its cost in fee — sum each once. Now a jump from 2.9% to 3.4% has a named cause instead of a shrug.
Decide policy for the awkward cases: multi-currency, negative, and failed
Needs a humanThree real cases break a naive build, so pick a rule for each up front. Multi-currency: Stripe issues a separate payout per currency and bank account, and the amounts are not comparable — never sum USD and EUR into one number; post one block per payout instead. Negative payouts: on a heavy-refund or chargeback day, deductions can exceed sales and Stripe debits your bank, so payout.amount comes back negative — label it clearly ('Stripe debited •••1234') rather than printing a confusing minus. Failed payouts: also subscribe to payout.failed (bank rejected, account on hold) and post a loud, distinct alert — a missing deposit is exactly the thing that must never pass silently. Write these rules down now; the code in the next steps just enforces them.
Build and post the Slack summary
Runs itselfAssemble a Block Kit message to a dedicated channel like #stripe-payouts. Lead with the headline — '$8,412.50 landed in •••1234 · arrives Jun 30' — then a compact row (gross · fees · refunds · net), then the effective fee rate and the charge count, and the fee breakdown line if there were add-ons. Finish with a button linking straight to the payout in Stripe (https://dashboard.stripe.com/payouts/po_…) so anyone who wants the line-by-line is one click away. Post via a free Slack incoming webhook for the simple case, or chat.postMessage if you want threading or an @-mention on failures. Keep it skimmable: the goal is a five-second read that means you don't open Stripe, not the dashboard rebuilt inside Slack.
Make it reliable — idempotent, mode-aware, loud on failure
Runs itselfStripe retries a webhook until it gets a 2xx, so dedupe or you'll double-post: store the payout id (or the event's evt_ id) in n8n static data or a Sheet and skip if you've already handled it. Keep test mode and live mode fully separate — different API keys and a different signing secret — so a test payout triggered during the build never lands in the real channel. Always respond 2xx fast after validating, then do the work, so Stripe doesn't mark the endpoint failing. And honor the failure path from the policy step: a payout.failed event, or a Stripe API error while itemizing, posts a clear 'couldn't summarize payout po_…' alert rather than dying quietly. The entire point is to stop checking by hand, so silence can never be allowed to mean a successful, ordinary day.
02The stack
What it’s built on
- Stripe
- The source of truth for money movement. The payout.paid webhook is the trigger, the Payouts API gives you the deposit's amount and bank destination, and the Balance Transactions API itemizes the charges, refunds, disputes, and fees that make up each payout — the same data your dashboard reconciles against, so the summary is trustworthy by construction.Stripe's API, webhooks, and balance-transaction data are included with your account at no extra cost — you're already paying the processing fees this summary finally puts a number on.
- Slack
- Where the summary lands so finance and the founder see what hit the bank without logging into Stripe. A free incoming webhook covers the post; Block Kit gives you the skimmable card with the net, the effective fee rate, and the dashboard button; chat.postMessage adds threading or an @-mention for failed payouts.Incoming Webhooks and the Slack API are free on every plan, including the free tier — there's no upcharge to receive this post.
- n8n
- The glue that does the work: a Webhook node receives and verifies the Stripe signature, paginated HTTP Request nodes pull the payout and its balance transactions, a Code/Set node runs the totals, fee breakdown, and the Σ net === payout.amount reconciliation guard, and a Slack node posts the card — plus the idempotency check and the failure alerting.n8n is open-source and self-hostable for free, which covers this end to end; their cloud tier just saves you running the server. Payouts are low-volume — a handful of events a day at most — so it sits comfortably inside the smallest plan either way.
03Questions
Before you build
Why summarize payouts instead of just totaling yesterday's charges?
Because a charge isn't money in the bank — a payout is. Stripe pays out on a rolling delay (US standard is 2-day), bundling several days of sales and then subtracting the refunds, disputes, and fees that settled in that window, so the deposit will never match any single day's gross sales. Totaling charges tells you what you sold; summarizing the payout tells you what actually arrived and what Stripe kept on the way. Those are different questions, and the second is the one your bank balance and your bookkeeping actually need answered.
Will the numbers match my Stripe dashboard exactly?
Yes, if you do one thing: reconcile to the payout. List the balance transactions tied to the payout id, sum the net field, and confirm it equals payout.amount to the cent before you post — that single guard (Σ net === payout.amount) is what guarantees the match. The usual cause of a mismatch isn't a bug, it's missing a page of transactions on a busy payout or dropping a type like disputes or adjustments. Reconcile once against a known payout in the dashboard and it lines up from then on.
How do I see exactly what Stripe charged me and where the fees went?
Two places, and the recipe reads both. Ordinary card processing lives in each charge's fee field, broken out further in its fee_details[] array (processing vs. application fee vs. tax). The extras that quietly raise your effective rate — Radar for Fraud Teams, Instant Payout fees, monthly Billing or account fees — arrive as standalone balance transactions of type stripe_fee with a plain-English description. Summing both, and showing the add-ons on their own line, is what turns 'fees went up' into a named, fixable cause.
I sell in multiple currencies — does this still work?
Yes, with one rule: don't sum across currencies. Stripe issues a separate payout per currency and bank account, and converting them into a single blended figure produces a number that means nothing. The build posts one Slack block per payout, each in its own currency with its own effective fee rate, so a USD deposit and a EUR deposit read as the two distinct events they actually are. Each still reconciles to its own payout amount independently.
What happens if a payout fails or comes back negative?
Both get handled loudly, because a missing or surprising deposit is the worst thing to leave silent. Subscribe to payout.failed alongside payout.paid and post a distinct alert when Stripe can't deposit (bank rejected, account on hold). And on a heavy-refund or chargeback day, deductions can exceed sales — Stripe then debits your bank and payout.amount is negative, which the summary labels clearly as a debit rather than printing a baffling minus sign. Silence and confusion are the two failure modes; the build is built to avoid both.
Can I build this myself, or should you build it?
Every webhook, endpoint, field, and edge case is written out above on purpose — if you've got a few hours and some API comfort, build it and own it outright. If you'd rather not own the signature verification, the pagination, the fee breakdown, and the reconciliation guard, we'll build it on the Stripe and Slack accounts you already pay for, reconcile it to the cent against your dashboard, document every piece, and hand you the keys. You own the workflow — no monthly retainer, and nothing stops working if you walk away. It starts with a free 20-minute teardown of what you reconcile by hand today, not a contract.
Related recipes
Reporting & dashboards
Send a daily revenue digest to Slack
Every morning starts the same way: open Shopify, wait for the dashboard to load, read yesterday's number, close the tab. This does it for you. Once a day it pulls yesterday's orders from Shopify, totals net sales, order count, AOV, and units, compares it to the same weekday last week so the number actually means something, and posts a clean digest to a Slack channel before you've finished your coffee. Build it yourself with the steps below, or we build it on your own accounts, wire the math and the comparison, and hand you the keys so you own it with no monthly fee. Free teardown of what you check by hand every morning first.
- Shopify
- Slack
Reporting & dashboards
Automate a daily ad spend vs revenue report
To know if yesterday's ad spend paid off, someone opens Ads Manager for the spend, opens Shopify for the revenue, and does the division in their head — across two timezones and two definitions of a 'sale.' This does it for you. Once a day it pulls yesterday's spend from Meta, yesterday's revenue from Shopify, lines the two day-windows up so they're actually comparable, computes blended ROAS (and shows Meta's own attributed ROAS next to it so the gap is visible), and posts one clean line to Slack. Build it yourself from the steps below, or we build it on your own accounts, reconcile the windows and the currency, and hand you the keys so you own it with no monthly fee. Free teardown of how you check whether ads are working first.
- Meta Ads
- Shopify
- Slack
Reporting & dashboards
Track refund rate and reasons in a dashboard
Your refunds line goes up and all you can see is the dollar total — not what percentage of orders it is, whether it's getting worse, or which products and reasons are driving it. This wires the answer up: a Google Apps Script pulls every refund and its return reason from the Shopify Admin API into a logged-and-normalized refunds tab, alongside an orders tab for the denominator, and a dashboard tab built on QUERY formulas shows refund rate over time, reasons ranked, and the products doing the returning — recalculating on its own as new rows land. Build it yourself with the steps below, or we build it on your own Shopify and Google accounts, reconcile it against your Shopify reports, and hand you the keys so you own it with no monthly fee. Free teardown of your current refund reporting first.
- Shopify
- Google Sheets
Reporting & dashboards
Monitor subscription MRR and churn from Recharge
Recharge knows exactly how much recurring revenue you have and how fast subscribers are leaving — but only if you log in, and only in its own definitions. This puts the numbers where you'll see them. On a schedule it pulls your active subscriptions from Recharge, normalizes every billing interval to a monthly figure so MRR is comparable, snapshots it to a Google Sheet so you have a history the API can't reconstruct, counts the period's cancellations with their reasons, and posts MRR, net movement, and churn to Slack. Build it yourself with the steps below, or we build it on your own Recharge, Sheets, and Slack accounts, reconcile it against your dashboard, and hand you the keys so you own it with no monthly fee. Free teardown of what you check by hand first.
- Recharge
- Slack
- Google Sheets