Skip to main content

Overview

This tutorial shows how to implement Fingerprint to prevent survey fraud, where participants try to claim an incentive multiple times, use fake identities, or use devices that show signs of automation or tampering. You’ll begin with a starter app that includes a mock survey page and a basic submission flow. From there, you’ll add the Fingerprint JavaScript agent to identify each visitor and use server-side logic with Fingerprint data to check if they’ve already submitted the survey. Additionally, you’ll see how to use Smart Signals to block or flag automated or suspicious devices submitting responses. 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 survey submissions 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 back end. For language- or framework-specific setups, see our quickstarts.
Estimated time: < 15 minutes

Prerequisites

Before you begin, make sure you have the following:
  • A copy of the starter repository (clone with Git or download as a ZIP)
  • Node.js (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 for a free Fingerprint trial, or log in if you already have an account.
  2. After signing in, go to the API keys page in the dashboard.
  3. Save your public API key, which you’ll use to initialize the Fingerprint 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 and open it in your editor.
Terminal
git clone https://github.com/fingerprintjs/use-case-tutorials.git
  1. This tutorial will be using the survey-fraud folder. The project is organized as follows:
Project structure
.
├── public/
│   ├── index.html      # Survey form
│   └── index.js        # Front-end logic to handle survey submissions
├── server/
│   ├── db.js           # SQLite setup and helpers for storing survey responses
│   ├── server.js       # Node server that processes submissions
│   └── survey.js       # Survey validation and fraud detection logic
└── .env.example        # Example environment variables file
  1. Install dependencies:
Terminal
npm install
  1. Copy or rename .env.example to .env, then add your Fingerprint API keys:
Terminal
FP_PUBLIC_API_KEY=your-public-key
FP_SECRET_API_KEY=your-secret-key
  1. Start the server:
Terminal
npm run dev
  1. Visit http://localhost:3000 to view the mock survey page from the starter app. You can test out the basic flow by filling in the fields and clicking Submit survey. You’ll notice you can submit multiple responses by simply changing the email address.

3. Add Fingerprint to the front end

In this step, you’ll load the Fingerprint client when the page loads and trigger identification when the user clicks Submit survey. The client 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 survey data. The server will then call the Fingerprint Events API 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 Fingerprint JavaScript agent:
public/index.js
const fpPromise = import(
  `https://fpjscdn.net/v3/${window.FP_PUBLIC_API_KEY}`
).then((FingerprintJS) => FingerprintJS.load({ region: "us" }));
  1. Make sure to change region to match your workspace region (e.g., eu for Europe, ap for Asia, us for Global (default)).
  2. Near the bottom of public/index.js, the Submit survey button already has an event handler set up for sending a survey submission. Inside this handler, request visitor identification from Fingerprint using the get() method and include the returned requestId when sending the survey data to the server:
public/index.js
submitBtn.addEventListener("click", async () => {
  // ...
  const q4El = document.getElementById("q4");

  const fp = await fpPromise;
  const { requestId } = await fp.get();

  const data = {
    firstName: firstNameEl.value,
    email: emailEl.value,
    q1: q1El.value,
    q2: q2Selected ? q2Selected.value : null,
    q3: q3Selected ? q3Selected.value : null,
    q4: q4El.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, check out our documentation on using 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 survey submission logic, initialize the Fingerprint Server API client, and fetch the full visitor identification event so you can access the trusted visitorId and Smart Signals.
  1. In the back end, the server/server.js file already defines API routes for the app. Notice that the /api/survey route simply passes the request body to a submitSurvey function that is defined in the server/survey.js file.
server/server.js
app.post("/api/survey", async (req, reply) => {
  const result = await submitSurvey(req.body);
  return reply.send(result);
});
  1. The server/survey.js file contains the logic for handling survey submissions. Start by importing and initializing the Fingerprint Server API client there, and load your environment variables with dotenv.
server/survey.js
import { db } from "./db.js";
import { config } from "dotenv";
import {
  FingerprintJsServerApiClient,
  Region,
} from "@fingerprintjs/fingerprintjs-pro-server-api";

config();

const fpServerApiClient = new FingerprintJsServerApiClient({
  apiKey: process.env.FP_SECRET_API_KEY,
  region: Region.Global,
});
  1. Make sure to change region to match your workspace region (e.g., EU for Europe, AP for Asia, Global for Global (default)).
  2. Update the submitSurvey function to also extract the requestId from the passed request body and use it to fetch the full identification event details from Fingerprint:
server/survey.js
export async function submitSurvey(body) {
  const { email, requestId } = body;

  const event = await fpServerApiClient.getEvent(requestId);

  if (checkForDuplicateSubmission(email)) {
    return { success: false, message: "Duplicate survey submission." };
  }

  const saveResult = saveSurveySubmission(body);
  if (!saveResult.success) return saveResult;

  return { success: true, message: "Survey submitted successfully." };
}
Using the requestId, the Fingerprint server client 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 our demo playground. For additional checks to ensure the validity of the data coming from your front end, view how to protect from client-side tampering and replay attacks in our documentation.

5. Block bots and suspicious devices

This optional step uses the Bot Detection and Suspect Score Smart Signals, which are only available on paid plans.
A simple but powerful way to prevent fraudulent survey submissions is to block automated responses coming from bots. The event object includes the Bot Detection Smart Signal 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 submitSurvey function in server/survey.js, check the bot signal returned in the event object:
server/survey.js
export async function submitSurvey(body) {
  const { email, requestId } = body;

  const event = await fpServerApiClient.getEvent(requestId);

  const botDetected = event.products?.botd?.data?.bot?.result !== "notDetected";
  if (botDetected) {
    console.error("Bot detected.");
    return { success: false, message: "Survey submission failed." };
  }

  // ...
}
You can also use Fingerprint’s Suspect Score to flag high-risk survey submissions. The Suspect Score is a weighted representation of all Smart Signals present in the identification payload and can help identify suspicious or manipulative activity. While you typically wouldn’t reject a submission based only on this score, this example shows how you might include it. In production, you might instead flag the response for review or require an extra verification step before issuing the incentive.
  1. Below the bot detection check, add a condition that reads the Suspect Score from the event object and blocks the submission if it exceeds a chosen threshold (for example, 20):
server/survey.js
export async function submitSurvey(body) {
  const { email, requestId } = body;

  const event = await fpServerApiClient.getEvent(requestId);

  const botDetected = event.products?.botd?.data?.bot?.result !== "notDetected";
  if (botDetected) {
    console.error("Bot detected.");
    return { success: false, message: "Survey submission failed." };
  }

  const suspectScore = event.products?.suspectScore?.data?.result || 0;
  if (suspectScore > 20) {
    console.error(`High Suspect Score detected: ${suspectScore}`);
    return { success: false, message: "Survey submission failed." };
  }

  // ...
}

6. Prevent multiple survey submissions per visitor

Next, use the trusted visitorId from the event object to enforce a one-response-per-visitor rule. If the same visitor (visitorId) tries to submit another survey to claim an additional reward, reject the submission. In production, you may choose to allow a limited number of responses per visitor, require additional verification, or flag repeat participants for review. Note: The starter app includes a SQLite database with a table already created for you:
SQLite database tables
survey_submissions - Stores survey responses
	id INTEGER PRIMARY KEY AUTOINCREMENT
	visitorId TEXT
	firstName TEXT
	email TEXT
	q1 TEXT
	q2 TEXT
	q3 TEXT
	q4 TEXT
	createdAt INTEGER
  1. Update the existing duplicate-submission helper function at the bottom of server/survey.js so it checks both the email address and the visitorId. This ensures a visitor cannot submit again even if they change their email:
server/survey.js
// Check for duplicate submission
function checkForDuplicateSubmission(email, visitorId) {
  const submission = db
    .prepare(
      `SELECT * FROM survey_submissions 
       WHERE email = ? OR visitorId = ?`
    )
    .get(email, visitorId);

  return submission ? true : false;
}
  1. Update submitSurvey to retrieve the visitorId, use it to enforce the one-submission-per-visitor rule, and record successful survey submissions. Also add the visitorId to the body object before passing it to the saveSurveySubmission helper function:
server/survey.js
export async function submitSurvey(body) {
  // ...

  if (suspectScore > 20) {
    console.error(`High Suspect Score detected: ${suspectScore}`);
    return { success: false, message: "Survey submission failed." };
  }

  const visitorId = event.products.identification.data.visitorId;
  if (checkForDuplicateSubmission(email, visitorId)) {
    console.error("Duplicate survey submission detected.");
    return { success: false, message: "Duplicate survey submission." };
  }

  const saveResult = saveSurveySubmission({ ...body, visitorId });
  if (!saveResult.success) return saveResult;

  return { success: true, message: "Survey submitted successfully." };
}
  1. Finally update the saveSurveySubmission function to extract and include the visitorId when saving survey responses, so it can be referenced later:
server/survey.js
// Save survey submission
function saveSurveySubmission(data) {
  const { firstName, email, q1, q2, q3, q4, visitorId } = data;

  try {
    db.prepare(
      `
      INSERT INTO survey_submissions (visitorId, firstName, email, q1, q2, q3, q4, createdAt)
      VALUES (?, ?, ?, ?, ?, ?, ?, ?)
    `
    ).run(visitorId, firstName, email, q1, q2, q3, q4, Date.now());
    return { success: true };
  } catch (error) {
    console.error("Failed to save survey submission.", error);
    return { success: false, message: "Failed to save survey submission." };
  }
}
This gives you a system to detect and block survey submission fraud. You can extend it by limiting how often a visitor can submit responses, adding extra verification before rewarding high-value incentives, reviewing only recent submissions, and tailoring enforcement to your business rules.
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.

7. Test your implementation

Now that everything is wired up, you can test the full protected survey submission flow using the survey page.
  1. Start your server if it isn’t already running and open http://localhost:3000:
Terminal
npm run dev
  1. Reset the database by clicking Reset demo at the bottom of the survey page.
  2. Fill out the form and click Submit survey. The submission should be accepted.
  3. Change some details, like the email, and submit again. The second submission will be rejected because the same visitor has already submitted the survey.
  4. Open the page in an incognito window and try submitting again. Your visitor ID will remain the same, and the submission is still blocked.
  5. 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 automated submissions are denied.

Next steps

You now have a working survey submission flow secured with Fingerprint. From here, you can expand the logic with more Smart Signals, fine-tune rules based on your incentive policies, or add additional checks for suspicious or high-risk respondents. To dive deeper, explore our other use case tutorials for more step-by-step examples. Check out these related resources: