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

> ## Agent Instructions
> Reconify's API reference is read-only for customer data. Do not invent endpoints or authentication behavior beyond the OpenAPI contract.
> The public OpenAPI document contains only the documented external /v2 contract.

# Read the results

> Work through a results file in the order that finds real problems first, and make a run reproducible for an auditor.

A results file tells you what matched, what didn't, and why, but only if you read it in the right order. This page covers that order, then how to make a run auditable and byte-identical.

## Start with the summary

Every result carries a `summary` object. Read it before anything else:

```json theme={"theme":{"light":"github-light","dark":"github-dark"}}
{
  "currency": "NGN",
  "total_left": 842,
  "total_right": 851,
  "matched": 810,
  "unmatched_left": 12,
  "unmatched_right": 18,
  "amount_diff_count": 6,
  "timing_diff_count": 4,
  "duplicate_count": 2,
  "match_rate_pct": 96.2,
  "reconciled_rate_pct": 97.6
}
```

`result_mode` only appears in this object when a run sets it explicitly, either per pair in `reconify.yaml` or with `--result-mode`. A default `all` run omits the field rather than printing it.

Every monetary field Reconify emits is in minor units: a `total_left` amount of `184230000` means `1,842,300.00`. A healthy run looks like high `matched` and low everything else.

## Two rates, two questions

`match_rate_pct` and `reconciled_rate_pct` answer different questions, and it's easy to misread one for the other.

`match_rate_pct` counts only exact one-to-one reference matches: same reference, amount within tolerance, date within window. `reconciled_rate_pct` is broader: it includes amount diffs, timing diffs, and grouped outcomes, every case where the two sides genuinely reconciled to each other, just not perfectly. Both are percentages of `max(total_left, total_right)`.

Watch this with grouped passes: `one_to_many` inflates `total_right` because N right rows correspond to one left row. A fully reconciled grouped dataset can legitimately report a sub-100% `reconciled_rate_pct`. That's expected, not a sign of a broken mapping.

## Work through the buckets

Order matters here: each step tells you something the next step depends on.

<Steps>
  <Step>
    ### Check duplicates first

    A non-zero `duplicate_count` means rows in one source share a grouping key (`group_col`, falling back to `ref_col`). It's an annotation, not a filter: every row, duplicate or not, still went through matching.

    `duplicate_count` is a count of transactions across all duplicate groups, not a count of groups. See [Matching algorithm](/cli/concepts/matching-algorithm#duplicates-are-an-annotation-not-a-filter) for the full mechanics, including the installment-payment case where legitimate repeats need a separate `group_col`.
  </Step>

  <Step>
    ### Read amount\_diff and timing\_diff next

    Both mean the reference matched on both sides; only the amount or the date disagreed. `timing_diff` far more often means a `date_layout` or `tz` mistake than a real reconciling item: check that before assuming the two sides genuinely settled on different days. `amount_diff` usually means one side is gross and the other net of fees, refunds, or reserves.
  </Step>

  <Step>
    ### Then look at unmatched rows

    Three usual causes: the reference format differs between sources (`TXN-099` on one side, `099` on the other), the row genuinely has no counterpart yet, or the date falls outside `date_window`. Check reference format first; it's the most common and the cheapest to confirm.
  </Step>

  <Step>
    ### Finally, resolve ambiguous groups

    `ambiguous_group` events come from `one_to_many` or `many_to_many` passes where more than one left row shares the same reference, so grouping was undetermined. These rows are excluded from matching entirely and need manual resolution; Reconify won't guess which left row a group belongs to.
  </Step>
</Steps>

## Account for every currency unit

Reconify's discrepancy identity should account for every unmatched or ambiguous minor unit on both sides:

```text theme={"theme":{"light":"github-light","dark":"github-dark"}}
total_discrepancy = unmatched_amount_left + unmatched_amount_right
                  + amount_diff_total
                  + ambiguous_amount_left + ambiguous_amount_right
```

A zero `total_discrepancy` alongside the `matched` count you expect means a clean run. Non-zero is exactly the money still unexplained; it's the number to chase before signing off on a reconciliation.

## Summarise a finished run

```bash theme={"theme":{"light":"github-light","dark":"github-dark"}}
reconify explain results.ndjson --top 10
```

`explain` reads a completed JSON, JSON-stream, or NDJSON result without rerunning reconciliation. It returns `reconify.engine.explanation.v1`: the original summary, deterministic finding counts, and up to `--top` exception event payloads (`10` by default). Use `--top 0` to keep the counts and drop the payloads entirely.

If the input file was written with `result_mode: summary_only`, `explain` still reports the counts but can't produce payloads that were never in the file to begin with.

## Emit less

`result_mode` controls which events the writer emits: `all` (every event, the default), `exceptions_only` (unmatched, diffs, duplicates, ambiguous groups; clean matches suppressed), or `summary_only` (counts only, no item events). Set it per pair in the config, or override at runtime with `--result-mode`; the flag wins over the config value.

```bash theme={"theme":{"light":"github-light","dark":"github-dark"}}
reconify reconcile --pair left_source_vs_right_source --result-mode exceptions_only --out exceptions.ndjson
```

The guarantee that matters: filtering happens at the writer boundary. Classification counts, monetary totals, and `currency` in the summary are always computed from the full reconciliation. Suppressing events never changes the numbers, only which rows you can see.

## Make the run auditable

```bash theme={"theme":{"light":"github-light","dark":"github-dark"}}
reconify reconcile --config reconify.yaml --pair left_source_vs_right_source --format json --audit --out results.audit.json
```

`--audit` adds a `run_info` object to structured output: a run ID, UTC timestamp, tool version, resolved input paths, a SHA-256 hash and `size_bytes`/`mod_time` for each input file, and a snapshot of the applied pair config. It's supported for `json`, `json-stream`, and `ndjson`, not `csv` or `table`. For NDJSON, `run_info` is always the first line of the file.

## Make it byte-identical

```bash theme={"theme":{"light":"github-light","dark":"github-dark"}}
reconify reconcile \
  --config reconify.yaml \
  --pair left_source_vs_right_source \
  --format json \
  --audit \
  --deterministic \
  --audit-fixed-timestamp "2026-01-01T00:00:00Z" \
  --out results.audit.json
```

`--deterministic` sorts JSON output sections for stable key ordering; it only affects buffered `json` output. `--audit-fixed-timestamp` fixes the run ID and timestamp instead of letting them change on every run. Together, two runs on the same input files produce the same output bytes, which you can prove directly:

```bash theme={"theme":{"light":"github-light","dark":"github-dark"}}
shasum -a 256 results.audit.json
```

Run the command twice and compare hashes. They should match exactly when `--deterministic` and `--audit-fixed-timestamp` are both set and the inputs haven't changed.

## Fail a pipeline on exceptions

`--fail-if-unmatched` exits `3` if either side has unmatched rows after a completed run. `--fail-if-exceptions` exits `4` if any `amount_diff`, `timing_diff`, or unmatched event was emitted; it's a superset of `--fail-if-unmatched`, and when both flags are set and both conditions hold, `4` takes precedence over `3`.

Both flags complete the run and write output first. The exit code is a policy decision your CI makes afterward, not a sign the run itself failed.
