sphoro-sdk
v1.0.1
Published
SphoroJS SDK — device fingerprinting and behavioral analytics
Readme
@sphoro/sdk — SphoroJS
Device fingerprinting + behavioral analytics SDK for Indian fintech / NBFC lending.
Zero PII. DPDP Act & GDPR compliant. Scores every loan applicant before your credit engine sees them.
Installation
npm install @sphoro/sdkOr via CDN / <script> tag (browser global):
<script src="https://cdn.sphoro.com/sdk/v1/sphoro.min.js"></script>Quick start
Script tag (loan form page)
Drop this on your loan application page. SphoroJS starts collecting device and behavioral signals immediately while the applicant fills the form.
<script src="https://cdn.sphoro.com/sdk/v1/sphoro.min.js"></script>
<script>
const sphoro = new SphoroScore({
apiKey: 'txk_live_YOUR_KEY',
onScore: (result) => {
// Called automatically after score() resolves
console.log(result.score, result.riskLevel, result.flags);
},
});
// On form submit:
document.getElementById('loan-form').addEventListener('submit', async (e) => {
e.preventDefault();
const result = await sphoro.score();
if (result.riskLevel === 'CRITICAL' || result.riskLevel === 'HIGH') {
// Hard block or route to manual review
return showFraudBlock(result);
}
// Attach Sphoro IDs to your loan application payload
submitLoanApplication({
...getFormData(),
sphoro_session_id: result.sessionId,
sphoro_device_id: result.deviceId,
sphoro_score: result.score,
sphoro_risk_level: result.riskLevel,
});
});
</script>ES module (React / Next.js / Vite)
import SphoroScore from '@sphoro/sdk';
const sphoro = new SphoroScore({
apiKey: import.meta.env.VITE_SPHORO_API_KEY,
debug: import.meta.env.DEV,
});
// In your form submit handler:
const result = await sphoro.score();NestJS LOS integration
// loan-application.service.ts
import { Injectable } from '@nestjs/common';
import { PrismaService } from '../prisma/prisma.service';
@Injectable()
export class LoanApplicationService {
constructor(private prisma: PrismaService) {}
async create(dto: CreateLoanApplicationDto) {
const app = await this.prisma.loanApplication.create({
data: {
...dto,
sphoroSessionId: dto.sphoroSessionId, // store for lookup
sphoroScore: dto.sphoroScore,
sphoroRiskLevel: dto.sphoroRiskLevel,
},
});
// Hard block CRITICAL risk before underwriting
if (dto.sphoroRiskLevel === 'CRITICAL') {
throw new ForbiddenException('Application blocked by fraud detection');
}
return app;
}
// After loan outcome is known, feed back to SphoroAI:
async reportOutcome(sessionId: string, outcome: 'PAID' | 'DEFAULTED') {
await fetch(`https://api.sphoro.com/v1/session/${sessionId}/outcome`, {
method: 'PATCH',
headers: { 'X-API-Key': process.env.SPHORO_API_KEY! },
body: JSON.stringify({ outcome }),
});
}
}Config
new SphoroScore(config: SphoroConfig)| Option | Type | Default | Description |
|---|---|---|---|
| apiKey | string | required | Your txk_live_... key from the partner dashboard |
| endpoint | string | https://api.sphoro.com/v1 | API base URL — override for self-hosted |
| sessionTimeout | number | 1800000 (30 min) | Session TTL in ms |
| collectBehavior | boolean | true | Enable behavioral signal collection |
| autoCollect | boolean | true | Start device collection immediately on new SphoroScore() |
| debug | boolean | false | Log to console |
| onScore | (result) => void | — | Called every time score() resolves |
| onCollect | (signals) => void | — | Called once device signals are collected |
Methods
score(): Promise<SphoroScoreResult>
Scores the current session. Sends device + behavioral signals to POST /v1/score. Falls back to the built-in SphoroShield local rule engine if the API is unreachable (returns scoredBy: 'sphoro_local').
const result = await sphoro.score();
// result.score 0–1000 (higher = riskier)
// result.riskLevel 'LOW' | 'MEDIUM' | 'HIGH' | 'CRITICAL'
// result.flags string[] — e.g. ['EMULATOR', 'PASTED_ID_FIELDS']
// result.variables Record<string, string|number|boolean> — 50+ ML features
// result.scoredBy 'sphoro_remote' | 'sphoro_local'collect(): Promise<SphoroDeviceSignals>
Runs the full device fingerprinting pipeline. Called automatically on init when autoCollect: true. Call manually if you set autoCollect: false and want to control timing.
getBehavior(): SphoroBehaviorSignals | null
Returns a real-time snapshot of behavioral signals collected so far. Call any time after init.
flush(): Promise<void>
POSTs behavioral signals to /v1/behavior with keepalive: true. Call from beforeunload to ensure signals reach the server even when the user navigates away mid-form.
window.addEventListener('beforeunload', () => sphoro.flush());getSessionId(): string
Returns the txs_... session ID for this page load.
getDeviceId(): string
Returns the txd_... persistent device ID (stored in localStorage).
destroy(): void
Detaches behavioral listeners. Call when unmounting a React component.
Score result
interface SphoroScoreResult {
sessionId: string; // txs_...
deviceId: string; // txd_...
score: number; // 0–1000
riskLevel: 'LOW' | 'MEDIUM' | 'HIGH' | 'CRITICAL';
flags: string[];
variables: Record<string, string|number|boolean>;
scoredBy: 'sphoro_remote' | 'sphoro_local';
processingMs?: number;
timestamp: number;
}Risk thresholds:
| Risk level | Score range | Recommended action | |---|---|---| | LOW | 0–249 | Normal BRE flow | | MEDIUM | 250–499 | Flag for soft review | | HIGH | 500–749 | Manual underwriting | | CRITICAL | 750–1000 (or critical flag) | Hard block |
Critical flags that force CRITICAL regardless of score: WEBDRIVER, BOT_UA, EMULATOR, HEADLESS.
SphoroShield rules
25 built-in rules. Each fires independently in the local engine; all fire server-side when using the remote API.
| ID | Flag | Weight | Triggers when |
|---|---|---|---|
| TX001 | WEBDRIVER | 450 | navigator.webdriver, Selenium/Playwright/Nightmare present |
| TX002 | HEADLESS | 380 | HeadlessChrome/HeadlessFirefox UA or zero outer dimensions |
| TX003 | BOT_UA | 350 | UA matches bot/crawler/curl/wget/python-requests/axios/httpx |
| TX004 | EMULATOR | 280 | Android Emulator UA (sdk_gphone, generic, Android SDK Built) |
| TX005 | VIRTUAL_MACHINE | 220 | WebGL renderer is SwiftShader, LLVMpipe, VirtualBox, VMware, Parallels |
| TX006 | BROWSER_TAMPERED | 160 | navigator.plugins prototype chain tampered |
| TX007 | DEVTOOLS_OPEN | 60 | DevTools open (outer - inner > 160px) |
| TX010 | CANVAS_BLOCKED | 90 | Canvas API blocked/spoofed |
| TX011 | AUDIO_BLOCKED | 50 | AudioContext API blocked |
| TX012 | NO_PLUGINS | 55 | Zero browser plugins (and not a bot UA) |
| TX013 | FEW_FONTS | 60 | Fewer than 5 detected fonts |
| TX014 | NO_LOCALSTORAGE | 35 | localStorage unavailable |
| TX015 | TOUCH_MISMATCH | 90 | Zero touch points but UA says Android/iPhone/iPad |
| TX016 | TOUCH_SPOOF | 40 | 5+ touch points but non-mobile UA |
| TX017 | INCOGNITO | 25 | localStorage throws (likely private browsing) |
| TX020 | RAPID_FORM_FILL | 120 | First field filled in under 2 seconds |
| TX021 | EXCESSIVE_PASTE | 75 | More than 5 copy/paste events |
| TX022 | AUTOFILL_DETECTED | 40 | Browser autofill detected |
| TX023 | SUPERHUMAN_TYPING | 130 | Average keystroke interval < 30ms |
| TX024 | ROBOTIC_MOUSE | 95 | Mouse straight-line ratio > 0.92 with 10+ moves |
| TX025 | NO_MOUSE_MOVEMENT | 65 | Fewer than 5 mouse moves after 10 seconds |
| TX026 | TAB_SWITCHING | 50 | More than 15 tab visibility switches |
| TX027 | NO_SCROLL | 45 | Zero scroll after 5 seconds on page |
| TX028 | PASTED_PHONE | 60 | Phone/mobile field filled via paste |
| TX029 | PASTED_ID_FIELDS | 70 | PAN / Aadhaar field filled via paste |
| TX030 | FORM_RESETS | 35 | Form reset button used more than twice |
ML variables
50+ named variables returned in result.variables. Pipe these into your BRE or ML model.
Device signals (dev_*):
| Variable | Type | Description |
|---|---|---|
| dev_canvas_hash | string | Canvas rendering fingerprint |
| dev_audio_hash | string | AudioContext oscillator fingerprint |
| dev_fonts_hash | string | Detected font set hash |
| dev_font_count | number | Number of detected fonts |
| dev_math_hash | string | JS Math operation fingerprint |
| dev_css_hash | string | CSS media query fingerprint |
| dev_webgl_renderer | string | GPU renderer string (max 60 chars) |
| dev_webgl_vendor | string | GPU vendor string (max 40 chars) |
| dev_webgl_exts | number | Supported WebGL extension count |
| dev_cpu_cores | number | navigator.hardwareConcurrency |
| dev_memory_gb | number | navigator.deviceMemory (-1 if unavailable) |
| dev_screen | string | 1920x1080 format |
| dev_pixel_ratio | number | window.devicePixelRatio |
| dev_timezone | string | IANA timezone (e.g. Asia/Kolkata) |
| dev_language | string | navigator.language |
| dev_connection | string | Effective connection type (4g, 3g, etc.) |
| dev_plugin_count | number | Browser plugin count |
| dev_touch_points | number | navigator.maxTouchPoints |
| dev_is_touch | boolean | Touch-capable device |
| dev_battery_pct | number | Battery level 0–100 (-1 if unavailable) |
| dev_is_headless | boolean | Headless browser detected |
| dev_is_emulator | boolean | Android emulator detected |
| dev_is_vm | boolean | Virtual machine detected |
| dev_webdriver | boolean | WebDriver/Selenium present |
| dev_browser_tampered | boolean | Browser internals modified |
| dev_adblock | boolean | Ad blocker detected |
| dev_incognito | boolean | Private browsing likely |
| dev_no_cookies | boolean | Cookies disabled |
| dev_sdk_version | string | SDK version that collected these signals |
Behavioral signals (beh_*):
| Variable | Type | Description |
|---|---|---|
| beh_session_sec | number | Total session duration in seconds |
| beh_active_ratio | number | Fraction of session with user activity |
| beh_idle_ratio | number | Fraction of session idle (>5s no input) |
| beh_scroll_depth | number | Max scroll depth percentage |
| beh_paste_count | number | Total copy/paste/cut events |
| beh_tab_switches | number | Visibility change count |
| beh_focus_lost | number | Window focus loss count |
| beh_form_resets | number | Form reset count |
| beh_right_clicks | number | Right-click count |
| beh_typing_avg_ms | number | Average keystroke interval (ms) |
| beh_typing_std_ms | number | Keystroke interval std deviation |
| beh_backspace_ratio | number | Backspaces / total keystrokes |
| beh_total_keystrokes | number | Total keystrokes |
| beh_long_pauses | number | Pauses > 3 seconds mid-typing |
| beh_rhythm_hash | string | Typing rhythm fingerprint |
| beh_mouse_moves | number | Total tracked mouse positions |
| beh_mouse_distance | number | Total mouse travel distance (px) |
| beh_mouse_avg_speed | number | Average mouse speed (px/ms) |
| beh_mouse_straight | number | Straight-line ratio (0–1; robots near 1) |
| beh_mouse_clicks | number | Total mouse clicks |
| beh_hesitations | number | Mouse hesitation events (slow + pause) |
| beh_autofill | boolean | Browser autofill used |
| beh_rapid_fill | boolean | First field completed under 2 seconds |
| beh_field_order | string | Comma-separated field fill order |
Device signals type
interface SphoroDeviceSignals {
deviceId: string;
sessionId: string;
timestamp: number;
// Browser
userAgent: string; platform: string; language: string; languages: string[];
timezone: string; timezoneOffset: number; cookiesEnabled: boolean; doNotTrack: string | null;
// Screen
screenWidth: number; screenHeight: number; screenColorDepth: number;
screenPixelRatio: number; viewportWidth: number; viewportHeight: number;
availScreenWidth: number; availScreenHeight: number;
// Hardware
hardwareConcurrency: number; deviceMemory: number | null; maxTouchPoints: number;
// Fingerprints
canvasHash: string; canvasSupported: boolean;
webglVendor: string; webglRenderer: string; webglVersion: string; webglSupportedExtensions: number;
audioHash: string; audioSupported: boolean;
fontsHash: string; fontCount: number; detectedFonts: string[];
mathFingerprint: string; cssFingerprint: string;
// Network + storage
connectionType: string; connectionDownlink: number | null; connectionRtt: number | null;
pluginsHash: string; pluginCount: number;
localStorageEnabled: boolean; sessionStorageEnabled: boolean; indexedDBEnabled: boolean;
notificationsPermission: string; geolocationAvailable: boolean;
// Anomalies
isHeadless: boolean; isEmulator: boolean; isBot: boolean; hasDevTools: boolean;
webdriverPresent: boolean; isTouchDevice: boolean; isVirtualMachine: boolean;
browserTampered: boolean; adBlockerDetected: boolean; incognitoLikely: boolean;
batteryLevel: number | null; batteryCharging: boolean | null;
sdkVersion: string; sdkName: string;
}Building
cd packages/sdk
npm run build # tsc → dist/
npm run dev # tsc --watchOutput:
dist/index.js— CJS module (main)dist/index.d.ts— TypeScript declarations (types)- The
window.SphoroScoreglobal is set automatically when bundled for browser
Prisma schema integration
Add one column to your existing loan_applications table — no schema redesign:
model LoanApplication {
id String @id @default(cuid())
// ... your existing fields ...
sphoroSessionId String?
sphoroScore Int?
sphoroRiskLevel String?
createdAt DateTime @default(now())
}Then use GET /v1/session/:sphoroSessionId to pull full session detail on demand.
