Android SDK for Gate/AI authentication using Play Integrity, DPoP (RFC 9449), and hardware-backed device keys.
- 🔐 Hardware-Backed Device Keys - Android Keystore with StrongBox preference
- 🛡️ Play Integrity Attestation - Device verification for production apps
- 🔧 Development Mode - Bypass Play Integrity for testing/emulators
- 🔑 DPoP Token Generation - RFC 9449 compliant per-request proofs
- ♻️ Automatic Token Refresh - Transparent token lifecycle management
- 🔄 DPoP Nonce Handling - Automatic retry on 401 challenges
- 📊 Analytics Headers - Device info, app version, locale tracking
- 🧵 Thread-Safe - Coroutine-based with mutex synchronization
The SDK is distributed through JitPack. Add the JitPack repository, then depend on the tagged release:
// settings.gradle.kts
dependencyResolutionManagement {
repositories {
google()
mavenCentral()
maven { url = uri("https://jitpack.io") }
}
}
// app/build.gradle.kts
dependencies {
implementation("com.github.GateAI-net:gate-android:1.2.0")
}import com.gateai.sdk.core.*
// Initialize the client
val configuration = GateAIConfiguration(
baseUrl = "https://yourteam.in.gate-ai.net",
packageName = packageName,
signingCertSha256 = "AA:BB:CC:DD:EE:FF:...", // Your app's SHA-256 fingerprint
cloudProjectNumber = 123456789012L, // Google Cloud project number (see Play Integrity Setup)
logLevel = GateAIConfiguration.LogLevel.INFO
)
val client = GateAIClient.create(applicationContext, configuration)
// Make authenticated requests
lifecycleScope.launch {
try {
val requestBody = """
{
"model": "gpt-4",
"messages": [{"role": "user", "content": "Hello!"}]
}
""".trimIndent()
val response = client.performProxyRequest(
path = "v1/chat/completions",
method = HttpMethod.POST,
body = requestBody.toByteArray(),
additionalHeaders = mapOf("Content-Type" to "application/json")
)
if (response.status == 200) {
val result = String(response.body)
println(result)
}
} catch (e: Exception) {
Log.e("GateAI", "Request failed", e)
}
}# For debug keystore
keytool -list -v -keystore ~/.android/debug.keystore -alias androiddebugkey -storepass android -keypass android
# For release keystore
keytool -list -v -keystore /path/to/your/release.keystore -alias your-aliasLook for the SHA256: line and copy the colon-delimited hex string.
Automatic resource discovery requires Android SDK 1.2.0 or later and is included in the framework SDKs.
Use a development token when testing in an Android emulator. The development token lets your emulator app authenticate with Gate/AI without using Google Play Integrity. This applies to native Android, React Native, Capacitor, and Flutter apps.
Create a development token for your gate in the portal, follow the setup below, then rebuild and reinstall your debug app. You do not need to pass the token in application code:
- Set
GATE_AI_DEV_TOKENin the environment of the Gradle build, or creategateai.local.propertiesin the Android Gradle root (next tosettings.gradle/settings.gradle.kts). For framework apps this is usually the app'sandroid/directory. - Add
gateai.local.propertiesto your.gitignore. In that local file, setGATE_AI_DEV_TOKEN=your-tokenwithout quotes. An environment variable takes precedence, including an explicitly empty value. - Add the following configuration to the host app module's build file, merging it into any existing
android/buildTypesblocks. Do not put it in the SDK library's build file ordefaultConfig.
For Kotlin DSL (app/build.gradle.kts):
// app/build.gradle.kts — put this import at the top of the file
import java.util.Properties
// Place the following after your existing plugins { ... } block.
val gateAILocalProperties = Properties().apply {
val localFile = rootProject.file("gateai.local.properties")
if (localFile.exists()) localFile.inputStream().use { load(it) }
}
val gateAIDevToken = providers.environmentVariable("GATE_AI_DEV_TOKEN")
.orElse(gateAILocalProperties.getProperty("GATE_AI_DEV_TOKEN", ""))
android {
buildTypes {
getByName("debug") {
resValue("string", "gate_ai_dev_token", gateAIDevToken.get())
}
}
}For Groovy (app/build.gradle):
// app/build.gradle — place after plugins { ... }
def gateAILocalProperties = new Properties()
def gateAILocalFile = rootProject.file("gateai.local.properties")
if (gateAILocalFile.exists()) {
gateAILocalFile.withInputStream { gateAILocalProperties.load(it) }
}
def gateAIDevToken = providers.environmentVariable("GATE_AI_DEV_TOKEN")
.orElse(gateAILocalProperties.getProperty("GATE_AI_DEV_TOKEN", ""))
android {
buildTypes {
debug {
resValue "string", "gate_ai_dev_token", gateAIDevToken.get()
}
}
}Sync Gradle, then rebuild and reinstall the debug app whenever the token changes. Android Studio builds must see the environment variable in the IDE's process; use the ignored local file if you normally launch the IDE from the Dock or launcher.
The native SDK automatically reads the gate_ai_dev_token string resource. No token argument is needed in Kotlin, JavaScript, or Dart. If no token is configured, the SDK asks Google Play Integrity to verify the app and device instead. Explicit developmentToken configuration remains supported in debug apps and takes precedence over the resource.
Keep the resource confined to the debug build type. This setup includes the token in the debug APK, but excludes it from release APKs/AABs. The SDK also ignores all development tokens in non-debuggable apps. Use the normal production Play Integrity setup for release builds; development tokens bypass attestation and should not be distributed.
For production apps, Play Integrity requires proper setup in Google Cloud Console and Google Play Console.
- Google Play Console Account
- Google Cloud Console Access
- App Published or In Testing (Internal, Closed, Open Alpha/Beta, or Production)
- Go to Google Play Console
- Select your app
- Navigate to Release → Setup → App Integrity
- Link a Google Cloud project (create one if needed) and note its project number — the numeric value shown on the Google Cloud Console "Project info" card
- Set that number as
cloudProjectNumberin yourGateAIConfiguration. Always set it: Google requires it for any build not installed from the Play Store — which includes every development build you run from Android Studio or install withadb— and it is harmless for Play Store installs. Without it, development builds fail with Integrity error-16before any network call.
- Go to Google Cloud Console
- Select the project associated with your app
- Navigate to APIs & Services → Library
- Search for "Play Integrity API"
- Click Enable
The Play Integrity API validates your app using:
- Package Name - Must match exactly
- SHA-256 Certificate Fingerprint - From your signing key
If you're using Google Play App Signing (recommended):
- In Play Console, go to Release → Setup → App Signing
- Copy the SHA-256 certificate fingerprint under "App signing key certificate"
- Use this fingerprint in your
GateAIConfiguration
Use the SHA-256 fingerprint from your release keystore:
keytool -list -v -keystore your-release.keystore -alias your-aliasGate/AI verifies integrity tokens server-side by calling Google's decodeIntegrityToken API, which is authorized against your cloud project. You must grant Gate/AI's service account access — no keys or secrets are exchanged, and you can revoke the grant at any time:
- In Google Cloud Console, open the project linked in Step 1
- Navigate to IAM & Admin → IAM → Grant access
- Add this principal:
gateai-integrity-check@gateai-506813.iam.gserviceaccount.com - Assign the role Service Usage Consumer
- Save
Until this grant exists, attestation fails server-side even when your app obtains integrity tokens successfully.
- Build a signed APK/AAB with your release key
- Upload to Play Console (Internal Testing track is fine)
- Install from Play Store for the fully Play-recognized path — or test a development build directly if your gate has "Require Play-recognized app" turned off in the Gate/AI Portal
- Run your app and test authentication
Enable debug logging to see detailed error messages:
logLevel = GateAIConfiguration.LogLevel.DEBUGThis error occurs when the Play Integrity API cannot verify your app. The improved error message will now show you the specific cause:
API_NOT_AVAILABLE
- Device doesn't support Play Integrity (rare)
- Solution: Use the debug-only development token setup for testing on this device
APP_NOT_INSTALLED
- App wasn't installed from Google Play Store
- Solution: Install from Play Store or use
developmentTokenfor testing
PLAY_SERVICES_NOT_FOUND / PLAY_SERVICES_VERSION_OUTDATED
- Google Play Services missing or outdated
- Solution: Update Google Play Services on the device
GOOGLE_SERVER_UNAVAILABLE / NETWORK_ERROR
- Network connectivity issue
- Solution: Check internet connection and retry
PLAY_STORE_ACCOUNT_NOT_FOUND
- No Google account signed in
- Solution: Sign in to a Google account on the device
NO_ERROR or Generic Errors
- Usually means app is not properly linked in Play Console
- Solution:
- Verify package name matches exactly
- Verify SHA-256 certificate matches signing key
- Ensure app is published in at least Internal Testing
- Wait 24-48 hours after first upload for Google to process
- Use the debug-only development token setup for testing
Ensure the package name in your GateAIConfiguration matches:
- Your
AndroidManifest.xml→<manifest package="..."> - Your
build.gradle.kts→applicationId - Your Google Play Console listing
Ensure you're using the correct certificate:
- For Google Play App Signing: Use the fingerprint from Play Console → App Signing
- For Manual Signing: Use the fingerprint from your actual release keystore
- Don't use debug keystore fingerprints for production
After first upload to Play Console:
- Google needs to process and approve your app for Play Integrity
- This can take 24-48 hours
- Use the debug-only development token setup for immediate testing
For local development, follow Development tokens without application code.
Makes an authenticated request through the Gate/AI proxy with automatic DPoP nonce retry.
suspend fun performProxyRequest(
path: String,
method: HttpMethod,
body: ByteArray? = null,
additionalHeaders: Map<String, String> = emptyMap()
): RawResponseStarts an OpenAI Realtime session. Follow OpenAI's WebRTC guide with one change: instead of fetching an ephemeral key and posting your SDP offer to OpenAI yourself, call createRealtimeCall. It takes the offer and your session configuration and returns the SDP answer to apply to the peer connection. Use whichever WebRTC library you already use.
suspend fun GateAIClient.createRealtimeCall(
offerSdp: String,
sessionJson: String,
additionalHeaders: Map<String, String> = emptyMap()
): RealtimeCallAnswer // sdp + callId ("rtc_…", how the session appears in your logs)val answer = client.createRealtimeCall(
offerSdp = offer.description,
sessionJson = """{"type":"realtime","model":"gpt-realtime"}""",
additionalHeaders = mapOf("X-App-Feature" to "voice-assistant")
)
peerConnection.setRemoteDescription(
observer,
SessionDescription(SessionDescription.Type.ANSWER, answer.sdp)
)The gate meters every model response and applies your usage limits for the whole session. If a device's token budget runs out mid-session the gate ends it: the peer connection disconnects and the data channel closes, and the next createRealtimeCall returns a 429 with the quota details. Ephemeral client secrets (/v1/realtime/client_secrets) are not used through a gate and are refused with 403. See the Realtime guide.
Generates authorization headers without making a request.
suspend fun authorizationHeaders(
path: String,
method: HttpMethod,
nonce: String? = null
): Map<String, String>Gets the current access token if available.
suspend fun currentAccessToken(): String?Forces a fresh token to be minted on the next request.
fun clearCachedState()Set user status for analytics tracking.
client.userStatus = "premium" // or "free", "trial", etc.The SDK includes these headers on all requests:
| Header | Set by | Description |
|---|---|---|
X-Client-Locale |
Automatic | User's language/region (e.g., "en-US") |
X-App-Version |
Automatic | App version from manifest |
X-OS-Version |
Automatic | Android OS version |
X-Device-Identifier |
Automatic | Android ID (per-app, per-device) |
X-Device-Type |
Automatic | Device manufacturer and model |
X-Device-Model |
Automatic | Raw hardware model identifier (e.g., "SM-G991U") |
X-Environment |
Automatic | development (debuggable build) or production |
X-SDK-Version |
Automatic | Gate/AI SDK version (e.g., "1.1.0") |
X-User-Status |
Developer (client.userStatus) |
Custom user segment (e.g., "free", "premium") |
X-User-Tier |
Developer (client.userTier) |
User's plan tier (e.g., "free", "pro"); matched exactly (case-sensitive) against Portal-configured per-tier usage limits |
X-User-Identifier |
Developer (client.userIdentifier) |
Opaque user/account ID from your own system |
X-App-Feature |
Developer (client.appFeature) |
Feature tag for cost attribution (e.g., "chat"); can be overridden per request via additionalHeaders |
X-Quota-Anchor-Day |
Developer (client.quotaAnchorDay) |
Day-of-month (1-31) the user's subscription renews; anchors billing-cycle usage windows |
Developer-set headers are only sent when the corresponding property is set.
Note:
X-User-Identifiershould be an opaque ID (e.g., a UUID or database key) — never an email address or name. Do not send PII in analytics headers.
Gates can enforce per-device usage limits over daily, calendar-month, rolling 30-day, or user-billing-cycle windows (configured in the Gate/AI portal). The SDK surfaces everything your app needs to render limit UI without parsing headers or JSON yourself.
Key per-tier limits off the user's plan by setting client.userTier. Limits
configured per tier in the Gate/AI Portal match this value exactly
(case-sensitive):
client.userTier = "pro"Anchor the billing-cycle window by telling the SDK which day of the month the user's subscription renews (read it from Play Billing / RevenueCat):
client.quotaAnchorDay = 15 // subscription renews on the 15thDays 29-31 are fine — the server clamps to short months. Changing the value
later (e.g., after a resubscribe) is safe; windows are computed server-side at
read time. The X-Quota-Anchor-Day header is consumed by the Gate/AI proxy
and stripped before the request reaches the AI provider.
Read remaining quota from any successful response — when the gate has
device limits configured, the proxy adds X-Quota-Requests-Remaining,
X-Quota-Tokens-Remaining, X-Quota-Requests-Reset, X-Quota-Tokens-Reset,
X-Quota-Requests-Limit, and X-Quota-Tokens-Limit headers (each reset is
the ISO 8601 reset time of that metric's binding window; each limit is that
window's total budget), exposed as response.quotaStatus:
val response = client.performProxyRequest(
path = "v1/chat/completions",
method = HttpMethod.POST,
body = requestBody.toByteArray(),
additionalHeaders = mapOf("Content-Type" to "application/json")
)
response.quotaStatus?.let { quota ->
println("${quota.requestsRemaining} requests left, resets at ${quota.requestsResetAt}")
// Render a usage meter: requestsUsed/tokensUsed and the 0.0-1.0 fractions
quota.requestsUsedFraction?.let { progressBar.progress = it.toFloat() }
}Handle the structured 429 — when a request is rejected for exceeding a
limit, the thrown GateApiException carries a parsed RateLimitInfo:
try {
client.performProxyRequest(...)
} catch (e: GateApiException) {
val info = e.rateLimitInfo
if (info != null) {
when (info.window) {
is RateLimitInfo.Window.Cycle,
is RateLimitInfo.Window.Monthly ->
showPaywall(resetsAt = info.resetsAt) // upgrade moment
else ->
showTryAgainLater(info.message)
}
} else {
// Not a Gate/AI rate-limit rejection
throw e
}
}info.window decodes unknown-safely: window types added in future server
versions arrive as Window.Unknown(rawValue) instead of failing to parse.
import com.gateai.sdk.network.GateApiException
import com.gateai.sdk.playintegrity.IntegrityException
try {
val response = client.performProxyRequest(...)
} catch (e: IntegrityException) {
// Play Integrity failure - see error message for details
Log.e("GateAI", "Integrity error: ${e.message}", e)
} catch (e: GateApiException) {
// API error (network, auth, etc.)
Log.e("GateAI", "API error: ${e.message}", e)
} catch (e: Exception) {
// Other errors
Log.e("GateAI", "Unexpected error", e)
}Create the GateAIClient once and reuse it:
class MyApplication : Application() {
lateinit var gateClient: GateAIClient
private set
override fun onCreate() {
super.onCreate()
gateClient = GateAIClient.create(this, configuration)
}
}Configure the debug-only resource using the development token setup, then initialize your client normally.
try {
val response = client.performProxyRequest(...)
if (response.status == 200) {
// Success
} else {
// Handle HTTP error
}
} catch (e: IntegrityException) {
// Show user-friendly message, fall back to alternative
} catch (e: Exception) {
// Log and show generic error
}logLevel = if (BuildConfig.DEBUG) {
GateAIConfiguration.LogLevel.DEBUG
} else {
GateAIConfiguration.LogLevel.WARN
}- Minimum SDK: API 24 (Android 7.0)
- Target SDK: API 34 (Android 14)
- Kotlin: 1.9+
- Google Play Services: Required for Play Integrity on production devices
Apache License 2.0
For issues, questions, or feature requests:
- GitHub Issues: https://github.com/YOUR_ORG/GateAI/issues
- Email: support@gate-ai.net
- Getting Started - Detailed setup and first release guide
- Publishing Guide - How to publish to Maven Central/JitPack
- Distribution - Quick reference for distribution commands
- Changelog - Version history and release notes