LendEasy/Docs
Get API keys
GuidesBorrowers & CustomersForms

Forms

Define typed customer and loan intake forms with immutable publications, capture entries validated against the active schema, and gate access with per-form derived permissions.

The customer profile answers “who is this party?” A form answers a configured set of program questions such as employment, income, hardship circumstances, or application declarations. Keeping the two separate prevents every product-specific field from becoming permanent customer-schema clutter.

Definition and entry

Record Mutable? Purpose
Definition draft Yes, until published The one working copy of a form’s name and schema.
Publication Never The immutable schema version entries validate against.
Form entry Replaced as a whole Typed answers for one customer or loan target.

A stable formKey (lowercase, ^[a-z][a-z0-9_]{2,63}$) identifies the concept. A definition targets CUSTOMER or LOAN and declares an entryMode: SINGLE allows one current entry per target, MULTIPLE allows many.

Definition lifecycle

Publishing the pending draft creates an immutable publication and makes it the active version. A later publish replaces the active publication; a draft identical to the current publication publishes as a no-op.

Definitions are managed under ADMIN_LMS_FORM_DEFINITION: POST /v1/form-definitions creates a draft, PUT /v1/form-definitions/{formKey}/draft replaces the single mutable draft in place, POST /v1/form-definitions/{formKey}/publish activates it, and POST /v1/form-definitions/{formKey}/retire (with a required reason) stops new entries while keeping existing ones readable. GET /v1/form-definitions/{formKey}/history records CREATED, PUBLISHED, and RETIRED events—draft saves are not events, and each publication event preserves the exact schema that was published.

Creating a definition atomically registers its derived permissions from the immutable key: READ_LMS_FORM_<KEY>, WRITE_LMS_FORM_<KEY>, and DELETE_LMS_FORM_<KEY>—for consumer_income, that is READ_LMS_FORM_CONSUMER_INCOME and siblings. Access is granted per form, not through one blanket forms permission.

Typed schema

{
  "formKey": "consumer_income",
  "targetType": "CUSTOMER",
  "entryMode": "SINGLE",
  "name": "Consumer income",
  "schema": {
    "sections": [{
      "fields": [
        {
          "fieldKey": "employment_status",
          "dataType": "SINGLE_SELECT",
          "required": true,
          "options": [
            { "code": "EMPLOYED", "label": "Employed" },
            { "code": "SELF_EMPLOYED", "label": "Self-employed" },
            { "code": "RETIRED", "label": "Retired" }
          ]
        },
        { "fieldKey": "gross_monthly_income", "dataType": "DECIMAL", "required": true }
      ]
    }]
  }
}

Each field has a fieldKey and a dataType from TEXT, LONG_TEXT, INTEGER, DECIMAL, BOOLEAN, DATE, DATE_TIME, SINGLE_SELECT, or MULTI_SELECT, with options for the select types. Field keys keep their types forever—a publication that changes an existing key’s type is rejected—so historical entries always decode against a consistent registry. Server-side validation remains authoritative even if your UI renders the same definition.

Submit an entry

curl -X POST "$BASE/v1/customers/412/forms/consumer_income/entries" \
  -H "Authorization: Bearer $TOKEN" \
  -H "LendEasy-Tenant: demo-lender" \
  -H "Content-Type: application/json" \
  -H "Idempotency-Key: maya-income-entry-v1" \
  -d '{
    "answers": {
      "employment_status": "EMPLOYED",
      "gross_monthly_income": "6400"
    }
  }'
{
  "entryId": 5501,
  "formKey": "consumer_income",
  "targetId": "412",
  "answers": {
    "employment_status": "EMPLOYED",
    "gross_monthly_income": "6400"
  },
  "createdAt": "2026-08-11T15:40:00Z",
  "lastModifiedAt": "2026-08-11T15:40:00Z"
}

Requires WRITE_LMS_FORM_CONSUMER_INCOME; loan-targeted forms use POST /v1/loans/{loanId}/forms/{formKey}/entries. Unlike other LMS mutations, entry create and update return the rendered entry rather than a command envelope. The complete entry is accepted or rejected atomically—required active fields must be present and option codes must exist. On a SINGLE-mode form, a second create for the same target returns 409 naming the existing entry.

Validation failures

An invalid entry returns the LMS error shape with field-level causes:

{
  "developerMessage": "Request was understood but rejected; inspect errors for field-level causes.",
  "httpStatusCode": "400",
  "defaultUserMessage": "The request could not be applied.",
  "userMessageGlobalisationCode": "validation.msg.validation.errors.exist",
  "errors": [{
    "parameterName": "gross_monthly_income",
    "defaultUserMessage": "A required field is missing.",
    "userMessageGlobalisationCode": "validation.msg.validation.errors.exist"
  }]
}

Clients should branch on userMessageGlobalisationCode and parameterName, not parse the human message.

Read, update, remove

  • GET /v1/customers/{customerId}/forms/{formKey}/entries and the loan twin list current entries with limit/offset paging, sort/order, and createdFrom/createdTo ranges (READ_LMS_FORM_<KEY>).
  • GET /v1/form-entries/{entryId} returns one entry with typed answers for active fields; removed entries return 404.
  • PUT /v1/form-entries/{entryId} replaces the complete answer set: omitted optional fields clear, an identical replacement records no change, and an optional changeReason explains the update. The response is the rendered entry.
  • DELETE /v1/form-entries/{entryId} soft-removes the entry with a required reason (DELETE_LMS_FORM_<KEY>); it is idempotent and history is retained.
  • GET /v1/form-entries/{entryId}/history returns semantic events—created, answers updated with field-level before/after deltas, removed—newest first, so a decision can cite the values actually available when it ran.
Use forms for typed facts and Notes for human context. A sentence in a note cannot satisfy a required form field or silently replace the answers a decision was based on.
Unified search across guides, recipes & the API referenceEsc