LOS Flow
This guide describes the Loan Origination System (LOS) flow end-to-end. It gives you everything you need to build your own origination front end against our public API — the same journey a borrower or sales agent follows to take an application from creation to a decision.
Origination is workflow-driven: the server owns the process and tells your application what to do next. Your job is to start a workflow, render the form it asks for, submit the answers, and advance — repeating until the workflow reaches a final state. You never hard-code the sequence of screens; you follow the workflow.
All REST paths below are relative to your gateway base URL:
https://openapi-dev.alpha.looms.cloud
Key concepts
- Workflow — an origination journey is a server-side workflow you start and then drive step by step. Each step tells you what to render and which transitions are allowed.
- JSON form structures — most steps present a form. The form is described by a JSON structure that you fetch and render dynamically; you collect the user's answers and submit them back. Forms are not fixed in your code, so the platform can change a journey without you shipping a new build.
- Application identity — starting a workflow creates an instance; its id also serves as the application's request id, which threads through every later call.
- Status — an application's status is a two-level value (a main status and a sub-status) resolved against status dictionaries.
Sequence
Step-by-step
1. Authenticate
Obtain an OAuth2 access token and send it as a bearer token on every request:
Authorization: Bearer <access_token>
See the Quick Start for obtaining credentials and a token. Tokens expire — when a request returns 401 Unauthorized, refresh the token and retry the request once. All data is automatically scoped to the organization your credentials belong to.
2. Start an application
List the available onboarding workflows, then start an instance of the one you want.
GET /state-machine/api/state-machines?purpose=ONBOARD
POST /state-machine/api/state-machine-instances/start
Content-Type: application/json
{ "stateMachineId": "<id from the list above>" }
The start response contains an id — this is your instance id. Keep it: it identifies this application for the rest of the flow and is also used as the application's request id.
3. Drive the workflow (the main loop)
Repeat until the workflow reaches a final state.
a. Read the current state for your instance id:
GET /state-machine/api/state-machine-instances/current-state/{instanceId}
The response tells you the current step and whether it is a start/final step, the action to take (render a form, wait for server-side processing, or show a final result), the form id(s) to render, and the request id used to correlate form submissions to this application.
b. If the step renders a form, work with the Forms API. The current state gives you the form id(s); fetch each form's JSON structure and render it dynamically:
GET /form-middleware/v1/form/{formId}
Collect the user's answers and submit them. Carry the application's request id in the x-request-id header so the submission is tied to this application; the submit returns a submission id:
POST /form-middleware/v1/form/{formId}/submission
x-request-id: <requestId>
Content-Type: application/json
{ "data": { /* the user's answers, keyed by the form's field names */ } }
To pre-fill a step the user has already answered, batch-read existing submissions by their form ids:
GET /form-middleware/v1/submission/bulk?_id__in={formId1},{formId2}
x-request-id: <requestId>
c. Advance the workflow to the next step:
POST /state-machine/api/state-machine-instances/{instanceId}/continue
Content-Type: application/json
{ "condition": "SUCCESS" }
d. If the step is processing on the server (no form), wait for it to finish — either via the real-time channel or by re-reading the current state on a short interval — then continue.
Let the user go back one step, or return to a specific earlier form to amend an answer:
POST /state-machine/api/state-machine-instances/step-back/{instanceId}
POST /state-machine/api/state-machine-instances/return-to-prev-state
Content-Type: application/json
{
"requestId": "<instanceId>",
"formId": "<formId>",
"returnType": "USER_DATA",
"context": { "reason": "Need additional data" }
}
4. Detect completion
The current state's action tells you when the journey is over. Final actions are SUCCESS (approved/completed), REJECTED, and ERROR. When you receive one, stop the loop and show the corresponding result screen; the state also indicates whether the user can return to a dashboard.
5. Track applications
GET /relation-store/loan-applications?page={n}&limit={n}&sort=updated_at&sortDir=desc
List an organization's applications (paged and filterable). Filter to top-level applications with application_type=eq:MAIN. Fetch a single application's full detail by its request id:
GET /relation-store/loan-applications/{requestId}
Status model
An application carries numeric status ids rather than status text. Fetch the two reference dictionaries once and cache them, then map an application's sub-status id to its label:
GET /relation-store/application-statuses/main
GET /relation-store/application-statuses/main-sub
Each returns entries of { id, code, label }. The status you display is the application's sub-status id resolved against the sub-status dictionary.
6. Documents and contracts
Documents generated for an application:
POST /dms-v2/nodes/search
Content-Type: application/json
{ "level": 3, "page": 1, "limit": 20, "metadata": { "requestId": "<requestId>" } }
GET /dms-v2/nodes/{nodeId}/download # returns the file (application/octet-stream)
Contracts generated for an application:
GET /contract-manager/contract/bundle/list?applicationId={requestId}&page=1&size=20
GET /contract-manager/file/{fileUrl} # returns the contract file
Each contract carries a status and signingMethod, so you can present only the ones relevant to the user.
Real-time updates with STOMP
The platform pushes workflow state changes over STOMP 1.2 on a WebSocket, so your UI can react the moment the server advances a step (instead of polling). The reference client is @stomp/stompjs.
npm install @stomp/stompjs
import { Client, type IMessage } from "@stomp/stompjs";
// Provided per environment: the notifications WebSocket URL and the environment
// segment used in topic names. `userUuid` is your authenticated user id
// (the `sub` claim of your access token).
const client = new Client({
brokerURL: `${WS_URL}?userUuid=${userUuid}&userId=${userUuid}&transport=websocket`,
connectHeaders: { Authorization: `Bearer ${accessToken}` },
reconnectDelay: 5000,
heartbeatIncoming: 5000,
heartbeatOutgoing: 5000,
onConnect: () => {
// All state changes for the current user:
client.subscribe(`/topic/state_change.${env}.${userUuid}.#`, (message: IMessage) => {
const event = JSON.parse(message.body);
// event.stateMachineInstanceId changed → re-fetch its current state.
// Treat the message as a signal to refresh, not as the source of truth.
});
},
});
client.activate();
// On teardown: client.deactivate();
Topics
Subscribe to the scope you need. {env} is your environment segment, {userUuid} is your user id, and {instanceId} is a workflow instance id:
| Topic | Scope |
|---|---|
/topic/state_change.{env}.{userUuid}.# | Every state change for the user |
/topic/state_change.{env}.{userUuid}.instance.{instanceId} | A single application/instance |
/topic/state_change.{env}.{userUuid}.dashboard | Dashboard-level changes |
The server only delivers events whose {userUuid} segment matches the authenticated user.
Message payloads
A state-change message:
{
"userUuid": "...",
"stateMachineInstanceId": "...",
"stateMachineId": "...",
"currentStateId": "...",
"currentStateName": "...",
"updated": 1700000000000
}
A deletion message:
{ "type": "DELETE", "userUuid": "...", "stateMachineInstanceId": "..." }
When you receive a message, re-fetch the current state for that stateMachineInstanceId (step 3a) and react to the new action.
If you don't want a WebSocket, you can poll GET .../current-state/{instanceId} on a short interval while a step is processing. Polling alone is enough to build a fully working client; STOMP simply makes it real-time.
Request context
Beyond the bearer token:
- Requests are scoped to your organization, derived from your credentials.
- Form submissions must include the request id of the application they belong to, so the platform can correlate the answers to the right application.
Endpoint reference
| Step | Method & path |
|---|---|
| List onboarding workflows | GET /state-machine/api/state-machines?purpose=ONBOARD |
| Start a workflow instance | POST /state-machine/api/state-machine-instances/start |
| Read current state | GET /state-machine/api/state-machine-instances/current-state/{instanceId} |
| Advance the workflow | POST /state-machine/api/state-machine-instances/{instanceId}/continue |
| Step back | POST /state-machine/api/state-machine-instances/step-back/{instanceId} |
| Return to a previous form | POST /state-machine/api/state-machine-instances/return-to-prev-state |
| Fetch a form's JSON structure | GET /form-middleware/v1/form/{formId} |
| Submit form answers | POST /form-middleware/v1/form/{formId}/submission |
| Bulk-read submissions (prefill) | GET /form-middleware/v1/submission/bulk?_id__in=... |
| List applications | GET /relation-store/loan-applications |
| Application detail | GET /relation-store/loan-applications/{requestId} |
| Status dictionaries | GET /relation-store/application-statuses/main, GET /relation-store/application-statuses/main-sub |
| List / download documents | POST /dms-v2/nodes/search, GET /dms-v2/nodes/{nodeId}/download |
| List / download contracts | GET /contract-manager/contract/bundle/list, GET /contract-manager/file/{fileUrl} |
| Real-time updates | STOMP over WebSocket — topic /topic/state_change.{env}.{userUuid}.# |
Scope
This page covers the borrower-facing origination journey only — starting a workflow, completing its forms, tracking status, and retrieving documents and contracts. That is everything required to reproduce the origination experience. Administrative and operational concerns (user provisioning, audit logging, back-office automation) are outside this flow and are not part of the public integration surface.