Mobile SDKs
How to install, configure and use the Castle Mobile SDKs to perform device fingerprinting and monitor user activity.
Introduction
The mobile SDK is a central component of the Castle integration, and provides device fingerprinting, behavioral analysis, and client-side event monitoring.
All the client-side SDKs, have two main purposes:
- Create request tokens, Generate a unique token to be passed to your server and used when calling Castle's Risk and Filter APIs.
- Tracking client-side events, Track important in-app events directly from the client to the Castle API.
Configuration
Once installed, the SDK needs to be configured using your Publishable Key, which can be found in the Dashboard for users with administrator access.
import CastleSDK
// Place the below in UIApplicationDelegate application:didFinishLaunchingWithOptions:
try Castle.configure(withPublishableKey: "{Publishable-API-Key}")@import CastleSDK;
// Place the below in UIApplicationDelegate application:didFinishLaunchingWithOptions:
NSError *error = nil;
BOOL success = [Castle configureWithPublishableKey:@"{Publishable-API-Key}" error:&error];
if (!success) {
NSLog(@"Castle configure failed: %@", error);
}import io.castle.Castle;
// Place the below in Application class onCreate method
Castle.configure(application, "{Publishable-API-Key}");import io.castle.Castle
// Place the below in Application class onCreate method
Castle.configure(application, "{Publishable-API-Key}")import Castle from "@castleio/react-native-castle";
await Castle.configureWithPublishableKey("{Publishable-API-Key}");import 'package:castle_flutter/castle.dart';
await Castle.configure(publishable_key: "{Publishable-API-Key}");Configuration for iOS and Android SDKs can be done asynchronous or off main thread. For both platforms configure already off loads initialisation tasks to background threads to ensure fast execution. Configure always issues one throw away token asynchronous to ensure that code paths are warmed up (device library initialisation) and all later created tokens have nearly constant execution time.
It is recommended to wait approximately 200 ms between calling configure and creating the first token, allowing the SDK time to collect sensor information.
Creating request tokens
The SDK generates a request token, createRequestToken, which is a required field in the Risk and Filter APIs. It's recommended that you forward the request token as a request header to every request to your API.
Request tokens don't live foreverA new request token value should be generated for each request to your backend. A token expires after 120 seconds and should only be used for a single request to your backend. It's recommended that you implement the token generation as a client-side middleware which generates a new request token with each request to your backend.
Examples on how to pass the request token to your server
// NSURLRequest
request.setValue(
Castle.createRequestToken(),
forHTTPHeaderField: Castle.requestTokenHeaderName
)// NSURLRequest
[request setValue:[Castle createRequestToken] forHTTPHeaderField:Castle.requestTokenHeaderName];// OkHttp
requestBuilder.header(
Castle.requestTokenHeaderName,
Castle.createRequestToken()
);
// HttpURLConnection
httpUrlConnection.setRequestProperty(
Castle.requestTokenHeaderName,
Castle.createRequestToken()
);
// Volley
headers.put(
Castle.requestTokenHeaderName,
Castle.createRequestToken()
);// OkHttp
requestBuilder.header(
Castle.requestTokenHeaderName,
Castle.createRequestToken()
)
// HttpURLConnection
httpUrlConnection.setRequestProperty(
Castle.requestTokenHeaderName,
Castle.createRequestToken()
)
// Volley
headers.put(
Castle.requestTokenHeaderName,
Castle.createRequestToken()
)const requestTokenHeaderName = await Castle.requestTokenHeaderName();
const requestToken = await Castle.createRequestToken();
let requestHeaders = new Headers();
requestHeaders.append(requestTokenHeaderName, requestToken);const requestTokenHeaderName = await Castle.requestTokenHeaderName;
const requestToken = await Castle.createRequestToken;
Map<String, String> requestHeaders = {};
requestHeaders[requestTokenHeaderName] = requestToken;The createRequestToken method doesn't accept any options and generates tokens synchronously and supports being called off main thread.
Tracking client-side events
Step 1. Exposing the user object from your backend
The SDK offer two methods of sending data: screen and custom (described below) depending on which type of action the user performs. Common for these calls is that a user object needs first be set on the global Castle instance. The contents of the user object are the same as for the the Risk and Filter APIs.
def castle_user
return {
id: 'ca1242f498', # required
email: '[email protected]', # required
phone: '+1415232183', # required if email is not present
name: 'Michael Brown',
registered_at: '2012-12-02T00:30:08.276Z',
traits: {
plan: 'premium'
}
}
endIn order to prevent user information from being spoofed by a bad actor, it is required that you encode the user information as a signed JWT on your backend.
From your backend code, you encode the user as a JWT and sign it using your Castle API Secret. Later, when Castle receives the JWT, the integrity of the user data will be verified to ensure that the data isn't being tampered with.
Below is an example of how to generate a JWT using the Ruby language, and exposing it as an endpoint called from your mobile app:
# Put behind authorization to have access to the logged in user
post '/castle_user_jwt' do
JWT.encode(castle_user, ENV.fetch('CASTLE_API_SECRET'), 'HS256')
endStep 2. Setting the user in your app
Now that your server API exposes the /castle_user_jwt endpoint, you'll request it from your mobile app and put it into the setUserJwt method. This method caches the user object for subsequent requests to screen and custom.
Castle.userJwt("eyJhbGc..xnH0")[Castle setUserJwt:@"eyJhbGc..xnH0"];Castle.setUserJwt("eyJhbGc..xnH0");Castle.setUserJwt("eyJhbGc..xnH0")await Castle.userJwt('eyJhbGc..xnH0');await Castle.userJwt("eyJhbGc..xnH0");Step 3. Sending screen views from your app
Passing the title of each screen view to Castle offers your analysts detailed insights into user patterns that will help uncover complex fraud patterns. It's recommended that you centralize this logic so that you won't have to explicitly call screen for each new screen view. At the very least, it's recommended that you call screen for each step in the user onboarding flow, as well as any critical views such as transactions or user settings.
Castle.screen(name: "Onboarding – Verify documents")[Castle screenWithName:@"Onboarding – Verify documents"];Castle.screen("Onboarding – Verify documents");Castle.screen("Onboarding – Verify documents")await Castle.screen('Onboarding – Verify documents');await Castle.screen("Onboarding – Verify documents");Step 4. Sending custom events from your app
Any user action that isn't covered by screen views can be tracked by calling the custom function. This allows you to send custom properties related to the user action.
let properties = [
"product": "iPhone 13 Pro",
"price": 1099.99
]
Castle.custom(name: "Added to Cart", properties: properties)NSDictionary *properties = @{
@"product": @"iPhone 13 Pro",
@"price": 1099.99
};
[Castle customWithName:@"Added to Cart"
properties:properties];Castle.custom("Added to Cart", Map.of(
"product", "iPhone 13 Pro",
"price", 1099.99
));Castle.custom("Added to Cart", mapOf(
"product" to "iPhone 13 Pro",
"price" to 1099.99
))await Castle.customWithProperties('Added to Cart', {
product: 'iPhone 13 Pro',
price: 1099.99
});await Castle.custom("Added to Cart", {
"product": "iPhone 13 Pro",
"price": 1099.99
});Custom properties support all basic types like String, Int, Long, ... . List and Map types are not supported. Custom and Screen events are fire-and-forget designed, and handle the core of its work asynchronous, to ensure fast execution.
Additional configuration
The SDK allows for additional configuration options. Please refer to the documentation packaged with the SDK code for full details.
let configuration = CastleConfiguration(publishableKey: "{Publishable-API-Key}")
configuration.isDebugLoggingEnabled = false // Default
configuration.flushLimit = 20 // Default
configuration.maxQueueLimit = 1000 // Default
configuration.isScreenTrackingEnabled = true // Default
configuration.isApplicationLifecycleTrackingEnabled = true // Default
configuration.baseURLAllowList = [URL(string: "https://example.com")!]
try Castle.configure(configuration)CastleConfiguration *configuration = [CastleConfiguration configurationWithPublishableKey:@"{Publishable-API-Key}"];
configuration.debugLoggingEnabled = NO; // Default
configuration.flushLimit = 20; // Default
configuration.maxQueueLimit = 1000; // Default
configuration.screenTrackingEnabled = YES; // Default
configuration.enableApplicationLifecycleTracking = YES; // Default
configuration.baseURLAllowList = @[[NSURL URLWithString:@"https://example.com"]];
NSError *error = nil;
BOOL success = [Castle configure:configuration error:&error];
if (!success) {
NSLog(@"Castle configure failed: %@", error);
}Configuration configuration = new CastleConfiguration.Builder()
.publishableKey("{Publishable-API-Key}")
.debugLoggingEnabled(false) // Default
.flushLimit(20) // Default
.maxQueueLimit(1000) // Default
.screenTrackingEnabled(false) // Default
.applicationLifecycleTrackingEnabled(true) // Default
.baseURLAllowList(listOf("https://example.com/"))
.build();
try {
Castle.configure(application, configuration);
} catch (ConfigurationError e) {}val configuration = Configuration.Builder()
.publishableKey("{Publishable-API-Key}")
.debugLoggingEnabled(false) // Default
.flushLimit(20) // Default
.maxQueueLimit(1000) // Default
.screenTrackingEnabled(false) // Default
.applicationLifecycleTrackingEnabled(true) // Default
.baseURLAllowList(listOf("https://example.com/"))
.build()
try {
Castle.configure(application, configuration)
} catch (e: ConfigurationError) {}const configuration = await Castle.configure({
publishableKey: "{Publishable-API-Key}",
debugLoggingEnabled: false,
flushLimit: 20, // default: 20
maxQueueLimit: 1000 // default: 1000
});await Castle.configure(
publishable_key: "{Publishable-API-Key}",
debugLoggingEnabled: false, // Default
flushLimit: 20, // Default
maxQueueLimit: 1000 // Default
);The flushLimit is the number of events that will trigger a flush and the maxQueueLimit is the maximum number of events that will be stored in the queue before the oldest events starts to get purged. This would only happen if the Castle API stops, or the user doesn’t have an internet connection for a longer period. It's recommended that you don't change these settings until you've consulted with Castle support.
Application lifecycle tracking tracks whole app state changes like foreground and background moves. Screen tracking tracks in-app navigation of active Activies or UIViewControllers. On iOS this uses method swizzling on UIViewController.viewDidAppear
Queue flushing
The SDK queues API calls to save battery life, and it only flushes queued events to the Castle API whenever the app is installed, updated, opened or closed, or when the queue reaches flushLimit.
Ad Tracking (Advertising Identifier)
The SDK can use the platform advertising identifier to improve device fingerprinting, but it does not auto-collect this value on either platform. You must collect the identifier in your app according to platform privacy rules and pass it to the SDK.
On Android it needs the com.google.android.gms.permission.AD_ID permission. If it is not available return null and the sdk will ignore the value.
For iOS NSUserTrackingUsageDescription needs to be added to the Info.plist file.
import AdSupport
import AppTrackingTransparency
import CastleSDK
let configuration = CastleConfiguration(publishableKey: "{Publishable-API-Key}")
configuration.isAdvertisingTrackingEnabled = true
configuration.adSupportBlock = {
guard status == .authorized else { return "" }
return ASIdentifierManager.shared().advertisingIdentifier.uuidString
}
try Castle.configure(configuration)@import AppTrackingTransparency;
@import AdSupport;
@import CastleSDK;
CastleConfiguration *configuration = [CastleConfiguration configurationWithPublishableKey:@"{Publishable-API-Key}"];
configuration.enableAdvertisingTracking = YES;
configuration.adSupportBlock = ^NSString * {
if (status != ATTrackingManagerAuthorizationStatusAuthorized) {
return @"";
}
return [ASIdentifierManager.sharedManager.advertisingIdentifier UUIDString];
};
NSError *error = nil;
BOOL success = [Castle configure:configuration error:&error];
if (!success) {
NSLog(@"Castle configure failed: %@", error);
}import com.google.android.gms.ads.identifier.AdvertisingIdClient;
import io.castle.Configuration;
Configuration configuration = new Configuration.Builder()
.publishableKey("{Publishable-API-Key}")
.adIdProvider(() -> {
try {
return AdvertisingIdClient.getAdvertisingIdInfo(getApplicationContext()).getId();
} catch (Exception ignored) {
return null;
}
})
.build();import com.google.android.gms.ads.identifier.AdvertisingIdClient
import io.castle.Configuration
val configuration = Configuration
.Builder()
.publishableKey("{Publishable-API-Key}")
.adIdProvider {
try {
AdvertisingIdClient.getAdvertisingIdInfo(applicationContext).id
} catch (_: Exception) {
null
}
}
.build()Automatic Token Forwarding
Castle can automatically append a request token to outbound calls, by addinf the token as X-Castle-Request-Toke header field. Non-allowlisted URLs are left untouched.
For iOS, interception is implemented via a custom URLProtocol, it applies to Apple networking that goes through URLLoadingSystem.
For android, interception is implemented for all requests that go through the created OkHttpClient.
Low level network calls like native usage of libcurl are not intercepted.
let configuration = CastleConfiguration(publishableKey: "{Publishable-API-Key}")
configuration.isTokenAutoForwardingEnabled = true
configuration.baseURLAllowList = [URL(string: "https://example.com")!]
try? Castle.configure(configuration)
let url = URL(string: "https://example.com/v1/task")!
URLSession.shared.dataTask(with: url) { _, _, _ in
}.resume()CastleConfiguration *configuration =
[CastleConfiguration configurationWithPublishableKey:@"{Publishable-API-Key}"];
configuration.tokenAutoForwardingEnabled = YES;
configuration.baseURLAllowList = @[ [NSURL URLWithString:@"https://example.com"] ];
NSError *error = nil;
[Castle configure:configuration error:&error];
NSURL *url = [NSURL URLWithString:@"https://example.com/v1/task"];
[[[NSURLSession sharedSession] dataTaskWithURL:url
completionHandler:^(NSData *_Nullable data,
NSURLResponse *_Nullable response,
NSError *_Nullable error) {
}] resume];Configuration configuration = new Configuration.Builder()
.publishableKey("{Publishable-API-Key}")
.baseURLAllowList(java.util.Collections.singletonList("https://example.com/"))
.build();
Castle.configure(application, configuration);
OkHttpClient client = new OkHttpClient.Builder()
.addInterceptor(Castle.castleInterceptor())
.build();val configuration = Configuration.Builder()
.publishableKey("{Publishable-API-Key}")
.baseURLAllowList(listOf("https://example.com/"))
.build()
Castle.configure(application, configuration)
val client = OkHttpClient.Builder()
.addInterceptor(Castle.castleInterceptor())
.build()