To use anti-crawler rules for your Android apps, integrate the app protection software development kit (SDK) first. This topic walks you through the required steps.
Background
The app protection SDK signs every request your app sends. WAF verifies these signatures to distinguish legitimate traffic from bots and other malicious actors, protecting your app's backend services.
Limitations
Supported Android ABI architectures: arm64-v8a and armeabi-v7a
Minimum Android API level: 16
After calling
init, wait at least 2 seconds before callingvmpSign. The SDK needs this time to fully initialize its security capabilities. This is a recommended delay and can be adjusted as needed. A shorter delay may reduce protection effectiveness.
Prerequisites
Before you begin, ensure that you have:
The SDK package for Android apps (
tigertally-X.Y.Z-xxxxxx-android.tgz), which includesAliTigerTally_X.Y.Z.aarandAliCaptcha_X.Y.Z.aar. To get the SDK, submit a ticket.The SDK authentication key (appkey). After you enable Bot Management, go to Bot Management > App Protection and click Obtain and Copy AppKey.

Each Alibaba Cloud account has one unique appkey, which applies to all WAF-protected domains and works across Android, iOS, and HarmonyOS app integrations. Sample appkey format: **OpKLvM6zliu6KopyHIhmneb_u4ekci2W8i6F9vrgpEezqAzEzj2ANrVUhvAXMwYzgY_vc51aEQlRovkRoUhRlVsf4IzO9dZp6nN_Wz8pk2TDLuMo4pVIQvGaxH3vrsnSQiK**Step 1: Create a project
Open Android Studio and create an Android project using the configuration wizard. The project directory looks like the following:

Step 2: Integrate the AAR package
Extract
tigertally-X.Y.Z-xxxxxx-android.tgzand copy all AAR files from the resulting folder into thelibsdirectory of your main module. The exact path may vary depending on your project configuration.
Open
build.gradlefor your app module. Add thelibsdirectory as a local dependency source and declare compilation dependencies for both AAR files and their required third-party libraries:ImportantReplace
X.Y.Zin the AAR file names with the actual version number from the SDK package.dependencies { // ... implementation files('libs/AliTigerTally_X.Y.Z.aar') implementation files('libs/AliCaptcha_X.Y.Z.aar') // Required third-party libraries implementation 'com.alibaba:fastjson:1.2.83_noneautotype' implementation 'com.squareup.okhttp3:okhttp:3.11.0' implementation 'com.squareup.okio:okio:1.14.0' }If you use ProGuard for code obfuscation, add the following
-keeprules to your ProGuard configuration file to preserve the SDK's API methods:-keep class com.aliyun.TigerTally.** {*;} -keep class com.aliyun.captcha.* {*;} -keepclassmembers,allowobfuscation class * { @com.alibaba.fastjson.annotation.JSONField <fields>; } -keep class com.alibaba.fastjson.** {*;}
Step 3: Filter SO CPU architectures
If your project does not already include SO files, add the following ABI filter to your build.gradle file:
android {
defaultConfig {
ndk {
abiFilters 'arm64-v8a', 'armeabi-v7a'
}
}
}Step 4: Declare app permissions
Add the following permissions to your AndroidManifest.xml.
Required:
<uses-permission android:name="android.permission.INTERNET"/>Optional — grant these to improve device signal collection accuracy:
| Permission | Grant type | Description |
|---|---|---|
android.permission.BLUETOOTH | Install-time | Collects Bluetooth state for device fingerprinting. |
android.permission.READ_PHONE_STATE | Install-time | Reads device identifiers such as IMEI. |
android.permission.ACCESS_WIFI_STATE | Install-time | Reads Wi-Fi connection details (SSID, BSSID). |
android.permission.ACCESS_NETWORK_STATE | Install-time | Reads network connectivity state. |
android.permission.READ_EXTERNAL_STORAGE | Runtime (Android 6.0+) | Reads device storage for signal collection. |
android.permission.WRITE_EXTERNAL_STORAGE | Runtime (Android 6.0+) | Writes to device storage for signal collection. |
Install-time permissions are automatically granted at install. Runtime permissions on Android 6.0 and later must be requested dynamically at runtime; they are not automatically granted at install time.
Step 5: Add the integration code
Add imports
import com.alibaba.fastjson.*;
import com.aliyun.tigertally.*;Set data signing
Complete the following steps in order: set a user account identifier, initialize the SDK, and then sign requests.
1. Set the user account
Assign a custom end-user identifier so you can apply targeted WAF policies per user.
public static int setAccount(String account)| Parameter | Type | Required | Description |
|---|---|---|---|
account | String | No | A string that identifies the user. Use a desensitized format. |
Return value: 0 on success, -1 on failure.
Sample code:
// For guest users, skip setAccount and initialize directly.
// After the user logs in, call setAccount and re-initialize.
String account = "user001";
TigerTallyAPI.setAccount(account);2. Initialize the SDK
Call init once to collect device information. You can call it again later to start a new collection cycle for different business contexts.
public static int init(Context context, String appkey, int collectType,
Map<String, String> otherOptions, TTInitListener listener);Parameters:
| Parameter | Type | Required | Default | Description |
|---|---|---|---|---|
context | Context | Yes | — | Your application context. |
appkey | String | Yes | — | The SDK authentication key. |
collectType | int | Yes | — | The data collection mode. See the table below. |
otherOptions | Map\<String, String\> | No | null | Additional options. |
listener | TTInitListener | No | null | Callback for initialization result. |
`collectType` values:
| Value | Description |
|---|---|
TT_DEFAULT | Collects all data. |
TT_NO_BASIC_DATA | Excludes basic device data: device name, Android version, and screen resolution. |
TT_NO_IDENTIFY_DATA | Excludes device identifiers: IMEI, IMSI, SimSerial, BuildSerial (SN), and MAC address. |
TT_NO_UNIQUE_DATA | Excludes unique identifiers: Open Anonymous Device Identifier (OAID), Google Advertising ID, and Android ID. |
TT_NO_EXTRA_DATA | Excludes extended device data: malicious/gray-area app list, LAN IP, DNS IP, Wi-Fi details (SSID, BSSID), nearby Wi-Fi list, location, and sensor data. |
TT_NOT_GRANTED | Excludes all of the above privacy data. |
Combine multiple exclusion flags with |. For example, TT_NO_BASIC_DATA | TT_NO_UNIQUE_DATA excludes both categories. Select a mode that satisfies your compliance requirements while preserving enough data for effective bot detection.
`otherOptions` values:
| Key | Description | Default | Required |
|---|---|---|---|
IPv6 | 0: Use IPv4. 1: Use IPv6. | 0 | No |
Intl | 0: Report to the Chinese mainland. 1: Report outside the Chinese mainland. Set to 1 for WAF instances outside the Chinese mainland; otherwise, use the default of 0. | 0 | No |
CustomUrl | Domain name of the data reporting server. Example: https://cloudauth-device.us-west-1.aliyuncs.com | — | No |
CustomHost | Host of the data reporting server. Example: cloudauth-device.us-west-1.aliyuncs.com | — | No |
For most international regions, settingIntlis sufficient. UseCustomUrlandCustomHostonly when reporting to a specific endpoint, such as the US (West) site.
Initialization callback:
public interface TTInitListener {
// code: the status code of the initialization call
void onInitFinish(int code);
}Callback status codes (`TTCode`):
| Code | Value | Description |
|---|---|---|
TT_SUCCESS | 0 | SDK initialized successfully. |
TT_NOT_INIT | -1 | SDK is not initialized. |
TT_NOT_PERMISSION | -2 | Required Android permissions are not fully granted. |
TT_UNKNOWN_ERROR | -3 | Unknown system error. |
TT_NETWORK_ERROR | -4 | Network error. |
TT_NETWORK_ERROR_EMPTY | -5 | Network error — response body is empty. |
TT_NETWORK_ERROR_INVALID | -6 | Network error — response format is invalid. |
TT_PARSE_SRV_CFG_ERROR | -7 | Failed to parse server configuration. |
TT_NETWORK_RET_CODE_ERROR | -8 | Gateway did not return a value. |
TT_APPKEY_EMPTY | -9 | Appkey is empty. |
TT_PARAMS_ERROR | -10 | Parameter error. |
TT_FGKEY_ERROR | -11 | Key calculation error. |
TT_APPKEY_ERROR | -12 | SDK version does not match the appkey version. |
Return value: 0 on success, -1 on failure.
Sample code:
// Replace with your actual appkey.
final String appkey = "******";
Map<String, String> options = new HashMap<>();
options.put("IPv6", "0"); // Use IPv4
options.put("Intl", "1"); // Report outside the Chinese mainland
// To report to the US (West) site specifically:
// options.put("CustomUrl", "https://cloudauth-device.us-west-1.aliyuncs.com");
// options.put("CustomHost", "cloudauth-device.us-west-1.aliyuncs.com");
// Full data collection
int ret = TigerTallyAPI.init(this.getApplicationContext(), appkey, TigerTallyAPI.TT_DEFAULT, options, null);
// Selective privacy exclusion — combine flags with |
int privacyFlag = TigerTallyAPI.TT_NO_BASIC_DATA | TigerTallyAPI.TT_NO_UNIQUE_DATA;
int ret = TigerTallyAPI.init(this.getApplicationContext(), appkey, privacyFlag, options, null);
// Exclude all privacy data
int ret = TigerTallyAPI.init(this.getApplicationContext(), appkey, TigerTallyAPI.TT_NOT_GRANTED, options, null);
Log.d("AliSDK", "ret:" + ret);3. Hash data (custom signing only)
Skip this step if you are using default signing.
For custom signing, call vmpHash to compute a hash of the request data. Pass the request body for POST, PUT, and PATCH requests, or the full URL for GET and DELETE requests. Then add the returned whash string to the ali_sign_whash HTTP request header.
public enum RequestType { GET, POST, PUT, PATCH, DELETE }
public static String vmpHash(RequestType type, byte[] input);| Parameter | Type | Required | Description |
|---|---|---|---|
type | RequestType | Yes | The HTTP request method. |
input | byte[] | Yes | The data to hash. Cannot be an empty string. For GET/DELETE, use the full URL (including path and query parameters). |
Return value: A whash string.
Sample code:
// GET request
String url = "https://tigertally.aliyun.com/apptest";
String whash = TigerTallyAPI.vmpHash(TigerTallyAPI.RequestType.GET, url.getBytes());
Log.d("AliSDK", "whash:" + whash);
// POST request
String body = "hello world";
String whash = TigerTallyAPI.vmpHash(TigerTallyAPI.RequestType.POST, body.getBytes());
Log.d("AliSDK", "whash:" + whash);4. Sign data
Call vmpSign to generate a wtoken string for request authentication. Add the wtoken to the wToken HTTP request header.
Default signing: pass the request body as
input.Custom signing: pass the whash from
vmpHashasinput.
public static String vmpSign(int type, byte[] input);| Parameter | Type | Required | Description |
|---|---|---|---|
type | int | Yes | The signature type. Must be 1. |
input | byte[] | Yes | The data to sign. For default signing: the request body. For custom signing: the whash string. If the request body is empty, pass null or "".getBytes("UTF-8"). |
Return value: A wtoken string.
Sample code:
// Default signing
String body = "i am the request body, encrypted or not!";
String wtoken = TigerTallyAPI.vmpSign(1, body.getBytes("UTF-8"));
Log.d("AliSDK", "wToken:" + wtoken);
// Custom signing — GET request
String url = "https://tigertally.aliyun.com/apptest";
String whash = TigerTallyAPI.vmpHash(TigerTallyAPI.RequestType.GET, url.getBytes());
String wtoken = TigerTallyAPI.vmpSign(1, whash.getBytes());
Log.d("AliSDK", "whash:" + whash + ", wtoken:" + wtoken);
// Custom signing — POST request
String body = "hello world";
String whash = TigerTallyAPI.vmpHash(TigerTallyAPI.RequestType.POST, body.getBytes());
String wtoken = TigerTallyAPI.vmpSign(1, whash.getBytes());
Log.d("AliSDK", "whash:" + whash + ", wtoken:" + wtoken);For custom signing, when you configure scenario-specific anti-bot policies in the console, set Custom Signature Field to
ali_sign_whash.When computing the whash for a GET request, the input URL must exactly match the final URL used in the network request. Some frameworks automatically URL-encode Chinese characters or parameters — account for this when passing the URL.
vmpHashdoes not accept empty strings. The input URL must include a path or query parameter.If
vmpHashorvmpSignreturns one of the following strings, an initialization error occurred:"you must call init first"—initwas not called before signing."you must input correct data"— the input data is invalid."you must input correct type"— the input type is invalid.
Perform two-factor authentication
If WAF determines that a request is suspicious, it can trigger a CAPTCHA challenge to verify the user. Use the following methods to handle this flow.
1. Check whether a challenge is required
After receiving a response from your server, inspect the cookie and body fields to determine if WAF has flagged the request. Merge all Set-Cookie entries from the response headers into a single cookie string before calling this method.
public static int cptCheck(String cookie, String body)| Parameter | Type | Required | Description |
|---|---|---|---|
cookie | String | Yes | All cookies from the response headers, merged into a single string. |
body | String | Yes | The full response body. |
Return value: 0 — request passed; 1 — CAPTCHA challenge required.
Sample code:
String cookie = "key1=value1;key2=value2;";
String body = "....";
int recheck = TigerTallyAPI.cptCheck(cookie, body);
Log.d("AliSDK", "recheck:" + recheck);2. Show the CAPTCHA slider
If cptCheck returns 1, create and display a CAPTCHA slider to the user.
public static TTCaptcha cptCreate(Activity activity, TTOption option, TTListener listener);| Parameter | Type | Required | Description |
|---|---|---|---|
activity | Activity | Yes | The current activity. |
option | TTOption | Yes | Slider configuration. |
listener | TTListener | Yes | Callback for verification result. |
Return value: A TTCaptcha object with show(), dismiss(), and getTraceId() methods.
`TTOption` fields:
| Field | Type | Default | Description |
|---|---|---|---|
cancelable | boolean | — | Whether tapping outside the slider dismisses it. |
customUri | String | — | A custom CAPTCHA page (local HTML file or remote URL). |
language | String | — | The language for the slider UI (for example, "cn"). |
`TTListener` callbacks:
public interface TTListener {
void success(TTCaptcha captcha, String data); // data = certifyId (token)
void failed(TTCaptcha captcha, String code); // code = error code
}Slider error codes:
| Code | Description |
|---|---|
1001 | Verification failed. |
1002 | System exception. |
1003 | Parameter error. |
1005 | Verification canceled by user. |
8001 | Slider invocation error. |
8002 | Abnormal slider verification data. |
8003 | Internal slider verification exception. |
8004 | Network error. |
The failed callback fires when an exception is detected after the user completes the sliding gesture, not necessarily when the user abandons the challenge.Sample code:
TTCaptcha.TTOption option = new TTCaptcha.TTOption();
// option.customUri = "file:///android_asset/ali-tt-captcha-demo.html";
option.language = "cn";
option.cancelable = false;
TTCaptcha captcha = TigerTallyAPI.cptCreate(this, option, new TTCaptcha.TTListener() {
@Override
public void success(TTCaptcha captcha, String data) {
Log.d(TAG, "captcha check success:" + data);
}
@Override
public void failed(TTCaptcha captcha, String code) {
Log.d(TAG, "captcha check failed:" + code);
}
});
captcha.show();Best practice example
The following end-to-end example shows the complete integration flow: initialization, request signing, sending the request, and handling the CAPTCHA challenge.
package com.aliyun.tigertally.apk;
import androidx.appcompat.app.AppCompatActivity;
import android.os.Bundle;
import android.util.Log;
import com.aliyun.TigerTally.TigerTallyAPI;
import com.aliyun.TigerTally.captcha.api.TTCaptcha;
import okhttp3.MediaType;
import okhttp3.OkHttpClient;
import okhttp3.Request;
import okhttp3.RequestBody;
import okhttp3.Response;
public class DemoActivity extends AppCompatActivity {
private final static String TAG = "TigerTally-Demo";
private final static String APP_HOST = "******";
private final static String APP_URL = "******";
private final static String APP_KEY = "******";
private final static OkHttpClient okHttpClient = new OkHttpClient();
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_demo);
doTest();
}
private void doTest() {
Log.d(TAG, "captcha flow");
new Thread(() -> {
// Step 1: Initialize the SDK.
Map<String, String> options = new HashMap<>();
// options.put("Intl", "1"); // Uncomment for international reporting.
int ret = TigerTallyAPI.init(this, APP_KEY, TigerTallyAPI.TT_DEFAULT, options, null);
// int ret = TigerTallyAPI.init(this, APP_KEY, TigerTallyAPI.TT_NOT_GRANTED, null, null); // No privacy data
Log.d(TAG, "tiger tally init: " + ret);
// Step 2: Wait at least 2 seconds before signing — initialization is asynchronous.
try {
Thread.sleep(2000);
} catch (InterruptedException e) {
e.printStackTrace();
}
// Step 3: Sign the request data.
String data = "hello world";
String whash = null, wtoken = null;
// Custom signing
whash = TigerTallyAPI.vmpHash(TigerTallyAPI.RequestType.POST, data.getBytes());
wtoken = TigerTallyAPI.vmpSign(1, whash.getBytes());
Log.d(TAG, "tiger tally vmp: " + whash + ", " + wtoken);
// Default signing (no vmpHash needed)
// wtoken = TigerTallyAPI.vmpSign(1, data.getBytes());
// Log.d(TAG, "tiger tally vmp: " + wtoken);
// Step 4: Send the request and check whether a CAPTCHA is required.
doPost(APP_URL, APP_HOST, whash, wtoken, data, (code, cookie, body) -> {
int recheck = TigerTallyAPI.cptCheck(cookie, body);
Log.d(TAG, "captcha check result: " + recheck);
if (recheck == 0) return;
this.runOnUiThread(this::doShow);
});
}).start();
}
// Show the CAPTCHA slider.
public void doShow() {
Log.d(TAG, "captcha show");
TTCaptcha.TTOption option = new TTCaptcha.TTOption();
// option.customUri = "file:///android_asset/ali-tt-captcha-demo.html";
option.language = "cn";
option.cancelable = false;
TTCaptcha captcha = TigerTallyAPI.cptCreate(this, option, new TTCaptcha.TTListener() {
@Override
public void success(TTCaptcha captcha, String data) {
Log.d(TAG, "captcha check success:" + data);
}
@Override
public void failed(TTCaptcha captcha, String code) {
Log.d(TAG, "captcha check failed:" + code);
}
});
captcha.show();
}
// Send a POST request with the wToken and optional whash headers.
public static void doPost(String url, String host, String whash, String wtoken, String body, Callback callback) {
Log.d(TAG, "start request post");
int responseCode = 0;
String responseBody = "";
StringBuilder responseCookie = new StringBuilder();
try {
Request.Builder builder = new Request.Builder()
.url(url)
.addHeader("wToken", wtoken) // Required: WAF signature token
.addHeader("Host", host)
.post(RequestBody.create(MediaType.parse("text/x-markdown"), body.getBytes()));
if (whash != null) {
builder.addHeader("ali_sign_whash", whash); // Required for custom signing
}
Response response = okHttpClient.newCall(builder.build()).execute();
responseCode = response.code();
responseBody = response.body() == null ? "" : response.body().string();
for (String item : response.headers("Set-Cookie")) {
responseCookie.append(item).append(";");
}
Log.d(TAG, "response code:" + responseCode);
Log.d(TAG, "response cookie:" + responseCookie);
Log.d(TAG, "response body:" + (responseBody.length() > 100 ? responseBody.substring(0, 100) : ""));
if (response.isSuccessful()) {
Log.d(TAG, "success: " + response.code() + ", " + response.message());
} else {
Log.e(TAG, "failed: " + response.code() + ", " + response.message());
}
response.close();
} catch (Exception e) {
e.printStackTrace();
responseCode = -1;
responseBody = e.toString();
} finally {
if (callback != null) {
callback.onResponse(responseCode, responseCookie.toString(), responseBody);
}
}
}
public interface Callback {
void onResponse(int code, String cookie, String body);
}
}After the SDK is integrated, it automatically:
Signs every outgoing request with a
wTokenheader for WAF to verify.Optionally attaches an
ali_sign_whashheader for custom signing scenarios.Enables your app to detect and respond to WAF CAPTCHA challenges.