FlexPoint Docs
Engineering

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 resultMeaning
<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:

  1. Initial pre-credit sync. Immediately after the Save is accepted, ProcessLeadCreditOrderJob invalidates the lead's freshness (hydrated_at = null) and dispatches a high-priority App\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.
  2. 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 sLastModifiedTimestamp and fires MLM's change webhook, which the portal turns into a second SyncMlmLeadDataJob. That sync's hydrateLiabilities step 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

ConcernClass
HTTP entry (place order on a lead)App\Http\Controllers\LeadCreditOrderController
Async order workerApp\Jobs\ProcessLeadCreditOrderJob (queue lead-create)
Order action (records the outcome)App\Actions\Leads\OrderLeadCreditReport
MLM transport (Save + parse)App\Adapters\CreditBureau\LendingQbCreditAdapter
Vendor port / DTOsApp\Contracts\Vendor\CreditBureau\{CreditBureauProvider, CreditOrderRequest, CreditOrderResult}
Failure typeApp\Exceptions\Vendor\CreditBureauException
Inbound change webhookApp\Http\Controllers\MlmLoanWebhookController
Post-credit deep syncApp\Jobs\SyncMlmLeadDataJobApp\Actions\Leads\SyncMlmLeadData

Observability

Every credit order is a LeadCreditOrder row (PendingCompleted / 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.

On this page