Credit Ordering
How the portal orders a credit report through MeridianLink — the synchronous loan.save contract, asynchronous credit delivery via the change webhook, and the two-phase lead sync.
MeridianLink (MLM) has no dedicated credit-pull operation. A credit order is
a Loan.asmx Save carrying a special LOXML <credit> element on the
applicant. The call is synchronous only to the extent that MLM tells you it
accepted the request; the credit report itself is produced asynchronously
and arrives later through the MLM change webhook.
Understanding that split — a fast "request accepted" acknowledgement versus a delayed "data landed" signal — is the key to this subsystem.
The save response never contains the credit report
A successful Save returns a bare
<result status="OK"/>. That means the reissue request was
accepted, not that a report exists. There is no
reportId, no scores, and no liabilities in the response. Do not
treat the absence of credit data in the save response as a failure — it is
expected.
The order contract
MLM's credit-order result is deliberately simple:
Save result | Meaning |
|---|---|
<result status="OK"/> (or OKWithWarning) | The reissue request was accepted. Credit data will follow asynchronously. |
An arbitrary "system error" (non-OK status, or a nested <error>) | The request did not go through. No credit was pulled. |
The transport is App\Adapters\CreditBureau\LendingQbCreditAdapter, which speaks
the App\Contracts\Vendor\CreditBureau\CreditBureauProvider port. It issues the
Save as a raw application/soap+xml POST (so it is Http::fake()-able) and
authenticates with the requesting user's MLM session ticket in the
<sTicket> element — a credit pull is a user-session action and never falls back
to system/OAuth credentials. parse() maps OK/OKWithWarning to a
CreditOrderResult and any other outcome to a CreditBureauException whose
message is safe to surface to the UI.
Ordering flow (synchronous phase)
User (Connect Services → Access Credit Report)
│
▼
LeadCreditOrderController::store
│ creates a Pending LeadCreditOrder placeholder (so the HTTP
│ response carries a real order id) and dispatches:
▼
ProcessLeadCreditOrderJob (queue: lead-create, high priority)
│
▼
OrderLeadCreditReport ──► LendingQbCreditAdapter::order ──► MLM Loan.asmx Save
│ │
│ ◄────────────── <result status="OK"/> ◄─────────────┘
▼
placeholder → Completed (reportId may be null — data comes later)The order is placed on a lead (App\Http\Controllers\LeadCreditOrderController)
via App\Jobs\ProcessLeadCreditOrderJob, which runs on the dedicated
lead-create Horizon queue so an interactive reissue is never stuck behind the
bulk leads re-sync backlog. The controller pre-creates a Pending
LeadCreditOrder placeholder; the job updates it in place with the real outcome
(Completed or Error).
Failure is surfaced, not swallowed
When MLM returns a "system error", OrderLeadCreditReport persists
an Error order and the adapter throws
CreditBureauException. ProcessLeadCreditOrderJob
records the error on the placeholder and then re-throws, so
the job itself fails (visible in Horizon / failed_jobs) rather than
reporting a healthy run for an order that never happened. Because a failed
save pulls nothing, the job is safe to retry.
Credit delivery (asynchronous phase)
Because the report is not in the save response, the portal syncs a lead's MLM file in two phases:
- Initial pre-credit sync. Immediately after the
Saveis accepted,ProcessLeadCreditOrderJobinvalidates the lead's freshness (hydrated_at = null) and dispatches a high-priorityApp\Jobs\SyncMlmLeadDataJob. This runs while MLM is still working the reissue, so it captures whatever consumer data is already present — but not the credit/liabilities, which have not landed yet. - Webhook-triggered post-credit sync. When MLM finishes the reissue, it
writes the credit and liability data into the file. That bumps the file's
sLastModifiedTimestampand fires MLM's change webhook, which the portal turns into a secondSyncMlmLeadDataJob. That sync'shydrateLiabilitiesstep is what pulls the newly-added credit data into the lead.
MLM (async): reissue completes, credit/liabilities written to file
│ new sLastModifiedTimestamp
▼
MlmLoanWebhookController POST /api/v1/loans/mlm/webhook
│ scope → resolve lead → self-write guard → rate cap
▼
SyncMlmLeadDataJob ──► SyncMlmLeadData ──► hydrateLiabilities (credit data)The webhook receiver (App\Http\Controllers\MlmLoanWebhookController) resolves
the lead and dispatches the sync only when the change is genuinely newer than
what the portal already holds. That freshness decision — the
sLastModifiedTimestamp high-water mark that suppresses the echo of the
portal's own Save — is documented in
MLM Change Webhook & Freshness.
The post-credit fill carries a later timestamp than the triggering Save, so it
is recognised as a real external change and synced.
What the lead sync pulls
App\Actions\Leads\SyncMlmLeadData orchestrates the full deep fetch through the
MLM service-account SOAP ticket (Loan.asmx rejects the LQB-format
credit-bureau ticket and the OAuth token). It hydrates, in order: loan-level
fields, the applications graph, borrowers, then the child collections —
liabilities (the credit data), assets, employment, income sources, REO,
conditions, fees, appraisal orders — and finally the origination party. It stamps
hydrated_at on success, which is the freshness signal the lead detail page and
edit-time gate poll.
Components
| Concern | Class |
|---|---|
| HTTP entry (place order on a lead) | App\Http\Controllers\LeadCreditOrderController |
| Async order worker | App\Jobs\ProcessLeadCreditOrderJob (queue lead-create) |
| Order action (records the outcome) | App\Actions\Leads\OrderLeadCreditReport |
MLM transport (Save + parse) | App\Adapters\CreditBureau\LendingQbCreditAdapter |
| Vendor port / DTOs | App\Contracts\Vendor\CreditBureau\{CreditBureauProvider, CreditOrderRequest, CreditOrderResult} |
| Failure type | App\Exceptions\Vendor\CreditBureauException |
| Inbound change webhook | App\Http\Controllers\MlmLoanWebhookController |
| Post-credit deep sync | App\Jobs\SyncMlmLeadDataJob → App\Actions\Leads\SyncMlmLeadData |
Observability
Every credit order is a LeadCreditOrder row (Pending → Completed / Error)
with the vendor's PII-redacted response retained in raw for after-the-fact
inspection. The asynchronous delivery is auditable through the MLM webhook
receipts — see the Observability section of
MLM Change Webhook & Freshness:
each inbound entry records whether it dispatched a sync or was skipped, and which
lead it touched.
MLM Change Webhook & Freshness
How the portal stays current with MeridianLink — the inbound change webhook, the sLastModifiedTimestamp self-write guard, and what a webhook re-syncs.
Desktop Underwriter (AUS)
How the portal runs Desktop Underwriter, reads its findings and errors, and mirrors a loan's past AUS orders from MeridianLink as the source of truth.