Bank Reconciliation
Import a CSV, OFX, or QFX bank statement and let Muin auto-match transactions against your invoices and expenses, with AI suggestions for the remainder.
Bank Reconciliation is where the month-end close actually happens. You import a bank statement, Muin auto-matches the obvious transactions against your existing invoices and expenses, and an AI review drawer walks you through the remainder — suggesting ranked matches with a confidence score so you are confirming, not typing. Everything the bank sent stays in the system; nothing gets silently dropped.
If Payments Received is where money lands on an invoice and Bills is where it leaves on a payable, Reconciliation is where you prove the bank’s ledger and yours agree.
The Flow at a Glance
Import Statement (CSV / OFX / QFX / QBO) → Start Reconciliation Session →
Auto-Match (rules + AI) → Review Unmatched (per-transaction drawer) →
Confirm Matches → Session Marked Complete
| Stage | Surface |
|---|---|
| Import | Finance > Reconciliation, Import Statement button (also bound to the R keyboard shortcut) |
| Statements list | Table of imported statements with period, transaction count, closing balance, status |
| Start session | Start Reconciliation action on an imported statement row |
| Auto-match | Auto-Match action on an in_progress session row |
| Review drawer | Review action on an in_progress session row |
| Complete | Session status flips to completed once all transactions are matched or excluded |
Every stage is durable — you can close the page mid-session, come back tomorrow, and resume exactly where you left off.
Importing a Statement
Click Import Statement (or press R) and pick a file. The file input accepts .csv, .ofx, .qfx, and .qbo. Muin handles each format natively — no conversion step, no third-party upload service.
Limits are enforced at the API boundary:
| Limit | Value | Why |
|---|---|---|
| Maximum file size | 20 MB | Large-bank monthly statements comfortably fit; anything larger is almost certainly the wrong file |
| Maximum transactions per import | 10,000 | Keeps the per-import memory footprint reasonable; forces very large banks to be split into monthly chunks |
| Duplicate detection | SHA-256 of the file bytes | Uploading the same CSV twice returns the original statement instead of creating a duplicate — no phantom transactions |
If your file exceeds the transaction limit, split it by month and import separately. If the file type is not supported, the banner will tell you what was rejected and you can retry with a different file without re-opening the picker.
Success and error states
The panel above the statements table shows exactly one banner per import:
- Success — green, shows immediately after a successful import. The new statement appears in the table with status
imported. - Error — red with a Retry button that resets the upload and re-opens the file picker. The error text surfaces the underlying reason (unsupported format, empty file, too many transactions, file too large).
The Statements Table
Once imported, every statement has a row in the Bank Statements view. The columns are deliberately the ones you need to spot a wrong import:
| Column | What It Shows |
|---|---|
| Bank | Parsed bank name + original file name |
| Account | Masked account number (****1234) — Muin never stores the full number |
| Period | The statement’s start and end dates, parsed from the transactions |
| Transactions | Count of parsed transaction rows |
| Balance | Closing balance, formatted in the statement’s currency |
| Status | imported, processing, reconciled, or failed |
| Actions | Start Reconciliation when imported; a ✓ Reconciled marker when the session is reconciled |
Click Start Reconciliation to create a session for the statement. The statement stays linked to the session — you can see which session closed which statement from the history.
Auto-Match: Rules + AI
Once a session is running, click Auto-Match. Muin runs every unmatched transaction through a two-stage matcher:
Stage 1 — rule-based scoring (0–100)
Each potential match (an invoice, expense, or donation in the tenant) gets a confidence score built from three signals:
| Signal | Weight | Rule |
|---|---|---|
| Amount match | up to 50 points | Exact match = 50, within 1% = 45, within 5% = 35, within 10% = 20 |
| Date proximity | up to 30 points | Same day = 30, within 1 day = 25, within 3 days = 20, within 7 days = 10 |
| Description similarity | up to 20 points | Invoice number in description = 20; shared keywords with the entity description = 10–15 |
Any candidate scoring below 50 is dropped — suggesting a 40% match is worse than suggesting nothing. The top five remaining candidates advance.
Stage 2 — AI re-ranking
When the rule-based stage produces multiple plausible candidates, Muin sends the bank transaction plus the candidate summaries to the AI ranker, which can adjust scores based on semantic cues the keyword matcher misses (“ACME Corp Inc” vs. “ACME Corporation” on an invoice). The AI ranker is budget-limited per session to keep costs bounded — once the session budget is consumed, the remaining transactions fall back to rule-based scoring alone.
What gets applied
A threshold (default 80) decides what auto-applies vs. what surfaces as a suggestion:
| Confidence | Outcome |
|---|---|
| ≥ threshold (default 80) | Automatically applied — transaction is matched to the entity, no review needed |
| 50 – threshold | Left unmatched with ranked suggestions available in the review drawer |
| < 50 | Dropped — transaction stays unmatched with no suggestions |
You can adjust the threshold from the API (min_confidence query parameter, range 50–100), but the UI defaults to 80 and most users leave it there. The session’s match rate on the sessions table shows how well the auto-matcher did at a glance.
The Review Drawer
Click Review on an in_progress session to open the reconciliation review drawer. This is where you work through transactions the auto-matcher could not confidently resolve.
Every unmatched bank transaction appears as a row. Expand a row to see up to five ranked match suggestions pulled live from the reconciliation API. Each suggestion shows:
| Element | What It Means |
|---|---|
| Entity-type badge | invoice, expense, donor, etc. — which record this suggestion is pointing at |
| Confidence badge | Color-coded: green ≥ 80, amber ≥ 60, blue otherwise |
| Reason text | A short human-readable explanation of why this candidate was suggested (amount match, date proximity, description keyword) |
| Confirm button | One click matches the transaction to the suggested entity and records who confirmed it |
| AI feedback button | Thumb-up / thumb-down on the AI’s suggestion — the feedback is scoped to the reconciliation_match feature and feeds future model tuning |
Confirming a suggestion posts a single match action to the bulk match endpoint. The session’s match counters update immediately and the transaction moves out of the unmatched list. You can confirm as many as you want in one sitting, or just the ones you are sure of — the session stays in_progress and waits for you to come back to the rest.
Nonprofit tenants — donor suggestions
If the organization has the nonprofit module enabled and the matcher finds a donor candidate (rather than an invoice or expense), the confirm flow opens a dedicated nonprofit reconciliation tab that lets you record the match as a donation with the right receipt metadata. This keeps donations out of the generic invoice/expense paths where they would otherwise produce an accountant-confusing ledger entry.
The Sessions Table
Switch the header filter to Reconciliation Sessions to see every session — in-progress, completed, partial, or cancelled. The columns mirror what a reviewer needs to pick up someone else’s work:
| Column | Shows |
|---|---|
| Session | Session name + start timestamp |
| Period | Statement start / end date |
| Progress | N matched / M total transactions, plus unmatched and excluded counts |
| Match Rate | A bar + percentage of transactions matched against total |
| Status | in_progress, completed, partial, or cancelled |
| Actions | Auto-Match + Review while in progress; View Report once completed |
The review button disables itself once there are no unmatched transactions left, so a reviewer does not wander into an empty drawer.
What Reconciliation Does Not Do (Today)
- Live bank-feed sync. Reconciliation is statement-file-based today. Automatic transaction sync from Plaid or the bank’s API is tracked as external work and not part of this flow.
- Create invoices or expenses on the fly. The matcher matches against entities that already exist. If a transaction has no plausible match, you excluded it or create the invoice/expense separately first.
- Edit parsed transactions. The raw transactions are trusted as the bank sent them. If the parse is wrong, fix the statement at the bank and re-import; the duplicate-detection will keep the original out of your way.
- POS receipt reconciliation. POS orders have their own payment settlement path — this flow is for bank-statement transactions, not individual POS receipts.
Where Reconciliation Fits
| You are here | You might also want |
|---|---|
| Reconciliation | Payments Received — to record a payment against an invoice before matching |
| Reconciliation | Bills — to record an expense before matching it to a bank debit |
| Reconciliation | AR Aging — to see which invoices are still unpaid after reconciling |
The goal: at the end of every month, every transaction on the bank statement is either matched to a Muin record or explicitly excluded — and the two ledgers agree without a spreadsheet.