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.
| Package | Platform | Status |
|---|---|---|
@quantum-elixir/device-sdk-rn | React Native (iOS+Android) | Stable |
QuantumElixirDevice | iOS Swift (CocoaPods + SPM) | Stable |
tech.quantumelixir:device-android | Android Kotlin (Maven) | Stable |
quantum_elixir_device | Flutter (wraps native) | Beta |
What signals it collects
| Category | iOS | Android |
|---|---|---|
| Hardware | Model, OS version, total memory | Model, brand, OS version, total memory |
| Integrity | Cydia / Sileo presence, sandbox-write probe, suspicious dylibs, URL-scheme handlers (RootHide/ElleKit aware) | Magisk / SuperSU / KernelSU / APatch detection, SELinux enforcement, debuggable APK |
| Network | Connection type, MITM probe | Connection type, Wi-Fi SSID, MITM probe |
| App | Bundle ID, version, install source (App Store vs sideloaded) | Package ID, version, install source (Play vs sideloaded) |
| Locale | Locale, timezone, language | Locale, timezone, language |
| Attestation | AppAttest token (Apple-signed) | Play Integrity token (Google-signed) |
React Native — 2-command install
npm install @quantum-elixir/device-sdk-rn
npx pod-install # iOSThat'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:
- Issue a public anti-fraud API key (read-only, scoped to
device-session:create). - Mobile SDK calls your backend; your backend forwards to
/api/device-sessionwith 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)
/api/device-sessionRequest 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
/api/sdk-configThe 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.