LendEasy/DocsLMS + Servicing·v1
Start integrating
Guides›Money & Accounting›Payments & repayments

Payments & repayments

Value-dated posting credits the borrower on the day they paid, and every terminal payment reconciles three ways—provider settlement, payment intent, and ledger transaction.

The most expensive sentence in payments is “the processor accepted it.” Acceptance is a fact about a wire; it is not money. So a payment is an operational intent first and a ledger transaction later, and two guarantees hold across the whole lifecycle: settled money posts value-dated to the day the borrower paid, and every terminal payment is checked across provider settlement, payment intent, and ledger transaction, so the three records can never silently disagree. Payments are loan-scoped: every intent lives under /v1/loans/{loanId}/payments.

There is no public cancel route

SETTLED_PENDING_POST is intentionally visible: money has settled, but the loan balance is not authoritative until ledger posting succeeds. Money received outside LendEasy enters as RECORDED_EXTERNAL.

CANCELLED is a system outcome—for example, cancelling autopay sweeps that enrollment’s pending unsubmitted intents. Once a debit is with the rail there is nothing left to cancel, and a route that pretended otherwise would return success while the money kept moving.

Two guards run at creation

Harbor’s first $517.14 installment is due September 12, 2026. The borrower authorizes an ACH debit one day earlier from a verified, borrower-owned instrument.

curl -X POST "$BASE/v1/loans/7204/payments" \
  -H "Authorization: Bearer $TOKEN" \
  -H "LendEasy-Tenant: demo-lender" \
  -H "Content-Type: application/json" \
  -H "Idempotency-Key: harbor-2026-09-installment" \
  -d '{
    "instrumentId": 704,
    "amount": "517.14",
    "currency": "USD",
    "rail": "ACH",
    "effectiveDate": "2026-09-12",
    "source": "BORROWER"
  }'
{ "paymentId": 9011, "loanRef": "7204", "status": "CREATED", "attemptNumber": 1 }

The 201 response carries a Location header and does not invent a transaction reference. A postedTransactionRef appears only after the debit reaches the settlement gate and posts—a reference minted at creation would be quoted back to a borrower as proof of a payment that has not yet happened, and sometimes never will.

The two guards are the overpayment guard, which rejects an amount that, together with other in-flight intents, exceeds the loan’s total outstanding (subject to the product’s overpayment tolerance), and the PAYMENT_HOLD restriction, which blocks creation outright.

Posting is backdated to the day paid

Suppose the ACH debit settles on September 15. Posting is backdated to effectiveDate: 2026-09-12, the date the borrower paid; a future effective date posts on that business date instead. Until settlement, the loan summary exposes the covering inFlightPaymentAmount.

Date Payment state Ledger balance changed? Delinquency treatment
Sep 11 SUBMITTED No No suppression yet if effective date is future.
Sep 12 PROCESSING No $517.14 is recognized as covering amount in flight.
Sep 15 POSTED Yes, value-dated Sep 12 Schedule is satisfied as of Sep 12.

In-flight protection is provisional, scoped to the covering amount, and visible—not a hidden balance adjustment. If the debit fails or returns, the protection ends and downstream delinquency is recalculated from authoritative posted history.

The ledger performs the split

Harbor’s first scheduled payment allocates $92.75 to interest and $424.39 to principal in this regular-period illustration.

First payment allocation

opening principal       $8,400.00
periodic interest          $92.75
payment                   $517.14
principal allocation      $424.39
closing principal       $7,975.61
Result$7,975.61 principal remaining

The payment service does not calculate this split independently. It asks the authoritative ledger to post and records the resulting transaction reference. A second implementation of allocation would drift from the first within a release or two, and the borrower would then be shown one number in the app and another on their statement. GET /v1/loans/7204/payments/9011 returns the intent with its embedded state timeline:

{
  "paymentId": 9011,
  "loanRef": "7204",
  "status": "POSTED",
  "attemptNumber": 1,
  "amount": "517.14",
  "currency": "USD",
  "rail": "ACH",
  "effectiveDate": "2026-09-12",
  "source": "BORROWER",
  "createdAt": "2026-09-11T17:03:00Z",
  "postedTransactionRef": "30411",
  "timeline": [
    { "occurredAt": "2026-09-11T17:03:00Z", "source": "LMS", "state": "SUBMITTED" },
    { "occurredAt": "2026-09-15T14:22:08Z", "source": "LMS", "state": "POSTED", "postedTransactionRef": "30411" }
  ]
}

GET /v1/loans/7204/payments lists intents with status, source, and rail filters in an { "items": [...] } list. There is no separate timeline route—the timeline is part of the payment read.

LendEasy records it and moves nothing

Use POST /v1/loans/{loanId}/payments/record-external for a lockbox item, wire, branch payment, or payment collected by another authorized system. The request carries the amount, effectiveDate, a unique externalRecordRef, a source of EXTERNAL, IMPORT, or STAFF, and a required rationale; STAFF records also require an evidenceRef. LendEasy moves no money—the intent is created as RECORDED_EXTERNAL and flows into reconciliation, while posting, allocation, and receipts work the same as rail payments.

A retry is a new intent

There is no public return route: returns arrive from the provider as normalized inbound events — see payment rails & processors. A return reverses the posted allocation on the ledger with explicit linkage, and the payment moves to RETURNED carrying the return code, the returned amount, and any scheduled retry.

Return-code policy then decides what happens next—whether a retry is allowed, whether the instrument is invalidated, and how autopay reacts. A retry is always a new intent linked to the original through retryRootPaymentId with an incremented attemptNumber; the returned payment is never resubmitted. Reusing the original would collapse two attempts into one record, and the count of times a borrower’s account was debited is exactly what a dispute turns on. An unfamiliar return code fails closed into review and blocks secondary automation. The per-rail policy and its return-code table are described in autopay failure policies.

A receipt exists only after posting

GET /v1/loans/{loanId}/payments/{paymentId}/receipt streams a receipt PDF as an attachment, and only for a POSTED payment—requesting one for any other status fails validation. The receipt shows the posted amount, value date, rail, and transaction reference. A later return does not alter an issued receipt; the payment timeline shows what happened next.

A key is bound to its payload

  • The Idempotency-Key header is optional (1–50 characters). Replaying a completed request returns the original result with x-served-from-cache: true.
  • An in-flight duplicate returns 409 with Retry-After.
  • Payment creation binds the key to a payload fingerprint; reusing a key with a different body fails with 422. Without that binding, a reused key would answer a second, larger debit with the first payment’s result—and the caller would believe money had moved that never did.
  • Provider events deduplicate by provider event ID and payment intent, and posting uses a ledger-side uniqueness guard, so concurrent callbacks cannot double-post.
  • The overpayment guard is evaluated at creation against current outstanding and in-flight amounts, not against a stale client-side figure.

Three records, checked against each other

Every terminal payment is checked across provider settlement, payment intent, and ledger transaction. A settled debit with no posting raises a SETTLED_NOT_POSTED exception and is never resubmitted to the rail: the money already arrived, and sending it again would take a second installment from a borrower who paid one. A posting with no settlement evidence is a critical drift exception—a loan crediting money nobody can find is the error that grows quietly. See Reconciliation & drift.

Unified search across guides, recipes & the API referenceEsc