[{"data":1,"prerenderedAt":209},["ShallowReactive",2],{"doc:\u002Fdocs\u002Fdetect-multiple-accounts":3},{"page":4,"toc":189,"updated":208},{"path":5,"title":6,"seoTitle":7,"description":8,"blocks":9},"\u002Fdocs\u002Fdetect-multiple-accounts","Detect multiple accounts with device fingerprinting","Detect Multiple Accounts: Multi-accounting Detection Guide","Detect multi-accounting, duplicate accounts and ban evasion with a visitor ID: store it with every account, count accounts per device, and review the rest.",[10,13,15,20,28,31,93,96,98,103,106,129,131,135,138,141,145,147,150,152,155,157,160,162,165,167],{"type":11,"text":12},"p","**Multi-accounting** is one person running several accounts: to claim a sign-up bonus again, to start another free trial, to dodge a ban, or to vote and review as a crowd. Each account has a new email address, so email-based rules never see the pattern. The device usually stays the same.",{"type":11,"text":14},"Fingerly gives every device a **visitor ID**. Store it with each account, and accounts that share a device are one query away. This guide shows the whole path: identify at sign-up, store the visitor ID, count accounts per device, decide, and link accounts afterwards.",{"type":16,"level":17,"text":18,"id":19},"heading",2,"What a visitor ID can and cannot tell you","what-a-visitor-id-can-and-cannot-tell-you",{"type":21,"items":22},"list",[23,24,25,26,27],"In a browser, the same device keeps its visitor ID after cookies and storage are cleared, because the server recognises it rather than reading a value back.","In an iOS or Android app, the identity is a random install ID. On iOS it survives a reinstall, and on Android a reinstall gets a new visitor ID.","A visitor ID is scoped to your organization, and a device not seen for 180 days is issued a new one.","It identifies a device, not a person. A household sharing a tablet is several people behind one visitor ID, and one person on three devices is three.","A client that gives away almost nothing comes back `identifiable: false` with a fresh visitor ID each time. Do not count those as one device, and do not count them as unique.",{"type":16,"level":17,"text":29,"id":30},"Detect duplicate accounts","detect-duplicate-accounts",{"type":32,"steps":33},"steps",[34,54,67,80],{"title":35,"blocks":36},"Identify at sign-up",[37,39],{"type":11,"text":38},"Call `identify()` when the visitor submits the form, with a tag that names the action, and send the request ID with it.",{"type":40,"samples":41},"code",[42,46,50],{"label":43,"lang":44,"code":45},"JavaScript","ts","import { load } from '@fingerly\u002Fweb-js'\n\n\u002F\u002F consent: 'granted' only once you have consent (or another lawful basis)\nconst fingerly = await load({ apiKey: 'fly_pk_development_…', consent: 'granted' })\n\nasync function submit() {\n  let requestId: string | undefined\n  try {\n    ({ requestId } = await fingerly.identify({ tag: 'signup' }))\n  } catch {\n    \u002F\u002F Carry on: your server treats a missing request ID as missing evidence.\n  }\n  await fetch('\u002Fapi\u002Fsignup', { method: 'POST', headers: { 'Content-Type': 'application\u002Fjson' }, body: JSON.stringify({ email, requestId }) })\n}",{"label":47,"lang":48,"code":49},"iOS","swift","let requestId = try? await fingerly.identify(tag: \"signup\").requestId\ntry await api.signUp(email: email, requestId: requestId)",{"label":51,"lang":52,"code":53},"Android","kotlin","val requestId = runCatching { fingerly.identify(tag = \"signup\").requestId }.getOrNull()\napi.signUp(email, requestId)",{"title":55,"blocks":56},"Store the visitor ID with every account",[57,59,65],{"type":11,"text":58},"Take the visitor ID from the stored event your server reads, never from what the client sends. Index it.",{"type":40,"samples":60},[61],{"label":62,"lang":63,"code":64},"SQL","text","ALTER TABLE accounts ADD COLUMN visitor_id text;\nALTER TABLE accounts ADD COLUMN signup_request_id text;\nCREATE INDEX accounts_visitor_id ON accounts (visitor_id);",{"type":11,"text":66},"Also store the visitor ID on logins, orders and redemptions. The value comes from joining it with what you already know.",{"title":68,"blocks":69},"Count accounts per device and decide",[70,72,78],{"type":11,"text":71},"The server code below uses two helpers from the [use-case recipes](\u002Fdocs\u002Fuse-cases): `verifiedEvent(requestId, tag)`, which reads the stored event with a secret key and returns it only when it belongs to the action, is recent and has not been used before, and `fired(event, ...groups)`, which says whether a [signal group](\u002Fdocs\u002Fsignals) fired.",{"type":40,"samples":73},[74],{"label":75,"lang":44,"code":76,"check":77},"Node.js","import { fired, verifiedEvent } from '.\u002Ffingerly.server'\n\nexport async function signUpDecision(requestId: unknown) {\n  const event = await verifiedEvent(requestId, 'signup')\n  if (!event) return { action: 'verify' as const }\n\n  const existing = await accounts.countByVisitor(event.visitor_id, { days: 30 })\n  const banned = await bannedVisitors.has(event.visitor_id)\n\n  if (banned) return { action: 'review' as const, event }\n  if (existing >= 3) return { action: 'refuse' as const, event }\n  if (fired(event, 'device_farm', 'bot', 'android_emulator', 'ios_simulator', 'cloned_app')) {\n    return { action: 'review' as const, event }\n  }\n  if (existing >= 1 || event.suspect_level !== 'low') return { action: 'verify' as const, event }\n  return { action: 'allow' as const, event }\n}",false,{"type":11,"text":79},"`accounts` and `bannedVisitors` are your own tables. Fingerly returns the visitor ID, and counting happens in your database.",{"title":81,"blocks":82},"Link accounts after the fact",[83,85,91],{"type":11,"text":84},"When you find one bad account, its siblings are the other rows with the same visitor ID. To see what a device did in the last 30 days, list its events with a secret key.",{"type":40,"samples":86},[87,89],{"label":62,"lang":63,"code":88},"SELECT visitor_id, count(*) AS accounts\nFROM accounts\nWHERE visitor_id IS NOT NULL\nGROUP BY visitor_id\nHAVING count(*) > 1\nORDER BY accounts DESC;",{"label":75,"lang":44,"code":90},"const history = await fingerly.events.list({\n  visitor: 'X9pL2mRc7KvT4bQw8NdF',\n  from: new Date(Date.now() - 30 * 24 * 60 * 60 * 1000),\n  limit: 200,\n})",{"type":11,"text":92},"Events are readable for 30 days. Keep what you need for longer in your own records. See [data retention](\u002Fdocs\u002Fdata-retention).",{"type":16,"level":17,"text":94,"id":95},"Detect ban evasion","detect-ban-evasion",{"type":11,"text":97},"Ban evasion is multi-accounting with a motive: a banned user comes back under a new address. When you ban an account, add its visitor ID to a list of banned visitors. At sign-up, a visitor ID on that list is a strong reason to hold the new account for review.",{"type":21,"items":99},[100,101,102],"Hold for review rather than refuse. The ban may have followed a shared device, and the next person to use it did nothing.","Do not tell the visitor which check caught them.","On Android, a banned user who reinstalls gets a new visitor ID, so combine the device with other evidence such as a verified phone number.",{"type":16,"level":17,"text":104,"id":105},"A starting policy","a-starting-policy",{"type":107,"columns":108,"rows":111},"table",[109,110],"Situation","Action",[112,115,118,120,123,126],[113,114],"The visitor ID is on your banned list","Create the account on hold for review.",[116,117],"The device created 3 or more accounts in 30 days","Refuse, or create the account without its free benefits.",[119,114],"`device_farm`, `bot`, `android_emulator`, `ios_simulator` or `cloned_app` fired",[121,122],"The device already has an account, the level is above `low`, or the request is unscored","Ask for verification, such as a phone number, before granting benefits.",[124,125],"No usable identification","Ask for verification. The device cannot be counted.",[127,128],"Otherwise","Allow.",{"type":11,"text":130},"Pick the limit that fits your product. A family sharing a tablet may reasonably create two accounts, and a device creating twenty is not a household.",{"type":132,"tone":133,"text":134},"callout","tip","Run it in observe mode first. Log the decision it would have made next to what happened for a week or two, then add friction a real customer can pass before you refuse anyone.",{"type":132,"tone":136,"text":137},"note","A visitor ID is evidence, not a verdict on a person. Prefer verification and review to outright refusal, and say in your terms that limits apply per device.",{"type":16,"level":17,"text":139,"id":140},"Common questions","common-questions",{"type":16,"level":142,"text":143,"id":144},3,"How do I detect multiple accounts without cookies?","how-do-i-detect-multiple-accounts-without-cookies",{"type":11,"text":146},"Identify at sign-up with a Fingerly SDK, read the stored event on your server, and store its `visitor_id` with the account. In a browser the same device keeps its visitor ID after cookies and storage are cleared. In an app the identity is a random install ID.",{"type":16,"level":142,"text":148,"id":149},"Does a VPN or a new email address defeat it?","does-a-vpn-or-a-new-email-address-defeat-it",{"type":11,"text":151},"Neither changes the device, so neither by itself changes the visitor ID. The visitor ID comes from the device and not from the address or the email, and a VPN is reported separately as a signal.",{"type":16,"level":142,"text":153,"id":154},"What about people who share a device?","what-about-people-who-share-a-device",{"type":11,"text":156},"They share a visitor ID, so a per-device limit has to leave room for a household. Start with a limit above one, and use verification and review to separate real customers from farmed accounts.",{"type":16,"level":142,"text":158,"id":159},"Can I find all accounts that share a device?","can-i-find-all-accounts-that-share-a-device",{"type":11,"text":161},"Yes, if you stored the visitor ID with each account: group your accounts by it. Fingerly keeps events for 30 days and has no API for counting accounts, so the account-to-device record is yours.",{"type":16,"level":142,"text":163,"id":164},"What if the device cannot be identified?","what-if-the-device-cannot-be-identified",{"type":11,"text":166},"Some clients give away almost nothing and come back `identifiable: false`. Their `fingerprint_suppressed` signal fires, and they get a new visitor ID every time, so ask for verification instead of counting them.",{"type":168,"columns":17,"cards":169},"cards",[170,175,180,185],{"title":171,"text":172,"href":173,"icon":174},"Sign-up abuse recipe","The full server helper and policy.","\u002Fdocs\u002Fuse-cases\u002Fsign-up-abuse","users",{"title":176,"text":177,"href":178,"icon":179},"Visitor identification","How stable a visitor ID is.","\u002Fdocs\u002Fvisitor-identification","fingerprint",{"title":181,"text":182,"href":183,"icon":184},"Prevent promo abuse","One redemption per device.","\u002Fdocs\u002Fprevent-promo-abuse","gift",{"title":186,"text":187,"href":188,"icon":21},"Reading events","List what a device did.","\u002Fdocs\u002Freading-events",[190,191,192,194,196,198,200,201,202,203,204,205,206,207],{"id":19,"text":18,"level":17},{"id":30,"text":29,"level":17},{"id":193,"text":35,"level":142},"step-identify-at-sign-up",{"id":195,"text":55,"level":142},"step-store-the-visitor-id-with-every-account",{"id":197,"text":68,"level":142},"step-count-accounts-per-device-and-decide",{"id":199,"text":81,"level":142},"step-link-accounts-after-the-fact",{"id":95,"text":94,"level":17},{"id":105,"text":104,"level":17},{"id":140,"text":139,"level":17},{"id":144,"text":143,"level":142},{"id":149,"text":148,"level":142},{"id":154,"text":153,"level":142},{"id":159,"text":158,"level":142},{"id":164,"text":163,"level":142},"2026-11-12T11:59:51.000Z",1794493376922]