Browser fingerprinting gathers details about a browser and the device it runs on, such as screen resolution, installed fonts, time zone, language settings, and hardware properties, and combines them into an identifier for that browser. Because the identifier comes from the browser and device rather than something stored in them, it stays the same when someone clears cookies or opens a private window.
That makes it useful anywhere you need to recognize a returning device. You can spot one device behind many new accounts, flag a login from a device an account has never used, limit promo codes to one redemption per device, slow down automated traffic, and more.
In this tutorial, you'll add browser fingerprinting to a Python app built with FastAPI, using Fingerprint. Fingerprint's visitor ID is derived from 100+ browser and hardware signals, and doesn't rely on IP addresses or cookies. You'll identify visitors in the browser with the JavaScript agent, then use the Fingerprint Python Server SDK to retrieve and verify each identification on your server before acting on it.
The example is a signup form that blocks bots and stops one device from creating multiple accounts. The same steps apply to most flows where you want to know which device you're dealing with.
Want to try it on your own traffic? Create a free account or contact our team to see how Fingerprint's device intelligence fits your stack.
How the flow works
Before we get to the code, here's what happens when someone submits the signup form:
- The Fingerprint JavaScript agent loads on the signup page and collects browser and device signals.
- When the visitor submits the form, the agent sends an identification request to Fingerprint and gets back an
event_id. - The frontend sends the
event_idalong with the form data to your FastAPI server. It doesn't send the visitor ID. - Your server uses the
event_idto fetch the full identification event from the Fingerprint Server API, including the visitor ID and other risk signals. - Your server runs its checks on that event and decides whether to create the account.
The reason the frontend only sends the event_id is that anything coming from the browser can be modified. If your server accepted a visitor ID straight from the request, an attacker could send a different one with every signup. Fetching the event from Fingerprint server to server means the visitor ID your server acts on is the one Fingerprint actually issued. You can also test for event staleness and replayed payloads to block reused identification events.
Prerequisites
To follow along, you'll need:
- Python 3.10 or later
- A Fingerprint account
- Basic familiarity with Python and JavaScript
Get your API keys
Fingerprint uses two keys for this setup:
- A Public API key, used by the JavaScript agent in the browser
- A Server API key, used by your backend to call the Server API
You'll find both on the API keys page in the Fingerprint dashboard. Create a Server API key if you don't have one yet, and keep it out of your frontend code and version control.
This tutorial assumes your workspace is in the US region. If you chose the EU or Asia region when you set up your workspace, you'll need to update the region in two places later on: once in the JavaScript agent and once in the Python SDK. See our documentation on regions for details.
Set up the project
Create a project folder and a virtual environment:
mkdir fingerprint-python-starter && cd fingerprint-python-starter
python -m venv venv
source venv/bin/activate # On Windows: venv\Scripts\activateCreate a requirements.txt file:
fastapi==0.162.0
uvicorn[standard]==0.54.0
pydantic==2.14.1
python-dotenv==1.2.4
fingerprint-server-sdk==9.9.0Then install the dependencies:
pip install -r requirements.txtThis tutorial uses version 9.x of the Fingerprint Python Server SDK.
Create a .env file in the project root and add your Server API key:
FINGERPRINT_SERVER_API_KEY=your-server-api-keyNext, create server.py with a basic FastAPI app. It serves the signup page and has a placeholder route for account creation:
from fastapi import FastAPI
from fastapi.responses import FileResponse
from pydantic import BaseModel
app = FastAPI()
class SignupRequest(BaseModel):
username: str
password: str
event_id: str
@app.get("/")
async def index():
return FileResponse("index.html")
@app.post("/api/create-account")
async def create_account(body: SignupRequest):
# Fingerprint checks go here
return {"message": "Account created"}
if __name__ == "__main__":
import uvicorn
uvicorn.run(app, host="127.0.0.1", port=3000)Serving the page from the same FastAPI app keeps everything on one origin, so you don't need to configure CORS. If your frontend lives somewhere else, add FastAPI's CORSMiddleware and allow your frontend's domain.
Finally, create an empty index.html file in the project root. This is the signup page the / route serves, and you'll fill it in during the next section.
When you're done, your project looks like this:
fingerprint-python-starter/
├── .env
├── index.html
├── requirements.txt
└── server.pyAdd Fingerprint to the frontend
Open index.html and add a simple signup form, a few lines of styling, and the Fingerprint JavaScript agent loaded from the CDN. Replace YOUR_PUBLIC_API_KEY with your public API key:
<!doctype html>
<html lang="en">
<head>
<meta charset="UTF-8" />
<meta name="viewport" content="width=device-width, initial-scale=1" />
<title>Sign up</title>
<style>
body {
font-family: system-ui, sans-serif;
background: #f4f4f5;
margin: 0;
min-height: 100vh;
display: grid;
align-content: center;
justify-items: center;
gap: 1rem;
}
h1 {
margin: 0;
font-size: 1.5rem;
}
form {
background: #fff;
padding: 1.5rem;
border-radius: 12px;
box-shadow: 0 2px 12px rgba(0, 0, 0, 0.08);
display: grid;
gap: 1rem;
width: min(320px, 90vw);
}
label {
display: grid;
gap: 0.25rem;
font-size: 0.9rem;
}
input {
padding: 0.5rem;
border: 1px solid #d4d4d8;
border-radius: 6px;
font: inherit;
}
button {
padding: 0.6rem;
border: none;
border-radius: 6px;
background: #18181b;
color: #fff;
font: inherit;
cursor: pointer;
}
</style>
</head>
<body>
<h1>Create an account</h1>
<form id="signup-form">
<label>
Username
<input name="username" required />
</label>
<label>
Password
<input name="password" type="password" required />
</label>
<button type="submit">Sign up</button>
</form>
<p id="result"></p>
<script>
const fpPromise = import('https://fpjscdn.net/v4/YOUR_PUBLIC_API_KEY').then((Fingerprint) =>
Fingerprint.start({ region: 'us' })
)
const form = document.querySelector('#signup-form')
const result = document.querySelector('#result')
form.addEventListener('submit', async (e) => {
e.preventDefault()
const formData = Object.fromEntries(new FormData(form))
const fp = await fpPromise
const { event_id } = await fp.get()
const response = await fetch('/api/create-account', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ ...formData, event_id }),
})
const data = await response.json()
result.textContent = data.message ?? data.detail
})
</script>
</body>
</html>A few things to note about this code:
- The agent starts as soon as the page loads. That gives it time to collect signals before the visitor submits the form, since the agent needs a moment between
start()andget(). fp.get()runs when the form is submitted and returns both anevent_idand avisitor_id. Only theevent_idgoes to the server.- If your workspace is in the EU or Asia region, change
regionto"eu"or"ap".
For production, set up one of the proxy integrations so ad blockers don't stop the agent from loading. If you're using a framework like React or Vue instead of plain HTML, the frontend quickstarts cover each one, and the server code below stays the same. The same goes for native apps. The iOS and Android SDKs also give you an event identifier to send to your server, so everything from here on works for a mobile frontend too.
You can also attach your own identifier, like an account ID, order ID, or session ID, to an identification event as a linked ID by passing it to get(), for example fp.get({ linkedId: "account_123" }). Once it's attached, you can search for events by that ID through the Server API and see which devices have been associated with an account over time. On a signup page, where the account ID doesn't exist yet, you can add it from your server after creating the account using the update event endpoint. See the get() reference for details.
Retrieve the identification event on the server
Now set up the Fingerprint Python Server SDK. At the top of server.py, load your Server API key and create the client:
import asyncio
import os
import fingerprint_server_sdk
from dotenv import load_dotenv
from fastapi import FastAPI, HTTPException
from fastapi.responses import FileResponse
from fingerprint_server_sdk import ApiException
from fingerprint_server_sdk.configuration import Region
from pydantic import BaseModel
load_dotenv()
api_key = os.getenv("FINGERPRINT_SERVER_API_KEY")
if not api_key:
raise ValueError("FINGERPRINT_SERVER_API_KEY environment variable is required")
# Use Region.EU or Region.AP if your workspace is in one of those regions
configuration = fingerprint_server_sdk.Configuration(api_key=api_key, region=Region.US)
client = fingerprint_server_sdk.FingerprintApi(configuration)Every check in this tutorial rejects the signup the same way, so add a small helper below the client. It logs the specific reason on the server and returns a generic message to the browser, which keeps attackers from learning which check they failed:
def block(reason: str):
print(f"Blocked signup: {reason}")
raise HTTPException(status_code=403, detail="Request blocked")Then update the /api/create-account route to fetch the event using the event_id from the request:
@app.post("/api/create-account")
async def create_account(body: SignupRequest):
try:
event = await asyncio.to_thread(client.get_event, body.event_id)
except ApiException:
block("event not found")
if not event.identification:
block("no identification data in event")
# More checks go here
return {"message": "Account created"}The SDK client is synchronous, so asyncio.to_thread runs the request in a worker thread and keeps it from blocking FastAPI's event loop. If the event_id doesn't exist or belongs to a different workspace, the SDK raises an ApiException and the signup is rejected.
The returned event contains the visitor ID, browser and device details, and Smart Signals like Bot Detection. To see the full structure with your own browser, try the demo playground.
Fetching the event server side proves the visitor ID came from Fingerprint, but it doesn't prove the event belongs to this request. Someone could capture a valid event_id and reuse it later, which is called a replay attack. In production, you can guard against this by rejecting events older than a few seconds, storing event IDs you've already processed, and confirming that the request's origin and IP address match the event. Protecting from client-side tampering and replay attacks walks through each of these checks and more.
Block bots
Automated signups are one of the most common sources of fake accounts. The event includes a bot field from Bot Detection with one of three values:
badfor automation tools, headless browsers, and other signs of automationgoodfor known bots like search engine crawlersnot_detectedwhen there's no sign of bot activity
Import BotResult alongside ApiException:
from fingerprint_server_sdk import ApiException, BotResultThen add the check to the route:
if event.bot == BotResult.BAD:
block("bad bot detected")This is a good place to layer in more Smart Signals as you need them, such as proxy detection or browser tampering detection. You can also use the Suspect Score, which combines many signals into one number, to decide when to add friction instead of blocking outright. The Smart Signals reference lists everything available.
Prevent duplicate signups from the same device
Now for the main check. By saving the visitor ID with each new account, you can tell when the same device tries to sign up again. This example uses SQLite since it ships with Python.
Import sqlite3 at the top of server.py, then add a function to create the table and call it on startup:
import sqlite3
def init_database():
conn = sqlite3.connect("database.db")
conn.execute(
"""
CREATE TABLE IF NOT EXISTS accounts (
id INTEGER PRIMARY KEY AUTOINCREMENT,
username TEXT,
password TEXT,
visitor_id TEXT
)
"""
)
conn.commit()
conn.close()
init_database()In the route, after the bot check, look up the visitor ID. If an account already exists for it, block the signup. Otherwise, create the account:
visitor_id = event.identification.visitor_id
conn = sqlite3.connect("database.db")
try:
(existing,) = conn.execute(
"SELECT COUNT(*) FROM accounts WHERE visitor_id = ?", (visitor_id,)
).fetchone()
if existing > 0:
block(f"visitor {visitor_id} already has an account")
conn.execute(
"INSERT INTO accounts (username, password, visitor_id) VALUES (?, ?, ?)",
(body.username, body.password, visitor_id),
)
conn.commit()
finally:
conn.close()
return {"message": "Account created"}Blocking every repeat signup is the simplest rule, but it isn't always the right one. Families share computers and coworkers share devices, so in a real app you might allow a small number of accounts per device, only count signups from the last day or week, or send repeat signups to a manual review queue instead of rejecting them.
This example stores passwords in plain text to keep the focus on Fingerprint. In a real application, hash passwords with a library built for it and follow your framework's authentication practices.
Test it
Start the server:
python server.pyOpen http://127.0.0.1:3000 in your browser and sign up with any username and password. You'll see "Account created."
Now try signing up again with a different username. This time you'll see "Request blocked," and your terminal will log the reason:
Blocked signup: visitor <visitor_id> already has an accountTry it again in a private or incognito window of that same browser. The signup is still blocked, because Fingerprint recognizes the same browser even without its cookies.
You can also try submitting the form from a headless browser using a tool like Playwright or Puppeteer, and you'll see it blocked by the bot check.
Here's the complete server.py:
import asyncio
import os
import sqlite3
import fingerprint_server_sdk
from dotenv import load_dotenv
from fastapi import FastAPI, HTTPException
from fastapi.responses import FileResponse
from fingerprint_server_sdk import ApiException, BotResult
from fingerprint_server_sdk.configuration import Region
from pydantic import BaseModel
load_dotenv()
api_key = os.getenv("FINGERPRINT_SERVER_API_KEY")
if not api_key:
raise ValueError("FINGERPRINT_SERVER_API_KEY environment variable is required")
# Use Region.EU or Region.AP if your workspace is in one of those regions
configuration = fingerprint_server_sdk.Configuration(api_key=api_key, region=Region.US)
client = fingerprint_server_sdk.FingerprintApi(configuration)
def init_database():
conn = sqlite3.connect("database.db")
conn.execute(
"""
CREATE TABLE IF NOT EXISTS accounts (
id INTEGER PRIMARY KEY AUTOINCREMENT,
username TEXT,
password TEXT,
visitor_id TEXT
)
"""
)
conn.commit()
conn.close()
init_database()
app = FastAPI()
class SignupRequest(BaseModel):
username: str
password: str
event_id: str
def block(reason: str):
print(f"Blocked signup: {reason}")
raise HTTPException(status_code=403, detail="Request blocked")
@app.get("/")
async def index():
return FileResponse("index.html")
@app.post("/api/create-account")
async def create_account(body: SignupRequest):
try:
event = await asyncio.to_thread(client.get_event, body.event_id)
except ApiException:
block("event not found")
if not event.identification:
block("no identification data in event")
if event.bot == BotResult.BAD:
block("bad bot detected")
visitor_id = event.identification.visitor_id
conn = sqlite3.connect("database.db")
try:
(existing,) = conn.execute(
"SELECT COUNT(*) FROM accounts WHERE visitor_id = ?", (visitor_id,)
).fetchone()
if existing > 0:
block(f"visitor {visitor_id} already has an account")
conn.execute(
"INSERT INTO accounts (username, password, visitor_id) VALUES (?, ?, ?)",
(body.username, body.password, visitor_id),
)
conn.commit()
finally:
conn.close()
return {"message": "Account created"}
if __name__ == "__main__":
import uvicorn
uvicorn.run(app, host="127.0.0.1", port=3000)Use the same pattern beyond signups
Everything in this tutorial up to the duplicate check is reusable. The frontend sends an event_id, the server fetches the event and screens out bots. What changes from flow to flow is the decision you make with the visitor ID:
- Logins: compare the visitor ID with devices you've seen for that account before, and add a verification step when it's new.
- Checkouts: log the visitor ID with each order so that when a customer files a chargeback, you can show the disputed purchase came from the same device as their earlier orders.
- Promotions and free trials: allow one redemption or trial per device instead of per email address.
- Content and API abuse: rate limit by visitor ID, which holds up better than rate limiting by IP address.
The Fingerprint demo collection shows these patterns working end to end, and the new account fraud tutorial goes deeper on the signup case.
Recognizing devices from your Python backend
Browser fingerprinting gives your Python backend a way to recognize the device behind a request, even when cookies are cleared or the visitor switches to a private window. In this tutorial, you added the Fingerprint JavaScript agent to a signup page, sent the event ID to a FastAPI server, and used the Python Server SDK to retrieve the full identification event before blocking bots and repeat signups from the same device.
The same building blocks carry over to logins, checkouts, promotions, and any other flow where knowing the device changes the decision. Start with the checks that matter most for your app, then layer in more Smart Signals as you learn what abuse looks like on your own traffic.
Ready to add device intelligence to your Python app?
Install the Fingerprint browser agent and use our Python SDK to validate visitor IDs and detect fraud from your backend.
Frequently Asked Questions
What is browser fingerprinting?
Browser fingerprinting identifies a browser by combining details like browser version, screen resolution, installed fonts, and hardware properties into an identifier. Fingerprint's visitor ID is derived from 100+ browser and hardware signals, and doesn't rely on IP addresses or cookies, so it stays stable when cookies are cleared or IP address changes.
Why use browser fingerprinting instead of cookies?
Cookies live in the browser, so anyone can clear or block them, and they don't carry over to private browsing. An identifier built from the browser and device itself is much harder to reset when someone is trying to look like a new visitor.
What's the difference between browser fingerprinting and device fingerprinting?
Browser fingerprinting identifies a specific browser using the signals available inside it, like screen, font, and graphics details. Device fingerprinting is a broader term that also covers native mobile apps, where an SDK can read device-level attributes directly. On the web, the two terms are often used interchangeably. Fingerprint offers a JavaScript agent for websites and native SDKs for iOS and Android apps.
How do I prevent multiple accounts from the same device?
Store a device identifier with each new account and check it at signup. If the same device has already created an account, or more accounts than you allow within a set time window, block the signup or add a verification step like email or phone confirmation. Shared devices are common, so a limit usually works better than blocking every repeat. This tutorial shows the approach using Fingerprint's visitor ID in a FastAPI app.





