Skip to main content
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. For matching and tolerance behavior, see Reconciliation Rules. For the underlying tables, see Database Architecture.
Every canonical field below has a corresponding entry in the Canonical Field Registry, which is also the contract import profiles use to map external files into these models.

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, or indirectly from a client_internal_ledger file import. Key fields (from PaymentIntentCreateRequest / PaymentIntentSnapshot):
FieldTypeNotes
externalReferencestringClient’s stable business reference. Unique per (organizationId, externalReference) — this is the idempotency key for the intake endpoint.
sourceAmount / sourceCurrencydecimal string / currency codeWhat leaves the source side. Currency can be a fiat ISO code or a token symbol (USDC).
destinationAmount / destinationCurrencydecimal string / currency codeWhat the beneficiary should receive.
paymentTypestablecoin | bank | cross_border | otherBroad category. Drives reconciliation rule selection.
paymentSubtypestringNarrower rail/corridor label, e.g. wire, swift, usd_mxn, polygon_usdc.
directiondebit | creditFrom the client’s perspective. Defaults to debit.
beneficiaryAccount / beneficiaryNamestringDestination account/wallet identifier and display name.
stablecoin / chainstringToken symbol (USDC) and chain label (polygon) when relevant.
statusPaymentIntentStatusinitiated, submitted, pending, sent, completed, failed, returned, reversed, cancelled, unknown. Set from statusOnClientSide on create.
referencesarray of { type, value }Structured identifiers, e.g. provider_transfer_id, tx_hash, written into PaymentIntentReference rows.
metadataJSONClient context (cost center, batch id, corridor) plus ReconLayer bookkeeping fields like clientCompletedAt, statusOnClientSide, expectedFxRate, developerFeeExpected.
Creating a PaymentIntent always creates exactly one ReconciliationCase in the same transaction (status: 'open', expectedAmount copied from sourceAmount). The create response returns both paymentIntentId and caseId, with status: 'reconciling' and verdict: null — the case hasn’t been evaluated yet because no evidence has arrived.

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.
FieldTypeNotes
sourcefile | api | webhook | manualIngestion channel.
sourceTypeImportSourceType | nullBusiness category: client_transfer_report, client_internal_ledger, bank_statement, onchain_report, psp_report, manual.
provider / integrationKeystring | nullVendor label (bridge, alchemy, icici) and technical connector id (bridge:webhook, icici:daily-xlsx).
sourceRefstringStable source-side identifier — provider transfer id, tx hash, or generated row reference. Unique per (organizationId, source, sourceRef) for idempotent ingestion.
payloadJSONThe untouched source payload. Never edited.
rowNumberint | nullSource row number for file imports.
normalizedType / normalizedIdstring | nullWhat this record was normalized into, and its id.
validationStatuspending | valid | warning | failed | nullSet during import row validation.
signature / signatureValidstring | boolean | nullWebhook 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.
FieldTypeNotes
typeprovider_transfer | onchain_transfer | bank_transferWhat kind of movement this is.
phasesource | intermediary_in | transfer | intermediary_out | destination | nullRole of this leg in a multi-hop route.
statuspending | confirmed | failed | reversed | missingCurrent state of the leg. Defaults to pending.
reconciliationScoperequired | optional | ignoredWhether this leg counts toward “the case is fully evidenced.” Defaults to required.
routeGroupIdstring | nullGroups legs that belong to the same routed path or retry.
sequenceint | nullOrder of this leg within its route group.
providerTransferIdstring | nullFirst-class provider transfer id (commonly queried for matching).
txHash / chainId / fromAddress / toAddress / tokenAddresson-chain identifiersFirst-class for onchain_transfer legs.
provider / integrationKeystring | nullMirrors the normalized source’s vendor identity.
amount / currencydecimal / string | nullThe amount moved on this leg.
occurredAtdatetime | nullWhen the movement happened (block time, bank posting time, provider event time).
metadataJSONExtra 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).

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.
FieldTypeNotes
statusopen | resolved | archivedWorkflow state.
reconciliationStatusunreconciled | tentatively_reconciled | reconciledWhat the matcher/evaluator concluded.
expectedAmount / actualAmountdecimal | nullExpected (copied from the intent’s sourceAmount) vs. observed amount.
providerFee, networkFee, developerFee, fxSpread, roundingDeltadecimal | nullIndividually-tracked components of any delta — never collapsed into one number.
unexplainedDeltadecimal | nullWhat’s left after subtracting expected − actual − all the above fee/FX/rounding components. This is the field operations actually watches; it’s indexed.
exceptionTypeExceptionType | nullCanonical exception category (see below).
notesstring | nullOperator notes.
reconciledAt / lastRunAtdatetime | nullWhen 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:
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

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} both return a computed evidenceCoverage object and a flowLegs array summarizing the intent’s legs:
{
  "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: confirmedpresent, pendingwarning, failed/reversedfailed, missingmissing, 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 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

How ReconLayer Works

Conceptual walkthrough of the expectation → evidence → case lifecycle.

Reconciliation Rules

How tolerances, time windows, SLAs, and match strategies are configured.

Database Architecture

The Prisma schema behind these objects.

Canonical Field Registry

Every mappable field, by model and source type.