Submit a Lock Request
This page covers the lock-without-LOS workflow from getting pricing through submitting the lock request: search for rates, create a price quote, set the scenario from the selected result, create the lock, run pre-lock validations, and submit the lock request.
Authentication
Obtain an access token before any other call.
Endpoint: POST {{protocol}}{{domainAuth}}/oauth/token
Headers:
| Header | Value |
|---|---|
| Authorization | Basic cHBlLWxlbmRlcnByaWNlOmxwc2VjcmV0 |
| Content-Type | multipart/form-data |
Body (form data):
username: {{username}}
password: {{password}}
grant_type: password
client_id: ppe-lenderprice
Response:
{
"access_token": "......",
"token_type": "bearer",
"refresh_token": ".....",
....
}
Use access_token as Authorization: Bearer {{accessToken}} on all subsequent calls. See the Authentication guide for more detail.
Step 1: Get Pricing
Search for available rates using your loan criteria.
Endpoint: POST /rest/v1/lp-ppe-api/pricing/search?userIdToUse={{userId}}
Headers:
| Header | Value |
|---|---|
| Authorization | Bearer {{accessToken}} |
Body:
The pricing search accepts over 200 fields to fine-tune results. Key fields include:
{
"accessCriteria": {
"companyIds": [],
"marketPlaceSearch": false,
"userContext": {
"groups": []
}
},
"brokerCriteria": {
"adjustments": [],
"rateTypes": [],
"sortView": "LenderPaid",
"leadSourceType": "Retail"
},
"criteria": {
"purchasePrice": 500000,
"loanAmount": 400000,
"computeApr": false,
"mortgageTypes": [],
"pmiType": "Monthly",
"loanPurpose": "Purchase",
"loanType": "Fixed"
},
"property": {
"state": "CA",
"county": "Los Angeles",
"propertyType": "SingleFamily",
"occupancyType": "PrimaryResidence",
"numberOfUnit": 1
},
"search": {
"current": true
}
}
userIdToUsein the URL is whoever is performing the action;userIdToImpersonateis whoever the pricing should come from (they can be different users).brokerCriteria.adjustmentswill not accept new adjustments directly from the search payload. If an adjustment needs to be made, it has to come from the rate grid.- For the initial search, set
search.current = truein the payload.
Response:
The response contains resultMap.resultRates.data with an array of lineResult objects. Save the lineResult you want to lock -- you will use it in subsequent steps.
Pricing When a Lock Already Exists
Once a lock exists, price through the price quote instead, so all added concessions, extensions, and relock fees are correctly applied:
Endpoint: POST /rest/v1/lp-ppe-api/price-quote/{{priceQuoteId}}/search?userIdToUse={{userId}}&lockStatusId={{nextLockStatusId}}
nextLockStatusId is the status of the action the user wants to perform next (for a relock worst case, the relock worst-case status id; for a reprice, the reprice status id). Get it from Get Next Lock Actions.
Step 2: Create a Price Quote
Create a new Price Quote with the given search data. This is the container for the entire lock lifecycle.
Endpoint: POST /rest/v1/lp-ppe-api/price-quote/create
Headers:
| Header | Value |
|---|---|
| Authorization | Bearer {{accessToken}} |
Body:
{
"search": "{{search}}",
"name": "Test1",
"borrowers": [
{
"printPositionType": "Borrower",
"firstname": "Jane",
"lastname": "Doe",
"borrowerId": "BorId1"
}
]
}
search is the full search object used in Step 1.
Response (partial):
{
"id": "65a1b2c3d4e5f6a7b8c9d001",
"companyId": "65a1b2c3d4e5f6a7b8c9d010",
"userId": "65a1b2c3d4e5f6a7b8c9d011",
"name": "Test1",
"integrationRef": {
"DUMMY": "65a1b2c3d4e5f6a7b8c9d001"
},
"loanApplication": {
"mismoVersionId": "2.3.1",
"borrower": [ ... ]
}
}
The response id is the priceQuoteId -- save it, it is used in every subsequent step. The loanApplication object follows the ten03Application MISMO format.
Save integrationRef.DUMMY as well -- it is required to update the same loan later. If your company wants to use its own integration name (and put its own loan unique identifier in this field), that requires a code change on the Lender Price EXT side.
Updating the Price Quote
Updates use the same create endpoint. Include the integrationRef from the create response so the call updates the existing loan instead of creating a new one:
{
"search": "{{modified_search}}",
"name": "Test1",
"integrationRef": {
"DUMMY": "65a1b2c3d4e5f6a7b8c9d001"
},
"borrowers": [
{
"printPositionType": "Borrower",
"firstname": "JaneUpdated",
"lastname": "DoeUpdated",
"borrowerId": "BorId1"
},
{
"printPositionType": "CoBorrower",
"firstname": "Jane2Updated",
"lastname": "Doe2Updated",
"borrowerId": "BorId2"
}
]
}
Step 3: Get Price Quote
Retrieve the Price Quote. Use this whenever you need the current lock state -- in particular the latest lockEventId.
Endpoint: POST /rest/v1/lp-ppe-api/los/price-quote/get/{{priceQuoteId}}
Headers:
| Header | Value |
|---|---|
| Authorization | Bearer {{accessToken}} |
Response (partial):
{
"id": "65a1b2c3d4e5f6a7b8c9d001",
"name": "Test1",
"integrationRef": { "DUMMY": "65a1b2c3d4e5f6a7b8c9d001" },
"currentLock": {
"id": "65a1b2c3d4e5f6a7b8c9d002",
"priceQuoteId": "65a1b2c3d4e5f6a7b8c9d001",
"lockExpirationDateMarker": "2025-03-20T19:05:21.710Z",
"lastStatus": { ... },
"lockEvents": [
{
"id": "65a1b2c3d4e5f6a7b8c9d003",
"scenario": { ... },
"status": { ... },
"lockId": "65a1b2c3d4e5f6a7b8c9d002"
}
]
}
}
currentLock.lockEvents always holds the list of events -- always take the last one for the current lockEventId and scenario.
Step 4: Set Scenario From Result
Set the scenario from the pricing result the user selected. This must be done each time pricing is selected by the user while the loan is UNLOCKED.
Endpoint: POST /rest/v1/lp-ppe-api/price-quote/setScenarioFromResult/EllieMaeQuoteRequest
Headers:
| Header | Value |
|---|---|
| Authorization | Bearer {{accessToken}} |
Body:
{
"priceQuoteId": "{{priceQuoteId}}",
"search": "{{search}}",
"lineResult": "{{lineResult}}"
}
priceQuoteId-- theidfrom Create a Price Quote.search-- the search that was sent in Get Pricing.lineResult-- the pricing line the user selected from the Get Pricing results.
Even though the URL mentions EllieMae, this call does not involve EllieMae. Once the loan is locked, use the EllieMaeBuyRequest variant instead (see Post-Lock Operations).
Step 5: Lock With Scenario
Create the Lock. This is done only the first time after the first Set Scenario From Result. It creates the Lock and the very first event, Priced.
Condition: the user must be assigned to a Lock Policy within a user group workflow.
Endpoint: POST /rest/v1/lp-ppe-api/price-quote/lockWithScenario
Headers:
| Header | Value |
|---|---|
| Authorization | Bearer {{accessToken}} |
Body:
{
"priceQuoteId": "{{priceQuoteId}}",
"search": "{{search}}",
"lineResult": "{{lineResult}}",
"exportAndLock": false
}
Response (partial):
{
"currentLock": {
"id": "65a1b2c3d4e5f6a7b8c9d002",
"priceQuoteId": "65a1b2c3d4e5f6a7b8c9d001",
"lockEvents": [
{ "id": "65a1b2c3d4e5f6a7b8c9d003", "scenario": { ... }, "status": { ... } }
]
}
}
From this response, save:
lockId=currentLock.idlockEventId=currentLock.lockEvents[currentLock.lockEvents.length - 1].id
These two values, along with priceQuoteId, do not change for the life of the loan (lockEventId advances with each event -- always re-read the last event from Get Price Quote).
Step 6: Get Next Lock Actions
Get the available next lock status actions, as defined by the lock policy.
Endpoint: GET /rest/v1/lp-ppe-api/price-quote/getNextLockStatus/{{lockId}}/{{lockEventId}}
Headers:
| Header | Value |
|---|---|
| Authorization | Bearer {{accessToken}} |
Response:
Returns the list of statuses the user can move to. Identify the one you need by its logic fields, never by name (see Identifying Lock Statuses):
[
{
"id": "65a1b2c3d4e5f6a7b8c9d030",
"root": true,
"primeState": false,
"autoStatus": false,
"autoStatusLockStatusId": null,
"code": "Priced",
"buttonName": "Price",
"lockStatusLogic": "Other",
"requireReprice": false,
"worseCase": false,
...
},
{
"id": "65a1b2c3d4e5f6a7b8c9d031",
"root": false,
"code": "Lock Requested",
"buttonName": "Request Lock",
"lockStatusLogic": "Requested",
"requireReprice": true,
...
}
]
For a lock request, use the status with lockStatusLogic == "Requested" as your lockRequestId.
Step 7: Pre-Lock Validation Checks
Before submitting the lock request, run these validation checks.
Check Lock Desk Hours
Optional -- Returns whether the user is within business hours according to the lock policy.
Loan providers have operating hours where lock requests can be entered. These operating hours are referred to as desk hours, and they differ based on the loan provider. Before submitting a lock request, first confirm that the request is being made during the loan provider's desk hours.
Endpoint: GET /rest/v1/lp-ppe-api/checkLockStatus/{{lockId}}/{{lockRequestId}}
Headers:
| Header | Value |
|---|---|
| Authorization | Bearer {{accessToken}} |
Response:
{
"isInLockDeskHours": true,
"isPostLockRequest": false
}
Check Rate Period Status
Checks whether the previously selected rate is still active (has not expired).
Endpoint: GET /rest/v1/lp-ppe-api/isRatePeriodStillActive/{{priceQuoteId}}/{{lockRequestId}}
Headers:
| Header | Value |
|---|---|
| Authorization | Bearer {{accessToken}} |
Response:
true
Returns a boolean (true/false) indicating if the rate period is still active.
Step 8: Submit Lock Request (No SellSide)
Submit the lock request event.
Endpoint: POST /rest/v1/lp-ppe-api/price-quote/addLockEvent
Headers:
| Header | Value |
|---|---|
| Authorization | Bearer {{accessToken}} |
Body:
{
"priceQuoteId": "{{priceQuoteId}}",
"lockId": "{{lockId}}",
"lockStatusId": "{{lockRequestId}}",
"lockEventExceptionInfo": null,
"lockEventExtensionInfo": null,
"scenario": "{{scenario}}",
"hasSellSide": false,
"lockDateUpdate": null,
"lockEventId": "{{lockEventId}}",
"lockEventComment": "Whatever comment you would like"
}
lockStatusId-- the status withlockStatusLogic == "Requested"from Get Next Lock Actions.scenario--currentLock.lockEvents[last].scenariofrom Lock With Scenario or Get Price Quote.lockEventId--currentLock.lockEvents[last].idfrom the same response.
Response:
{
"isAutoLock": true,
"queueItemId": "65a1b2c3d4e5f6a7b8c9d020"
}
Every addLockEvent returns a queueItemId, except pricing (root) events, which do not use the queue. Use the queue item to track when the action has been processed -- see Check Queue Item.
Step 9: Submit Lock Request (With SellSide)
Submit the lock event with sell-side line information. This is used when selling to the secondary market. The payload is the same as Step 8 with hasSellSide: true and a sellSideLine object.
Endpoint: POST /rest/v1/lp-ppe-api/price-quote/addLockEvent
Headers:
| Header | Value |
|---|---|
| Authorization | Bearer {{accessToken}} |
Body:
{
"priceQuoteId": "{{priceQuoteId}}",
"lockId": "{{lockId}}",
"lockStatusId": "{{lockRequestId}}",
"lockEventExceptionInfo": null,
"lockEventExtensionInfo": null,
"scenario": "{{scenario}}",
"hasSellSide": true,
"sellSideLine": {
"id": "65a1b2c3d4e5f6a7b8c9d050",
"mergeSet": null,
"createdBy": null,
"creationDate": "2023-05-18T18:39:17Z",
"modifiedBy": null,
"modifiedDate": "2023-05-18T18:39:17Z",
"origin": "Lenderprice",
"order": 0,
"name": null,
"comment": null,
"search": "{{search}}",
"pricedOnMarketPlace": false,
"searchPoint": {
"id": "65a1b2c3d4e5f6a7b8c9d051",
"mergeSet": null,
"createdBy": null,
"creationDate": "2023-05-18T18:39:17Z",
"modifiedBy": null,
"modifiedDate": "2023-05-18T18:39:17Z",
"referenceId": null,
"date": null,
"lineResult": "{{lineResult}}"
},
"currentSearchPoint": null,
"searchPointTarget": {
"id": "65a1b2c3d4e5f6a7b8c9d052",
"mergeSet": null,
"createdBy": null,
"creationDate": "2023-05-18T18:39:17Z",
"modifiedBy": null,
"modifiedDate": "2023-05-18T18:39:17Z",
"searchOrigin": "All",
"sameLender": true,
"lenderName": null,
"lenderId": null,
"sameProgram": true,
"ratePeriodName": null,
"ratePeriodId": null,
"rateProgramName": null,
"rateProgramId": null,
"rateGridName": null,
"rateGridId": null,
"rateRange": {
"@class": "com.cre8techlabs.entity.rate.adjustment.table.RangeGreaterEqThanLowerEqThan",
"from": null,
"to": null
},
"pointsRange": {
"@class": "com.cre8techlabs.entity.rate.adjustment.table.RangeGreaterEqThanLowerEqThan",
"from": null,
"to": null
},
"compensationRange": {
"@class": "com.cre8techlabs.entity.rate.adjustment.table.RangeGreaterEqThanLowerEqThan",
"from": null,
"to": null
},
"closingCostRange": {
"@class": "com.cre8techlabs.entity.rate.adjustment.table.RangeGreaterEqThanLowerEqThan",
"from": null,
"to": null
},
"useAlert": false,
"alertType": "Range",
"rateAlertRangeConfig": {
"id": "65a1b2c3d4e5f6a7b8c9d053",
"mergeSet": null,
"durationInDays": 90,
"cutOffDate": null,
"lastTimeSent": null,
"alertSentCount": 0
},
"rateAlertSubscriptionConfig": {
"id": "65a1b2c3d4e5f6a7b8c9d054",
"mergeSet": null,
"durationInDays": 90,
"cutOffDate": null,
"lastTimeSent": null,
"alertSentCount": 0,
"frequency": "EveryDay"
},
"emails": "",
"lastUpdate": null,
"rateAlert": null
}
},
"lockDateUpdate": null,
"lockEventId": "{{lockEventId}}",
"lockEventComment": "Whatever comment you would like"
}
Response: Same as Step 8 -- { "isAutoLock": ..., "queueItemId": ... }.
Once the lock request is submitted, continue with Approve or Reject a Lock.