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

# Ingestion Flows

> How expectations and evidence enter ReconLayer, and how the matching engine reconciles them.

ReconLayer's core job is to compare **what should happen** (an expectation) against **what actually happened** (evidence), and link the two together as a `ReconciliationCase`. Everything that flows into the system — API calls, provider webhooks, on-chain events, CSV uploads — ultimately resolves into one of two record types:

* A **`PaymentIntent`** — an expectation that a payment will move.
* A **`FlowLeg`** — a normalized piece of evidence describing one leg of a payment's actual journey (a bank credit, a provider transfer, an on-chain transfer, etc.), backed by an immutable **`RawRecord`**.

There is no single "ingestion endpoint." Instead, there are three independent entry points for evidence, plus direct API creation of expectations, and ReconLayer's matching engine runs after each one to see whether an expectation and its evidence can now be linked.

<Note>
  This page is the map. For the mechanics of each entry point, see [File Imports](/integrations/file-imports), [Inbound Webhooks](/integrations/webhooks), [Outbound Webhooks](/integrations/outbound-webhooks), [Bridge Integration](/integrations/bridge), and [On-chain Integrations](/integrations/onchain).
</Note>

## The three ways evidence enters ReconLayer

```mermaid theme={null}
flowchart TB
    subgraph A["1. Provider & on-chain evidence"]
        A1["POST /webhooks/bridge/:integrationKey"]
        A2["POST /v1/evidence/provider"]
        A3["POST /v1/evidence/onchain"]
    end

    subgraph B["2. File imports"]
        B1["POST /v1/import-batches\n(ImportProfile-driven CSV)"]
    end

    subgraph C["3. Integration polling"]
        C1["POST /v1/bridge-integrations/:id/poll-transfer\n+ background Bridge poll loop"]
        C2["POST /v1/onchain-integrations/:id/ingest\n(source: poll | push)"]
    end

    A1 --> N[ingestEvidence]
    A2 --> N
    A3 --> N
    B1 -->|psp_report / bank_statement / onchain_report rows| N
    B1 -->|client_internal_ledger rows| P[PaymentIntent + ReconciliationCase]
    C1 --> N
    C2 --> N

    N --> R["RawRecord + AuditEvent\n'evidence.received'"]
    R --> L["FlowLeg(s) + AuditEvent\n'leg.normalized'"]
    L --> M[Matching engine]
    P --> M
    M --> Case[ReconciliationCase updated\nMatchLink created if matched]
```

Every evidence-shaped payload — whether it arrives as a Bridge webhook, a `POST /v1/evidence/provider` call, an on-chain ingest, or a row in an imported file — is normalized into the same shape (a `RawRecord` plus zero or more `NormalizedLeg`s) and passed through a single `ingestEvidence` pipeline. That pipeline:

1. Checks for an existing `RawRecord` with the same `(organizationId, source, sourceRef)` and short-circuits as a **duplicate** if found. The response sets `duplicate: true` and returns the original `rawRecordId`, `flowLegIds`, and `matchedCaseIds`.
2. Persists the payload as an immutable `RawRecord` and logs an `evidence.received` audit event.
3. Creates one or more `FlowLeg` rows (and any `FlowLegReference`s) from the normalized legs, logging a `leg.normalized` audit event for each.
4. Runs the matching engine against open `ReconciliationCase`s — **unless** the record failed signature validation (`signatureValid === false`), in which case matching is skipped entirely and the evidence sits unmatched for manual review.

All of these paths return (or wrap) the same `EvidenceAcceptanceResponse` shape:

```json theme={null}
{
  "accepted": true,
  "duplicate": false,
  "rawRecordId": "rr_01h...",
  "flowLegIds": ["fl_01h..."],
  "matchedCaseIds": ["case_01h..."],
  "signatureValid": true,
  "source": "webhook",
  "sourceRef": "evt_abc123"
}
```

## Flow 1: Expectation-first

Use this path when the thing arriving first tells ReconLayer **what should happen**.

**Typical sources:**

* A direct `POST /v1/payment-intents` call from your backend.
* A `client_internal_ledger` row in a [file import](/integrations/file-imports).

**Sequence:**

<Steps>
  <Step title="Expectation is registered">
    A `PaymentIntent` is created — directly via the API, or via an imported ledger row. ReconLayer opens a `ReconciliationCase` with `status: open`, logs a `payment_intent.created` audit event, and queues a `payment_intent.created` [outbound webhook event](/integrations/outbound-webhooks) for any subscribed endpoints.
  </Step>

  <Step title="ReconLayer waits">
    No evidence exists yet. The case sits open until a matching `FlowLeg` arrives.
  </Step>

  <Step title="Evidence arrives later">
    A provider webhook, on-chain event, or file import produces a `RawRecord` and one or more `FlowLeg`s via `ingestEvidence`.
  </Step>

  <Step title="Matching runs">
    The matching engine searches for a `PaymentIntent` / `ReconciliationCase` that the new `FlowLeg`(s) satisfy — for example by `externalReference`, `providerTransferId`, or `txHash`. If found, a `MatchLink` is created and the case is updated.
  </Step>
</Steps>

## Flow 2: Evidence-first

Use this path when the thing arriving first tells ReconLayer **what actually happened**, before any expectation was registered.

**Typical sources:**

* A Bridge webhook at `POST /webhooks/bridge/{integrationKey}`.
* A `bank_statement`, `psp_report`, or `onchain_report` row in a [file import](/integrations/file-imports).
* An on-chain integration ingest (`POST /v1/onchain-integrations/{onchainIntegrationId}/ingest`).
* A direct `POST /v1/evidence/provider` or `POST /v1/evidence/onchain` call.

**Sequence:**

<Steps>
  <Step title="Evidence arrives">
    The provider or chain payload is normalized and passed to `ingestEvidence`, creating a `RawRecord` and one or more `FlowLeg`s.
  </Step>

  <Step title="Matching engine searches for an expectation">
    The engine looks for an open `ReconciliationCase` / `PaymentIntent` that this evidence could satisfy.
  </Step>

  <Step title="Matched">
    If a match is found, a `MatchLink` is created linking the `FlowLeg`(s) to the `ReconciliationCase`, and the case is updated.
  </Step>

  <Step title="Unmatched">
    If no match is found, the `FlowLeg`(s) remain in the system as unresolved evidence. They are not discarded — a later expectation (or a corrected import) can still match against them.
  </Step>
</Steps>

## Signature validation short-circuits matching

For evidence that carries a cryptographic signature (currently Bridge webhooks), `ingestEvidence` still creates the `RawRecord` and `FlowLeg`s even if the signature is invalid — but it **skips the matching step** (`matchedCaseIds` will be empty) and marks the `RawRecord` with `validationStatus: 'failed'`. The HTTP response for `POST /webhooks/bridge/{integrationKey}` returns `400 invalid_webhook_signature` in this case, while still reporting the `rawRecordId` and `sourceRef` that were persisted for audit purposes. See [Inbound Webhooks](/integrations/webhooks#signature-verification) for details.

## Idempotency and duplicates

* The canonical evidence endpoints (`POST /v1/evidence/provider`, `POST /v1/evidence/onchain`) require an `Idempotency-Key` header. Replaying the same key with the same body returns the original response with header `Idempotency-Replayed: true`; replaying with a different body returns `409 idempotency_key_conflict`; a concurrent in-flight request with the same key returns `409 idempotency_request_in_progress`.
* At the `ingestEvidence` layer — used by webhooks, polling, and imports too — duplicates are detected independently via the `(organizationId, source, sourceRef)` uniqueness constraint on `RawRecord`, so re-delivered webhooks, re-polled transfers, and re-uploaded files are all safe to retry.
* `POST /v1/import-batches` is itself idempotent on file content: re-uploading the same bytes for the same organization returns the existing `ImportBatch` with `duplicate: true` instead of reprocessing rows.

## Where each entry point lands

| Entry point                                                                    | Source type                                             | Produces                               |
| ------------------------------------------------------------------------------ | ------------------------------------------------------- | -------------------------------------- |
| `POST /v1/payment-intents`                                                     | —                                                       | `PaymentIntent` + `ReconciliationCase` |
| `POST /v1/import-batches` (`client_internal_ledger`)                           | `client_internal_ledger`                                | `PaymentIntent` + `ReconciliationCase` |
| `POST /v1/import-batches` (`bank_statement` / `psp_report` / `onchain_report`) | per profile                                             | `RawRecord` + `FlowLeg`(s)             |
| `POST /webhooks/bridge/{integrationKey}`                                       | `psp_report` (+ `onchain_transfer` leg when applicable) | `RawRecord` + `FlowLeg`(s)             |
| `POST /v1/evidence/provider`                                                   | `psp_report`                                            | `RawRecord` + `FlowLeg`                |
| `POST /v1/evidence/onchain`                                                    | `onchain_report`                                        | `RawRecord` + `FlowLeg`                |
| `POST /v1/bridge-integrations/{id}/poll-transfer` (+ background poll loop)     | `psp_report` (API source)                               | `RawRecord` + `FlowLeg`(s)             |
| `POST /v1/onchain-integrations/{id}/ingest`                                    | `onchain_report`                                        | `RawRecord` + `FlowLeg`                |

Continue to [File Imports](/integrations/file-imports) for the batch path, [Inbound Webhooks](/integrations/webhooks) and [Bridge Integration](/integrations/bridge) for provider-pushed evidence, [On-chain Integrations](/integrations/onchain) for chain indexer evidence, and [Outbound Webhooks](/integrations/outbound-webhooks) for how ReconLayer notifies *you* once an expectation is registered.
