---
updatedAt: 2026-08-21T11:06:05.000Z
agentTools:
  projectIndex: https://docs.castle.io/llms.txt
---

# Mobile SDKs

How to install, configure and use the Castle Mobile SDKs to perform device fingerprinting and monitor user activity.

<Callout icon="📘" theme="info">
  ### Looking for the source code?

  - [Android SDK](https://github.com/castle/castle-android)
  - [iOS SDK](https://github.com/castle/castle-ios)
  - [Flutter SDK](https://github.com/castle/castle-flutter)
  - [React Native SDK](https://github.com/castle/castle-react-native)
</Callout>

# 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](#creating-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](#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](https://dashboard.castle.io/settings) for users with administrator access.

```swift iOS (Swift)
import CastleSDK

// Place the below in UIApplicationDelegate application:didFinishLaunchingWithOptions:
try Castle.configure(publishableKey: "{Publishable-API-Key}")
```
```objectivec iOS (Obj-C)
@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);
}
```
```java Android (Java)
import io.castle.Castle;

// Place the below in Application class onCreate method
Castle.configure(application, "{Publishable-API-Key}");
```
```kotlin Android (Kotlin)
import io.castle.Castle

// Place the below in Application class onCreate method
Castle.configure(application, "{Publishable-API-Key}")
```
```javascript React Native
import Castle from "@castleio/react-native-castle";

await Castle.configureWithPublishableKey("{Publishable-API-Key}");
```
```javascript Flutter
import 'package:castle_flutter/castle.dart';

await Castle.configure(publishable_key: "{Publishable-API-Key}");
```

On Android, `configure` returns quickly and is safe to call from `Application.onCreate()`. On iOS, the synchronous `configure` must be called from the main thread; `Castle.configureAsync` (v4) is available where awaiting configuration is preferred. On both platforms, heavy initialisation is offloaded to background threads to ensure fast execution, and `configure` issues a single throw-away token asynchronously to warm up the SDK's code paths (device library initialisation) so that all later created tokens have nearly constant execution time. This warm-up is internal to the SDK and is independent of when the first real token is created.

Separately, the SDK needs a short window after `configure` — on the order of 200 ms — to collect sensor information. If `configure` is called early, for example in `application:didFinishLaunchingWithOptions:` (iOS) or `Application.onCreate` (Android), the time before the first real token — typically generated on a user action such as login or registration — already exceeds it comfortably. An explicit wait is only needed if a token must be created immediately after `configure`. This applies to both v3 and v4.

# 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.

<Callout icon="🚧" theme="warn">
  ### Request tokens don't live forever

  A 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.
</Callout>

#### Examples on how to pass the request token to your server

```swift iOS (Swift)
// NSURLRequest
request.setValue(
  Castle.createRequestToken(),
  forHTTPHeaderField: Castle.requestTokenHeaderName
)
```
```objectivec iOS (Obj-C)
// NSURLRequest
[request setValue:[Castle createRequestToken] forHTTPHeaderField:Castle.requestTokenHeaderName];
```
```java Android (Java)
// OkHttp
requestBuilder.header(
    Castle.requestTokenHeaderName,
    Castle.createRequestToken()
);

// HttpURLConnection
httpUrlConnection.setRequestProperty(
    Castle.requestTokenHeaderName,
    Castle.createRequestToken()
);

// Volley
headers.put(
    Castle.requestTokenHeaderName,
    Castle.createRequestToken()
);
```
```kotlin Android (Kotlin)
// createRequestToken() returns null until configure() has completed
val requestToken = Castle.createRequestToken()

if (requestToken != null) {
    // OkHttp
    requestBuilder.header(Castle.requestTokenHeaderName, requestToken)

    // HttpURLConnection
    httpUrlConnection.setRequestProperty(Castle.requestTokenHeaderName, requestToken)

    // Volley
    headers.put(Castle.requestTokenHeaderName, requestToken)
}
```
```javascript React Native
const requestTokenHeaderName = await Castle.requestTokenHeaderName();
const requestToken = await Castle.createRequestToken();

let requestHeaders = new Headers();
requestHeaders.append(requestTokenHeaderName, requestToken);
```
```dart Flutter
final requestTokenHeaderName = await Castle.requestTokenHeaderName;
final 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.

```ruby
def castle_user
  return {
    id: 'ca1242f498', # required
    email: 'mbrown@mailinator.com', # required
    phone: '+1415232183', # required if email is not present
    name: 'Michael Brown',
    registered_at: '2012-12-02T00:30:08.276Z',
    traits: {
      plan: 'premium'
    }
  }
end
```

In 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:

```ruby Ruby
# 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')
end
```

## Step 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`.

```swift iOS (Swift)
Castle.userJwt("eyJhbGc..xnH0")
```
```objectivec iOS (Obj-C)
[Castle setUserJwt:@"eyJhbGc..xnH0"];
```
```java Android (Java)
Castle.setUserJwt("eyJhbGc..xnH0");
```
```kotlin Android (Kotlin)
Castle.setUserJwt("eyJhbGc..xnH0")
```
```javascript React Native
await Castle.userJwt('eyJhbGc..xnH0');
```
```javascript Flutter
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.

```swift iOS (Swift)
Castle.screen(name: "Onboarding – Verify documents")
```
```objectivec iOS (Obj-C)
[Castle screenWithName:@"Onboarding – Verify documents"];
```
```java Android (Java)
Castle.screen("Onboarding – Verify documents");
```
```kotlin Android (Kotlin)
Castle.screen("Onboarding – Verify documents")
```
```javascript React Native
await Castle.screen('Onboarding – Verify documents');
```
```javascript Flutter
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.

```swift iOS (Swift)
let properties: [String: Any] = [
    "product": "iPhone 13 Pro",
    "price": 1099.99
]
Castle.custom(name: "Added to Cart", properties: properties)
```
```objectivec iOS (Obj-C)
NSDictionary *properties = @{
    @"product": @"iPhone 13 Pro",
    @"price": 1099.99
};
[Castle customWithName:@"Added to Cart"
            properties:properties];
```
```java Android (Java)
Castle.custom("Added to Cart", Map.of(
   "product", "iPhone 13 Pro",
   "price", 1099.99
));
```
```kotlin Android (Kotlin)
Castle.custom("Added to Cart", mapOf(
   "product" to "iPhone 13 Pro",
   "price" to 1099.99
))
```
```javascript React Native
await Castle.customWithProperties('Added to Cart', {
  product: 'iPhone 13 Pro',
  price: 1099.99
});
```
```javascript Flutter
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.

```swift iOS (Swift)
let configuration = CastleConfiguration(publishableKey: "{Publishable-API-Key}")
configuration.isDebugLoggingEnabled = false // Default
configuration.flushLimit = 20 // Default
configuration.maxQueueLimit = 1000 // Default
configuration.isScreenTrackingEnabled = false // Default
configuration.isApplicationLifecycleTrackingEnabled = true // Default
configuration.isTouchCollectionEnabled = true // Default, iOS 4.3.0+
configuration.baseURLAllowList = [URL(string: "https://example.com")!]

try Castle.configure(configuration)
```
```objectivec iOS (Obj-C)
CastleConfiguration *configuration = [CastleConfiguration configurationWithPublishableKey:@"{Publishable-API-Key}"];
configuration.debugLoggingEnabled = NO; // Default
configuration.flushLimit = 20; // Default
configuration.maxQueueLimit = 1000; // Default
configuration.screenTrackingEnabled = NO; // Default
configuration.enableApplicationLifecycleTracking = YES; // Default
configuration.touchCollectionEnabled = YES; // Default, iOS 4.3.0+
configuration.baseURLAllowList = @[[NSURL URLWithString:@"https://example.com"]];

NSError *error = nil;
BOOL success = [Castle configure:configuration error:&error];
if (!success) {
    NSLog(@"Castle configure failed: %@", error);
}
```
```java Android (Java)
Configuration configuration = new Configuration.Builder()
  .publishableKey("{Publishable-API-Key}")
  .debugLoggingEnabled(false) // Default
  .flushLimit(20) // Default
  .maxQueueLimit(1000) // Default       
  .screenTrackingEnabled(false) // Default
  .applicationLifecycleTrackingEnabled(true) // Default
  .baseURLAllowList(java.util.Collections.singletonList("https://example.com/"))
  .build();

try {
  Castle.configure(application, configuration);
} catch (ConfigurationError e) {}
```
```kotlin Android (Kotlin)
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) {}
```
```javascript React Native
const configuration = await Castle.configure({
  publishableKey: "{Publishable-API-Key}",
  debugLoggingEnabled: false,
  flushLimit: 20, // default: 20
  maxQueueLimit: 1000 // default: 1000
});
```
```javascript Flutter
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.

Touch collection (`isTouchCollectionEnabled`, iOS 4.3.0 and later) collects touch interaction signals used for behavioral bot detection. It is enabled by default and has no Android equivalent. Disabling it weakens behavioral detection on iOS.

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`&#x20;

### 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.

```swift
import AdSupport
import AppTrackingTransparency
import CastleSDK

let configuration = CastleConfiguration(publishableKey: "{Publishable-API-Key}")

configuration.isAdvertisingTrackingEnabled = true // Default
configuration.adSupportBlock = {
    // Request authorization separately, e.g. once the app is active:
    // ATTrackingManager.requestTrackingAuthorization { _ in }
    let status = ATTrackingManager.trackingAuthorizationStatus
    guard status == .authorized else { return "" }
    return ASIdentifierManager.shared().advertisingIdentifier.uuidString
}

try Castle.configure(configuration)
```
```objectivec Objective-C
@import AppTrackingTransparency;
@import AdSupport;
@import CastleSDK;

CastleConfiguration *configuration = [CastleConfiguration configurationWithPublishableKey:@"{Publishable-API-Key}"];

configuration.enableAdvertisingTracking = YES; // Default
configuration.adSupportBlock = ^NSString * {
    // Request authorization separately, e.g. once the app is active:
    // [ATTrackingManager requestTrackingAuthorizationWithCompletionHandler:^(ATTrackingManagerAuthorizationStatus s) {}];
    ATTrackingManagerAuthorizationStatus status = ATTrackingManager.trackingAuthorizationStatus;
    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);
}
```
```java
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();
```
```kotlin
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, adding the token as the `X-Castle-Request-Token` header field (also available as `Castle.requestTokenHeaderName`). 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.

```swift
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()
```
```objectivec Objective-C
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];
```
```java
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();
```
```kotlin
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()
```

<br />