> ## Documentation Index
> Fetch the complete documentation index at: https://docs.nyumbazetu.com/llms.txt
> Use this file to discover all available pages before exploring further.

# Payments reconciliation

> Match bank and M-Pesa statement lines against recorded payments — import evidence, review what the engine could not decide, and sign a period off.

**Finance → Reconciliation → `/reconciliation/overview`** <sup>Beta</sup>

Reconciliation answers one question: **does the money the bank says arrived match the payments the
system recorded?**

Everything else follows from that. A statement line with no payment behind it is money you have
received but not credited to anyone. A payment with no statement line behind it is money you think
you have but the bank has never seen.

> statement imported → lines matched against payments → what the engine could not
> decide goes to a human → exceptions resolved → the period is signed off.

<Note>
  Importing a statement **never creates, changes or allocates a payment**. Statement lines are
  *evidence*. The only way this module writes a payment is the explicit **Post payment** action on a
  line you have reviewed, and that action is idempotent — pressing it twice cannot double-post.
</Note>

## Before you start: two switches

Reconciliation is off until both of these are on. Neither reports an error when it is off — a run
simply completes having examined nothing, which looks identical to "everything is already
reconciled".

<Steps>
  <Step title="Turn the module on for the organization">
    The feature flag `payments_reconciliation` must be enabled for your organization or branch.
    Ask an administrator, or see [Feature flags](/guides/feature-flags).
  </Step>

  <Step title="Enrol each bank account">
    A bank account participates only when **Reconciliation enabled** is switched on for it, under
    [Bank accounts](/guides/bank-accounts). Accounts are enrolled one at a time on purpose: you
    reconcile the accounts you have statements for, not every account you have ever created.
  </Step>
</Steps>

If the Overview shows a run with **0 lines** and you know you imported a statement, one of these two
is the reason. The Overview names whichever one is missing.

## Step 1 — Import a statement

Statements come in through the **Bulk Upload wizard**, not through a separate uploader on this
page. The Imports tab is the *history* of what has been filed.

<Steps>
  <Step title="Open the wizard">
    **Bulk Upload → Bank Statements (for reconciliation)**.
  </Step>

  <Step title="Choose the account and the format">
    Both are required and neither can be guessed from the file — several banks export the same
    column layout, and filing a statement against the wrong account would attribute one
    organization's money to another.

    | Format                                     | Use it for                                                                                                                          |
    | ------------------------------------------ | ----------------------------------------------------------------------------------------------------------------------------------- |
    | **M-Pesa paybill statement**               | The paybill export from the M-Pesa org portal. Has separate *Paid In* / *Withdrawn* columns and a *Receipt No.*                     |
    | **NCBA statement (CSV)**                   | NCBA's CSV export.                                                                                                                  |
    | **Generic statement (CSV, signed amount)** | A plain CSV with one signed *Amount* column — positive in, negative out. A separate withdrawn column is **ignored** by this format. |
  </Step>

  <Step title="Map the columns (usually nothing to do)">
    The parser recognises the standard headers for each format. Map a column only when your export
    uses a name it does not recognise.

    The single most valuable column is the **Receipt / Reference No.** — the M-Pesa receipt or bank
    reference. An exact match on it reconciles a line automatically with no human step. Without it,
    every line needs a person.
  </Step>
</Steps>

**Rows the parser cannot read are reported, never silently dropped.** A statement routinely carries
header blurb and subtotal rows, so one unreadable row does not cost you the other four hundred. The
run summary lists each rejected row with its number and the reason — most often *No readable
transaction date* or *No readable amount*, which means that column did not map.

If the import staged **0 lines**, nothing was filed. The run is marked *Failed*, not *Succeeded*.

## Step 2 — Matching

A match run starts automatically after an import, and you can start one any time with **Reconcile
now**. Each unresolved line is scored against the payments on that account within the date window.

| Tier                       | What it means                                                       | What happens                                                                                                                                  |
| -------------------------- | ------------------------------------------------------------------- | --------------------------------------------------------------------------------------------------------------------------------------------- |
| **Tier 0 — exact receipt** | The line's receipt number equals the payment's confirmation number. | Reconciled automatically. This is identity, not a guess.                                                                                      |
| **Scored**                 | Amount, reference, payer phone and recency agree to a degree.       | Offered as a **suggestion** for a human to approve. Never auto-applied unless auto-approve is switched on and the score clears the threshold. |
| **No candidate**           | Nothing plausible on that account.                                  | Raises an exception.                                                                                                                          |

Confidence bands are 95 / 80 / 60. Auto-approve is **off** by default: the engine proposes, a person
disposes.

<Warning>
  A failed run is shown with its reason on the Overview. It means the run stopped, **not** that
  everything reconciled — the counts on a failed row are what it managed before it stopped.
</Warning>

## Step 3 — Work the worklist

**Reconciliation → Worklist** is the queue of lines that need a person. Open a line to reach the
**workbench**, which puts the statement evidence beside the candidate payments and shows the
per-component score for each one, so you can see *why* something was suggested.

From the workbench:

| Action             | Use when                                                                                                                                                                            |
| ------------------ | ----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| **Approve**        | The suggested payment is the right one.                                                                                                                                             |
| **Reject**         | This candidate is wrong; the others stay on offer.                                                                                                                                  |
| **Match manually** | You know the payment and the engine did not find it.                                                                                                                                |
| **Post payment**   | The bank received money the system never recorded. Creates the missing payment through the normal payment path, with a receipt derived from the line so a retry cannot double-post. |
| **Ignore**         | The line is not a payment — a bank charge, a transfer, a reversal. Requires a reason, which is kept on the audit trail.                                                             |

Every one of these is recorded on an append-only trail with who did it and when.

## Step 4 — Exceptions

An exception is a typed finding, not a generic error:

| Type                     | Meaning                                                                                              |
| ------------------------ | ---------------------------------------------------------------------------------------------------- |
| **Missing in platform**  | Money on the statement with no payment recorded. Usually resolved with **Post payment**.             |
| **Missing in statement** | A payment with no statement evidence. Either the statement is incomplete or the payment is not real. |
| **Duplicate**            | Two statement lines claim the same payment.                                                          |
| **Amount mismatch**      | The reference agrees but the amount does not.                                                        |
| **Already matched**      | A re-imported line conflicts with one already matched.                                               |
| **Stale**                | Unresolved past the staleness threshold — nobody has worked it.                                      |

## Step 5 — Sign the period off

**Reconciliation → Periods** closes the loop. A period covers one bank account for one month, and
it cannot be signed off while anything in it is unresolved — that refusal is the control.

A signed period can be reopened by an administrator, but the reason is mandatory and is kept.

## Re-importing the same statement

Safe. There are two guards:

* **Line level** — each line is hashed on its identity fields, so the same transaction cannot be
  staged twice even if the file is re-exported with different column order or spacing.
* **File level** — a file already staged for that account is recognised and skipped.

A row that was *rejected* is not fingerprinted, so fixing the file or the mapping and re-uploading
imports it normally.

## Troubleshooting

| What you see                                   | What it means                                                                                                                 |
| ---------------------------------------------- | ----------------------------------------------------------------------------------------------------------------------------- |
| Run completed, **0 lines**, no error           | The account is not enrolled, or the feature flag is off for its branch. See *Before you start*.                               |
| Import says **0 line(s) staged, N unreadable** | The parser could not read those rows. The run summary lists each one and why — usually the date or amount column did not map. |
| Run shows **Failed** with a reason             | The run stopped on that error. Nothing was silently skipped; fix the cause and press **Reconcile now**.                       |
| Worklist is empty but a statement was imported | Every line matched automatically, or the lines belong to a branch you cannot see.                                             |

## Related

* [Bank accounts](/guides/bank-accounts) — enrolling an account
* [Payments](/guides/payments-receive-pay) — how payments are recorded in the first place
* [Bulk upload](/guides/bulk-upload) — the import wizard and its run log
* [Allocations](/guides/allocations) — what happens to a payment after it is reconciled
