Fraud
With Fraud, you can detect GPS spoofing, proxy and VPN usage, and device tampering.
Along with Regions, you can also detect a user's country and state and mark specific regions as allowed or denied to comply with regulations.
Together, Fraud and Regions provide the following user context:
{ "fraud": { "verified": true, "passed": false, "bypassed": false, "blocked": false, "mocked": true, "jumped": false, "compromised": false, "inaccurate": false, "proxy": false, "sharing": false, "lastMockedAt": "2023-07-27T17:18:28.536Z", "lastJumpedAt": "2023-07-27T17:18:28.536Z", "lastCompromisedAt": null, "lastInaccurateAt": null, "lastProxyAt": null, "lastSharingAt": null }, "country": { "code": "US", "name": "United States", "flag": "🇺🇸", "passed": true, "allowed": true }, "state": { "code": "NJ", "name": "New Jersey", "passed": true, "allowed": true, "distanceToBorder": 1092, "inBufferZone": false, "inExclusionZone": false }}
#
QuickstartFirst, sign up for Radar and get an API key.
Then, enable Fraud on the Settings page.
From there, integrate the SDK, complete the steps below, and call Radar.trackVerified()
or Radar.startTrackingVerified()
. Radar will perform fraud and jurisdiction checks as described below.
#
How it worksYou can call Radar.trackOnce()
to accurately detect a user's current geofences, current place, or current country and state.
However, users can spoof a device's location. For example, a gaming app user may spoof their location to access sports betting features only available in specific states. Or, a retail app user may spoof their location to access offers only available inside a store geofence.
To ensure you can trust a user's location, you can call Radar.trackVerified()
instead. Radar will collect a variety of fraud signals and perform fraud and jurisdiction checks, calculating flags and a signed geolocation token that you can use for fraud detection and geo-compliance in your app.
#
Fraud flagsIf fraud detection is enabled, Radar exposes the following flags in user.fraud
:
mocked
: Indicates whether a user's location is being mocked, such as in a simulator or using a location spoofing app (e.g., Fake GPS location).jumped
: Indicates whether the user moved too far too fast (e.g., "jumped" across the country in only a few seconds).compromised
: Indicates whether the user's device or app has been compromised (e.g., rooted, jailbroken) according to the Play Integrity API on Android or App Attest service on iOS.inaccurate
: Indicates whether the user's location accuracy is too low to pass verification.sharing
: Indicates whether the user is using screen sharing or remote desktop software (e.g., using TeamViewer).proxy
: Indicates whether the user's IP address is a known proxy or VPN.verified
: Indicates whether the request was made withRadar.trackVerified()
.
While you can work with individual flags, Radar also exposes a single user.fraud.passed
flag that indicates whether all fraud checks passed.
Additionally, the lastMockedAt
, lastJumpedAt
, lastCompromisedAt
, lastInaccurateAt
, lastProxyAt
, and lastSharingAt
timestamps indicate the last time that the user failed each fraud check.
#
Bypassing checks for testingIf desired, you can bypass fraud checks for individual users using the Mark as Bypassed button on the user detail page.
If a user is marked as bypassed, user.fraud.bypassed = true
and user.fraud.passed = true
, regardless of whether the user passes fraud checks.
#
Blocking usersYou can also manually block a user using the Mark as Blocked button on the user detail page.
If a user is blocked, user.fraud.blocked = true
and user.fraud.passed = false
, regardless of whether the user passes fraud checks.
#
Allowed states and countriesWith Regions, you can mark specific countries or states as allowed or denied to comply with regulations. For example, a sportsbook or daily fantasy sports app may only be allowed to operate in specific US states.
Radar exposes your settings in user.country.allowed
and user.state.allowed
.
Additionally, you can enable buffer zones and exclusion zones for different states. If buffer zones and exclusion zones are enabled, user.state.inBufferZone
and user.state.inExclusionZone
indicates whether the user is within a buffer zone or exclusion zone.
While you can work with individual flags, Radar also exposes user.state.passed
and user.country.passed
flags that indicate whether all jurisdiction checks passed.
#
Platform-specific configuration#
Android#
Initialize SDKTo support the sharing
flag on Android, pass fraud = true
to Radar.initialize()
.
Radar.initialize( context = this, publishableKey = "prj_test_pk...", fraud = true)
#
Play Integrity APITo support the compromised
flag on Android, enable the Play Integrity API on the Settings page.
If the user's device or app does not meet basic integrity checks, user.fraud.compromised = true
.
You must add the Play Integrity API dependency before calling Radar.trackVerified()
.
Add the dependency to the dependencies
section of your app's build.gradle
file:
dependencies { implementation 'com.google.android.play:integrity:1.2.0'}
If Radar.trackVerified()
returns ERROR_FORBIDDEN
, check the logs. The Play Services version on the device may be out of date.
#
SSL pinningTo enable SSL pinning and prevent man-in-the-middle attacks, add a res/xml/network_security_config.xml
file:
<?xml version="1.0" encoding="utf-8"?><network-security-config> <!-- for React Native --> <domain-config cleartextTrafficPermitted="true"> <domain includeSubdomains="true">localhost</domain> </domain-config>
<!-- for SSL pinning --> <domain-config cleartextTrafficPermitted="false"> <domain includeSubdomains="true">api-verified.radar.io</domain> <pin-set> <pin digest="SHA-256">15ktYXSSU2llpy7YyCgeqUKDBkjcimK/weUcec960sI=</pin> <pin digest="SHA-256">15ktYXSSU2llpy7YyCgeqUKDBkjcimK/weUcec960sI=</pin> </pin-set> </domain-config></network-security-config>
Learn more about Network Security Configuration on Android.
#
iOS#
App AttestTo support the compromised
flag on iOS, enable App Attest and configure a list of valid App IDs (e.g., A1B2C3D4E5.com.yourapp.app1,A1B2C3D4E5.com.yourapp.app2
) on the Settings page.
If App Attest indicates that the user's device or app has been compromised, user.fraud.compromised = true
.
App Attest requires iOS 14 and above. If Radar.trackVerified()
returns ERROR_FORBIDDEN
, check the logs. The iOS version on the device may not support App Attest.
#
SSL pinningTo enable SSL pinning and prevent man-in-the-middle attacks, add the following to your Info.plist
file:
<key>NSAppTransportSecurity</key><dict> <key>NSAllowsArbitraryLoads</key> <false/> <key>NSPinnedDomains</key> <dict> <key>api-verified.radar.io</key> <dict> <key>NSIncludesSubdomains</key> <true/> <key>NSPinnedLeafIdentities</key> <array> <dict> <key>SPKI-SHA256-BASE64</key> <string>15ktYXSSU2llpy7YyCgeqUKDBkjcimK/weUcec960sI=</string> </dict> <dict> <key>SPKI-SHA256-BASE64</key> <string>15ktYXSSU2llpy7YyCgeqUKDBkjcimK/weUcec960sI=</string> </dict> </array> </dict> </dict></dict>
Learn more about SSL pinning on iOS.
#
Web and desktopThe web SDK supports the mocked
, proxy
, and jumped
flags with Radar.trackOnce()
.
On desktop, if the end user installs the Radar Verify Mac app or Radar Verify Windows app, the web SDK also supports the sharing
flag and more advanced mocked
and proxy
detection with Radar.trackVerified()
. If Radar.trackVerified()
returns ERROR_DESKTOP_APP
, it means the Radar Verify app is not running.
#
Verifying user locationsOnce the above configuration is complete, you can verify a user's location with just a few lines of code.
Choose your implementation pattern depending on your use cases:
- Less regulated gaming use cases and non-gaming use cases should generally choose Manual location checks, either basic or advanced.
- Sportsbook and iGaming use cases subject to strict regulatory requirements should generally choose Automatic location checks with caching.
#
Manual location checks#
Advanced fraud checksFor more advanced checks, call Radar.trackVerified()
. This requires you to follow the Platform-specific configuration steps above and use the desktop app on web.
Radar.trackVerified()
returns a RadarVerifiedLocationToken
with:
passed
: A boolean indicating whether the user passed all fraud and jurisdiction checks (user.fraud.passed && user.country.passed && user.state.passed
)token
: A tamper-proof JSON Web Token (JWT) that you can send to your server to validate the signature, signed with the JSON Web Token (JWT) Secret Key found under Fraud on the Settings page.expiresAt
: The datetime when the token expires, by default in 20 minutes, but earlier when close to the border (e.g., 1 minute within 1 mile of the border in New Jersey).expiresIn
: The number of seconds until the token expires.user
: The user, with detailed information inuser.fraud
,user.country
, anduser.state
.events
: The events generated, if any.
- Swift
- Kotlin
- Web
- React Native
- Capacitor
- Flutter
- Maui
Radar.trackVerified { (status, token) in if token?.passed == true { // allow access to feature, send token to server } else { // deny access to feature, show error message }}
Radar.trackVerified { status, token -> if (token?.passed == true) { // allow access to feature, send token to server } else { // deny access to feature, show error message }}
Radar.trackVerified().then((result) => { const { user, token, expiresIn, passed } = result; if (passed) { // allow access to feature, send token to server } else { // deny access to feature, show error message }}).catch((err) => { // deny access to feature, show error message});
Radar.trackVerified().then((result) => { const { token } = result; if (token?.passed) { // allow access to feature, send token to server } else { // deny access to feature, show error message }}).catch((err) => { // deny access to feature, show error message});
// standard callbackRadar.trackVerified().then((result) => { const { user } = result; if (user?.fraud?.passed && user?.country?.allowed && user?.state?.allowed) { // allow access to feature } else { // deny access to feature, show error message }}).catch((err) => { // deny access to feature, show error message});
// token callbackRadar.trackVerifiedToken().then((result) => { const { token } = result; // send token to server}).catch((err) => { // deny access to feature, show error message});
// standard callbackvar res = await Radar.trackVerified();// do something with res['status'], res['location'], res['events'], res['user']
// token callbackvar res = await Radar.trackVerifiedToken();// do something with res['status'], res['token']
// standard callbackvar (status, location, events, user) = await Radar.TrackVerified();
// token callbackvar (status, token) = await Radar.TrackVerifiedToken();
You can send the JWT to your server and validate the signature using a JWT library. For example, in JavaScript:
const jwt = require('jsonwebtoken');
try { const decoded = jwt.verify(token, SECRET_KEY); const { user } = decoded; // token is valid, check user.fraud, user.state, user.country to allow or deny access to feature} catch (err) { // token is invalid, deny access to feature, show error message}
#
Basic fraud checksFor more basic fraud checks, call Radar.trackOnce()
instead of Radar.trackVerified()
. The callback will return a user
instead of a token
. See Foreground tracking on iOS or Foreground tracking on Android.
- Swift
- Kotlin
- Web
- React Native
- Capacitor
- Flutter
- Maui
Radar.trackOnce { (status, location, events, user) in if user?.fraud?.passed == true && user?.country?.allowed == true && user?.state?.allowed == true { // allow access to feature } else { // deny access to feature, show error message }}
Radar.trackOnce { status, location, events, user -> if (user?.fraud?.passed == true && user.country?.allowed == true && user.state?.allowed == true) { // allow access to feature } else { // deny access to feature, show error message }}
Radar.trackOnce({ fraud: true }).then((result) => { const { user } = result; if (user.fraud.passed && user.country.allowed && user.state.allowed) { // allow access to feature } else { // deny access to feature, show error message }}).catch((err) => { // deny access to feature, show error message});
Radar.trackOnce().then((result) => { const { user } = result; if (user?.fraud?.passed && user?.country?.allowed && user?.state?.allowed) { // allow access to feature } else { // deny access to feature, show error message }}).catch((err) => { // deny access to feature, show error message});
Radar.trackOnce().then((result) => { const { user } = result; if (user?.fraud?.passed && user?.country?.allowed && user?.state?.allowed) { // allow access to feature } else { // deny access to feature, show error message }}).catch((err) => { // deny access to feature, show error message});
var res = await Radar.trackOnce();// do something with res['status'], res['location'], res['events'], res['user']
var (status, _, _, user) = await Radar.TrackOnce();if (status == RadarStatus.Success && user.Fraud.Passed && user.Country.Allowed && user.State.Allowed){ // allow access to feature}else{ // deny access to feature, show error message}
#
Automatic location checks with cachingYou can also call Radar.startTrackingVerified()
to fetch and cache location tokens in the background automatically on connection changes, on the specified interval
, or more frequently if token.expiresIn < interval
(based on current state, distance to border, and so on).
Instead of calling Radar.trackVerified()
, which always fetches a fresh location token, you can instead call Radar.getVerifiedLocationToken()
, which returns a cached location token immediately if the last location token is still valid, or fetches a fresh location token if not.
If you set a delegate on iOS with Radar.setVerifiedDelegate()
or a receiver on Android with Radar.setVerifiedReceiver()
, fresh location tokens are also delivered to RadarVerifiedDelegate.didUpdateToken()
and RadarVerifiedReceiver.onTokenUpdated()
, respectively.
For example, to automatically verify the user's location at least every 20 minutes (1200 seconds), without ranging beacons:
- Swift
- Kotlin
- React Native
- Capacitor
- Maui
Radar.setVerifiedDelegate(delegate)
Radar.startTrackingVerified(interval: 1200, beacons: false)
// get a cached token if available, or fetch a fresh token if notRadar.getVerifiedLocationToken { (status, token) in if token?.passed == true { // allow access to feature, send token to server } else { // deny access to feature, show error message }}
// delivers fresh tokens to RadarVerifiedDelegatefunc didUpdateToken(_ token: RadarVerifiedLocationToken) { // send token to server}
Radar.setVerifiedReceiver(receiver)
Radar.startTrackingVerified(interval = 1200, beacons = false)
// get a cached token if available, or fetch a fresh token if notRadar.getVerifiedLocationToken { (status, token) in if (token?.passed == true) { // allow access to feature, send token to server } else { // deny access to feature, show error message }}
// delivers fresh tokens to RadarVerifiedReceiveroverride fun onTokenUpdated(context: Context, token: RadarVerifiedLocationToken) { // send token to server}
Radar.startTrackingVerified({ interval: 1200, beacons: false,});
// get a cached token if available, or fetch a fresh token if notRadar.getVerifiedLocationToken().then((result) => { const { token } = result; if (token?.passed) { // allow access to feature, send token to server } else { // deny access to feature, show error message }}).catch((err) => { // deny access to feature, show error message});
// delivers fresh tokens to listenerRadar.on('token', (result) => { const { token } = result; // send token to server});
Radar.addListener('token', (result) => { const { token } = result; // send token to server});
Radar.startTrackingVerified({ token: true, interval: 1200, beacons: false,});
Radar.TokenUpdated += (token) => { // send token to server};
Radar.StartTrackingVerified(true, 1200, false);
#
User and device risk scoresYou can use risk scores to identify high risk users and devices. Risk scores are based on a "strikes" system:
- No strikes = no risk
- 1 strike = low risk
- 2 strikes = medium risk
- 3 strikes = high risk
Users and device receive strikes for the following fraud signals within a lookback period, by default the last 90 days:
mocked
flag: 2 strikesjumped
,compromised
,sharing
, andproxy
flags: 1 strike- More than 5 user IDs per device ID: 1 strike
- More than 5 device IDs per user ID: 1 strike
When the risk score changes for a user or device, it also changes for all associated users and devices (i.e., all users associated with that device ID, or all device IDs associated with that user).
The fraud signals, number of strikes per fraud signal, and lookback period are configurable.
To receive real-time email alerts when users reach high risk, and to receive a weekly report with high risk users, set a Notification email under Fraud on the Settings page.
You can also automatically block high risk users.
#
Error handlingOn errors or failed fraud checks, you may want to display a message to the end user. For example:
- On
ERROR_PERMISSIONS
: Unable to determine your location. Please make sure you've granted location permissions and try again. - On
ERROR_LOCATION
: Unable to determine your location. Please make sure location services and wi-fi are enabled and try again. - On
ERROR_NETWORK
: Unable to determine your location. Please make sure you're connected to the Internet and try again. - On
country.allowed == false
orstate.allowed == false
: Unable to verify your location. Please make sure you're in an allowed area and try again. - On
fraud.proxy == true
: Unable to verify your location. Please disconnect from any VPNs or proxy servers you may be using and try again. - On other error cases, or as a fallback: Unable to verify your location. Please contact support.