The two sides of every payment
ReconLayer keeps two things separate and compares them:- What should happen β a
PaymentIntent. A declaration, usually made via your own system, of an expected payment: amounts, currencies, beneficiary, and corridor. - What actually happened β evidence, arriving as
RawRecords from webhooks, file imports, or the evidence API, normalized intoFlowLegs (real movements of money).
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.
Two ways to start: expectation-first vs. evidence-first
ReconLayer supports two valid entry points into the model:- Expectation-first
- Evidence-first
You (the client) know about the payment before ReconLayer sees any evidence of it. Typical
sources:
- A direct call to
POST /v1/payment-intentswhen you initiate or observe a payment on your side. - A
client_internal_ledgerfile import β each valid row creates aPaymentIntent.
PaymentIntent and, in the same transaction, an open
ReconciliationCase with reconciliationStatus: 'unreconciled'. The case then waits for
evidence.Step by step: a cross-border payout
Hereβs a concrete walkthrough combining both entry points, following a singleexternalReference.
Declare the expected payment
Your system calls
ReconLayer creates a
POST /v1/payment-intents:PaymentIntent and, atomically, a ReconciliationCase with
status: 'open', reconciliationStatus: 'unreconciled', and expectedAmount: "1000.00". The
response returns: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.Provider evidence arrives
A payout provider sends a webhook (or you call
POST /v1/evidence/provider)
confirming a transfer. ReconLayer:- Stores the payload as a
RawRecord(source: "webhook",sourceType: "psp_report",provider: "bridge"). - Normalizes it into a
FlowLeg(type: "provider_transfer",phase: "intermediary_in",status: "confirmed",providerTransferId: "tr_bridge_001",amount: "1000.00",currency: "USD"). - The matcher looks for a
PaymentIntentwhosePaymentIntentReferencehastype: "provider_transfer_id"andvalue: "tr_bridge_001"β or whoseexternalReference/FlowLegReference{type: "external_reference"}matches a reference found in the leg or payload. - On a match, it creates a
MatchLink(matchType: "provider_id",confidence: "deterministic") and anAuditEvent(eventType: "match.created"). - The case is re-evaluated:
reconcile()comparesexpectedAmountto the selected legβsamount, computesunexplainedDelta, and updatesreconciliationStatus.
On-chain evidence arrives
A second leg β the actual on-chain transfer β arrives via
POST /v1/evidence/onchain 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.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.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).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 |
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:ReconciliationRulerows 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.- 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.
Where to go next
Core Concepts
Full field-level reference for PaymentIntent, RawRecord, FlowLeg, FlowLegReference, and
ReconciliationCase.
Reconciliation Rules
How tolerances, time windows, SLAs, and match strategies are configured per organization.
Database Architecture
The Prisma schema and table relationships behind this model.
Create or reuse a payment intent
The endpoint that starts the expectation-first flow.
