Autopay
Autopay initiates payments and never posts them—one enrollment per loan, one idempotently keyed intent per due obligation, and a per-rail return-code policy that decides what each failure costs.
Autopay exists so that a borrower who intends to pay on time does not have to remember to. What it must never become is a second way for money to move. It reads the current due obligation and creates a normal payment intent with source: "AUTOPAY", which then meets the same validation, restrictions, and settlement path as a payment somebody typed by hand. An initiator that wrote to the ledger directly would produce balances no restriction could stop and no return could unwind. Each loan has at most one current enrollment at /v1/loans/{loanId}/autopay.
Never a second enrollment on one loan
Three amount policies, none that drift
What a draw is allowed to take is decided at enrollment, not at initiation:
amountPolicy.type |
Draw amount | Typical use |
|---|---|---|
FULL_DUE |
All currently due components at initiation. | Statement-noticed variable amounts, like Harbor. |
MIN_DUE |
The minimum due for the target installment after posted activity. | Keeping an account current without sweeping extra charges. |
FIXED |
The authorized fixedAmount (with currency), capped by current eligibility. |
Voluntary recurring overpayment or a payment plan. |
FIXED does not silently rise to cover a larger amount due: a borrower who authorized one figure should never find a larger one taken from their account because a late charge landed the week before. FULL_DUE is accepted only when the product’s statement lead leaves room for the ten-day advance notice described below.
Timing is stated, never inferred
timingPolicy says exactly when a draw is initiated relative to each due date:
initiateDaysBeforeDue—how many days ahead the payment intent is created;businessDayConvention—PRECEDING,FOLLOWING, orNONEwhen the computed date lands on a non-business day; andonScheduleChange—FOLLOW_NEW_SCHEDULEto track a rescheduled due date automatically, orREVIEW_REQUIREDto hold draws for human review after a schedule change.
That last choice matters most in hardship. A re-amortized loan whose autopay kept drawing on the old calendar would take the old amount on a schedule the borrower no longer has, days after somebody granted them relief.
Harbor enrolls once and draws each cycle
Harbor Personal Loan
Fixed-rate installment loan
- Contract principal
- $8,400.00
- Nominal rate
- 13.25% fixed
- Term
- 18 monthly payments
- Prepaid origination fee
- $252.00
- Payment
- $517.14
- Disclosed APR
- 17.291%
curl -X POST "$BASE/v1/loans/7204/autopay" \
-H "Authorization: Bearer $TOKEN" \
-H "LendEasy-Tenant: demo-lender" \
-H "Content-Type: application/json" \
-H "Idempotency-Key: harbor-autopay-enrollment-01" \
-d '{
"instrumentId": 704,
"authorizationRef": "evd_JUN_AP_73",
"timingPolicy": { "initiateDaysBeforeDue": 0, "businessDayConvention": "FOLLOWING" },
"amountPolicy": { "type": "FULL_DUE" }
}'
The command receipt ({ "resourceId": 31 }) confirms enrollment; GET /v1/loans/7204/autopay returns its current state:
{
"loanRef": "7204",
"status": "ACTIVE",
"instrument": { "instrumentId": 704, "maskedDisplay": "••6789" },
"timingPolicy": { "initiateDaysBeforeDue": 0, "businessDayConvention": "FOLLOWING", "onScheduleChange": "FOLLOW_NEW_SCHEDULE" },
"amountPolicy": { "type": "FULL_DUE" },
"authorizationRef": "evd_JUN_AP_73",
"consecutiveFailureCount": 0,
"effectiveDate": "2026-08-21"
}
The instrument must be ACTIVE and VERIFIED and belong to the borrower. Each cycle, autopay re-reads the due facts, instrument status, and restrictions, then creates one idempotently keyed payment intent per enrollment and due obligation—scheduler retries cannot create a duplicate draw. A manual payment that already covers the installment suppresses the draw; autopay does not send a $0 request or charge the original schedule amount anyway.
No authorization on file, no draw
authorizationRef is required at enrollment: it references the borrower’s preauthorized-transfer authorization evidence, and nothing is drawn without it. For transfers that vary in amount, Regulation E § 1005.10(d) describes advance notice of the amount and date, generally at least 10 days before the scheduled transfer.
The due-date statement is the notice artifact for FULL_DUE enrollments, which is why enrollment is coupled to the product’s statementLeadDays: a lead shorter than the notice window rejects the enrollment rather than producing draws without compliant notice. Catching that at enrollment turns a recurring compliance failure across a whole product into one rejected configuration change. Cancelling autopay is treated as a Regulation E revocation.
Pause keeps the authorization; cancel revokes it
POST /v1/loans/{loanId}/autopay/pause, …/resume, and …/cancel each require a reasonCode from a fixed list, with an optional comment:
BORROWER_REQUEST · STAFF_REQUEST · PAYMENT_RETURN_ACCOUNT_OWNERSHIP · RESTRICTION_CASCADE · FAILURE_POLICY · LOAN_CLOSED · REVIEW_RESOLVED · OTHER_DOCUMENTED
Pause stops initiation but keeps the enrollment and its authorization. Resume works from PAUSED or REVIEW_REQUIRED and clears the review reason and failure count; missed cycles are not automatically swept into catch-up draws, so a borrower coming back from a two-month pause meets one draw rather than three arriving on the same morning. Cancel works from any state, cancels the enrollment’s pending unsubmitted payment intents, and is the revocation of record. A request that would repeat the current state is a successful no-op, and human calls carry the LendEasy-Case header when case management is enabled. Restrictions cascade too: an AUTOPAY_HOLD or COLLECTIONS_HOLD pauses the enrollment in the same transaction, and deactivating the instrument moves it into review.
Return handling is tenant policy, per rail
How a returned draw is handled is a tenant-level decision made once per rail, not product configuration—an ACH R10 and a card decline are different accusations and deserve different reactions. GET /v1/autopay/failure-policies returns the current policy for each rail, and PUT /v1/autopay/failure-policies/{rail} (rail ACH or CARD) replaces one rail’s policy in full:
{
"automaticRetry": true,
"retryAfterBusinessDays": 2,
"maxRetryAttempts": 2,
"maxConsecutiveFailedCycles": 3,
"runNextScheduledCycle": true,
"exhaustionAction": "REVIEW_REQUIRED",
"returnCodes": [
{ "returnCode": "R01", "retryAllowed": true, "instrumentAction": "KEEP", "autopayAction": "CONTINUE", "caseAction": "NONE", "recommendedAction": "NONE", "treatAsUnauthorized": false },
{ "returnCode": "R10", "retryAllowed": false, "instrumentAction": "INVALIDATE", "autopayAction": "CANCELLED", "caseAction": "OPEN_PAYMENT_FAILURE", "recommendedAction": "CANCEL_AUTOPAY", "treatAsUnauthorized": true }
]
}
Each return-code row decides retry eligibility, the instrument’s fate (KEEP or INVALIDATE), the enrollment reaction (CONTINUE, REVIEW_REQUIRED, or CANCELLED), whether a payment-failure case opens, and whether the code is treated as an unauthorized-debit claim. The base fields govern retry pacing and exhaustion: the enrollment’s consecutiveFailureCount accumulates until maxConsecutiveFailedCycles triggers the exhaustionAction. An unknown return code always fails closed into review and blocks secondary automation—retrying a code nobody has classified yet is how an unauthorized-debit claim becomes a second unauthorized debit.
Six signals worth watching
- draws initiated versus due obligations;
- draws suppressed by prior manual payments;
- statement lead versus the notice window on
FULL_DUEenrollments; - settlement and return rates by rail and instrument;
- consecutive failures and enrollments in
REVIEW_REQUIRED; and - return codes falling through to the fail-closed review path.
An active enrollment that skips an eligible due date, or two intents for the same due obligation, breaks a rule the platform enforces and raises a reconciliation incident.