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

# How ReconLayer Works

> The expectation, evidence, and reconciliation model end to end — for a new engineer integrating with ReconLayer.

ReconLayer reconciles payments that move across bank rails, stablecoins, and payout providers. At
its core, the model answers one question per payment: **did what was supposed to happen actually
happen, and if not, why?**

This page walks through the full lifecycle of a payment in ReconLayer, end to end, using the
actual object names and field names you'll see in the API.

## The two sides of every payment

ReconLayer keeps two things separate and compares them:

1. **What should happen** — a `PaymentIntent`. A declaration, usually made via your own system,
   of an expected payment: amounts, currencies, beneficiary, and corridor.
2. **What actually happened** — evidence, arriving as `RawRecord`s from webhooks, file imports, or
   the evidence API, normalized into `FlowLeg`s (real movements of money).

The reconciliation engine compares these two and produces a `ReconciliationCase`: one row per
`PaymentIntent`, holding the verdict, any exception, and a full breakdown of fees, FX, and rounding
that explain (or fail to explain) the difference between expected and actual amounts.

<Tip>
  For precise field-by-field definitions of every object mentioned on this page, see
  [Core Concepts](/architecture/core-concepts).
</Tip>

## Two ways to start: expectation-first vs. evidence-first

ReconLayer supports two valid entry points into the model:

<Tabs>
  <Tab title="Expectation-first">
    You (the client) know about the payment before ReconLayer sees any evidence of it. Typical
    sources:

    * A direct call to
      [`POST /v1/payment-intents`](/api-reference/payment-intents/create-or-reuse-a-payment-intent)
      when you initiate or observe a payment on your side.
    * A `client_internal_ledger` file import — each valid row creates a `PaymentIntent`.

    ReconLayer creates the `PaymentIntent` and, in the same transaction, an open
    `ReconciliationCase` with `reconciliationStatus: 'unreconciled'`. The case then waits for
    evidence.
  </Tab>

  <Tab title="Evidence-first">
    Evidence of money movement arrives before — or without — any stored expectation. Typical
    sources:

    * A provider/PSP webhook, submitted via
      [`POST /v1/evidence/provider`](/api-reference/reconciliation-cases/submit-provider-evidence).
    * An on-chain transfer event, submitted via
      [`POST /v1/evidence/onchain`](/api-reference/reconciliation-cases/submit-on-chain-evidence)
      or ingested through a configured on-chain integration.
    * A `bank_statement`, `psp_report`, or `onchain_report` file import — each valid row becomes a
      `FlowLeg`.

    ReconLayer stores the `RawRecord`, normalizes it into a `FlowLeg`, and attempts to match it to
    an existing `PaymentIntent`. If no match is found yet, the evidence is retained — and may
    later surface as a `missing_expected_record` exception once a case opens, or stay as
    unattached evidence.
  </Tab>
</Tabs>

Both paths converge on the same model. Reconciliation happens whenever the system has *both*
enough expectation and enough evidence to compare them — regardless of which arrived first.

## Step by step: a cross-border payout

Here's a concrete walkthrough combining both entry points, following a single
`externalReference`.

<Steps>
  <Step title="Declare the expected payment">
    Your system calls
    [`POST /v1/payment-intents`](/api-reference/payment-intents/create-or-reuse-a-payment-intent):

    ```json theme={null}
    {
      "externalReference": "ACME-2026-0001",
      "sourceAmount": "1000.00",
      "sourceCurrency": "USD",
      "destinationAmount": "82500.00",
      "destinationCurrency": "INR",
      "paymentType": "cross_border",
      "paymentSubtype": "usd_inr",
      "beneficiaryAccount": "IN1234567890",
      "beneficiaryName": "Ravi Kumar",
      "clientCompletedAt": "2026-05-12T10:00:00Z",
      "statusOnClientSide": "completed"
    }
    ```

    ReconLayer creates a `PaymentIntent` and, atomically, a `ReconciliationCase` with
    `status: 'open'`, `reconciliationStatus: 'unreconciled'`, and `expectedAmount: "1000.00"`. The
    response returns:

    ```json theme={null}
    {
      "caseId": "rc_001",
      "paymentIntentId": "pi_001",
      "externalReference": "ACME-2026-0001",
      "status": "reconciling",
      "verdict": null,
      "outcome": "created"
    }
    ```

    `verdict: null` because no evidence has arrived yet — there's nothing to reconcile against.

    Calling this endpoint again with the same `externalReference` and the same canonical fields is
    idempotent: ReconLayer treats it as a **replay** (`outcome: "reused"`) and reuses the existing
    case. If the canonical fields (amounts, currencies, beneficiary account) *don't* match, the
    API returns a conflict describing exactly which fields differ.
  </Step>

  <Step title="Provider evidence arrives">
    A payout provider sends a webhook (or you call
    [`POST /v1/evidence/provider`](/api-reference/reconciliation-cases/submit-provider-evidence))
    confirming a transfer. ReconLayer:

    1. Stores the payload as a `RawRecord` (`source: "webhook"`, `sourceType: "psp_report"`,
       `provider: "bridge"`).
    2. Normalizes it into a `FlowLeg` (`type: "provider_transfer"`, `phase: "intermediary_in"`,
       `status: "confirmed"`, `providerTransferId: "tr_bridge_001"`, `amount: "1000.00"`,
       `currency: "USD"`).
    3. The matcher looks for a `PaymentIntent` whose `PaymentIntentReference` has
       `type: "provider_transfer_id"` and `value: "tr_bridge_001"` — or whose
       `externalReference`/`FlowLegReference{type: "external_reference"}` matches a reference
       found in the leg or payload.
    4. On a match, it creates a `MatchLink` (`matchType: "provider_id"`, `confidence:
       "deterministic"`) and an `AuditEvent` (`eventType: "match.created"`).
    5. The case is re-evaluated: `reconcile()` compares `expectedAmount` to the selected leg's
       `amount`, computes `unexplainedDelta`, and updates `reconciliationStatus`.
  </Step>

  <Step title="On-chain evidence arrives">
    A second leg — the actual on-chain transfer — arrives via
    [`POST /v1/evidence/onchain`](/api-reference/reconciliation-cases/submit-on-chain-evidence) or
    an on-chain integration feed. This becomes a `FlowLeg` with `type: "onchain_transfer"`,
    `phase: "transfer"`, `txHash`, `chainId`, `fromAddress`, `toAddress`, `tokenAddress`. If a
    `PaymentIntentReference{ type: "tx_hash" }` matches, a `MatchLink` with `matchType: "tx_hash"`
    is created.
  </Step>

  <Step title="The destination bank file arrives">
    A bank statement file (`sourceType: "bank_statement"`, e.g. via an `ImportProfile` for
    `icici:daily-xlsx`) is imported. The matching row becomes a `RawRecord` and a `FlowLeg`
    (`type: "bank_transfer"`, `phase: "destination"`, `currency: "INR"`, `amount: "82500.00"`),
    carrying a `FlowLegReference{ type: "bank_reference", value: "BANK-OUT-789" }`. If that
    reference matches the expected payment, a `MatchLink` with `matchType: "reference_exact"` is
    created.
  </Step>

  <Step title="The case reconciles">
    Once the required legs are present, `reconcile()` evaluates the selected leg (the one closest
    to `phase: "destination"` with `status: "confirmed"`) against `expectedAmount`. If
    `unexplainedDelta` is `0` (or within the configured `amountTolerance`),
    `reconciliationStatus` becomes `reconciled`, `reconciledAt` is set, and the derived `verdict`
    becomes `matched` (or `matched_with_exception` if an `exceptionType` is also set).
  </Step>
</Steps>

```mermaid theme={null}
sequenceDiagram
  participant Client
  participant API as ReconLayer API
  participant Engine as reconciliation-engine

  Client->>API: POST /v1/payment-intents (expected payment)
  API->>API: create PaymentIntent + ReconciliationCase (open, unreconciled)
  API-->>Client: { caseId, paymentIntentId, verdict: null, outcome: "created" }

  Note over API: ...later...

  Client->>API: POST /v1/evidence/provider (provider webhook)
  API->>API: create RawRecord -> normalize FlowLeg
  API->>API: find matching PaymentIntent via provider_transfer_id
  API->>API: create MatchLink (matchType: provider_id)
  API->>Engine: reconcile(expected, actual, fees...)
  Engine-->>API: { result, unexplainedDelta, matchReason }
  API->>API: update ReconciliationCase (reconciliationStatus, unexplainedDelta)
  API-->>Client: 202 accepted { matchedCaseIds: ["rc_001"] }

  Client->>API: GET /v1/reconciliation-cases/rc_001
  API-->>Client: { verdict: "matched", evidenceCoverage, flowLegs, ... }
```

## Reading the result: verdicts and exceptions

`ReconciliationCase` does not store a `verdict` column — it's derived from
`reconciliationStatus` and `exceptionType` every time a case is read:

| `reconciliationStatus`   | `exceptionType` | derived `verdict`        |
| ------------------------ | --------------- | ------------------------ |
| `reconciled`             | `null`          | `matched`                |
| `reconciled`             | set             | `matched_with_exception` |
| `tentatively_reconciled` | (any)           | `needs_review`           |
| `unreconciled`           | (any)           | `unreconciled`           |

The case export endpoint additionally supports `sla_risk` and `delayed` as verdict filter values —
`slaRisk` is a separate computed boolean (`status: 'open'` and open for more than 240 minutes).

`exceptionType` is one of: `missing_expected_record`, `missing_evidence`, `missing_callback`,
`asset_mismatch`, `amount_mismatch`, `fee_variance`, `fx_variance`, `unexplained_variance`,
`state_mismatch`, `timing_delay`, `settlement_reversed`, `invalid_signature`, `duplicate_record`,
`chain_reorg`.

## What governs matching and tolerances

Two things make reconciliation organization-specific rather than hardcoded:

* **`ReconciliationRule`** rows configure amount tolerance, matching time windows, per-phase SLA
  minutes, and which match strategies (`provider_id`, `tx_hash`, `reference_exact`,
  `amount_and_time_window`) are enabled. See
  [Reconciliation Rules](/architecture/reconciliation-rules).
* **The canonical field registry** defines exactly which fields each evidence source type can
  populate, so import profiles and adapters all normalize into the same shapes. See
  [Canonical Field Registry](/architecture/field-registry).

## Where to go next

<CardGroup cols={2}>
  <Card title="Core Concepts" icon="layer-group" href="/architecture/core-concepts">
    Full field-level reference for PaymentIntent, RawRecord, FlowLeg, FlowLegReference, and
    ReconciliationCase.
  </Card>

  <Card title="Reconciliation Rules" icon="sliders" href="/architecture/reconciliation-rules">
    How tolerances, time windows, SLAs, and match strategies are configured per organization.
  </Card>

  <Card title="Database Architecture" icon="database" href="/architecture/database">
    The Prisma schema and table relationships behind this model.
  </Card>

  <Card title="Create or reuse a payment intent" icon="play" href="/api-reference/payment-intents/create-or-reuse-a-payment-intent">
    The endpoint that starts the expectation-first flow.
  </Card>
</CardGroup>
