Skip to Content
Webhooks

Webhooks

Webhooks allow you to receive real-time notifications when events occur in LoanSense. Instead of polling our API, you register an endpoint URL and we POST event data to it as things happen.

Getting Started

To start receiving webhooks, contact us at dev@myloansense.com with:

  • Your endpoint URL — the HTTPS URL where you want to receive webhook POSTs
  • Which event types you want to subscribe to

We’ll provision a subscription and provide you with your webhook signing secret, which you’ll use to verify that incoming requests are genuinely from LoanSense.

Security

Signature Verification

Every webhook request includes an X-Webhook-Signature header containing an HMAC-SHA256 signature of the request body, signed with your webhook secret. You should always verify this signature before processing the payload.

import crypto from "crypto"; function verifyWebhookSignature(rawBody, signature, secret) { const expected = crypto .createHmac("sha256", secret) .update(rawBody) .digest("hex"); const a = Buffer.from(signature, "hex"); const b = Buffer.from(expected, "hex"); if (a.length !== b.length) return false; return crypto.timingSafeEqual(a, b); }

Always use a timing-safe comparison when verifying signatures. A standard === comparison can leak information through response time differences.

Request Headers

Every webhook POST includes these headers:

HeaderDescription
X-Webhook-SignatureHMAC-SHA256 hex digest of the request body
X-Webhook-EventThe event type (e.g. loan.results.batch)
X-Lender-IdYour lender identifier
Content-TypeAlways application/json

Responding to Webhooks

Your endpoint should return a 200 status code to acknowledge receipt. If we receive a non-2xx response, the delivery will be marked as failed.


Event Types

loan.results.batch

Sent when a batch of processed loan results is ready. If a file contains more records than your configured batch size, results are split across multiple webhook deliveries. Use the batch_number and total_batches fields to reassemble the full result set.

In addition to the standard headers, batch webhooks include:

HeaderDescription
X-Batch-NumberCurrent batch number (1-indexed)
X-Total-BatchesTotal number of batches for this file
FieldTypeDescription
eventstring"loan.results.batch"
file_idstringUnique identifier for the processed file
lender_idstringYour lender identifier
batch_numbernumberCurrent batch (1-indexed)
total_batchesnumberTotal batches for this file
total_recordsnumberTotal records across all batches
resultsarrayArray of Loan Result objects
timestampstringISO 8601 timestamp of when the batch was sent

Loan Result Schema

Each object in the results array contains:

FieldTypeDescription
borrower_idstringUnique borrower identifier (as provided by your organization)
loan_idstringLoan number from the original file, used as the grouping key
current_student_loan_paymentnumberAggregated monthly payment across all federal loans (or forbearance estimate if $0)
new_student_loan_paymentnumberLowest available payment option (extended, IBR, forbearance, or current)
monthly_student_loan_payment_savingsnumberDifference between current and new payment
current_dtinumberDebt-to-income ratio before LoanSense
new_dtinumberEstimated debt-to-income ratio after payment reduction
lostringAssigned loan officer name
lo_emailstringAssigned loan officer email (lowercased)
met_criteriastring"yes" if monthly savings ≥ $50, otherwise "no"
has_private_loansstring"yes" if borrower has private student loans, otherwise "no"
loansense_linkstringPersonalized LoanSense link for the borrower
private_loan_linkstring | nullCampus Door refinance link (only if borrower has private loans, otherwise null)
notesstring"No income data" if income information is missing, otherwise empty string

borrower.status.updated

Sent when a borrower’s status changes in the LoanSense workflow. Borrowers progress through a 4-step process from initial signup through DTI recalculation readiness.

FieldTypeDescription
eventstring"borrower.status.updated"
lender_idstringYour lender identifier
borrower_idstringUnique borrower identifier
loan_idstringLoan number associated with the borrower
statusstringNew status value (see Status Values below)
stepnumberStep number in the workflow (1–4)
previous_statusstringPrevious status value
previous_stepnumberPrevious step number
timestampstringISO 8601 timestamp of the status change
metadataobjectAdditional context about the status change

Status Values

Borrowers progress through these statuses in order:

StepStatusDescription
1consumer_signupBorrower has signed up with LoanSense
2appointment_scheduledBorrower has scheduled an appointment with a LoanSense counselor
3paperwork_submittedBorrower’s paperwork has been submitted to their loan servicer
4ready_to_recalculateProcess complete — borrower is ready for DTI recalculation

Statuses always progress forward (1 → 2 → 3 → 4). A borrower will not move backward to a previous step.


Example: Receiving Webhooks

A complete example of a webhook receiver in Node.js:

import crypto from "crypto"; import express from "express"; const app = express(); app.use(express.raw({ type: "application/json" })); const WEBHOOK_SECRET = "{{WEBHOOK_SECRET}}"; app.post("/webhooks/loansense", (req, res) => { const signature = req.headers["x-webhook-signature"]; const event = req.headers["x-webhook-event"]; // Verify signature const expected = crypto .createHmac("sha256", WEBHOOK_SECRET) .update(req.body) .digest("hex"); const a = Buffer.from(signature, "hex"); const b = Buffer.from(expected, "hex"); if (a.length !== b.length || !crypto.timingSafeEqual(a, b)) { return res.status(401).send("Invalid signature"); } const payload = JSON.parse(req.body); switch (event) { case "loan.results.batch": console.log( `Received batch ${payload.batch_number}/${payload.total_batches}` + ` for file ${payload.file_id} (${payload.results.length} records)` ); // Process loan results... break; case "borrower.status.updated": console.log( `Borrower ${payload.borrower_id} moved to` + ` ${payload.status} (step ${payload.step})` ); // Update borrower status in your system... break; } res.status(200).json({ received: true }); });

Use express.raw() (not express.json()) so you have access to the raw request body bytes for signature verification. Parse the JSON yourself after verifying.

Last updated on