[{"data":1,"prerenderedAt":191},["ShallowReactive",2],{"doc:\u002Fdocs\u002Fdetect-emulators-root-jailbreak":3},{"page":4,"toc":177,"updated":190},{"path":5,"title":6,"seoTitle":7,"description":8,"blocks":9},"\u002Fdocs\u002Fdetect-emulators-root-jailbreak","Detect emulators, rooted and jailbroken devices","Detect Emulators, Rooted and Jailbroken Devices in Apps","What the Fingerly iOS and Android SDKs report about emulators, simulators, rooted and jailbroken devices and cloned apps, and how to act on it from your server.",[10,13,15,20,69,71,74,80,88,90,93,95,100,102,105,109,112,117,121,124,128,130,133,135,138,140,143,145,148,150,153,155],{"type":11,"text":12},"p","Fraud in mobile apps often runs from devices that are not what they seem: an **emulator** driven by a script, a **rooted** or **jailbroken** phone with tooling attached, or a cloned copy of your app. The Fingerly iOS and Android SDKs, and the React Native, Flutter and Lynx packages over them, collect raw input about the environment. The server derives a signal for each finding and adds it to the suspect score.",{"type":11,"text":14},"These are signals for your own policy, not verdicts. A rooted phone is not a fraudster, and a clean device is not proven honest. Fingerly never blocks anyone on its own.",{"type":16,"level":17,"text":18,"id":19},"heading",2,"What the signals tell you","what-the-signals-tell-you",{"type":21,"columns":22,"rows":27},"table",[23,24,25,26],"Finding","Platform","Signal group key","Default weight",[28,33,38,42,46,49,54,57,61,65],[29,30,31,32],"Emulator","Android","`android_emulator`","9",[34,35,36,37],"Simulator","iOS","`ios_simulator`","16",[39,30,40,41],"Rooted device","`rooted_device`","12",[43,35,44,45],"Jailbroken device","`jailbroken_device`","10",[47,30,48,32],"Cloned or repackaged app","`cloned_app`",[50,51,52,53],"Instrumentation attached","Android and iOS","`frida_detected`","14 and 14",[55,51,56,53],"Intercepted app traffic","`mitm_attack`",[58,51,59,60],"Debugger or developer tooling","`developer_tools`","8 and 16",[62,51,63,64],"Mass-provisioned or freshly reset device","`device_farm`","8 and 8",[66,51,67,68],"No hardware proof offered","`attestation_missing`","3 and 3",{"type":11,"text":70},"The weights are the shipped defaults, listed in the [signals reference](\u002Fdocs\u002Fsignals), and you can change them with [risk weights](\u002Fdocs\u002Frisk-weights). A weight of `9` means the signal adds 9 points to the [suspect score](\u002Fdocs\u002Fsuspect-score) when it fires. With the default threshold of `30`, the level becomes `medium` from 15 points and `high` from 30.",{"type":16,"level":17,"text":72,"id":73},"Where to read them","where-to-read-them",{"type":75,"items":76},"list",[77,78,79],"The **stored event**, read with a secret key, lists the groups that fired in `triggers`, heaviest first. See [Get an event](\u002Freference\u002Fget-event#response).","The **`visitor.suspect` webhook** names the individual signal and its group when a session reaches `high`.","The **app** is never told which signals fired. It can read `suspectLevel` unless you [hide the score on the key](\u002Fdocs\u002Fapi-keys#hide-the-score), as a convenience and never as the decision.",{"type":81,"samples":82},"code",[83],{"label":84,"lang":85,"code":86,"check":87},"Server","ts","const MODIFIED_DEVICE = [\n  'android_emulator',\n  'ios_simulator',\n  'rooted_device',\n  'jailbroken_device',\n  'cloned_app',\n  'frida_detected',\n  'mitm_attack',\n]\n\nconst event = await fingerly.events.get(requestId)\nconst found = event.triggers.filter((trigger) => MODIFIED_DEVICE.includes(trigger.signal))\n\nif (found.length > 0) return stepUp()   \u002F\u002F ask for a second factor, or hold for review",false,{"type":11,"text":89},"To make one of these decisive, read it from `event.triggers` as above, or raise its weight in risk weights so the level moves on its own. See [signals that matter for account takeover](\u002Fdocs\u002Fuse-cases\u002Faccount-takeover) for a policy that uses them.",{"type":16,"level":17,"text":91,"id":92},"Prove the report comes from your app","prove-the-report-comes-from-your-app",{"type":11,"text":94},"The signals are derived from raw input, and a modified app can forge raw input. Hardware attestation is what resists that: on Android, a key in the device keystore attests to a fresh challenge, and on iOS, App Attest has Apple vouch for the app. A failed check raises the `tampering` signal, which appears on stored events under `browser_tampering`.",{"type":75,"items":96},[97,98,99],"Android works without configuration on Android 7.0 (API 24) and newer. List your package names and signing certificates on the key for the server to check the attestation names your app.","iOS needs the App Attest capability and your Apple app ID on the key.","A key can **require attestation**, so a report that does not prove itself is refused with `403` instead of scored. Simulators and emulators are refused on such a key. Turn it on only once every device you need to serve can attest.",{"type":11,"text":101},"See [mobile app attestation](\u002Fdocs\u002Fmobile-attestation) for the states and for what each failure means.",{"type":16,"level":17,"text":103,"id":104},"Optional checks you can switch on","optional-checks-you-can-switch-on",{"type":75,"items":106},[107,108],"**iOS jailbreak apps.** To let the SDK check for well-known jailbreak apps, declare their URL schemes in `LSApplicationQueriesSchemes`. The SDK only queries the schemes you declare, and reports only how many answered. Without any, the report says it could not ask, so a zero is never read as nothing found. See [Optional: jailbreak app checks](\u002Fdocs\u002Fsdks\u002Fios#optional-jailbreak-app-checks).","**Android packages.** The SDK's manifest declares a `\u003Cqueries>` list of 17 root, hooking and cloning packages. It never requests `QUERY_ALL_PACKAGES`, it reports other apps only as yes or no and never by name, and you can remove entries. See [package visibility](\u002Fdocs\u002Fapp-store-privacy#package-visibility).",{"type":16,"level":17,"text":110,"id":111},"Test it","test-it",{"type":75,"items":113},[114,115,116],"Run your app in an Android emulator or the iOS Simulator with a development key. The event shows `android_emulator` or `ios_simulator` in **Identification > Events**.","Replay recent events against a policy change before you enforce it. See [test your integration](\u002Fdocs\u002Ftesting).","Start in observe mode: log what you would have done for a week or two, then add friction before you refuse anyone.",{"type":118,"tone":119,"text":120},"callout","warning","Do not treat a modified device as proof of fraud. Developers, testers and enthusiasts root, jailbreak and emulate for honest reasons. Prefer a second factor or review to an outright refusal.",{"type":16,"level":17,"text":122,"id":123},"Common questions","common-questions",{"type":16,"level":125,"text":126,"id":127},3,"How do I detect an emulator in my mobile app?","how-do-i-detect-an-emulator-in-my-mobile-app",{"type":11,"text":129},"Identify with the SDK at the action you protect, then read the stored event on your server. `android_emulator` and `ios_simulator` appear in `triggers` when the app runs in one. The app itself is never told which signals fired.",{"type":16,"level":125,"text":131,"id":132},"How do I detect a rooted or jailbroken device?","how-do-i-detect-a-rooted-or-jailbroken-device",{"type":11,"text":134},"The same way. `rooted_device` is reported on Android and `jailbroken_device` on iOS, each with a default weight you can change. The server derives them from the raw input the SDK sends.",{"type":16,"level":125,"text":136,"id":137},"Can these checks be bypassed?","can-these-checks-be-bypassed",{"type":11,"text":139},"A determined attacker with a modified app or device can hide what the SDK reads, which is why attestation is the evidence to add. It raises the cost of forging a report, and it does not make forgery impossible. Treat each answer as evidence.",{"type":16,"level":125,"text":141,"id":142},"Does it work in React Native and Flutter apps?","does-it-work-in-react-native-and-flutter-apps",{"type":11,"text":144},"Yes. Those packages are thin bridges to the native SDKs, so the same signals are reported on each platform.",{"type":16,"level":125,"text":146,"id":147},"Does the SDK show a permission prompt for these checks?","does-the-sdk-show-a-permission-prompt-for-these-checks",{"type":11,"text":149},"No. No SDK shows a permission prompt or a dialog. On Android, the permissions it declares are granted at install time. On iOS, no usage description is required.",{"type":16,"level":125,"text":151,"id":152},"Can Fingerly detect device farms?","can-fingerly-detect-device-farms",{"type":11,"text":154},"The `device_farm` group fires when a device looks mass-provisioned or freshly reset, or shares traits with many devices at once. Its cross-device part compares a device with your own traffic and adds nothing to the score until there are at least 1,000 identifications and 7 days of history.",{"type":156,"columns":17,"cards":157},"cards",[158,163,167,172],{"title":159,"text":160,"href":161,"icon":162},"Mobile app attestation","Hardware proof a report is genuine.","\u002Fdocs\u002Fmobile-attestation","shield",{"title":164,"text":165,"href":166,"icon":75},"Signals reference","Every signal group and its default weight.","\u002Fdocs\u002Fsignals",{"title":168,"text":169,"href":170,"logo":171},"iOS tutorial","Add the Swift SDK to an app.","\u002Fdocs\u002Fdevice-fingerprinting-ios","apple",{"title":173,"text":174,"href":175,"logo":176},"Android tutorial","Add the Kotlin SDK to an app.","\u002Fdocs\u002Fdevice-fingerprinting-android","android",[178,179,180,181,182,183,184,185,186,187,188,189],{"id":19,"text":18,"level":17},{"id":73,"text":72,"level":17},{"id":92,"text":91,"level":17},{"id":104,"text":103,"level":17},{"id":111,"text":110,"level":17},{"id":123,"text":122,"level":17},{"id":127,"text":126,"level":125},{"id":132,"text":131,"level":125},{"id":137,"text":136,"level":125},{"id":142,"text":141,"level":125},{"id":147,"text":146,"level":125},{"id":152,"text":151,"level":125},"2026-11-12T11:59:51.000Z",1794493376968]