Summary
Payment-plan retrieval is proposed and is not yet available. We are tightening the required-field contract onPOST /customers and POST /customers/batch
so that every customer we board arrives with the data needed to contact them lawfully and verify their
identity. The redundant loanId field is being removed at the same time.
The proposed required-field and loanId changes remain unenforced. We will notify partners
before introducing those changes.
Endpoints affected by the Create a Customer required-field changes:
POST /customersPOST /customers/batchcustomer-create rows (same per-customer payload);scheduleMode: "enroll"rows use origination enrollment and are not affected by the required-field changes
PUT /customers/{id} and loan, call, text, and payment endpoints are not affected by the proposed required-field changes. Customer updates do participate in the validation-status migration described below.
What is changing
id, taskType, streetAddress1, and city are unchanged. streetAddress2, doNotCall,
externalMetadata, and loanIds remain optional.
Why each change
At least one ofemail or phoneNumber. Every customer needs at least one channel we can reach
them on — a record with neither cannot be worked. Today phoneNumber is unconditionally required,
which turns away email-only accounts. Relaxing that while requiring one of the pair is both stricter
in effect and more flexible in practice.
At least one of timezone or zipCode. We may only place calls during hours permitted by the
borrower’s local time, so TCPA compliance depends on knowing where they are. Under the proposed contract, supplying either will be
sufficient: when timezone is omitted we will derive it from zipCode. If the ZIP cannot be resolved,
the proposed behavior is to reject the request with 422. This stricter ZIP behavior is not live.
Today, with sync: true, customer creation defaults to America/New_York when a supplied or fetched ZIP
cannot be resolved; if both timezone and ZIP are absent, it returns 422 for missing timezone.
Replacement schedule sync defaults to America/New_York even when the ZIP is absent.
state always required. State determines which lending and collections rules apply to the
account, and several states are restricted per-portfolio. We need it regardless of how the rest of the
record was populated.
birthday and ssnLastFour required. Both are used to verify a borrower’s identity before
discussing account details on a call. Without them, an agent cannot complete verification and the
conversation cannot proceed.
loanId removed. On POST /customers this field is accepted and then ignored — it has never had
any effect. loanIds is the field that actually controls which loans are imported, so a single loan
ID belongs there as a one-element array.
Migrating your payload
A request that is valid today:birthday and ssnLastFour added, loanId moved into
loanIds:
loanIds is an array of integers, not strings.
After the proposed field changes take effect, email/phoneNumber and timezone/zipCode will be
at-least-one pairs. The following minimal payload is not valid today:
What you can do now vs. what to wait for
Read this section carefully if you plan to migrate early — some of the new fields are not yet wired up. Safe to do now:- Start sending
birthday,ssnLastFour, andstateon every request. All three are already accepted and stored today. Birthday and SSN remain optional; supplied nonempty birthdays are validated, and state is already required unlesssync: true. - Move a single
loanIdvalue intoloanIds.loanIdsis already honored;loanIdis already ignored on this endpoint, so there is no behavior to lose.
- Dropping
phoneNumber(relying onemailalone) or droppingtimezone(relying onzipCodealone) on requests withoutsync: true. These requests return422today. LMS auto-fetch retains its existing enrichment and timezone fallback behavior.
Error behavior
Missing required fields return422 Unprocessable Content with the offending fields named.
For example, today a request missing both first name and phone after any LMS enrichment returns:
422 is also proposed; the current sync: true fallback described
above still applies.
Customer validation errors moved from 400 to 422
Customer creation, updates, replacement schedule sync, servicing cancellation and batch-job
UUID validation now return 422 for invalid input. Enrollment retains its structured detail
object and other validation errors use strings or framework field-error arrays. Invalid birthday,
phone or timezone inputs are rejected before writes, including cases previously reported as 500.
The customer upsert webhook validates phone, birthday and supplied timezone before persistence.
Empty or whitespace-only webhook birthdays now return 422. Endpoint-generated string validation
messages omit raw phone, birthday and timezone values. Framework schema-error arrays can include
submitted values in input; this change does not redact those arrays. These changes affect clients matching status codes or text.
LMS auto-fetch failures return 502; malformed optional provider birthdays are skipped without
clearing an existing birthday. Unexpected internal failures remain 500.
Accepted customer batches retain HTTP 202. Customer-create row validation failures use 422:
message prefixes, known provider failures use 502:, and unexpected internal failures use 500:.
Enrollment rows retain their structured error codes. Do not confuse those prefixes with the batch HTTP status. During rollout,
accept both 400 and 422 for migrated validation and update prefix matching.
Company/workflow configuration and replacement-mode Do Not Call refusals retain 400.
Enrollment Do Not Call conflicts retain 409; authentication and missing-resource statuses
remain unchanged. Do not retry invalid input unchanged; follow each operation’s idempotency
behavior before retrying writes after server failures.
Proposed payment-plan retrieval
Overview
Two proposed read-only endpoints, both authenticated withX-API-Key and scoped to the
company associated with that key:
The intended use is to board a payment plan into your servicing system and reconcile payments collected against it. The sections below describe the proposed integration behavior, not an available endpoint.
Proposed request: current plan
GET /loans/{id}/payment-plan
This endpoint always returns the loan’s current plan. A plan that was replaced by a newer
one is not returned here; fetch its scheduled payments by
planId instead.
Proposed request: scheduled payments of a plan
GET /loans/{id}/payment-plans/{planId}/scheduled-payments
The response is an ScheduledPaymentList. A
404 means the loan does not
exist for your company, or the planId does not belong to that loan.
Reading across two calls
planVersion changes whenever anything in the plan changes: a payment posts, credit is
applied, or the schedule is rebuilt. Both endpoints return it.
If you fetch the plan and its scheduled payments in separate calls, first check that both
responses have the same planId. A different planId means the plan you asked about has
been replaced, and the current-plan endpoint is describing its successor.
With the same planId, the identities below
hold only when both responses also have the same planVersion. If the planVersion values
differ, something changed in between; fetch again, or use the single-call form above.
Which scheduled payments to board
scheduledPayments[].status is the single field that answers this. Board a scheduled payment only when its status is one of:
Every other status means the scheduled payment is not a live obligation and must not be boarded:
OVERDUE and MISSED both describe non-payment, and the difference matters.
With OVERDUE the money is still owed on that scheduled payment, so keep
collecting against it. With MISSED the amount has already been carried onto a
later scheduled payment, so collecting against the MISSED one would count the
same money twice.Reading the money
remainingAmount is a sum over scheduled payments
totals.remainingAmount is the sum of amountRemaining across scheduled payments whose status is PENDING, PARTIALLY_PAID or OVERDUE.
remainingAmount is the balance, already computed from the scheduled payments.creditsApplied is money that paidToDate has already counted. It comes from
overpayments, where the excess on one scheduled payment is applied to another, so
subtracting it from discountedTotal alongside paidToDate would deduct the same
money twice.remainingAmount is summed from the scheduled payments, so it stays correct
however credit arises.amountRemaining is only meaningful on live scheduled payments
Every scheduled payment reports max(0, scheduledAmount - amountPaid - creditsApplied), including ones that are no longer obligations. A MISSED or POSTPONED scheduled payment therefore shows a non-zero amountRemaining even though that amount has moved elsewhere.
Sum amountRemaining only across scheduled payments whose status is PENDING, PARTIALLY_PAID or OVERDUE, or use totals.remainingAmount, which does this for you.
offPlatformPaymentsTotal is a portion of paidToDate
It reports how much of what paidToDate already counts was collected outside Finosu (payments with source: "EXTERNAL").
This is a portion of
paidToDate, not a separate amount to add to it. The
off-platform payments it reports are already counted there, so adding the two
together would count each of them twice.Totals you can verify against the scheduled payments
The proposed contract requires these identities for a response from the current-plan endpoint withincludeScheduledPayments=true, includePayments=true, and includeRetired=true. The first identity also holds across two calls when they report the same planId and planVersion: paidToDate from the current-plan endpoint, and the rows from the scheduled-payments endpoint with includeRetired=true.
Credits
creditsApplied is credit applied to a scheduled payment from the loan’s credit balance.
That balance comes from overpayments. When a payment covers more than the scheduled payment it was applied to, the excess is held as credit and later applied to another scheduled payment.
Credit is reported separately from amountPaid because the money was already counted once, as a payment against the scheduled payment the overpayment landed on. Counting it again would double it.
Dates and timing
scheduledPayments[].dueDateis authoritative for when a scheduled payment is actually due. It carries a UTC offset and reflects any rescheduling.cadencedescribes the rule the plan was created under, not the current scheduled payments. Its anchor days derive from the plan’s original start date, which does not change when a schedule is rebuilt — so after a reschedulecadencecan disagree with the actual due dates. Use it to understand the intended pattern; usedueDatefor real dates.transactedAtandfinalizedAtare Finosu-side timestamps, not bank confirmations.transactedAtis when the payment was submitted or took effect;finalizedAtis when Finosu considers it settled. For ACH, neither is a guarantee of when funds moved at your bank. Apply your own settlement rules.
Identifiers
planIdidentifies the plan and is stable for the life of the plan. It stays valid after the plan is replaced, so you can still fetch that plan’s scheduled payments.planVersionidentifies the plan’s state, not the plan itself. It changes whenever the plan does, so do not store it as an identifier.scheduledPaymentIdandpaymentIdare stable Finosu identifiers. Store them if you want to correlate across polls.referenceon a payment echoes the external reference you supplied when recording it, so you can join back to your own ledger.
When there is no plan
A200 response with paymentPlan: null means the loan exists but has no active plan. This is normal — the borrower may never have enrolled, or the plan may have completed or been cancelled.
A 404 is different: it means no loan with that reference exists for your company. Treat it as an integration error, not a business state.
Polling efficiently
Both endpoints return anETag. Send it back as If-None-Match and an unchanged plan responds 304 Not Modified with no body, which keeps a nightly sweep across a portfolio cheap.
includeScheduledPayments does not match a call with it.
Example
The plan originally had four installments of$206.67, due May 15, May 29, June 12, and June 26, for a total of $826.68. When the May 29 installment was missed, a new replacement installment for the same $206.67 was added for July 10. It was not added to the amount of an existing installment. The response retains the missed row for history, so it shows five rows but only four obligations across the life of the plan.
The response below is from GET /loans/L-88213/payment-plan?includeScheduledPayments=true&includePayments=true. Without the two parameters, the same call returns everything except the scheduledPayments and payments arrays.
A biweekly plan viewed in late June, showing five of the eight possible statuses: PAID (by two payments, the second of which overpaid it), MISSED, OVERDUE, PARTIALLY_PAID (from the resulting credit), and PENDING. SKIPPED, POSTPONED and CANCELLED are not shown here — see the status tables above for all eight.
scheduledAmount values may not sum to discountedTotal. scheduledPayments
includes scheduled payments that are no longer obligations, whose amounts were
moved onto others or forgiven, so they can total more than the plan itself. In
this example the five come to 1033.35 against a discountedTotal of 826.68.
The difference is the historical MISSED row from May 29. Its obligation is
represented by the new July 10 PENDING row, not by an increase to the June rows.discountedTotal is the authorized total, and remainingAmount the
outstanding balance.amountPaid: 300.00 against a scheduledAmount of 206.67. It reports only the outcome (PAID) — that it took two payments, and that the borrower was five days late completing it, is visible only in payments[]. Note the first of those payments has source: "EXTERNAL", which is why offPlatformPaymentsTotal is 100.00: money the lender collected and uploaded, already counted inside paidToDate.
The 93.33 excess (300.00 - 206.67) appears as creditsApplied on the fourth scheduled payment, which is why it is PARTIALLY_PAID with amountRemaining: 113.34 despite amountPaid: 0.00.
The second scheduled payment is MISSED. Its FAILED payment is in payments[]. The fifth row, due July 10 with status PENDING, is its replacement and carries the same $206.67 obligation. The June 12 and June 26 amounts are unchanged. Do not board the historical May 29 row as well, or you would collect the same obligation twice.
Checking the balance: remainingAmount (526.68) is the sum of amountRemaining across the three boardable scheduled payments (206.67 + 113.34 + 206.67), and equals discountedTotal - paidToDate (826.68 - 300.00). It is not discountedTotal - paidToDate - creditsApplied (433.35). That would deduct the 93.33 twice: once as the payment that created it, and again as the credit it became.
Error Responses
Proposed response field reference
These draft fields supplement the worked example above; they are not a published runtime schema.PaymentPlanEnvelope
PaymentPlan
ScheduledPaymentList
Returned byGET /loans/{id}/payment-plans/{planId}/scheduled-payments.
Cadence
PlanTotals
ScheduledPayment
Payment
The final contract and availability will be published in the API reference when released. No integration changes are required now.