[{"data":1,"prerenderedAt":112},["ShallowReactive",2],{"doc:\u002Fdocs\u002Fuse-cases":3},{"page":4,"toc":108,"updated":111},{"path":5,"title":6,"seoTitle":7,"description":8,"blocks":9},"\u002Fdocs\u002Fuse-cases","Use cases","Fraud Prevention Use Cases and Recipes","End-to-end recipes for account takeover, credential stuffing, sign-up abuse, promotion abuse and payment fraud: where to identify, what to tag and what to decide.",[10,13,40,45,52,55,57,68,70,74],{"type":11,"text":12},"p","Fingerly tells you who a visitor is and how suspicious the session looks. What to do about it depends on what the visitor is trying to do. Each recipe below takes one kind of fraud from the client to a decision on your server, with a policy you can start from.",{"type":14,"columns":15,"rows":19},"table",[16,17,18],"Recipe","Identify at","What decides it",[20,24,28,32,36],[21,22,23],"[Account takeover](\u002Fdocs\u002Fuse-cases\u002Faccount-takeover)","Login, password reset, account changes, payouts","Whether the account has used this device before, and its level.",[25,26,27],"[Credential stuffing](\u002Fdocs\u002Fuse-cases\u002Fcredential-stuffing)","Every login attempt","Automation, and how many failed attempts and accounts one device is behind.",[29,30,31],"[Sign-up abuse](\u002Fdocs\u002Fuse-cases\u002Fsign-up-abuse)","Account creation","How many accounts one device has created, and device farm and emulator signals.",[33,34,35],"[Promotion abuse](\u002Fdocs\u002Fuse-cases\u002Fpromotion-abuse)","Redeeming a code, a referral or a trial","One redemption per device per promotion.",[37,38,39],"[Payment fraud](\u002Fdocs\u002Fuse-cases\u002Fpayment-fraud)","Checkout, adding a card","The level, anonymised networks, and how many cards one device tries.",{"type":41,"level":42,"text":43,"id":44},"heading",2,"What every recipe shares","what-every-recipe-shares",{"type":46,"items":47},"list",[48,49,50,51],"**A tag per action.** The tag binds an identification to what it was made for, so a request ID from a harmless page cannot be spent at checkout.","**A server-side read.** Decisions use the event your backend reads with a secret key, never what the client reports.","**Your own records.** Fingerly returns the visitor ID; counting accounts, failed logins or redemptions per visitor happens in your database. Store `visitor_id` with every account, login, order and redemption.","**Failure is missing evidence.** When identification fails, the visitor continues and your server decides with less information.",{"type":41,"level":42,"text":53,"id":54},"The shared server helper","the-shared-server-helper",{"type":11,"text":56},"Every recipe starts the same way: read the stored event with a secret key, and refuse to trust it unless it belongs to this action, is recent, and has not been used before. Fingerly does not stop a request ID from being read twice, so the one-time check is yours: any store with an atomic \"add if absent\" works, such as Redis `SET` with `NX` and a ten-minute expiry.",{"type":58,"samples":59},"code",[60,64],{"label":61,"lang":62,"code":63},"Node.js","ts","\u002F\u002F fingerly.server.ts\nimport { load, FingerlyAPIError } from '@fingerly\u002Fnode'\n\nconst fingerly = load({ secretKey: process.env.FINGERLY_SECRET_KEY! })\nconst sleep = (ms: number) => new Promise((resolve) => setTimeout(resolve, ms))\n\nasync function readEvent(requestId: string) {\n  for (let attempt = 1; ; attempt++) {\n    try {\n      return await fingerly.events.get(requestId)\n    } catch (error) {\n      const status = error instanceof FingerlyAPIError ? error.status : undefined\n      if (status === 404 && attempt \u003C 4) await sleep(attempt * 250)   \u002F\u002F not readable yet\n      else if (status === 404 || status === 422) return null\n      else throw error\n    }\n  }\n}\n\n\u002F**\n * The stored event behind a request ID, or null when there is nothing to\n * trust: no ID, an unknown ID, another action's tag, too old, or used before.\n *\u002F\nexport async function verifiedEvent(requestId: unknown, tag: string, maxAgeMs = 2 * 60 * 1000) {\n  if (typeof requestId !== 'string' || requestId === '') return null\n\n  const event = await readEvent(requestId)\n  if (!event || event.tag !== tag) return null\n  if (Date.now() - Date.parse(event.occurred_at) > maxAgeMs) return null\n  if (!(await usedRequestIds.add(event.request_id))) return null   \u002F\u002F your store: true only the first time\n\n  return event\n}\n\n\u002F** Whether any of these signal groups fired. *\u002F\nexport const fired = (event: { triggers: Array\u003C{ signal: string }> }, ...groups: string[]) =>\n  event.triggers.some((trigger) => groups.includes(trigger.signal))",{"label":65,"lang":66,"code":67},"Python","python","# fingerly_server.py\nimport os\nimport time\nfrom datetime import datetime, timedelta, timezone\nfrom fingerly import Fingerly, FingerlyAPIError\n\nfingerly = Fingerly(secret_key=os.environ[\"FINGERLY_SECRET_KEY\"])\n\n\ndef _read_event(request_id):\n    for attempt in range(1, 5):\n        try:\n            return fingerly.events.get(request_id)\n        except FingerlyAPIError as error:\n            if error.status == 404 and attempt \u003C 4:\n                time.sleep(attempt * 0.25)  # not readable yet\n            elif error.status in (404, 422):\n                return None\n            else:\n                raise\n\n\ndef verified_event(request_id, tag, max_age=timedelta(minutes=2)):\n    \"\"\"The stored event behind a request ID, or None when there is nothing to trust.\"\"\"\n    if not isinstance(request_id, str) or not request_id:\n        return None\n\n    event = _read_event(request_id)\n    if event is None or event.tag != tag:\n        return None\n    if datetime.now(timezone.utc) - event.occurred_at > max_age:\n        return None\n    if not used_request_ids.add(event.request_id):  # your store: True only the first time\n        return None\n    return event\n\n\ndef fired(event, *groups):\n    \"\"\"Whether any of these signal groups fired.\"\"\"\n    return any(trigger.signal in groups for trigger in event.triggers)",{"type":11,"text":69},"`verifiedEvent` returns nothing when the client could not identify the visitor at all. Treat that as missing evidence: the policies below add friction a real customer can pass, rather than refusing outright.",{"type":71,"tone":72,"text":73},"callout","tip","New to the flow? Read [server-side verification](\u002Fdocs\u002Fserver-side-verification) first. It explains the checks this helper makes.",{"type":75,"columns":76,"cards":77},"cards",3,[78,83,88,93,98,103],{"title":79,"text":80,"href":81,"icon":82},"Account takeover","Known devices through, new ones challenged.","\u002Fdocs\u002Fuse-cases\u002Faccount-takeover","lock",{"title":84,"text":85,"href":86,"icon":87},"Credential stuffing","Stop scripted logins at the door.","\u002Fdocs\u002Fuse-cases\u002Fcredential-stuffing","bot",{"title":89,"text":90,"href":91,"icon":92},"Sign-up abuse","Limit accounts per device.","\u002Fdocs\u002Fuse-cases\u002Fsign-up-abuse","users",{"title":94,"text":95,"href":96,"icon":97},"Promotion abuse","One redemption per device.","\u002Fdocs\u002Fuse-cases\u002Fpromotion-abuse","gift",{"title":99,"text":100,"href":101,"icon":102},"Payment fraud","Review risky orders, stop card testing.","\u002Fdocs\u002Fuse-cases\u002Fpayment-fraud","card",{"title":104,"text":105,"href":106,"icon":107},"Migrate from FingerprintJS Pro","Map your existing integration.","\u002Fdocs\u002Fmigrate-from-fingerprintjs","repeat",[109,110],{"id":44,"text":43,"level":42},{"id":54,"text":53,"level":42},"2026-09-17T16:58:12.000Z",1789667797514]