> ## 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.

# SMS Pumping

> Learn how to detect and prevent SMS pumping fraud

## Overview

This tutorial walks through implementing Fingerprint to prevent SMS pumping attacks, where fraudsters use bots to repeatedly trigger one-time passcode (OTP) messages or verification texts to many phone numbers in order to test stolen numbers or abuse your SMS gateway for profit.

You'll begin with a starter app that includes a mock account creation page and a basic flow to send an SMS verification code. From there, you'll add the JavaScript agent to identify each visitor and use server-side logic with Fingerprint data to detect and block automated SMS verification requests.

By the end, you'll have a sample app that rejects SMS-pumping bots 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/83zQSS5Zl1U" 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 />

<Note>
  This tutorial requires the Bot Detection Smart Signal, which is only available on paid plans.
</Note>

## 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 `sms-pumping` folder. The project is organized as follows:

<Tree>
  <Tree.Folder name="public" defaultOpen>
    <Tree.File name="index.html - Account creation page" />

    <Tree.File name="index.js - Front-end logic for SMS requests" />
  </Tree.Folder>

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

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

    <Tree.File name="sms.js - SMS pumping prevention logic" />
  </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 account creation page from the starter app. You can test the basic flow by entering an email and phone number, then clicking **Send code via SMS**.
7. Then try triggering SMS requests using the included headless bot script `test-bot.js`. While the app is running, execute `node test-bot.js` and observe that the automated script successfully requests SMS codes. By default, the server does not distinguish between bots and real users:

```bash Terminal theme={"theme":"github-dark-dimmed"}
node test-bot.js
```

## 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 **Send code via SMS**. 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 account creation payload. The server will then call the [Fingerprint Events API](/reference/v3/server-api-get-event) to securely retrieve the full identification details, including bot detection and other signals.

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 **Send code via SMS** button already has an event handler for submitting the account details. Inside this handler, request visitor identification from Fingerprint using the `get()` method and include the returned `requestId` when sending the request to the server:

```javascript public/index.js theme={"theme":"github-dark-dimmed"}
submitBtn.addEventListener("click", async () => {
	// ...

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

  try {
    const res = await fetch("/api/send-sms", {
      method: "POST",
      headers: { "Content-Type": "application/json" },
      body: JSON.stringify({
        name,
        phone,
        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 server-side account creation 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 Bot Detection [Smart Signal](https://fingerprint.com/products/smart-signals/).

1. In the backend, the `server/server.js` file defines the API routes for the app. Update the `/api/send-sms` route there to also extract `requestId` from the request body and pass it into the `sendSMS` function:

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

2. The `server/sms.js` file contains the logic for handling SMS verification requests. Start by importing and initializing the Fingerprint Node Server SDK there, and load your environment variables with `dotenv`:

```javascript server/sms.js theme={"theme":"github-dark-dimmed"}
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,
});
```

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 `sendSMS` function to also extract `requestId` and use it to fetch the full identification event details from Fingerprint:

```javascript server/sms.js theme={"theme":"github-dark-dimmed"}
export async function sendSMS({ name, phone, requestId }) {
  if (!name || !phone) {
    return { success: false, message: "Name and phone number are required." };
  }

  const event = await fpServerApiClient.getEvent(requestId);

  sendVerificationCode(phone);

  return {
    success: true,
    message: "A verification code has been sent to your phone.",
  };
}
```

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 SMS pumping bots

SMS pumping attacks rely heavily on automated account creations, so rejecting bots outright can stop the abuse. Fingerprint returns `notDetected` if no bot activity is found, `good` for known bots, like search engines, and `bad` for other automation tools. Any visitor identification that does not return `notDetected` can be blocked from sending SMS codes.

1. Continuing in the `sendSMS` function in `server/sms.js`, check the bot signal returned in the `event` object and block bots:

```javascript server/sms.js theme={"theme":"github-dark-dimmed"}
export async function sendSMS({ name, phone, requestId }) {
  // ...

  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: "SMS sending failed." };
  }

  // ...
}
```

You can also add [Suspect Score](/docs/v3/suspect-score) as a secondary layer. The Suspect Score is a weighted representation of all Smart Signals present in the identification payload, helping to identify suspicious activity. While you may not normally block SMS requests based only on a high risk score, you could flag them for review, modify rate-limits, or require additional verification.

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

```javascript server/sms.js theme={"theme":"github-dark-dimmed"}
export async function sendSMS({ name, phone, requestId }) {
  // ...

  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: "SMS verification failed." };
  }

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

  // ...
}
```

## 6. Recognize repeat offenders by visitor ID

As a secondary measure, you can log the `visitorId` along with each SMS verification request to spot suspicious activity. This lets you recognize and block the same device even if they clear cookies, change IPs, or rotate phone numbers.

Note: The starter app includes a SQLite database with this table already created for you:

```text SQLite database tables theme={"theme":"github-dark-dimmed"}
sms_codes – Stores SMS verification attempts and associated visitor IDs
  id INTEGER PRIMARY KEY AUTOINCREMENT
  visitorId TEXT
  phone TEXT NOT NULL
  code TEXT NOT NULL
  createdAt INTEGER NOT NULL
  expiresAt INTEGER NOT NULL
```

1. Add a new helper function to the bottom of the `server/sms.js` file to check how many SMS codes have been sent in the last 24 hours for a specific `visitorId`:

```javascript server/sms.js theme={"theme":"github-dark-dimmed"}
// Count visitor's SMS code requests sent in the last 24 hours
function countRecentSMSRequests(visitorId) {
  const since = Date.now() - 24 * 60 * 60 * 1000;

  const row = db
    .prepare(
      `SELECT COUNT(*) AS count
       FROM sms_codes
       WHERE visitorId = ? AND createdAt >= ?`,
    )
    .get(visitorId, since);

  return row.count;
}
```

2. Also update the existing `sendVerificationCode` helper function to accept and use the `visitorId`:

```javascript server/sms.js theme={"theme":"github-dark-dimmed"}
// Generate, save, and send a random 6-digit verification code
function sendVerificationCode(phone, visitorId) {
  const code = Math.floor(100000 + Math.random() * 900000).toString();
  const createdAt = Date.now();
  const expiresAt = createdAt + 1000 * 60 * 10; // 10 minutes

  // No SMS integration in the tutorial. Just pretend. ;)
  console.log(`Pretending to send SMS to ${phone}`);

  db.prepare(
    "INSERT INTO sms_codes (visitorId, phone, code, createdAt, expiresAt) VALUES (?, ?, ?, ?, ?)",
  ).run(visitorId, phone, code, createdAt, expiresAt);
}
```

3. Update `sendSMS` to retrieve the `visitorId`, and use it to check for an unusually high volume of recent SMS code requests made by the visitor and when saving a new code:

```javascript server/sms.js theme={"theme":"github-dark-dimmed"}
export async function sendSMS({ name, phone, requestId }) {
  // ...

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

  const visitorId = event.products.identification.data.visitorId;

  if (countRecentSMSRequests(visitorId) >= 5) {
    console.error("Too many SMS verification codes sent in the last 24 hours.");
    return { success: false, message: "SMS verification failed." };
  }

  sendVerificationCode(phone, visitorId);

  return {
    success: true,
    message: "A verification code has been sent to your phone.",
  };
}
```

Together with the bot detection Smart Signal, this allows you to protect your verification flow and prevent SMS pumping attacks. No matter which phone number is used, you can monitor request velocity and tie activity back to the same browser or device. You can extend it by analyzing additional signals, changing rate limit thresholds, or varying your response based on risk.

<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 actual SMS sending that align with
  your production standards.
</Info>

## 7. Test your implementation

Now that everything is wired up, you can test the full verification flow.

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. Try requesting a code by entering a name and phone number and clicking **Send code via SMS**. You should see a success response.
3. If you request SMS verification five times, additional attempts from the same device will be blocked based on the recent-code check. Open the page in incognito mode to see that you are still blocked since Fingerprint still recognizes your browser with the same visitor ID.
4. While your demo is running, run the included headless bot test script from the `sms-pumping` folder. This will attempt to trigger SMS sends using a headless browser, which will be flagged by the Bot Detection signal and rejected:

```bash Terminal theme={"theme":"github-dark-dimmed"}
node test-bot.js
```

*Note: If you encounter errors launching the automated browser, make sure you have the testing browser installed:*

```bash Terminal theme={"theme":"github-dark-dimmed"}
npx puppeteer browsers install chrome
```

## Next steps

You now have a working verification flow that blocks SMS pumping attacks with Fingerprint. From here, you can expand the logic with more [Smart Signals](/docs/v3/smart-signals-reference), fine-tune rules based on your business policies, or layer in additional defenses or step-up verification.

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)
