📘 Public beta · Endpoints are stable; OpenAPI specs and SDKs ship monthly. See changelog →
Guides
Mobile device fraud detection

Mobile device fraud detection

Detect jailbroken / rooted / emulated / debugger-attached / sideloaded devices on iOS + Android. Use the signals to gate sensitive onboarding and transactions.

The shape

App startup
     │
     â–¼
1. Device SDK collects integrity signals
     │   @quantum-elixir/device-sdk-rn (or native)
     â–¼
2. SDK POSTs to /api/device-session
     │   returns sessionToken (30-min TTL)
     â–¼
3. User initiates sensitive action
     │
     â–¼
4. Backend: POST /api/evaluate
   { customerId, lane, sessionToken, ... }
     │
     â–¼
5. Anti-fraud rules + ML score using device signals
     │
     ├─ jailbroken/rooted    → block (hard rule, bypassMl)
     ├─ emulator              → review or flag (configurable)
     ├─ vpn/proxy/tor         → flag (don't block — many legit users)
     ├─ debugger attached     → block
     ├─ attestation failed    → review or block (per org)
     └─ all clean             → score normally

Setup (React Native)

npm install @quantum-elixir/device-sdk-rn
npx pod-install
import { ElixirDeviceSDK } from '@quantum-elixir/device-sdk-rn';
 
const sdk = new ElixirDeviceSDK({
  apiKey:           process.env.QE_PUBLIC_KEY!,
  endpoint:         'https://your-backend.example.com/api/proxy/device-session',
  challengeEndpoint: 'https://your-backend.example.com/api/proxy/attestation/challenge',
  onReady: (sessionToken) => store.set('sessionToken', sessionToken),
});
 
// Call on app boot AND before every sensitive submit
await sdk.collect();

Backend proxy

Your backend forwards the device-session POST to anti-fraud with your secret API key. Never embed your secret key in the mobile bundle.

app.post('/api/proxy/device-session', async (req, res) => {
  // Verify request came from your app (e.g. JWT, public-key issued to client)
  if (!isAuthorized(req)) return res.status(401).send();
 
  const sandboxRes = await fetch(
    'https://api.quantumelixir.tech/anti-fraud/api/device-session',
    {
      method: 'POST',
      headers: {
        Authorization: `Bearer ${process.env.QE_ANTI_FRAUD_KEY}`,
        'Content-Type': 'application/json',
      },
      body: JSON.stringify(req.body),
    },
  );
  res.status(sandboxRes.status).json(await sandboxRes.json());
});

What signals get collected

SignaliOSAndroid
jailbrokenCydia / Sileo / RootHide / ElleKit / sandbox-write proben/a
rootedn/aMagisk / SuperSU / KernelSU / APatch detection
emulatorSimulator detection (TARGET_OS_SIMULATOR)Build.HARDWARE / Build.FINGERPRINT heuristics
debuggerAttachedsysctl PTRACE checkDebug.isDebuggerConnected() + native ptrace check
sideloadedBundle ID prefix + provisioning profileINSTALLER_PACKAGE_NAME check
vpnNetwork reachability + interface nameNetworkCapabilities + VpnService check
proxyNSURLSessionConfiguration proxySystem proxy properties
torKnown Tor exit node IPSame
mitmCert chain mismatch when MITM probe URL configuredSame
attestation (iOS)AppAttest tokenn/a
attestation (Android)n/aPlay Integrity token

Default rule policy

Out-of-the-box rules (you can edit any of them):

Rule codeTriggerActionbypassMl
DEV-001jailbroken == trueblocktrue
DEV-002rooted == trueblocktrue
DEV-003emulator == truereviewfalse
DEV-004debuggerAttached == trueblocktrue
DEV-005sideloaded == truereviewfalse
DEV-006vpn == trueflagfalse
DEV-007mitm == trueblocktrue
DEV-008attestationVerdict == 'fail'reviewfalse

Adjust from the Anti-Fraud dashboard's Rules page. bypassMl: true means the action is never downgraded by ML suppression — use for hard-stop signals.

Common false positives

  • VPN users. Banking-app users on corporate VPNs are common. Don't block on vpn:true alone.
  • Modified-iOS users. RootHide and similar jailbreaks try to evade detection. The SDK occasionally has false positives on heavily-customized devices that aren't actually jailbroken. Surface a "device security concern" message and offer a support path; don't silently hard-block.
  • Genuine devs on debug builds. Internal testers might trip debuggerAttached. Whitelist your internal team's customer IDs.

Combining with biometric

Device intelligence + biometric step-up is the gold-standard pattern:

// 1. Device signals + anti-fraud
const fraud = await qe.antiFraud.evaluate({
  customerId, lane: 'transaction', sessionToken, externalId: txn.id, ...
});
if (fraud.decision === 'block') return reject('fraud_block');
 
// 2. Biometric step-up if action is high-value
if (txn.amount > 10_000_000) {  // > IDR 10M
  const stepUp = await qe.identity.authStepUp({ ... });
  if (!stepUp.allow) return reject('biometric_failed');
}
 
// 3. Commit
await commit(txn);

Rotating SDK config

The SDK polls GET /api/sdk-config on app boot and caches for 5–15 min. Change cert pins or toggle MITM probes via the dashboard — no app-store release needed.