Verify the visitor on your server with Python
Updated
A visitorId that the browser posts to your server can be edited by the person using it. Send your server the eventId (or the sealed result) instead and verify it there: call GET /v1/events/:eventId with your secret key using requests, read the visitorId from the answer, and use that. If the lookup fails, refuse the action. The browser reports nothing you act on.
Why you cannot trust the browser
Anything in a browser request can be changed: a user, an extension or a script can put another visitor ID in the body. If your server uses it for a limit or a ban, the attacker picks their own identity.
The fix is to make the server fetch the truth from the source, using a key the browser never holds.
The flow
The identify call in the browser returns an eventId. It is a pointer, not a claim.
- The browser identifies and posts the eventId with your form.
- Your Python server looks the event up with the secret key.
- It reads visitorId from the answer and uses only that.
- Any error means the visitor is unverified, and the action is refused.
Verify with the events API
Use requests against the documented endpoint. Keep the secret key (sk_) in your backend’s environment only. The call is free and rate limited, and an id that does not exist or belongs to another project answers 404. Check the id format first, then that the event matches the id and is recent, and use each eventId once.
import os
import re
import time
from datetime import datetime
import requests
EVENTS_URL = "https://api.fpchecker.com/v1/events/"
class UnverifiedVisitor(Exception):
pass
def verified_event(event_id: str) -> dict:
if not re.fullmatch(r"ev_[0-9A-HJKMNP-TV-Z]{20}", event_id):
raise UnverifiedVisitor("bad eventId")
try:
r = requests.get(
EVENTS_URL + event_id,
headers={"X-Secret-Key": os.environ["FPCHECKER_SECRET_KEY"]},
timeout=5,
)
except requests.RequestException as exc:
raise UnverifiedVisitor("lookup failed") from exc
if r.status_code != 200:
raise UnverifiedVisitor(f"events api answered {r.status_code}")
event = r.json()
if not isinstance(event.get("visitorId"), str):
raise UnverifiedVisitor("no visitorId in event")
try:
age = time.time() - datetime.fromisoformat(event["createdAt"].replace("Z", "+00:00")).timestamp()
except (KeyError, AttributeError, ValueError) as exc:
raise UnverifiedVisitor("no createdAt in event") from exc
if event.get("eventId") != event_id or not -60 <= age <= 300: # allow a minute of clock skew
raise UnverifiedVisitor("wrong or old event")
if not mark_event_used(event_id): # your store: True only the first time
raise UnverifiedVisitor("event already used")
return eventUse it in a handler
Take the visitor ID from the verified event and from nowhere else. Fail closed on any error.
@app.post("/claim")
def claim():
body = request.get_json(silent=True) or {}
try:
event = verified_event(str(body.get("eventId", "")))
except UnverifiedVisitor:
return {"error": "unverified"}, 403
visitor_id = event["visitorId"] # from our server's lookup, never from the body
if already_claimed(visitor_id): # your code
return {"error": "already_claimed"}, 409
return grant(visitor_id) # your codeSealed results, if you turn them on
With sealed results enabled in the console, a successful identify also returns a sealedResult that only your backend can open. In Python, unseal_result(sealed_result, [key_a, key_b]) from the SDK (pip install fingerprintchecker, then import fpchecker) opens it without another API call. Check that projectId is yours, reject a timestamp older than about five minutes and treat eventId as single-use.
Sealing is best effort, so fall back to the events lookup when the field is missing. A failure to open a blob means it did not come from us.
Verified is not the same as allowed
Verifying proves which device the event describes. Whether that device may claim a trial or a coupon is a separate question, answered by the eligibility call on your backend.
Frequently asked questions
Is the eventId secret?
No, it is a pointer. Only a caller with your secret key can read the event it points to.
Which do I use, the events API or sealed results?
The events API always works. Sealed results save a call when they are on, and the events lookup is the fallback.
What if the lookup fails or times out?
Refuse the action or ask for another check. An error must never be treated as a verified visitor.
Do I need a Python package?
Not for the events lookup, since plain requests works. The package adds helpers such as opening sealed results.
Try FingerPrint Checker free
Every new account gets 2,000 free checks. No card required to start.
Create a free account