> ## Documentation Index
> Fetch the complete documentation index at: https://docs.fingerprint.com/llms.txt
> Use this file to discover all available pages before exploring further.

# Loan Fraud

> Learn how to detect and prevent fraudulent loan applications

## Overview

This tutorial shows how to implement Fingerprint to prevent loan application fraud, where applicants try to game the system by submitting multiple applications using slightly altered personal details, fake identities, or devices that show signs of automation or tampering.

You'll begin with a starter app that includes a mock loan application page and a basic review flow. From there, you'll add the JavaScript agent to identify each visitor and use server-side logic with Fingerprint data to check if they've already submitted an application. Additionally, you'll see how to use Smart Signals to block or flag suspicious devices submitting applications. This way, you can flag or block repeat submissions from the same device or suspicious sources.

By the end, you'll have a sample app that detects duplicate or high-risk loan applications per visitor and can be customized to fit your use case and business rules.

This tutorial uses just plain JavaScript and a Node server with SQLite on the backend. For language- or framework-specific setups, see the quickstarts.

> Estimated time: \< 15 minutes

<iframe className="w-full aspect-video rounded-md" src="https://www.youtube.com/embed/vXqin7YBrUc" title="YouTube video player" frameborder="0" allow="accelerometer; autoplay; clipboard-write; encrypted-media; gyroscope; picture-in-picture; web-share" referrerpolicy="strict-origin-when-cross-origin" allowfullscreen />

## Prerequisites

Before you begin, make sure you have the following:

* A copy of the [starter repository](https://github.com/fingerprintjs/use-case-tutorials) (clone with Git or download as a ZIP)
* [Node.js](https://nodejs.org/) (v20 or later) and npm installed
* Your favorite code editor
* Basic knowledge of JavaScript

## 1. Create a Fingerprint account and get your API keys

1. [Sign up](https://dashboard.fingerprint.com/signup) for a free Fingerprint trial, or log in if you already have an account.
2. After signing in, go to the [**API keys**](https://dashboard.fingerprint.com/api-keys) page in the dashboard.
3. Save your **public API key**, which you'll use to initialize the JavaScript agent.
4. Create and securely store a **secret API key** for your server. Never expose it on the client side. You'll use this key on the backend to retrieve full visitor information through the Fingerprint Server API.

## 2. Set up your project

1. Clone or download the [starter repository](https://github.com/fingerprintjs/use-case-tutorials) and open it in your editor.

```bash Terminal theme={"theme":"github-dark-dimmed"}
git clone https://github.com/fingerprintjs/use-case-tutorials.git
```

2. This tutorial will be using the `loan-risk` folder. The project is organized as follows:

<Tree>
  <Tree.Folder name="public" defaultOpen>
    <Tree.File name="index.html - Loan application form" />

    <Tree.File name="index.js - Front-end logic to handle loan submissions" />
  </Tree.Folder>

  <Tree.Folder name="server" defaultOpen>
    <Tree.File name="db.js - SQLite database connection" />

    <Tree.File name="loans.js - Loan and risk detection logic" />

    <Tree.File name="server.js - Serves static files and loan endpoint" />
  </Tree.Folder>

  <Tree.File name=".env.example - Example environment variables" />
</Tree>

3. Install dependencies:

```bash Terminal theme={"theme":"github-dark-dimmed"}
npm install
```

4. Copy or rename `.env.example` to `.env`, then add your Fingerprint API keys:

```bash Terminal theme={"theme":"github-dark-dimmed"}
FP_PUBLIC_API_KEY=your-public-key
FP_SECRET_API_KEY=your-secret-key
```

5. Start the server:

```bash Terminal theme={"theme":"github-dark-dimmed"}
npm run dev
```

6. Visit [http://localhost:3000](http://localhost:3000) to view the mock loan application page from the starter app. You can test out the basic coupon flow by changing the inputs and clicking **Request loan**. You'll notice you can apply for loans with new personal details an unlimited amount of times.

## 3. Add Fingerprint to the frontend

In this step, you'll load the JavaScript agent when the page loads and trigger identification when the user clicks **Request loan**. The JavaScript agent returns both a `visitorId` and a `requestId`. Instead of relying on the `visitorId` from the browser, you'll send the `requestId` to your server along with the loan data. The server will then call the [Fingerprint Events API](/reference/v3/server-api-get-event) to securely retrieve the full identification details, including the verified `visitorId` and risk signals such as browser tampering or bot activity.

1. At the top of `public/index.js`, load the JavaScript agent:

```javascript public/index.js theme={"theme":"github-dark-dimmed"}
const fpPromise = import(`https://fpjscdn.net/v3/${window.FP_PUBLIC_API_KEY}`).then(
  (FingerprintJS) => FingerprintJS.load({ region: "us" }),
);
```

2. Make sure to change `region` to match your workspace region (e.g., `eu` for Europe, `ap` for Asia, `us` for Global (default)).
3. Near the bottom of `public/index.js`, the **Request loan** button already has an event handler set up for submitting a loan application. Inside this handler, request visitor identification from Fingerprint using the `get()` method and include the returned `requestId` when sending the loan data to the server:

```javascript public/index.js theme={"theme":"github-dark-dimmed"}
requestBtn.addEventListener("click", async () => {
  const fp = await fpPromise;
  const { requestId } = await fp.get();

  const data = {
    firstName: firstNameEl.value,
    lastName: lastNameEl.value,
    loanAmount: amountRange.value,
    monthlyIncome: incomeRange.value,
    loanTerms: termRange.value,
    requestId,
  };

  // ...
});
```

The `get()` method sends signals collected from the browser to Fingerprint servers, where they are analyzed to identify the visitor. The returned `requestId` acts as a reference to this specific identification event, which your server can later use to fetch the full visitor details.

For lower latency in production, use [Sealed Client Results](/docs/v3/sealed-client-results) to return full identification details as an encrypted payload from the `get()` method.

## 4. Receive and use the request ID to get visitor insights

Next, pass the `requestId` through to your loan application logic, initialize the [Fingerprint Node Server SDK](/reference/node-server-sdk), and fetch the full visitor identification event so you can access the trusted `visitorId` and [Smart Signals](https://fingerprint.com/products/smart-signals/).

1. In the backend, the `server/server.js` file already defines API routes for the app. Notice that the `/api/loan-risk` route simply passes the request body to a `requestLoan` function that is defined in the `server/loans.js` file.

```javascript server/server.js theme={"theme":"github-dark-dimmed"}
app.post("/api/loan-risk", async (req, reply) => {
  const result = await requestLoan(req.body);
  return reply.send(result);
});
```

2. The `server/loans.js` file contains the logic for handling loan applications and will contain the rest of the tutorial. Start by importing and initializing the Fingerprint Node Server SDK there, loading your environment variables with `dotenv`, and importing the `crypto` package for use later:

```javascript server/loans.js theme={"theme":"github-dark-dimmed"}
import { db } from "./db.js";
import { config } from "dotenv";
import { FingerprintJsServerApiClient, Region } from "@fingerprintjs/fingerprintjs-pro-server-api";
import crypto from "crypto";
const MONTHLY_RATE = 0.15;

config();

const fpServerApiClient = new FingerprintJsServerApiClient({
  apiKey: process.env.FP_SECRET_API_KEY,
  region: Region.Global,
});
```

3. Make sure to change `region` to match your workspace region (e.g., `EU` for Europe, `AP` for Asia, `Global` for Global (default)).
4. Update the `requestLoan` function to also extract the `requestId` from the passed request body and use it to fetch the full identification event details from Fingerprint:

```javascript server/loans.js theme={"theme":"github-dark-dimmed"}
export async function requestLoan(data) {
  const { firstName, lastName, loanAmount, monthlyIncome, loanTerms, requestId } = data;

  if ((!firstName || !lastName || !loanAmount || !monthlyIncome || !loanTerms, !requestId)) {
    console.error("Missing required fields.");
    return { success: false, message: "Missing required fields." };
  }

  const event = await fpServerApiClient.getEvent(requestId);

  // ...
}
```

Using the `requestId`, the getEvent will retrieve the full data for the visitor identification request. The returned object will contain the visitor ID, IP address, device, and browser details, and Smart Signals like bot detection, browser tampering detection, VPN detection, and more.

You can see a full example of the event structure and test it with your own device in the [demo playground](https://demo.fingerprint.com/playground).

For additional checks to ensure the validity of the data coming from your frontend, view [how to protect from client-side tampering and replay attacks](/docs/v3/protecting-from-client-side-tampering).

## 5. Block bots and suspicious devices

<Note>
  This optional step uses the Bot Detection and Suspect Score Smart Signals, which are only
  available on paid plans.
</Note>

A simple but powerful way to prevent fraudulent loan applications is to block automated applications that come from bots. The `event` object includes the [Bot Detection Smart Signal](https://fingerprint.com/products/bot-detection/) that flags automated activity, making it easy to reject bot traffic.

This signal returns `good` for known bots like search engines, `bad` for automation tools, headless browsers, or other signs of automation, and `notDetected` when no bot activity is found.

1. Continuing in the `requestLoan` function in `server/loans.js`, after the existing `loanData` object, check the bot signal returned in the `event` object:

```javascript server/loans.js theme={"theme":"github-dark-dimmed"}
export async function requestLoan(data) {
  // ...

  const loanData = { ... };

  const botDetected = event.products?.botd?.data?.bot?.result !== "notDetected";

  if (botDetected) {
    recordLoanApplication(loanData, "rejected");
    console.error("Bot detected.");
    return { success: false, error: "Loan application denied." };
  }

  // ...
}
```

Note that the code also records the loan application using the loan data passed from the request body. This data is used later when checking the applicant's data from previous loan applications.

You can also use Fingerprint's [Suspect Score](/docs/v3/suspect-score) to flag suspicious loan applications for manual review. The Suspect Score is a weighted representation of all Smart Signals present in the identification payload, helping to identify suspicious activity.

2. Below the bot detection check, add a condition that reads the Suspect Score from the `event` object and flags the loan application for review if it exceeds a chosen threshold (for example, 20):

```javascript server/loans.js theme={"theme":"github-dark-dimmed"}
export async function requestLoan(data) {
  // ...

  const suspectScore = event.products?.suspectScore?.data?.result || 0;

  if (suspectScore > 20) {
    console.error(`High Suspect Score detected: ${suspectScore}`);
    recordLoanApplication(loanData, "rejected");
    return { success: false, error: "Loan application denied." };
  }

  // ...
}
```

## 6. Detect inconsistent loan applications per visitor

Next, use the trusted `visitorId` from the `event` object to check for past loan applications submitted from the same device. When a new application is submitted, check if a record exists for the current `visitorId`. If it does, compare the stored personal details to the new ones. If they don't match, it indicates the applicant is trying to change their personal details to start a new loan, and the application is denied. This makes fraud detection enforceable at the device level, not just by account or identity details. Even if someone uses new personal information, fake identities, or alternate emails, their applications are still linked to the same device, helping you catch attempts to game the system.

Note: The starter app includes a SQLite database with the following table already created for you to store loan applications:

```text SQLite database tables theme={"theme":"github-dark-dimmed"}
loan_applications - Stores the loan applications
  id INTEGER PRIMARY KEY AUTOINCREMENT,
  visitorId TEXT,
  firstName TEXT NOT NULL,
  lastName TEXT NOT NULL,
  monthlyIncome INTEGER NOT NULL,
  loanAmount INTEGER NOT NULL,
  loanTerms INTEGER NOT NULL,
  status TEXT NOT NULL,
  createdAt INTEGER NOT NULL
```

1. First, within the `requestLoan` function, retrieve the `visitorId` from the `event` object and update the `loanData` variable to include it:

```javascript server/loans.js theme={"theme":"github-dark-dimmed"}
export async function requestLoan(data) {
  // ...

  const event = await fpServerApiClient.getEvent(requestId);
  const visitorId = event.products.identification.data.visitorId;

  const loanData = {
    firstName,
    lastName,
    monthlyIncome,
    loanAmount,
    loanTerms,
    visitorId,
  };

  // ...
}
```

2. Add some helper functions to the bottom of the `server/loans.js` file. The first one generates a hash based on personal details. This is just a simple way to quickly compare details for this demo:

```javascript server/loans.js theme={"theme":"github-dark-dimmed"}
// Generate personal hash
function genPersonalHash({ firstName, lastName, monthlyIncome }) {
  const norm = [
    firstName.trim().toLowerCase().replace(/\s+/g, ""),
    lastName.trim().toLowerCase().replace(/\s+/g, ""),
    Number(monthlyIncome),
  ].join("|");
  return crypto.createHash("sha256").update(norm).digest("hex");
}
```

3. The next helper function retrieves the first application submitted by the visitor (if it exists) and compares the personal details from that application to the current one:

```javascript server/loans.js theme={"theme":"github-dark-dimmed"}
// Check if the visitor details are consistent
function isConsistent({ visitorId, firstName, lastName, monthlyIncome }) {
  const personalHash = genPersonalHash({
    firstName,
    lastName,
    monthlyIncome,
  });

  const row = db
    .prepare(
      `SELECT firstName, lastName, monthlyIncome FROM loan_applications WHERE visitorId = ?
    ORDER BY createdAt ASC LIMIT 1`,
    )
    .get(visitorId);

  if (!row) return true;

  const originalHash = genPersonalHash({
    firstName: row.firstName,
    lastName: row.lastName,
    monthlyIncome: row.monthlyIncome,
  });

  return originalHash == personalHash;
}
```

4. To limit the total number of applications allowed by a visitor, regardless of the details entered, add another helper function to get the current number of applications made by the visitor:

```javascript server/loans.js theme={"theme":"github-dark-dimmed"}
// Check how many loan applications have been made by the visitor
function getApplicationCount(visitorId) {
  return db.prepare(`SELECT COUNT(*) FROM loan_applications WHERE visitorId = ?`).get(visitorId);
}
```

5. Also update the existing `recordLoanApplication` helper function to include the `visitorId` when recording loan applications:

```javascript server/loans.js theme={"theme":"github-dark-dimmed"}
function recordLoanApplication(data, status) {
  const { firstName, lastName, loanAmount, monthlyIncome, loanTerms, personalHash, visitorId } =
    data;

  db.prepare(
    `INSERT INTO loan_applications (visitorId, firstName, lastName, monthlyIncome, loanAmount, loanTerms, personalHash, status, createdAt) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?)`,
  ).run(
    visitorId,
    firstName,
    lastName,
    monthlyIncome,
    loanAmount,
    loanTerms,
    personalHash,
    status,
    Date.now(),
  );
}
```

6. Return to the `requestLoan` function and use the helper functions to enforce consistent personal details per visitor and limit the number of applications:

```javascript server/loans.js theme={"theme":"github-dark-dimmed"}
export async function requestLoan(data) {
  // ...

  if (suspectScore > 20) {
    console.error(`High Suspect Score detected: ${suspectScore}`);
    recordLoanApplication(loanData, "rejected");
    return { success: false, error: "Loan application denied." };
  }

  // Check if the visitor details are consistent
  if (!isConsistent({ visitorId, firstName, lastName, monthlyIncome })) {
    recordLoanApplication(loanData, "rejected");
    console.error("Visitor details are not consistent.");
    return { success: false, error: "Loan application denied." };
  }

  // Check if the visitor has made too many applications
  const applicationCount = getApplicationCount(visitorId);
  if (applicationCount >= 3) {
    recordLoanApplication(loanData, "rejected");
    console.error("Too many loan applications.");
    return { success: false, error: "Loan application denied." };
  }

  // ...
}
```

This gives you a system to detect and block loan application fraud. You can extend it by configuring what details are allowed to change, tailoring outcomes based your business rules, checking only recent submissions, and more.

<Info>
  This is a minimal example to show how to implement Fingerprint. In a real application, make sure
  to implement proper security practices, error checking, and data handling that align with your
  production standards.
</Info>

## 7. Test your implementation

Now that everything is wired up, you can test the full protected loan application flow using the loan application page.

1. Start your server if it isn't already running and open [http://localhost:3000](http://localhost:3000):

```bash Terminal theme={"theme":"github-dark-dimmed"}
npm run dev
```

2. Reset the database by clicking "Reset demo DB" at the bottom of the loan application page.
3. Keep the default values in the form and click **Request loan**. The application should be approved.
4. Change some personal details, like the name, and submit again. The second application will be rejected because the same visitor has already applied with different personal details.
5. Refresh the page and try submitting a few more applications using the default details. After several attempts, new submissions will be blocked.
6. Open the page in incognito and try to submit a new loan application, your visitor ID will remain the same and the application is still blocked.
7. Bonus: Test the flow using a headless browser or automation tool to see bot detection in action. A sample script is available in `test-bot.js`. While your app is running, run the script with `node test-bot.js` in your terminal and observe that the automated applications are denied.

## Next steps

You now have a working loan application flow secured with Fingerprint. From here, you can expand the logic with more [Smart Signals](/docs/v3/smart-signals-reference), fine-tune rules based on your lending policies, or add additional checks for suspicious or high-risk applicants.

To dive deeper, explore the other use case tutorials for more step-by-step examples.

Check out these related resources:

* [Node SDK Reference](https://github.com/fingerprintjs/fingerprintjs-pro-server-api-node-sdk)
* [Vue frontend quickstart](/docs/v3/vue-quickstart)
* [React frontend quickstart](/docs/v3/react-quickstart)
* [API reference for the Events endpoint](/reference/v3/server-api-get-event)
* [Use case tutorial: Detecting new account fraud](/docs/v3/new-account-fraud-use-case-tutorial)
* [Low-latency identification with Sealed Client Results](/docs/v3/sealed-client-results)
