LendEasy/DocsLMS + Servicing·v1
Start integrating
GuidesMoney & AccountingPayment rails & processors

Payment rails & processors

The Lending Core names no processor. It has three provider-neutral connections, exactly one way to send money out, and treats every settlement truth as a signed event coming back in.

Search the Lending Core for the name of a payment processor and you will not find one. Money movement is expressed as three provider-neutral connections, and a processor integration is one client bound to those three connections. Moov is the reference implementation for the ACH and card rails; the contract it satisfies is the same contract any other processor satisfies, which is why replacing a processor changes that client and nothing else.

The shape of that contract matters more than the vendor. There is exactly one outbound verb. Everything a processor later has to say arrives inbound as a normalized event. Submission acknowledgement is a transport fact; settlement truth arrives afterwards, separately, and is the only thing allowed to move a balance.

The three ports

The one outbound verb

submit carries only what a processor needs to move money and nothing that would let it infer the rest of the portfolio:

Field Meaning
intentKind PAYMENT or FUNDING.
intentRef The platform’s own identifier for the intent being submitted.
rail ACH, CARD, or EXTERNAL.
direction DEBIT or CREDIT.
amount, currencyCode The submitted figure, in one currency.
effectiveDate The date the money belongs to, which is not the date it settles.
providerTokenRef An opaque instrument token. The platform stores the token and the last four digits; it never holds an account or card number.
idempotencyKey Makes a repeated submission a no-op at the processor.

The acknowledgement returns a providerRef and an acceptedState. Neither posts anything. A 201 from a processor means a request was accepted for processing — it is not money, and the Lending Core never treats it as money.

What arrives inbound

Inbound events are normalized into the platform’s own vocabulary. Four concern money coming in and the instruments that carry it:

Event Effect
payment.settlement.updated Advances the payment’s rail state; may open the posting gate.
payment.returned Carries a return code and moves the intent to RETURNED.
instrument.verification.completed Confirms an instrument by a named method.
instrument.validation.completed Records a validation outcome against a stored token.

Money leaving the lender travels its own path and has its own pair of events, described under money going out.

Intake runs in three transactions

The order is the design, and it is not the obvious one:

  1. Record. The raw event is written to its own committed row before anything about it is trusted. A unique (provider, external_event_id, event_type) with ON CONFLICT DO NOTHING means a redelivered event returns DEDUPLICATED and goes no further.
  2. Verify. The signature is checked against the provider’s verifier. An event that fails verification is already recorded, and is marked failed rather than discarded — a forged or misconfigured event leaves evidence instead of silence.
  3. Normalize. Interpretation happens in a separate transaction, so a poison event marks its own row failed without rolling back the intake commit.

Writing the row first is what makes the unique constraint a usable deduplication anchor at all: an event cannot be recognized as a repeat unless the first copy was safely stored before anyone tried to understand it. Redelivery is the normal behaviour of every payment network, so it is handled by a constraint rather than by careful code.

The posting gate

Rail state, not elapsed time, decides when money is real. Each rail carries its own state column, and exactly one state per rail opens the gate:

Rail Posting state Everything else
ACH FUNDS_AVAILABLE Moves the intent to PROCESSING.
Card COMPLETED Moves the intent to PROCESSING.

Posting fires exactly once per intent. In the same database transaction it records funds_available_at, recalculates delinquency, and re-reads the credit balance — so there is no window in which a payment has posted but the loan does not yet know it. Nothing in the platform asserts how long a rail takes; a settlement duration is a property of the network, not a promise of the product.

Payment intent states

RECORDED_EXTERNAL and CANCELLED are reached outside the rail: money collected elsewhere, and an unsubmitted intent withdrawn by the system. CHARGED_BACK is declared in the state vocabulary for card disputes and is reserved for the rail that reports them.

Alongside status, each intent records how it came to exist. capture_mode is one of PROVIDER_INITIATED, AUTOPAY_GENERATED, EXTERNAL_RECORD, or IMPORT; source is one of BORROWER, STAFF, AUTOPAY, SERVICING, EXTERNAL, or IMPORT. The pair answers two different audit questions — which mechanism moved the money, and on whose authority — that a single field would blur.

Returns and chargebacks

A return is an inbound fact, not an operator action, and there is no public return route. When payment.returned arrives, one operation applies the whole consequence: the intent moves to RETURNED, the return code is stored, the ACH rail state is set to RETURNED, the original ledger transaction is reversed, and autopay takes the action its policy binds to that specific code.

An unrecognized return code fails closed. It does not default to “retry” and it is never ignored: it produces a recorded disposition that blocks secondary automation until a human resolves it. Retries are always new intents linked back to the original; a returned payment is never resubmitted. See Payments & repayments for the retry chain and Autopay for the per-rail return-code table.

Money going out

Disbursements, payouts, and credit-balance refunds are not payments in reverse. They live in their own intent table with their own normalizer and their own two events — funding.settlement.updated advances a payout, funding.failed ends one with its provider reason — and a refund begins at APPROVAL_REQUIRED with a second approver who is not the requester. The asymmetry is deliberate: pulling money the borrower authorized and pushing money out of the lender’s account carry different risks and deserve different controls. See Funding & disbursement.

Instrument confirmation

Instruments are confirmed by a named method rather than a boolean. The platform recognizes exactly MICRO_DEPOSIT, AGGREGATOR_AUTH, PRENOTE, EXISTENCE_CHECK, MIGRATED, and STAFF_ASSERTED; which one applies is the processor’s answer, not the platform’s assumption — a bank account proven through an aggregator reports AGGREGATOR_AUTH, a card checked for existence reports EXISTENCE_CHECK.

The distinction is load-bearing rather than decorative. VERIFIED means ownership was proven, and an existence-only check must never set it — so the method travels with the instrument and a later dispute is argued from what was actually checked instead of from a boolean that has forgotten. Instruments enter through POST /v1/customers/{customerId}/payment-instruments.

Idempotency, twice

Duplicate protection is applied independently on both sides of the boundary, because the two sides fail differently:

  • Outbound. Idempotency-Key is bound to a payload fingerprint at payment creation, so a replay returns the original result rather than a second debit. The full contract — replay, key reuse, and in-flight collision — is in Payments & repayments.
  • Inbound. The (provider, external_event_id, event_type) constraint absorbs redelivery. Each normalized answer commits on its own, so one failed event cannot un-settle its neighbours.

Every event names where it came from

The provider-event record carries the provider code that produced it, so the origin of a settlement is a stored fact rather than an inference from timing or reference format. A processor’s own reference — whatever shape that processor happens to use — is stored beside it, unparsed. The platform never tries to read meaning out of a vendor’s identifier string.

This matters most where two rails coexist. A portfolio migrated from one processor to another keeps settlements attributable to the rail that actually reported them, years later, without anyone having to remember which reference format belonged to whom.

Configuration

Rail configuration lives under lendeasy.providers.moov:

Property Purpose
mode Selects which client is bound to the ports.
secret Shared secret for event signature verification.

Nothing in the platform settles a payment on its own initiative. Settlement is always something a processor reports, and the pipeline that receives that report is the same pipeline in every environment — lower environments exercise the production path rather than a shortcut around it. Mode selection is fail-closed. A mode with no client bound to it throws at configuration time with an instruction naming what is missing, rather than starting and silently moving nothing. A deployment cannot believe it is connected when it is not — the failure happens at startup, in front of an engineer, instead of at the first payment, in front of a borrower.

What a processor integration supplies

Because the ports are neutral, onboarding a processor is a bounded list:

  1. A client answering as its own provider code against ProviderMoneyMovementPort.
  2. A mapping from the platform’s instrument token to the processor’s stored-instrument concept.
  3. Signature verification for that processor’s event format.
  4. A translation from the processor’s own event and status vocabulary into the six normalized events and the rail states that open the posting gate.

Everything downstream — the state machine, the posting gate, deduplication, the ledger, delinquency recalculation, reconciliation, receipts, every table and every route — is shared and unchanged.

Settlement timing, return-code behaviour, and instrument-verification methods are properties of the payment network and of your agreement with your processor. Configure return-code policy and approval thresholds with your own risk and compliance owners.
Unified search across guides, recipes & the API referenceEsc