Device Registry
Per-tenant device fingerprint registry with four built-in signals and IP-intelligence-derived impossible-travel detection.
The device registry observes device fingerprints across users and time, surfacing four signals you can fold into your rule conditions:
| Signal | Fires when |
|---|---|
newDeviceForSubject | This is the first time the fingerprint has been observed paired with this subject. |
deviceSharedAcrossSubjects | The fingerprint has been observed with ≥ 3 distinct subjects. |
impossibleTravel | The fingerprint was last seen in a different country within the implausible-travel window (1h). |
deviceRecentlyFirstSeen | The fingerprint is < 24h old. |
You compute the fingerprint hash client-side. PlatformXe never sees the raw signal set — only the hash you POST.
Two ways to use it
1. Auto-integration with /decide. When context.deviceFingerprint is present in a /v1/fraud/decide call, the engine automatically registers the observation and injects the signals into the rule evaluation context as device.*. You can write rule conditions like:
{
"all": [
{ "device.newDeviceForSubject": { "equals": true } },
{ "amount.value": { "gt": 50000 } }
]
}
This is the recommended path — one round-trip per decision, full audit trail.
2. Standalone observation. Call POST /v1/fraud/devices/seen directly when you want to register a session-start observation independent of any decision (e.g. on login).
Endpoint
POST /api/v1/fraud/devices/seen
| Property | Value |
|---|---|
| Scope | fraud:decide |
| Plan gate | Detection Pack addon |
| Rate limit | 10,000 requests / hour per API key |
| Idempotent | Yes (Idempotency-Key header honoured) |
Request body
| Field | Type | Required | Description |
|---|---|---|---|
fingerprint | string | Yes | Tenant-computed hash of the device fingerprint (≤ 256 chars). |
subjectId | string | Yes | The subject (user) observed using this device. |
ip | string | No | The client IP. Used for IP-intelligence-derived signals (impossibleTravel, ipIntel.*). |
geo | string | No | ISO 3166-1 alpha-2 country override. Takes precedence over IP intel's resolved country when present. |
context | object | No | Free-form context echoed back; rules can reference these fields under context.*. |
Response
{
"success": true,
"data": {
"fingerprintId": "dfp_2j7yhq8wcs9...",
"fingerprint": "fp_abc123...",
"signals": {
"newDeviceForSubject": true,
"distinctSubjectsSeen": 1,
"deviceSharedAcrossSubjects": false,
"impossibleTravel": false,
"deviceRecentlyFirstSeen": true,
"tenantTrustScore": null,
"firstSeenAt": "2026-05-03T09:14:21.412Z",
"lastSeenAt": "2026-05-03T09:14:21.412Z"
},
"ipIntel": {
"status": "available",
"cached": false,
"lookup": {
"country": "NG",
"region": "Lagos",
"asn": "AS37075",
"isVpn": false,
"isDatacenter": false,
"provider": "primary"
}
}
}
}
When IP intelligence is unavailable (no IP supplied, provider unconfigured, vendor outage), ipIntel carries { "status": "unavailable", "reason": "..." } instead. impossibleTravel then stays false rather than firing on partial data.
Field reference
| Path | Type | Notes |
|---|---|---|
signals.newDeviceForSubject | boolean | Fires on the first observation of (fingerprint, subjectId). |
signals.distinctSubjectsSeen | integer | Number of distinct subjects observed on this fingerprint, capped at 1,000 stored history. |
signals.deviceSharedAcrossSubjects | boolean | distinctSubjectsSeen >= 3. |
signals.impossibleTravel | boolean | Cross-country move within ≤ 1h of the last observation, when both geos are known. |
signals.deviceRecentlyFirstSeen | boolean | firstSeenAt is < 24h old. |
signals.tenantTrustScore | integer | null | Tenant-managed; lets ops mark known-good devices. |
signals.firstSeenAt | string | ISO-8601 of the first observation. |
signals.lastSeenAt | string | ISO-8601 of the most recent observation, post-upsert. |
ipIntel.lookup.country | string | null | ISO 3166-1 alpha-2 from the IP intel provider. |
ipIntel.lookup.asn | string | null | Autonomous system, e.g. AS37075. |
ipIntel.lookup.isVpn / isDatacenter | boolean | null | Provider-supplied. |
Rule examples
Block when a fresh device is sharing across users:
{
"name": "fresh-device-shared",
"weight": 70,
"appliesTo": { "actions": ["transfer", "login"] },
"condition": {
"all": [
{ "device.deviceRecentlyFirstSeen": { "equals": true } },
{ "device.deviceSharedAcrossSubjects": { "equals": true } }
]
}
}
Step up auth on impossible travel:
{
"name": "impossible-travel-step-up",
"weight": 50,
"verdictOverride": "step_up",
"appliesTo": { "actions": ["transfer"] },
"condition": {
"device.impossibleTravel": { "equals": true }
}
}
Soft review for VPN traffic above a threshold:
{
"name": "vpn-large-amount-review",
"weight": 25,
"appliesTo": { "actions": ["transfer"] },
"condition": {
"all": [
{ "ipIntel.isVpn": { "equals": true } },
{ "amount.value": { "gt": 100000 } }
]
}
}
Privacy
- Raw IPs never persist. The
ip_geo_cacheis keyed on a SHA-256 hash of the IP. - Fingerprints are tenant-computed hashes; PlatformXe does not derive them and never sees the underlying signal set.
- The
subjectsarray on each fingerprint row is capped at 1,000 entries (oldest dropped);geoscapped at 50. - IP intelligence cache TTL is 24h; cron
data-retentionevicts stale rows.
Errors
| HTTP | Code | Cause |
|---|---|---|
| 400 | BAD_REQUEST | Missing or oversized fingerprint, missing subjectId, non-string ip / geo. |
| 401 | UNAUTHORIZED | Missing or invalid API key. |
| 402 | DETECTION_PACK_REQUIRED | Detection Pack addon not enabled. |
| 403 | FORBIDDEN | API key has no organisation context, or wrong scope. |
| 429 | RATE_LIMITED | 10,000/hr ceiling exceeded. |
| 500 | INTERNAL_ERROR | Unexpected service failure. |