LendEasy/DocsLMS + Servicing·v1
Start integrating
Guides›Borrowers & Customers›Customer lifecycle

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

There is no archive or delete route for a customer. The record is permanent; child records are added and soft-removed around it, and the history ledger explains every change.

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, then PUT/DELETE /v1/customers/{customerId}/addresses/{addressId}
  • POST /v1/customers/{customerId}/contact-points, then PUT/DELETE /v1/customers/{customerId}/contact-points/{contactPointId}
  • POST /v1/customers/{customerId}/identifiers, then PUT/DELETE /v1/customers/{customerId}/identifiers/{identifierId}
  • POST /v1/customers/{customerId}/consents, then POST /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.

The LendEasy Lending Core is authoritative for the party record end to end. Where a lender keeps an existing core, that core can instead remain authoritative for the party identifier while LendEasy owns servicing-specific contact, consent, case, and interaction facts — the identity map makes that split explicit; see Bring your own system of record.
Unified search across guides, recipes & the API referenceEsc