Turn daily chaos into T+0 clarity with event-driven automations that match payouts, orders, fees, and refunds — before the month-end crunch.

Reconcile payments the day they happen. Learn an n8n finance ops workflow to auto-match orders, payouts, fees, and refunds for a same-day close.

Month end shouldn't feel like detective work. Yet here we are, spelunking CSVs, chasing "missing ₹1,243.80," and asking, "Did Stripe deduct that fee or did we forget a refund?" Let's be real: reconciliation belongs in the daily flow, not in next week's pile. With n8n, you can bring finance ops up to real time — and actually sleep on close day.

Why "Reconcile at Close" Beats "Reconcile Later"

  • Risk shrinks: Matching orders ↔ payouts ↔ fees the same day catches anomalies while they're small.
  • Cash clarity: T+0 visibility keeps your runway honest — no surprise dips.
  • Ops confidence: Support, finance, and product share a single truth: transaction-level reconciliation, always up to date.
  • Audit trail: Deterministic, repeatable jobs create a clean ledger of who matched what, when, and why.

You might be wondering, "Isn't this what the accounting system does?" Sort of. ERPs are great at ledgers. n8n is great at glue — pulling from gateways, banks, order systems, and CRMs and snapping the pieces together automatically.

The Core Idea

Move reconciliation from a monthly marathon to a daily event-driven loop:

  1. Ingest: Pull new payments, refunds, disputes, and bank payouts.
  2. Normalize: Coerce everything into one schema — amounts, currency, fees, timestamps.
  3. Match: Link orders ↔ transactions ↔ payouts via robust keys and fallbacks.
  4. Post: Write matched entries to your finance DB and optionally to your accounting system.
  5. Alert: Surface exceptions instantly with context so humans review just the weird stuff.

A Real-World Snapshot (D2C Example)

A mid-market D2C brand running subscriptions + one-off sales across Stripe, UPI, and COD was closing T+6 with a weekly CSV ceremony. After moving to n8n:

  • Close moved to T+0 for 92% of transactions; the rest flowed into a clean exceptions queue.
  • The monthly "mystery variance" dropped from 0.61% to 0.07%.
  • Support response time on payment issues fell by 48% because finance data was current and searchable.

No heroics. Just consistent, automated matching and a tidy source of truth.

The Data Model That Makes Matching Boring (in a good way)

Create a compact finance ops store (Postgres/MySQL/SQLite — even a cloud warehouse works). Keep it narrow and normalized:

-- Transactions from PSPs or bank statements
CREATE TABLE finance_transactions (
  id                   TEXT PRIMARY KEY,
  source               TEXT,           -- "stripe", "razorpay", "bank"
  kind                 TEXT,           -- "charge", "refund", "payout", "fee"
  order_id             TEXT,
  currency             TEXT,
  amount_gross_minor   BIGINT,         -- e.g., paise/cents
  amount_fee_minor     BIGINT,
  amount_net_minor     BIGINT,
  occurred_at          TIMESTAMP,
  payout_batch_id      TEXT,
  raw                  JSONB,
  fingerprint          TEXT,           -- for idempotency
  matched              BOOLEAN DEFAULT FALSE,
  created_at           TIMESTAMP DEFAULT NOW()
);

-- Exceptions that need human eyes
CREATE TABLE finance_exceptions (
  id SERIAL PRIMARY KEY,
  tx_id TEXT,
  reason TEXT,
  severity TEXT,
  context JSONB,
  created_at TIMESTAMP DEFAULT NOW(),
  resolved_at TIMESTAMP
);

A few practical choices:

  • Always store minor units (paise/cents) to avoid float drama.
  • Add a fingerprint hash (see below) to make runs idempotent.
  • Keep the raw payload to defend audits and speed debugging.

Building the n8n Flow (Step by Step)

1) Ingest: Time-boxed pulls + webhooks

  • Webhook triggers catch live events (payments succeeded, refunds created).
  • Scheduled pulls (every 10–15 minutes) sweep for stragglers or provider hiccups.
  • Bank data is often batch-based — fetch yesterday's payouts first thing each morning.

n8n tip: In a Function node, compute a stable fingerprint to avoid duplicates:

// Function Item in n8n
// Create a deterministic fingerprint across providers
const stable = {
  provider: $json.provider,
  kind: $json.kind,
  provider_tx_id: $json.id,
  amount_minor: $json.amount_minor,
  currency: $json.currency,
  occurred_at: $json.occurred_at,
};
const crypto = require('crypto');
$item.fingerprint = crypto.createHash('sha256')
  .update(JSON.stringify(stable))
  .digest('hex');
return $item;

Upsert into finance_transactions keyed on either the provider's transaction ID or the fingerprint.

2) Normalize: One schema to rule them all

Map different providers into the shared shape:

// Example: Map Stripe charge to normalized transaction
return {
  id: $json.id,
  source: 'stripe',
  kind: 'charge',
  order_id: $json.metadata?.order_id ?? null,
  currency: $json.currency.toUpperCase(),
  amount_gross_minor: $json.amount,
  amount_fee_minor: $json.balance_transaction.fee,
  amount_net_minor: $json.balance_transaction.net,
  occurred_at: new Date($json.created * 1000).toISOString(),
  payout_batch_id: null,
  raw: $json,
};

Do the same for refunds, disputes, and payout objects; ensure consistent casing for currency and ISO8601 timestamps.

3) Match: Orders ↔ Transactions ↔ Payouts

Use a tiered matching strategy:

  • Tier A (deterministic): provider_tx_id or order_id + exact amount + currency.
  • Tier B (strong fuzzy): (order_id || email) + amount within small tolerance + same day.
  • Tier C (fallback): last-resort heuristics — then push to exceptions.

Example n8n Function for Tier A:

// Match by order_id + net amount + currency
const { order_id, amount_net_minor, currency } = $json;
const rows = await $db.query(`
  SELECT id FROM orders
  WHERE id = $1 AND total_net_minor = $2 AND currency = $3
  LIMIT 1
`, [order_id, amount_net_minor, currency]);

if (rows.length) {
  $json.matched = true;
  $json.matched_order_id = rows[0].id;
} else {
  $json.matched = false;
}
return $json;

If payouts arrive as bank batches, match by payout_batch_id and ensure the sum of net transactions in the batch equals the payout amount (allow tiny rounding tolerances). Differences? File an exception with a computed variance.

4) Post: Write once, report many

  • Upsert matched rows (matched = TRUE) and link their matched_order_id.
  • Summaries: create daily materialized views — by provider, by currency, by payout.
  • Accounting push: optionally mirror matched entries to your accounting system via API or CSV export. Keep n8n the conductor, not the primary ledger.

Example SQL upsert:

INSERT INTO finance_transactions (id, source, kind, order_id, currency,
  amount_gross_minor, amount_fee_minor, amount_net_minor, occurred_at,
  payout_batch_id, raw, fingerprint, matched)
VALUES ($1,$2,$3,$4,$5,$6,$7,$8,$9,$10,$11,$12,$13)
ON CONFLICT (id) DO UPDATE
SET amount_fee_minor = EXCLUDED.amount_fee_minor,
    payout_batch_id = EXCLUDED.payout_batch_id,
    matched = EXCLUDED.matched;

5) Alert: Exceptions that feel humane

Not every mismatch deserves a siren. Categorize:

  • Severity HIGH: negative net, duplicate charge, payout variance > threshold.
  • Severity MEDIUM: refund with no original charge, timezone drift, rounding edge.
  • Severity LOW: metadata missing, cosmetic format issues.

n8n can route HIGH to a chat channel with a crisp, pre-filled summary and a one-click link to the exact record ID (plus a suggested fix). MEDIUM/LOW can batch into a daily digest so humans stay focused.

Operational Habits That Keep It Smooth

  • Idempotency everywhere: fingerprints, upserts, and dedupe gates mean you can re-run safely.
  • Clock discipline: normalize to UTC internally; format to local time only for reports.
  • Minor units only: it bears repeating — store paise/cents, not floats.
  • Test with dirty data: simulate partial refunds, chargebacks, failed webhooks, and time gaps.
  • Version your mappings: when a provider changes a payload, bump a mapping version and keep both until everything backfills.

What You'll See on Day 7

  • A dashboard that says: "Yesterday: ₹1,23,45,600 collected, ₹1,19,87,400 net, 18 exceptions."
  • Payout batches explained down to the fee.
  • Support tickets resolving with transaction-level receipts in under five minutes.
  • And most importantly, close day that feels… ordinary.

FAQ-ish Speed Round

What if an order splits across multiple charges? Aggregate by order_id and reconcile at the order level, not the transaction level.

How do I handle disputes/chargebacks? Treat them as negative adjustments tied to the original provider_tx_id. They should flip matched to false until resolved, or open a new exception with a "pending dispute" state.

Multiple currencies? Store in native currency minor units and add a separate reporting table for converted values using a daily FX rate snapshot. Keep raw native amounts authoritative.

Wrap-Up

Reconciliation doesn't have to be a late-night ritual. With n8n orchestrating ingest, normalize, match, post, and alert, you can reconcile at close — not next week. Start with one provider, one payout stream, and a simple exceptions queue. In a few days, you'll wonder how you ever lived in CSV purgatory.