Payments & repayments
Follow borrower-authorized money from intent through rail settlement, value-dated ledger posting, allocation, receipt, and provider-driven returns.
A payment is both an operational intent and, later, a ledger transaction. Keeping those records separate prevents “accepted by the processor” from being mistaken for settled money. Payments are loan-scoped: every intent lives under /v1/loans/{loanId}/payments.
Payment lifecycle
There is no public cancel route. CANCELLED is a system outcome—for example, cancelling autopay sweeps that enrollment’s pending unsubmitted intents.
Create a payment
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.
Two guards run at creation: the overpayment guard rejects an amount that, together with other in-flight intents, exceeds the loan’s total outstanding (subject to the product’s overpayment tolerance), and a PAYMENT_HOLD restriction blocks creation outright.
Value date and in-flight protection
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.
Allocation on Harbor
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.61The payment service does not calculate this split independently. It asks the authoritative ledger to post and records the resulting transaction reference. That keeps API history, statements, payoff, and accounting on one result. 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": [...] } envelope. There is no separate timeline route—the timeline is part of the payment read.
External payments
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.
Returns
There is no public return route: returns arrive from the provider. 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. 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.
Receipts
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.
Idempotency and concurrency
- The
Idempotency-Keyheader is optional (1–50 characters). Replaying a completed request returns the original result withx-served-from-cache: true. - An in-flight duplicate returns
409withRetry-After. - Payment creation binds the key to a payload fingerprint; reusing a key with a different body fails with
422. - 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.
Reconciliation invariant
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; it is not resubmitted to the rail. A posting with no settlement evidence is a critical drift exception. See Reconciliation & drift.