> ## 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.

# Core Concepts

> The canonical objects ReconLayer uses to model expectations, evidence, and reconciliation outcomes.

ReconLayer's canonical model has six tables, but almost everything you'll touch through the API
maps to five core objects: **PaymentIntent**, **RawRecord**, **FlowLeg**, **FlowLegReference**,
and **ReconciliationCase**, linked by **MatchLink** and narrated by **AuditEvent**. Every
integration — whether it's a direct API call, a webhook, a file import, or an on-chain feed —
ultimately produces rows in these tables.

This page defines each object precisely, in terms of the actual fields the API returns. For the
end-to-end flow (how these objects come together for a single payment), see
[How ReconLayer Works](/getting-started/how-reconlayer-works). For matching and tolerance
behavior, see [Reconciliation Rules](/architecture/reconciliation-rules). For the underlying
tables, see [Database Architecture](/architecture/database).

<Info>
  Every canonical field below has a corresponding entry in the
  [Canonical Field Registry](/architecture/field-registry), which is also the contract import
  profiles use to map external files into these models.
</Info>

## 1. PaymentIntent — what should happen

A `PaymentIntent` is the **expectation**: a client telling ReconLayer "this payment should occur,
here's what it should look like." It is created via
[`POST /v1/payment-intents`](/api-reference/payment-intents/create-or-reuse-a-payment-intent), or
indirectly from a `client_internal_ledger` file import.

Key fields (from `PaymentIntentCreateRequest` / `PaymentIntentSnapshot`):

| Field                                       | Type                                          | Notes                                                                                                                                                                         |
| ------------------------------------------- | --------------------------------------------- | ----------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| `externalReference`                         | `string`                                      | Client's stable business reference. Unique per `(organizationId, externalReference)` — this is the idempotency key for the intake endpoint.                                   |
| `sourceAmount` / `sourceCurrency`           | decimal string / currency code                | What leaves the source side. Currency can be a fiat ISO code or a token symbol (`USDC`).                                                                                      |
| `destinationAmount` / `destinationCurrency` | decimal string / currency code                | What the beneficiary should receive.                                                                                                                                          |
| `paymentType`                               | `stablecoin \| bank \| cross_border \| other` | Broad category. Drives [reconciliation rule selection](/architecture/reconciliation-rules).                                                                                   |
| `paymentSubtype`                            | `string`                                      | Narrower rail/corridor label, e.g. `wire`, `swift`, `usd_mxn`, `polygon_usdc`.                                                                                                |
| `direction`                                 | `debit \| credit`                             | From the client's perspective. Defaults to `debit`.                                                                                                                           |
| `beneficiaryAccount` / `beneficiaryName`    | `string`                                      | Destination account/wallet identifier and display name.                                                                                                                       |
| `stablecoin` / `chain`                      | `string`                                      | Token symbol (`USDC`) and chain label (`polygon`) when relevant.                                                                                                              |
| `status`                                    | `PaymentIntentStatus`                         | `initiated`, `submitted`, `pending`, `sent`, `completed`, `failed`, `returned`, `reversed`, `cancelled`, `unknown`. Set from `statusOnClientSide` on create.                  |
| `references`                                | array of `{ type, value }`                    | Structured identifiers, e.g. `provider_transfer_id`, `tx_hash`, written into `PaymentIntentReference` rows.                                                                   |
| `metadata`                                  | JSON                                          | Client context (cost center, batch id, corridor) plus ReconLayer bookkeeping fields like `clientCompletedAt`, `statusOnClientSide`, `expectedFxRate`, `developerFeeExpected`. |

<Note>
  Creating a `PaymentIntent` **always** creates exactly one `ReconciliationCase` in the same
  transaction (`status: 'open'`, `expectedAmount` copied from `sourceAmount`). The
  [create response](/api-reference/payment-intents/create-or-reuse-a-payment-intent) returns both
  `paymentIntentId` and `caseId`, with `status: 'reconciling'` and `verdict: null` — the case
  hasn't been evaluated yet because no evidence has arrived.
</Note>

### Replay and conflict behavior

POSTing the same `externalReference` again is idempotent at the business level:

* If the canonical fields (`sourceAmount`, `sourceCurrency`, `destinationAmount`,
  `destinationCurrency`, `beneficiaryAccount`, and — if previously set — `stablecoin`/`chain`)
  match the stored intent, the request is treated as a **replay**: the existing intent is updated
  (status, `beneficiaryName`, `paymentType`, etc.), a `payment_intent.replayed` audit event is
  written, and the response has `outcome: 'reused'`.
* If those canonical fields **don't** match, the service throws an `ExpectationConflictError`
  describing the field-by-field `mismatches` between the existing and incoming request — the
  client is asked to resolve the discrepancy rather than silently overwriting it.

## 2. RawRecord — what arrived, untouched

A `RawRecord` is the **immutable evidence layer**. Every inbound payload — a provider webhook, an
on-chain indexer event, a row from an uploaded file, or a manual entry — is stored as one
`RawRecord` before any normalization happens.

| Field                             | Type                                            | Notes                                                                                                                                                                 |
| --------------------------------- | ----------------------------------------------- | --------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| `source`                          | `file \| api \| webhook \| manual`              | Ingestion channel.                                                                                                                                                    |
| `sourceType`                      | `ImportSourceType \| null`                      | Business category: `client_transfer_report`, `client_internal_ledger`, `bank_statement`, `onchain_report`, `psp_report`, `manual`.                                    |
| `provider` / `integrationKey`     | `string \| null`                                | Vendor label (`bridge`, `alchemy`, `icici`) and technical connector id (`bridge:webhook`, `icici:daily-xlsx`).                                                        |
| `sourceRef`                       | `string`                                        | Stable source-side identifier — provider transfer id, tx hash, or generated row reference. Unique per `(organizationId, source, sourceRef)` for idempotent ingestion. |
| `payload`                         | JSON                                            | The untouched source payload. **Never edited.**                                                                                                                       |
| `rowNumber`                       | `int \| null`                                   | Source row number for file imports.                                                                                                                                   |
| `normalizedType` / `normalizedId` | `string \| null`                                | What this record was normalized into, and its id.                                                                                                                     |
| `validationStatus`                | `pending \| valid \| warning \| failed \| null` | Set during import row validation.                                                                                                                                     |
| `signature` / `signatureValid`    | `string \| boolean \| null`                     | Webhook signature verification result, when applicable.                                                                                                               |

`RawRecord.payload` is the audit anchor: when something looks wrong in a `FlowLeg`, you can always
trace it back to the exact bytes that were ingested.

## 3. FlowLeg — where value actually moved

A `FlowLeg` is one **normalized movement** derived from a `RawRecord`. Complex cross-border
payments produce multiple legs for a single `PaymentIntent`.

| Field                                                               | Type                                                                               | Notes                                                                                 |
| ------------------------------------------------------------------- | ---------------------------------------------------------------------------------- | ------------------------------------------------------------------------------------- |
| `type`                                                              | `provider_transfer \| onchain_transfer \| bank_transfer`                           | What kind of movement this is.                                                        |
| `phase`                                                             | `source \| intermediary_in \| transfer \| intermediary_out \| destination \| null` | Role of this leg in a multi-hop route.                                                |
| `status`                                                            | `pending \| confirmed \| failed \| reversed \| missing`                            | Current state of the leg. Defaults to `pending`.                                      |
| `reconciliationScope`                                               | `required \| optional \| ignored`                                                  | Whether this leg counts toward "the case is fully evidenced." Defaults to `required`. |
| `routeGroupId`                                                      | `string \| null`                                                                   | Groups legs that belong to the same routed path or retry.                             |
| `sequence`                                                          | `int \| null`                                                                      | Order of this leg within its route group.                                             |
| `providerTransferId`                                                | `string \| null`                                                                   | First-class provider transfer id (commonly queried for matching).                     |
| `txHash` / `chainId` / `fromAddress` / `toAddress` / `tokenAddress` | on-chain identifiers                                                               | First-class for `onchain_transfer` legs.                                              |
| `provider` / `integrationKey`                                       | `string \| null`                                                                   | Mirrors the normalized source's vendor identity.                                      |
| `amount` / `currency`                                               | decimal / string `\| null`                                                         | The amount moved on this leg.                                                         |
| `occurredAt`                                                        | `datetime \| null`                                                                 | When the movement happened (block time, bank posting time, provider event time).      |
| `metadata`                                                          | JSON                                                                               | Extra non-queryable context.                                                          |

### `reconciliationScope` and `shouldMatchLeg`

The reconciliation engine's `shouldMatchLeg()` helper treats any leg with
`reconciliationScope: 'ignored'` as invisible to matching — it is never selected as a match
candidate and never blocks case completeness. `required` and `optional` legs are both eligible for
matching; the distinction matters for `requiredLegsTotal` / `requiredLegsPresent` reporting on the
case summary (see below).

## 4. FlowLegReference — typed long-tail identifiers

`FlowLegReference` rows attach typed identifiers to a `FlowLeg` without adding nullable columns to
the leg table itself. Each row is `{ type, value }`, e.g. `{ type: "bank_reference", value:
"BANK-OUT-789" }` or `{ type: "external_reference", value: "ACME-2026-0001" }`.

The matcher specifically looks for a leg reference with `type: 'external_reference'` as one of its
candidate identifiers (see [Reconciliation Rules](/architecture/reconciliation-rules)).

## 5. ReconciliationCase — the truth row

Exactly one `ReconciliationCase` exists per `PaymentIntent` (1:1, enforced by a unique constraint
on `paymentIntentId`). It is the record operators review, resolve, or archive.

| Field                                                                    | Type                                                   | Notes                                                                                                                                                    |
| ------------------------------------------------------------------------ | ------------------------------------------------------ | -------------------------------------------------------------------------------------------------------------------------------------------------------- |
| `status`                                                                 | `open \| resolved \| archived`                         | Workflow state.                                                                                                                                          |
| `reconciliationStatus`                                                   | `unreconciled \| tentatively_reconciled \| reconciled` | What the matcher/evaluator concluded.                                                                                                                    |
| `expectedAmount` / `actualAmount`                                        | decimal `\| null`                                      | Expected (copied from the intent's `sourceAmount`) vs. observed amount.                                                                                  |
| `providerFee`, `networkFee`, `developerFee`, `fxSpread`, `roundingDelta` | decimal `\| null`                                      | Individually-tracked components of any delta — never collapsed into one number.                                                                          |
| `unexplainedDelta`                                                       | decimal `\| null`                                      | What's left after subtracting expected − actual − all the above fee/FX/rounding components. This is the field operations actually watches; it's indexed. |
| `exceptionType`                                                          | `ExceptionType \| null`                                | Canonical exception category (see below).                                                                                                                |
| `notes`                                                                  | `string \| null`                                       | Operator notes.                                                                                                                                          |
| `reconciledAt` / `lastRunAt`                                             | `datetime \| null`                                     | When the case last reconciled, and when the evaluator last ran.                                                                                          |

### The `verdict` field

`verdict` is **not stored** — it's derived at read time from `reconciliationStatus` and
`exceptionType`:

```text theme={null}
reconciled              + no exceptionType  -> "matched"
reconciled              + exceptionType set -> "matched_with_exception"
tentatively_reconciled                      -> "needs_review"
unreconciled (any other state)              -> "unreconciled"
```

The export endpoint additionally supports `sla_risk` and `delayed` as verdict values for
filtering (`ReconciliationCaseExportVerdict` enum: `matched`, `matched_with_exception`,
`needs_review`, `unreconciled`, `sla_risk`, `delayed`). `slaRisk` itself is a separate boolean on
the case summary: `true` when the case is `open` and has been open for more than 240 minutes
(`SLA_RISK_THRESHOLD_MINUTES`).

### `ExceptionType` enum

```text theme={null}
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
```

### Evidence coverage and flow legs on the case summary

`GET /v1/reconciliation-cases` and
[`GET /v1/reconciliation-cases/{caseId}`](/api-reference/reconciliation-cases/get-reconciliation-case-detail)
both return a computed `evidenceCoverage` object and a `flowLegs` array summarizing the intent's
legs:

```json theme={null}
{
  "evidenceCoverage": {
    "expected": "present",
    "provider": "present",
    "chain": "warning",
    "bank": "missing",
    "file": "not_provided"
  },
  "requiredLegsTotal": 2,
  "requiredLegsPresent": 1,
  "flowLegs": [
    { "id": "fl_001", "type": "provider_transfer", "status": "confirmed", "amount": "1000.00", "currency": "USD", "provider": "bridge", "providerTransferId": "tr_bridge_001", "txHash": null }
  ]
}
```

`evidenceCoverage` values are `present | missing | warning | failed | not_provided`, derived from
leg status: `confirmed` → `present`, `pending` → `warning`, `failed`/`reversed` → `failed`,
`missing` → `missing`, and `not_provided` when no leg of that type exists at all (`file` coverage
is always `not_provided` today — no leg type maps to it yet).

## MatchLink — why evidence was linked

`MatchLink` is the explainable join between a `ReconciliationCase`, a `FlowLeg`, and a
`RawRecord`. Each row records:

* `matchType`: `provider_id`, `tx_hash`, `reference_exact`, `amount_and_time_window`, or
  `manual_override`.
* `matchReason`: a human-readable sentence, e.g. *"External reference ACME-2026-0001 matched the
  expected payment."*
* `confidence`: `deterministic`, `heuristic`, or `manual`.

A `(caseId, legId, rawRecordId)` triple is unique — re-processing the same evidence against the
same case is idempotent (`upsert`).

## AuditEvent — the append-only narrative

Every meaningful state change writes an `AuditEvent`: `payment_intent.created`,
`payment_intent.replayed`, `match.created`, `match.none`, `case.evaluated`, and more. Each event
carries `eventType`, `actor` (e.g. `system:matcher`, `user:<clerk-id>`), an optional `message`, and
a `payload` JSON blob with event-specific context. Audit events are surfaced on both
`PaymentIntentDetail.auditEvents` and `ReconciliationCaseDetail.auditEvents`.

## Putting it together

```mermaid theme={null}
flowchart LR
  subgraph Expectation
    PI["PaymentIntent\n(POST /v1/payment-intents)"]
  end

  subgraph Evidence
    RR["RawRecord\n(webhook / file / evidence API)"]
    FL["FlowLeg\n(normalized movement)"]
    FLR["FlowLegReference\n(typed identifiers)"]
  end

  subgraph Outcome
    RC["ReconciliationCase\n(verdict, exceptionType, deltas)"]
    ML["MatchLink\n(explainable linkage)"]
  end

  PI -- "creates 1:1" --> RC
  RR -- "normalizes into" --> FL
  FL -- "carries" --> FLR
  FL -- "matcher links via" --> ML
  RR -- "matcher links via" --> ML
  ML -- "updates" --> RC
  PI -. "audit trail" .-> AE["AuditEvent"]
  RC -. "audit trail" .-> AE
```

<CardGroup cols={2}>
  <Card title="How ReconLayer Works" icon="diagram-project" href="/getting-started/how-reconlayer-works">
    Conceptual walkthrough of the expectation → evidence → case lifecycle.
  </Card>

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

  <Card title="Database Architecture" icon="database" href="/architecture/database">
    The Prisma schema behind these objects.
  </Card>

  <Card title="Canonical Field Registry" icon="table-list" href="/architecture/field-registry">
    Every mappable field, by model and source type.
  </Card>
</CardGroup>
