Order, fee, and refund data arrived per channel, and someone reconciled it by hand every night. One number out of place meant starting over. Closing day meant working past midnight.
What was actually wrong
The data all existed. The problem was that it was scattered across channels in different shapes, and matching it up was entirely a person's job.
How it was solved
- Order and settlement data collected automatically per sales channel, daily
- Fees, refunds, and shipping checked against an explicit rule
- Only the rows that disagree get flagged
- Results written to a ledger, summary posted to Slack
The shift is from "a person checks everything" to "a person checks exceptions." The workflow handles what reconciles; people see only what's flagged.
The rule — this one line is the whole case
Exactly what "checked against a rule" means is the heart of this. The shape is the same whatever your channel.
sale − fees − shipping − refunds = the payout WE calculate
diff = the payout the channel REPORTS − the payout we calculate
diff = 0 → matches. do nothing
diff > 0 → channel says it owes more. usually fee discounts or promotions
diff < 0 → channel says it owes less. a person must look at thisRendered as SQL, that becomes the following — it lives in the workflow's Reconcile Yesterday node.
SELECT channel, order_id, settled_on,
sale_amount, fee_amount, shipping_amount, refund_amount,
payout_reported,
(sale_amount - fee_amount - shipping_amount - refund_amount) AS payout_expected,
payout_reported
- (sale_amount - fee_amount - shipping_amount - refund_amount) AS diff
FROM settlements
WHERE settled_on = (CURRENT_DATE - INTERVAL '1 day')::date
ORDER BY abs(payout_reported
- (sale_amount - fee_amount - shipping_amount - refund_amount)) DESC;ORDER BY abs(...) DESC matters in practice. Rows are sorted by size of discrepancy, so reading only the first few on a busy morning still catches the largest losses first. Sorting on absolute value regardless of sign is deliberate: being overpaid can be clawed back later, so it needs looking at too.Choosing the tolerance
You have to decide whether a one-unit difference counts. That's TOLERANCE_KRW at the top of the Flag Mismatches Only node.
- `0` — flags any difference at all. Precise, but channels that round will generate dozens of rows daily.
- `1`–`2` (recommended starting point) — absorbs rounding error and catches everything else. Right for most sellers.
- `100` or more — only when alerts are overwhelming. Raising this is identical to increasing the amount of money you never look at. Find out why the discrepancies are large before raising it. A higher threshold hides the symptom rather than fixing the cause.
What you need
- n8n — self-hosted or Cloud. Core nodes only.
- One Postgres database — a single settlement ledger table. Adding channels later just adds rows.
- A Postgres credential — n8n → Credentials →
Postgres. Docker n8n with a host database needshost.docker.internal. - A Slack Bot User OAuth Token — scopes
chat:writeandchannels:read. - Some way to reach your channel's settlement data — its API where one exists, otherwise a downloaded settlement spreadsheet. Only the first node in this workflow knows which channel you're on.
Step 1 — the settlement ledger
CREATE TABLE IF NOT EXISTS settlements (
channel text NOT NULL, -- sales channel name
order_id text NOT NULL,
settled_on date NOT NULL, -- settlement date
sale_amount numeric NOT NULL DEFAULT 0,
fee_amount numeric NOT NULL DEFAULT 0,
shipping_amount numeric NOT NULL DEFAULT 0,
refund_amount numeric NOT NULL DEFAULT 0,
payout_reported numeric NOT NULL DEFAULT 0, -- what the channel says it will pay
created_at timestamptz NOT NULL DEFAULT now(),
updated_at timestamptz NOT NULL DEFAULT now(),
PRIMARY KEY (channel, order_id, settled_on)
);order_id alone would be wrong — one order can be settled across several settlement dates.Changing channel — only the first node changes
This workflow is deliberately channel-agnostic. Replace the Fetch Channel Settlement node with your channel's API and map its response to these eight fields; everything downstream runs unchanged.
channel · order_id · settled_on
sale_amount · fee_amount · shipping_amount · refund_amount · payout_reported- Channels with a settlement API — wire it straight in. Authentication varies enormously between channels, from a plain API key to request signing and caller-IP registration, so confirm the integration difficulty before you commit to a timeline.
- Channels with no API, or no access — download the settlement spreadsheet, paste it into a sheet, and swap the first node for a sheet read. Collection stays manual while reconciliation becomes automatic, which already removes most of the hours.
- Multiple channels — duplicate the workflow, change only the first node, and set a different
channelvalue. They accumulate in one table, so reconciliation and reporting merge automatically.
3.5h → 15 min figure comes from the original engagement, not from this workflow. Last verified: 2026-08-15.Setup (25 minutes)
- Create the table — run the Step 1 SQL.
- Register the Postgres credential — n8n → Credentials →
Postgres→ Test. - Register the Slack credential — create an app at api.slack.com/apps → add
chat:writeandchannels:read→ Install → paste thexoxb-token. Then/invite @your-botin the channel. - Import the workflow — n8n → Workflows →
...→ Import from File. - Fill in the collection node — put your channel's settlement API URL and auth into
Fetch Channel Settlement, mapping its response to the eight fields above. No API? Replace it with a sheet-read node. - Set the tolerance — pick
TOLERANCE_KRWinFlag Mismatches Only. Start at1. - Set channel and credentials — change the channel name in
Post to #settlementand attach credentials to all three nodes. - Verify against hand calculation — do not skip this. Take three of yesterday's settlement rows, compute
sale − fees − shipping − refundson a calculator, and compare againstpayout_expectedanddiffin the query output. A mismatch here means your field mapping is wrong. Mapping errors are the most dangerous failure because they produce quietly incorrect numbers. - Activate — it reconciles the previous day every morning at 07:00.
What happens in edge cases
- A day where everything matches — no per-row listing, just "N rows all matched" and the payout total. Confirmation that it ran, with nothing to read.
- A day with no settlements at all — nothing is sent. Channels commonly don't settle on weekends and holidays, so this is normal. But silence on a day that should have settlements is itself a signal — that's the "never ran" problem our
workflow-watchdogcase handles. - More than 20 mismatches — Slack gets the 20 largest by discrepancy plus an "…and N more" line, so the message doesn't get truncated into uselessness. Query the ledger for the full set.
- A channel revising settlement data later — the same key is overwritten on re-fetch. If you need the pre-revision values, add a history table; the base structure doesn't retain them.
- Partial refunds — put the refunded amount in
refund_amountand the rule applies as-is. Some channels push refunds to the following settlement date, which produces a large same-day diff. The same order appearing several days running suggests a carried-over settlement. - A channel changing its API schema — the collection node either fails or returns empty fields. Amounts arriving as 0 produce a large diff and get flagged, so it's visible. Failing loudly beats failing quietly — that's the intent.
I don't reconcile at 2am any more. The workflow does.— Store owner