Customer lifecycle
One customer, one identity, with every changeable fact hung around it — read as a single record, changed through focused routes, and explained afterwards by a history nothing rewrites.
Everything a lender knows about a person eventually changes: the surname, the address, the phone number, the permission to call it. The one thing that must not change is which person it is. So a customer is a stable numeric customerId with all the mutable facts arranged around it, and every change to one of them lands in a history ledger nothing rewrites. Infer identity from the facts instead and a borrower who moves house and takes a new surname becomes two customers, neither of whom owns the whole loan.
One customer, with everything hung around them
| Part | Owns | Why it is separate |
|---|---|---|
| Profile | Closed INDIVIDUAL or CORPORATE union, optional externalId |
Identifies the party; does not imply contact permission. |
| Addresses | Postal addresses by role (PRIMARY_RESIDENCE, MAILING, …) |
A statement address can differ from a residence or business address. |
| Phones and emails | EMAIL and PHONE values, texting permission, verification |
A stored number is not automatically permitted for texting. |
| Consents | Four regulated consent types with capture time, method, and evidence | Permission is granted and revoked independently. |
| Identifiers | Government IDs, masked on ordinary reads | Complete values are never returned by ordinary reads. |
| Loan parties | Roles on specific loans under /v1/loans/{loanId}/parties |
A customer’s authority belongs to a specific obligation. |
This separation prevents common servicing errors: mailing a notice to the wrong address type, treating a mobile number as SMS permission, or assuming the person who called is authorized on every loan.
There is no delete route for a customer
The profile is a closed union fixed at creation: customerType is INDIVIDUAL (with firstName, lastName, optional middleName, and dateOfBirth) or CORPORATE (with legalName). The optional externalId carries your own correlation key.
Never a customer with no way to reach them
Create the profile and any already-known children in one request, so a serviceable customer cannot exist with no usable address, phone, or email—the state in which a first statement has nowhere to go. Children ride along only at creation; afterwards each collection changes through its own focused route.
curl -X POST "$BASE/v1/customers" \
-H "Authorization: Bearer $TOKEN" \
-H "LendEasy-Tenant: demo-lender" \
-H "Content-Type: application/json" \
-H "Idempotency-Key: onboarding-maya-chen-001" \
-d '{
"profile": {
"customerType": "INDIVIDUAL",
"firstName": "Maya",
"lastName": "Chen",
"dateOfBirth": "1991-11-04",
"externalId": "crm-person-81042"
},
"addresses": [{
"addressType": "PRIMARY_RESIDENCE",
"line1": "428 Alder Way",
"city": "Sacramento",
"stateCode": "US-CA",
"postalCode": "95814",
"country": "US",
"isPrimary": true
}],
"contactPoints": [{
"contactType": "EMAIL",
"value": "maya.chen@example.test",
"isPrimary": true
}]
}'
{ "resourceId": 412 }
Requires CREATE_LMS_CUSTOMER. Mutations return a short command receipt—the resourceId plus, on updates, a changes map of what actually changed. Read the customer back afterwards for current state. The stored email starts UNVERIFIED, because a syntactically valid address proves nothing about who reads it: a notice sent to a mistyped address that happens to parse is a notice nobody received and everybody’s records show as delivered.
One read, and no child GET routes
GET /v1/customers/{customerId} is the only current-state read for child collections. It returns the profile plus current addresses, masked phone numbers and emails, consents, and masked identifiers; soft-removed children are omitted.
{
"customerId": "412",
"status": "ACTIVE",
"profile": {
"customerType": "INDIVIDUAL",
"firstName": "Maya",
"lastName": "Chen",
"dateOfBirth": "1991-11-04",
"externalId": "crm-person-81042"
},
"addresses": [{
"addressId": 311,
"addressType": "PRIMARY_RESIDENCE",
"line1": "428 Alder Way",
"city": "Sacramento",
"stateCode": "US-CA",
"postalCode": "95814",
"country": "US",
"isPrimary": true,
"status": "ACTIVE",
"verificationState": "UNVERIFIED"
}],
"contactPoints": [{
"contactPointId": 512,
"contactType": "EMAIL",
"maskedDisplay": "m•••@example.test",
"isPrimary": true,
"status": "UNVERIFIED"
}],
"consents": [{
"consentId": 71,
"consentType": "AUTODIALED_CALL",
"status": "GRANTED",
"capturedAt": "2026-08-01T17:05:00Z",
"captureMethod": "BORROWER_PORTAL"
}],
"identifiers": [{
"identifierId": 66,
"identifierType": "SSN",
"maskedDisplay": "•••-••-4821",
"verificationState": "UNVERIFIED",
"status": "ACTIVE"
}],
"createdAt": "2026-08-01T17:04:11Z",
"lastModifiedAt": "2026-09-15T14:22:08Z"
}
GET /v1/customers lists masked summaries—customerId, status, customerType, displayName, lastModifiedAt—newest-modified first with limit/offset paging. It has no free-text search parameter—a list route that matched on names would put borrower names into query strings, proxy logs, and browser history; use GET /v1/portfolio/search to find a customer by name, email, or phone.
Every change is recorded, not versioned
The profile changes through PUT /v1/customers/{customerId} (UPDATE_LMS_CUSTOMER_PROFILE): a merge-update where supplied fields change, omitted fields stay, and an explicit null clears a nullable field. customerType is immutable. Each child collection has its own routes:
POST /v1/customers/{customerId}/addresses, thenPUT/DELETE /v1/customers/{customerId}/addresses/{addressId}POST /v1/customers/{customerId}/contact-points, thenPUT/DELETE /v1/customers/{customerId}/contact-points/{contactPointId}POST /v1/customers/{customerId}/identifiers, thenPUT/DELETE /v1/customers/{customerId}/identifiers/{identifierId}POST /v1/customers/{customerId}/consents, thenPOST /v1/customers/{customerId}/consents/{consentId}/revoke
DELETE is a bodyless soft removal: the row disappears from the customer read but its history is retained. There are no version fields or If-Match preconditions anywhere on this surface; every change is recorded with field-level before/after values instead.
One active address per notice type
addressType is one of PRIMARY_RESIDENCE, SECONDARY_RESIDENCE, MAILING, LEGAL_NOTICE, STATEMENT, BILLING, SERVICE, BUSINESS, or EMPLOYER, and is immutable per address. PRIMARY_RESIDENCE, SECONDARY_RESIDENCE, MAILING, and LEGAL_NOTICE allow one active address each, so nothing has to choose between two addresses that both claim to be where legal notice goes. country is ISO 3166-1 alpha-2 and stateCode is the full ISO 3166-2 code matching it (US-CA). Verification, deliverability, and geolocation fields are accepted only from trusted integrations—a caller able to assert deliverability could clear its own undeliverable address and keep mailing into it. Each address also has its own ledger at GET /v1/customers/{customerId}/addresses/{addressId}/history.
Three facts, all checked at execution time
Verification state, canReceiveSms texting permission, and a consent record are different facts. Outbound servicing evaluates all of them at execution time; see Consents & contact details.
A new grant after revocation is a new consent record; it never edits away the revocation. Consents come from a fixed list of four types—ELECTRONIC_COMMUNICATION, AUTODIALED_CALL, ARTIFICIAL_VOICE, UNUSUAL_TIME—each with a captureMethod and optional evidence reference.
Complete values need both permissions
GET /v1/customers/{customerId}/sensitive returns complete identifier values, raw contact values, and consent evidence references. It requires both READ_LMS_CUSTOMER and READ_LMS_CUSTOMER_SENSITIVE; every call is audited and the response is never cached. POST /v1/customers/lookup finds customers by an exact identifier value (SSN, EIN, DRIVER_LICENSE, PASSPORT) under the same two permissions and answers with masked summaries only. See Customer identifiers.
The ledger explains a change, field by field
GET /v1/customers/{customerId}/history (READ_LMS_CUSTOMER_HISTORY) returns the change ledger, which is never rewritten for the customer and its children, newest first, with cursor paging. Filter by categories (PROFILE, ADDRESS, CONTACT_POINT, CONSENT, IDENTIFIER, DOCUMENT, NOTE, FORM_ENTRY, RESTRICTION, LOAN_PARTY), actions, and an occurredFrom/occurredTo window. Without the sensitive read permission, protected values appear as redacted placeholders.
{
"items": [{
"historyId": "h_01JZ7M5K9Y",
"occurredAt": "2026-09-15T14:22:08Z",
"category": "CONTACT_POINT",
"action": "UPDATE",
"subject": { "type": "CONTACT_POINT", "ref": "512" },
"summary": "Contact point updated",
"changes": { "canReceiveSms": { "before": false, "after": true } },
"actor": { "type": "STAFF", "ref": "9", "username": "ana.alvarez" },
"correlationId": "corr_01JZ7M5K9X"
}],
"nextCursor": null
}
GET /v1/customers/{customerId}/history/{historyId} returns one event with its field-level before/after changes; the correlationId links sibling events written by the same command. Documents linked to the customer surface in the same ledger under the DOCUMENT category; see Documents.