📘 Public beta · Endpoints are stable; OpenAPI specs and SDKs ship monthly. See changelog →
Products
Anti-Fraud Platform
Device SDK (mobile)

Device SDK (mobile)

The Quantum Elixir Device SDK collects device intelligence signals on iOS and Android, hands them to your backend, and gives you a sessionToken to forward to POST /api/evaluate.

Two-command React Native integration. Native iOS + Android packages also available.

PackagePlatformStatus
@quantum-elixir/device-sdk-rnReact Native (iOS+Android)Stable
QuantumElixirDeviceiOS Swift (CocoaPods + SPM)Stable
tech.quantumelixir:device-androidAndroid Kotlin (Maven)Stable
quantum_elixir_deviceFlutter (wraps native)Beta

What signals it collects

CategoryiOSAndroid
HardwareModel, OS version, total memoryModel, brand, OS version, total memory
IntegrityCydia / Sileo presence, sandbox-write probe, suspicious dylibs, URL-scheme handlers (RootHide/ElleKit aware)Magisk / SuperSU / KernelSU / APatch detection, SELinux enforcement, debuggable APK
NetworkConnection type, MITM probeConnection type, Wi-Fi SSID, MITM probe
AppBundle ID, version, install source (App Store vs sideloaded)Package ID, version, install source (Play vs sideloaded)
LocaleLocale, timezone, languageLocale, timezone, language
AttestationAppAttest token (Apple-signed)Play Integrity token (Google-signed)

React Native — 2-command install

npm install @quantum-elixir/device-sdk-rn
npx pod-install              # iOS

That's it. The Android side autolinks; iOS only needs pod-install. No native code changes required.

Usage

import { ElixirDeviceSDK } from '@quantum-elixir/device-sdk-rn';

const sdk = new ElixirDeviceSDK({
  apiKey:   process.env.QE_PUBLIC_KEY!,                    // public key, OK to ship in bundle
  endpoint: 'https://api.bank-xyz.com/proxy/device-session',
  challengeEndpoint: 'https://api.bank-xyz.com/proxy/attestation/challenge',
  onReady:  (token) => setSessionToken(token),
  onError:  (err) => console.warn('device-sdk', err),
});

// Call on app boot OR right before a sensitive submit:
await sdk.collect();

// Then in your normal checkout flow:
const tx = await fetch('/api/checkout', {
  method: 'POST',
  body: JSON.stringify({ ...checkout, sessionToken }),
});

Endpoint proxying

You almost certainly don't want to embed your server API key in a mobile binary. Instead:

  1. Issue a public anti-fraud API key (read-only, scoped to device-session:create).
  2. Mobile SDK calls your backend; your backend forwards to /api/device-session with the real key.

The SDK supports both modes (direct or proxied). Sandbox accepts public keys directly so you can prototype faster.

Native iOS

import QuantumElixirDevice
 
let sdk = ElixirDevice(
    apiKey: "qe_sk_public_...",
    endpoint: URL(string: "https://api.bank-xyz.com/proxy/device-session")!,
    challengeEndpoint: URL(string: "https://api.bank-xyz.com/proxy/attestation/challenge")!
)
sdk.collect { result in
    switch result {
    case .success(let session):
        currentSessionToken = session.sessionToken
    case .failure(let error):
        // SDK errors are non-fatal — proceed without device signals
    }
}

Native Android

import tech.quantumelixir.device.ElixirDevice
 
val sdk = ElixirDevice(
    apiKey = "qe_sk_public_...",
    endpoint = "https://api.bank-xyz.com/proxy/device-session",
    challengeEndpoint = "https://api.bank-xyz.com/proxy/attestation/challenge",
)
lifecycleScope.launch {
    val result = sdk.collect()
    result.onSuccess { sessionToken = it.sessionToken }
}

Server endpoint (under the hood)

POST/api/device-session
Auth · API keyScope · evaluate:write

Request body (assembled by the SDK; documented for reference / proxying):

{
  "device": {
    "platform": "iOS | Android",
    "osVersion": "17.4",
    "model": "iPhone15,4",
    "locale": "id_ID",
    "timezone": "Asia/Jakarta",
    "totalMemoryMb": 6144
  },
  "fingerprint": {
    "deviceId": "...",
    "advertisingId": "..."
  },
  "riskSignals": {
    "jailbroken": false,
    "rooted": false,
    "emulator": false,
    "vpn": false,
    "debuggerAttached": false,
    "sideloaded": false
  },
  "nativeDetection": {
    "rootHideSignals": [],
    "magiskSignals": [],
    "kernelSuSignals": []
  },
  "attestation": {
    "platform": "iOS",
    "token": "<AppAttest payload>"
  }
}

Response:

{
  "data": {
    "sessionToken": "ds_01HXY...",
    "expiresIn": 1800,
    "attestationVerdict": "pass | fail | null"
  },
  "ok": true
}

Token expires in 30 minutes. Forward it to POST /api/evaluate in sessionToken. The device payload merges into data on evaluate.

Remote SDK config

GET/api/sdk-config
Auth · API keyScope · evaluate:write

The SDK polls this on app boot (cache for 5–15 minutes) to fetch certificate pins and feature flags without re-deploying:

{
  "data": {
    "pins": { "api.bank-xyz.com": ["sha256/AAAA...", "sha256/BBBB..."] },
    "pinStrictMode": false,
    "wifiProbeEnabled": false,
    "mitmProbeEnabled": true,
    "fetchedAt": "2026-05-24T12:00:00Z",
    "ttlSeconds": 600
  },
  "ok": true
}

Use this to toggle MITM probes on/off during an investigation, or to rotate certificate pins without an app-store release.

What signals surface in the evaluate response

Device signals appear on the customer/alert detail page once they make it through evaluate. Specifically:

  • jailbroken, rooted — hard block by default (BLK-DEV-001, BLK-DEV-002).
  • emulator, debuggerAttached, sideloaded — alert flag.
  • vpn, proxy, tor — alert flag (not block — many legit users are on VPNs).
  • attestationVerdict: fail — alert flag; per-org rule decides if it escalates to block.

False positives on jailbreak detection are real

Modern jailbreaks (RootHide, Dopamine) actively try to spoof the SDK's checks. We update probe logic in sdk-config. Customers running heavily modified iOS / custom Android ROMs occasionally trip false positives — your rule policy should account for support escalation.