LendEasy/Docs
Get API keys
GuidesBorrowers & CustomersCustomer lifecycle

Customer lifecycle

Build a durable customer aggregate from a closed profile union plus addresses, contact points, consents, and identifiers—created together, read together, and changed through focused routes with a full history ledger.

A customer is the durable party record behind a consumer loan. The aggregate has a stable numeric customerId; mutable names, addresses, phone numbers, consents, and identifiers change around it without changing that identity, and every change lands in an append-only history ledger.

Aggregate, not a flat contact row

Part Owns Why it is separate
Profile Closed INDIVIDUAL or CORPORATE union, optional externalId Identifies the party; does not imply contact permission.
Addresses Typed postal locations (PRIMARY_RESIDENCE, MAILING, …) A statement address can differ from a residence or business address.
Contact points 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 Normalized government identifiers, 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.

Customer record

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.

Create the aggregate

Create the profile and any already-known children in one request. This avoids a serviceable customer with no usable address or contact point. Children can 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 command envelope—the resourceId plus, on updates, a changes map of what actually changed. Read the aggregate afterwards for canonical state. The stored email starts UNVERIFIED; storing a syntactically valid address does not prove control of it.

Read through the aggregate

GET /v1/customers/{customerId} is the only current-state read for child collections—there are no child GET routes. It returns the profile plus current addresses, masked contact points, 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; use GET /v1/portfolio/search to find a customer by name, email, or phone.

Focused child mutations

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 leaves the aggregate 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.

Address rules

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. 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. Each address also has its own ledger at GET /v1/customers/{customerId}/addresses/{addressId}/history.

Contact points and consents

Verification state, canReceiveSms texting permission, and a consent record are different facts. Outbound servicing evaluates all of them at execution time; see Consents & contact points.

A new grant after revocation is a new consent record; it never edits away the revocation. Consents come from a closed catalog of four types—ELECTRONIC_COMMUNICATION, AUTODIALED_CALL, ARTIFICIAL_VOICE, UNUSUAL_TIME—each with a captureMethod and optional evidence reference.

Sensitive reads and exact lookup

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.

History ledger

GET /v1/customers/{customerId}/history (READ_LMS_CUSTOMER_HISTORY) returns the append-only change ledger 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.

If you bring your own core, it can 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