The browser is a hostile input source. A fingerprinting agent running client-side produces a visitor ID and a set of signals, but if your Python backend simply trusts whatever the client posts, an attacker forges a clean result and walks through the front door. Real device intelligence lives on the server, where you fetch the authoritative verdict and decide what to do with it.
This guide shows the server-side pattern in Python: take a one-time request identifier from the client, retrieve the sealed result from your fingerprinting backend, validate it, and branch your logic on the score. The examples use FastAPI and Django but the shape is the same anywhere.
The trust boundary
The client SDK identifies the visitor and hands your frontend a short-lived requestId. Your frontend sends that ID to your backend alongside the action, for example a login attempt. Your backend then calls the fingerprinting server API to exchange the ID for the full, trustworthy result. This is the core of server-side verification, and it applies identically in Python.
The rules that make it safe:
- Treat the client-supplied visitor ID as a hint only; the server API response is authoritative.
- Reject results older than a few seconds to stop replay of a captured request ID.
- Confirm the result origin matches the domain you expect.
Verifying a result with FastAPI
A minimal verification dependency looks like this:
import time
import httpx
from fastapi import FastAPI, HTTPException
app = FastAPI()
PRYNT_API = "https://your-prynt-host/api/v1/events"
API_KEY = "your-server-key"
MAX_AGE_MS = 5000
async def verify_request(request_id: str) -> dict:
async with httpx.AsyncClient(timeout=3.0) as client:
resp = await client.get(
f"{PRYNT_API}/{request_id}",
headers={"Auth-API-Key": API_KEY},
)
if resp.status_code != 200:
raise HTTPException(400, "unverifiable request")
result = resp.json()
signals = result["products"]["identification"]["data"]
age_ms = int(time.time() * 1000) - signals["timestamp"]
if age_ms > MAX_AGE_MS:
raise HTTPException(400, "stale fingerprint")
return {
"visitor_id": signals["visitorId"],
"confidence": signals["confidence"]["score"],
"smart_signals": result["products"],
}
The freshness check is not optional. Without it, a leaked request ID can be replayed indefinitely.
Acting on the signals
Once you hold a verified result, the decision logic is ordinary Python. Read the suspect score and the individual Smart Signals, then branch:
@app.post("/login")
async def login(payload: LoginPayload):
device = await verify_request(payload.request_id)
signals = device["smart_signals"]
is_bot = signals.get("botd", {}).get("data", {}).get("bot", {}).get("result") == "bad"
is_vpn = signals.get("vpn", {}).get("data", {}).get("result") is True
if is_bot:
raise HTTPException(403, "automation detected")
if device["confidence"] < 0.5 or is_vpn:
return {"status": "step_up", "reason": "low_confidence_or_vpn"}
return {"status": "ok", "visitor_id": device["visitor_id"]}
The Smart Signals available here mirror the rest of the platform: bot classification, VPN and proxy detection, incognito, and location spoofing. Do not hard-block on any single one; combine them into a risk decision as covered in reason codes and explainable fraud.
Persisting device history
Device intelligence is far more powerful with memory. Store the visitor ID against each user and event so you can answer questions Python is well suited for:
| Question | How you answer it |
|---|---|
| Is this a new device for this user? | Compare visitor ID against stored history; see new device login detection |
| Is one device behind many accounts? | Group accounts by visitor ID; see multi-accounting detection |
| Is this login geographically impossible? | Compare timestamps and locations; see impossible travel detection |
A simple SQLAlchemy or Django ORM table keyed on (visitor_id, user_id, first_seen, last_seen) is enough to start. That history turns a per-request signal into the account-level intelligence that catches credential stuffing and takeover.
Django and background workers
In Django the same logic lives in a view or DRF serializer, and the API call fits naturally in a Celery task when you can afford asynchronous scoring, for example on signup where you queue a review rather than block the response. Keep the verification synchronous on the login path where the decision gates access, and push slower enrichment such as IP reputation lookups to a worker. For a broader integration checklist see protect a login form and rate limiting by device.
Frequently asked questions
Why verify fingerprints on the server instead of trusting the client?
Anything the browser sends can be forged. Server-side verification pulls the authoritative result from your fingerprinting backend using a one-time request ID, so an attacker cannot fake a clean verdict.
Does device intelligence in Python add latency to my endpoints?
The server-side lookup is a single fast request to your own backend. Run it only on sensitive actions like login and checkout, and cache the parsed result for the request lifecycle to keep it negligible.
Can I use this with Django and FastAPI both?
Yes. The pattern is framework-agnostic: receive the request ID, call the server API, validate freshness and origin, and branch on the score. Only the routing glue differs.
Python makes the server side of device intelligence straightforward: fetch the authoritative result, validate freshness and origin, and turn Smart Signals into a decision your application understands. Because Prynt is self-hostable, that backend can run inside your own network with no third-party data exposure. See the SDKs and docs to wire it up, or try live verdicts in the playground.
Run it yourself
Prynt is open-source, self-hostable device intelligence — visitor IDs, bot & fraud Smart Signals, and behavioral biometrics you own end to end.