unmatched. This page tells you which bucket a row lands in and why, so you can predict a result before you read it.
The passes, in plain terms
Reconify runs one or more matching passes against every pair. Each one answers a different business question:- Reference matching (
reference_one_to_one). Matches two rows that carry the same reference, like an invoice number or payment ID. This is the default, and the right choice for most reconciliations: it’s exact, fast, and doesn’t guess. - Name-token matching (
name_tokens_one_to_one). Matches rows by comparing description text when you don’t have a reliable shared reference. Treat it as a fallback, not a primary strategy: text similarity is never as certain as an exact ID. - One-to-many (
one_to_many). Matches one row against several rows that sum to it, like an invoice paid in three installments. Use this when one side of your data is more granular than the other. - Many-to-many (
many_to_many). Matches a group of rows on one side against a group on the other, like a Stripe payout that bundles many sales, refunds, and fees into one settlement. This is the common case for payout and settlement reconciliation. - Duplicate detection. Not a matching pass. It flags rows in the same source that share a grouping key, for visibility, without stopping those rows from matching normally.
The tiers
Reference matching
Reference matching is the primary path, and it runs first. When a left and right transaction share the samereference:
- Both
matchedwhen the amount is within tolerance and the date is within the window. amount_diffwhen the date is fine but the amount disagrees beyondamount_tolerance_minor.timing_diffwhen the amount is fine but the date falls outsidedate_window.
Best-candidate selection
When several right-side rows share the same reference, the engine doesn’t take the first one it encounters. An exact match, amount and date both within tolerance, wins immediately. Otherwise, the candidate with the smallest amount difference is preferred over the candidate with the smallest date difference, and ties break on the smallest difference regardless of where the candidate appears in the input file. Batch and streaming reconciliation use the same selection logic, so a large streamed run and a small in-memory run classify an identical input the same way. Transactions without a usable reference skip straight to name-token matching, or land inunmatched if name_mode is none.
Name-token matching
Setname_mode: "tokens" on a pair to compare the remaining unmatched transactions by Jaccard similarity on their name field’s word tokens. A score strictly greater than name_match_threshold counts as a match, still subject to the pair’s amount tolerance and date window.
name_match_threshold must be greater than 0 and less than 1; it defaults to 0.5. 1.0 is rejected by config validation, because the comparison is strict and a Jaccard score never reaches 1.0, so that threshold could never match anything.
Name-token matching only runs after reference matching, and only on rows reference matching left unresolved. It can never override a reference match, even a poor one.
Duplicates are an annotation, not a filter
A non-zeroduplicate_count does not mean rows were excluded from matching. Rows in the same source that share a group_key (from group_col, or reference when group_col is unset) are grouped and reported for visibility, and that grouping happens alongside matching, not before it.
Every row in a duplicate group, not just the first one, still goes through reference and name-token matching independently.
This is what makes group_col useful for installments: three payments against one invoice share an invoice number in reference but have distinct payment_id values in group_key. All three show up as a group of 3 in Duplicates for review, and all three can still match independently against the right side.
Set group_col to the per-row-unique column and the detector stops treating legitimate installments as a data-quality problem.
duplicate_count counts individual transactions across every group, not the number of groups. A group of 3 installments and a group of 2 repeated export rows together produce duplicate_count: 5, not 2.
Here are the common causes of a non-zero count:
- Repeated export row
- Reused references across unrelated transactions
- Partial settlement records
- Refunds sharing the original transaction’s reference.
Grouped passes
Reference and name-token matching are both one-to-one: one left row, one right row. Grouped passes handle the case where one side splits a transaction the other side records as a single row.One invoice, many payments (one_to_many)
one_to_many sums right rows sharing a grouping key and compares the total to one left amount. By default the key is reference; set group_by: name or group_by: group_key to group on something else.
Outcomes:
Split settlements (many_to_many)
many_to_many sums both sides of a shared grouping key and compares the two totals, for cases where each side splits the same business event differently:
Both sides sum to
14500, so the output contains one explainable many_to_many_matched event with lefts and rights arrays, instead of four unmatched rows on each side.
What grouped passes do not do
Grouped passes don’t search for combinations of rows that happen to sum to a target amount, and they don’t use fuzzy matching. Rows only group together when they share the configuredgroup_by key exactly.
Good keys are stable identifiers meant for exactly this: payout IDs, invoice IDs, settlement IDs, payment-run IDs, remittance references. A key that isn’t consistently populated on both sides produces unmatched rows, not a partial group.
Pass order
By default a pair runs reference matching, then name-token matching ifname_mode: tokens is set. For more control, declare passes as an explicit ordered list:
reference_one_to_one, name_tokens_one_to_one, one_to_many, and many_to_many. Each pass only sees rows that earlier passes left unmatched, which makes pass order a modeling decision with real consequences:
[reference_one_to_one, one_to_many]. The reference pass greedily takes the best 1-to-1 candidate for any left row whose reference appears on the right. Same-date installments get classifiedamount_diffand consumed there beforeone_to_manyever sees them. Useone_to_manyalone unless some rows are genuinely 1-to-1 and the installments fail both amount and date individually against every right row.reference_one_to_onebeforemany_to_manyon a shared reference. Same problem: the reference pass can pick off individual rows as matches or diffs before the group pass runs. Usemany_to_manyalone for settlement groups where the reference is the group key, or group on something else, likenameorgroup_key.- When
passesis set,name_mode: tokensis rejected by config validation. Addname_tokens_one_to_oneto the list explicitly instead.