What is Castle Android and how to recognise it
A Castle-enabled Android application sends a request token along with a client identifier. Look for the X-Castle-Request-Token and X-Castle-Client-Id headers in the application integration or a request you are authorized to inspect.
The token and client ID belong together. A token made for one device identifier should not be paired with a different identifier copied from another request.
The task takes the application's package details instead of a page URL. If the app sends x-acf-sensor-data instead of Castle headers, use the Akamai BMP guide.
Which app details to send
Use the application's Castle publicKey, which starts with pk_, together with its Android packageName, packageVersion and integer packageBuild. The version name and build number are different values: for example, 12.25.0-prod.01 and 312250001 are not interchangeable.
Keep these details tied to the same application build. Do not substitute a display label for the package name or send the build number as a version-name string.
If your client already uses a device UUID, send it as optional deviceId. Otherwise omit it and keep the ID returned with the solve. Pinning the UUID does not promise that every other device characteristic remains unchanged across unrelated requests.
The Castle Android API reference includes the complete request example with the public input names.
Where the token goes
Send solution.token as X-Castle-Request-Token and solution.clientId as X-Castle-Client-Id on the application request. The returned clientId equals deviceId.
The public result also includes deviceModel, describing the model used for that token. There is no public sessionId, phone-catalog key, manufacturer or OS-version field to copy into the request.
Keep the returned token and client ID as one pair. The token alone is not a login session or a replacement for the app's own authentication.
Common mistakes
The old names publishableKey, appLabel, appVersion and appBuild are not the public Castle Android input names. Use publicKey, packageName, packageVersion and packageBuild instead.
Do not confuse the package version name with its integer build number, or use a malformed device ID. deviceId must be a UUID when supplied.
Another mistake is copying only the token and leaving an older client-ID header in the HTTP session. Replace both with values from the same solution.
Solve it with SurSolver
Create a CastleAndroidToken task with the public key and package details, optionally adding your device UUID. Poll getTaskResult, then apply the returned token and client ID to the application request.
Use the Castle Android API reference for the exact payload and the four-field public solution. Keep this integration separate from Akamai BMP, even when one app contains both SDKs.
The Python script below creates the token task, stores the returned device UUID for the next run, and sends the token and client ID as the app request's Castle headers.
# castle-android-python.py - generate the Castle Android header and send it with the app API
# request.
# Install: pip install requests
# Run: python castle-android-python.py
import os
import time
import requests
# Your SurSolver API key.
CLIENT_KEY = "YOUR_API_KEY"
# The pk_ publishable key the app passes to Castle.
CASTLE_PUBLIC_KEY = "pk_YOUR_CASTLE_PUBLIC_KEY"
# The Android package name of the app.
APP_PACKAGE = "com.example.app"
# The versionName of the app.
APP_VERSION = "1.2.3"
# The versionCode of the app.
APP_BUILD = 456
# The app's protected API request URL.
APP_API_URL = "https://example.com/app-api"
# Its HTTP method; GET when unset.
APP_API_METHOD = None
# File that keeps the device id between runs; device-id.txt when unset.
DEVICE_ID_FILE = None
# Text that appears only in a response the site accepted.
SUCCESS_TEXT = "PASTE_ACCEPTED_MARKER"
API = "https://api.sursolver.com"
POLL_ATTEMPTS = 60
def call(path, body):
# API errors are HTTP 200 with errorId 1; anything else is a transport failure.
response = requests.post(f"{API}{path}", json=body, timeout=30)
response.raise_for_status()
data = response.json()
if data["errorId"] != 0:
raise RuntimeError(f'{data["errorCode"]}: {data["errorDescription"]}')
return data
def solve(task):
created = call("/createTask", {"clientKey": CLIENT_KEY, "task": task})
for _ in range(POLL_ATTEMPTS):
result = call("/getTaskResult", {"clientKey": CLIENT_KEY, "taskId": created["taskId"]})
if result["status"] == "ready":
return result["solution"]
time.sleep(3)
raise TimeoutError(f'task {created["taskId"]} was not ready in {POLL_ATTEMPTS} polls')
def without_empty(fields):
# Optional task fields are sent only when they have a value.
return {name: value for name, value in fields.items() if value not in (None, "")}
def main():
device_file = DEVICE_ID_FILE or "device-id.txt"
# One installation keeps one device id; reuse the id a previous run saved.
device_id = None
if os.path.exists(device_file):
with open(device_file, encoding="utf-8") as handle:
device_id = handle.read().strip()
solution = solve({
"type": "CastleAndroidToken",
"publicKey": CASTLE_PUBLIC_KEY,
"packageName": APP_PACKAGE,
"packageVersion": APP_VERSION,
"packageBuild": APP_BUILD,
**without_empty({"deviceId": device_id}),
})
with open(device_file, "w", encoding="utf-8") as handle:
handle.write(solution["deviceId"])
headers = {
"X-Castle-Request-Token": solution["token"],
"X-Castle-Client-Id": solution["clientId"],
}
# Add the headers and body the app sends with this request.
response = requests.request(APP_API_METHOD or "GET", APP_API_URL, headers=headers, timeout=30)
if not response.ok or SUCCESS_TEXT not in response.text:
raise SystemExit(f"The site did not accept the result (HTTP {response.status_code}).")
print("The site accepted the result.")
if __name__ == "__main__":
main()