Loan lifecycle
A loan moves only when a permissioned command says so, and it becomes an obligation only once the rail and the ledger agree that money actually left.
A loan advances only through explicit, permissioned lifecycle commands. It never drifts into a new state because a batch ran overnight or a provider said something encouraging. Approval authorizes a transition; money movement completes it, and only after the rail and the ledger agree—a gap that exists on purpose, because an obligation that starts before the money leaves is one the borrower is paying for and has not received.
SUBMITTED: proposed, not committed
Creating an application resolves the selected product, validates the proposed terms, and computes the schedule. It does not move funds or create an active receivable. The clientId is the numeric value of the LendEasy customerId, and every date-bearing body declares its own dateFormat and locale.
curl -X POST "$BASE/v1/loans" \
-H "Authorization: Bearer $TOKEN" \
-H "LendEasy-Tenant: demo-lender" \
-H "Content-Type: application/json" \
-H "Idempotency-Key: originate-harbor-2026-001" \
-d '{
"clientId": 412,
"productId": 3,
"loanType": "individual",
"principal": 8400.0,
"loanTermFrequency": 18,
"loanTermFrequencyType": 2,
"numberOfRepayments": 18,
"repaymentEvery": 1,
"repaymentFrequencyType": 2,
"interestRatePerPeriod": 13.25,
"interestType": 0,
"interestCalculationPeriodType": 1,
"amortizationType": 1,
"transactionProcessingStrategyCode": "advanced-payment-allocation-strategy",
"expectedDisbursementDate": "2026-08-12",
"submittedOnDate": "2026-08-11",
"externalId": "origination-HBR-2026-001",
"dateFormat": "yyyy-MM-dd",
"locale": "en",
"charges": [{ "chargeId": 12, "amount": 252.0 }]
}'
{ "officeId": 1, "clientId": 412, "loanId": 7204, "resourceId": 7204 }
To preview terms before committing anything, send the same body to POST /v1/loans?command=calculateLoanSchedule. It returns the computed amortization schedule—periods and totals—without persisting a loan.
While submitted, PUT /v1/loans/{loanId} modifies the application with the same field catalog as creation. Approval always evaluates the loan as it stands at decision time, never as it stood when somebody opened it—otherwise an approver would be authorizing terms that had been edited underneath them.
APPROVED: authorized, not funded
Lifecycle decisions are commands on the loan resource:
POST /v1/loans/7204?command=approve
POST /v1/loans/7204?command=reject
POST /v1/loans/7204?command=withdrawnByApplicant
Approve carries { "approvedOnDate": "2026-08-11", "approvedLoanAmount": 8400.0, "expectedDisbursementDate": "2026-08-12", "dateFormat": "yyyy-MM-dd", "locale": "en" }; reject and withdraw carry their own decision dates. Approval also computes and finalizes the APR disclosure automatically—if the disclosure cannot be produced, the approval fails.
An approved loan has no disbursed principal. There is no public disburse command: money moves through the funding API, and a payout in SUBMITTED or PROCESSING does not activate the loan or begin accrual. If funding fails, the loan stays approved and costs the borrower nothing—retrying is then a second payout attempt rather than an unwind of a contract that should never have started.
ACTIVE: funding and ledger agree
The loan becomes active when released funds are posted as a disbursement. The activation date is the posted value date, not the date a user clicked “fund.” From activation:
- the repayment schedule is live;
- interest accrues under the contract’s calculation settings;
- payments and autopay can operate;
- due-date statements are scheduled; and
- servicing actions evaluate against current loan facts.
For Juniper, the checkout down payment is a separate posted transaction. Reversing that transaction does not erase the loan; it triggers the product’s down-payment recovery or cancellation workflow.
One summary beats stitched associations
GET /v1/loans lists loans in the standard list shape—totalFilteredRecords plus pageItems—with externalId, accountNo, clientId, and status filters. The full model, including the authoritative repayment schedule, lives on the loan itself:
GET /v1/loans/7204?associations=repaymentSchedule,transactions
Response dates serialize as [year, month, day] arrays, so Harbor’s first installment appears as "dueDate": [2026, 9, 12] with "principalDue": 424.39 and "interestDue": 92.75. For servicing views—balances, overdue components, payoff projection, APR disclosure, autopay state, restrictions—read GET /v1/loans/7204/summary. Assembling the same picture from raw associations means every caller reimplements the arithmetic, and they will not all reimplement it the same way.
Active does not mean current
Delinquency, hardship, bankruptcy, fraud review, and a servicing restriction are facts or overlays on an active obligation—not replacement lifecycle states. Keeping them separate prevents a restriction from losing the underlying loan state.
| Overlay | Example | What it changes |
|---|---|---|
| Delinquency | 22 days past due | Collections, fees, reporting, and eligibility facts. |
| Restriction | Bankruptcy stay | Which actions and contacts are permitted. |
| Servicing plan | Active promise-to-pay | Outreach suppression and follow-up. |
| Schedule modification | Re-age or re-amortize | Future schedule and related evidence. |
| Accounting classification | Charged off | Receivable accounting and permitted recovery actions. |
Closure follows the posted balance
When all principal, interest, fees, and other due components reach zero—and there is no unresolved credit balance—the loan transitions to CLOSED_OBLIGATIONS_MET. In-flight payments do not close it early: a covering payment settles and posts first, because a loan closed on an unsettled debit reopens a fortnight later as a returned payment against an account the borrower has already been congratulated on paying off.
If the account has a credit balance, LendEasy keeps it open for refund or an approved disposition. If a payment later returns after normal closure, the original posting is reversed and the loan can reopen under the product’s state rules, with a new servicing case if required.
Three terminal outcomes, never aliases for payoff
Write-off, charge-off, and contract termination are governed commands: human calls carry a LendEasy-Case header naming an open case when case management is enabled, every execution writes a governance record, and maker-checker can park the command for a second approver. Their semantics differ:
- Charge-off changes accounting classification and may leave a recoverable balance (
POST /v1/loans/{loanId}/transactions?command=charge-off). - Write-off removes an amount from the receivable under approved accounting policy (
?command=writeoff). - Contract termination ends the contract under a specific contractual or legal basis (
POST /v1/loans/{loanId}?command=contractTermination).
Each action has a dedicated guide in the servicing action catalog. Undo, where supported, is its own command—undowriteoff, undo-charge-off, undoContractTermination—linked to the original; history is never deleted.
A command returns a receipt, not the loan
Every lifecycle command answers with a short receipt rather than the mutated resource:
{ "officeId": 1, "clientId": 412, "loanId": 7204, "resourceId": 7204 }
After a command, read the loan for its current state. An Idempotency-Key is optional on mutations; replaying a completed request returns the original result with x-served-from-cache: true, and an in-flight duplicate returns 409 with Retry-After.
The LendEasy Lending Core’s ledger is authoritative, and the servicing workspace runs natively on it. The same workflows can also run on a core you already operate — with bring your own core, the binding supplies the authoritative state and acknowledges actions, and LendEasy exposes freshness and drift instead of pretending a stale mirror is current.